Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WD-8294 - feat: Display connection and polling of models error #1682

Merged
merged 8 commits into from
Jan 24, 2024
171 changes: 129 additions & 42 deletions src/store/app/thunks.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { actions as appActions } from "store/app";
import { actions as generalActions } from "store/general";
import { actions as jujuActions } from "store/juju";
import type { RootState } from "store/store";
import { rootStateFactory } from "testing/factories";
import {
credentialFactory,
Expand All @@ -12,9 +13,51 @@ import {
jujuStateFactory,
} from "testing/factories/juju/juju";

import { logOut, connectAndStartPolling, connectAndListModels } from "./thunks";
import { logOut, connectAndStartPolling } from "./thunks";

describe("thunks", () => {
const consoleError = console.error;
let state: RootState;

beforeEach(() => {
console.error = jest.fn();
state = rootStateFactory.build({
general: generalStateFactory.build({
config: configFactory.build({
isJuju: true,
}),
controllerConnections: {
"wss://example.com": {
user: {
"display-name": "eggman",
identity: "user-eggman@external",
"controller-access": "",
"model-access": "",
},
},
},
credentials: {
"wss://example.com": credentialFactory.build(),
},
}),
juju: jujuStateFactory.build({
controllers: {
"wss://example.com": [
controllerFactory.build({
path: "/",
uuid: "uuid123",
version: "1",
}),
],
},
}),
});
});

afterEach(() => {
console.error = consoleError;
});

it("logOut", async () => {
const action = logOut();
const dispatch = jest.fn();
Expand All @@ -40,60 +83,104 @@ describe("thunks", () => {
});

it("connectAndStartPolling", async () => {
const action = connectAndStartPolling();
const dispatch = jest.fn();
const getState = jest.fn(() => rootStateFactory.build());
const getState = jest.fn(() => state);
const action = connectAndStartPolling();
await action(dispatch, getState, null);
const dispatchedThunk = await dispatch.mock.calls[1][0](
dispatch,
getState,
null,
expect(dispatch).toHaveBeenCalledWith(
appActions.connectAndPollControllers({
controllers: [["wss://controller.example.com", undefined, false]],
isJuju: true,
}),
);
expect(dispatchedThunk.type).toBe("app/connectAndListModels/fulfilled");
});

it("connectAndListModels", async () => {
const dispatch = jest.fn();
const getState = jest.fn(() =>
rootStateFactory.build({
general: generalStateFactory.build({
config: configFactory.build({
isJuju: true,
}),
controllerConnections: {
"wss://example.com": {
user: {
"display-name": "eggman",
identity: "user-eggman@external",
"controller-access": "",
"model-access": "",
},
},
},
credentials: {
"wss://example.com": credentialFactory.build(),
},
}),
juju: jujuStateFactory.build({
controllers: {
"wss://example.com": [
controllerFactory.build({
path: "/",
uuid: "uuid123",
version: "1",
}),
],
},
}),
it("connectAndStartPolling should catch error instanceof Error", async () => {
const dispatch = jest
.fn()
// Successfuly dispatch connectAndStartPolling/pending.
.mockImplementationOnce(() => {})
// Throw error when trying to dispatch connectAndPollControllers.
.mockImplementationOnce(() => {
// eslint-disable-next-line no-throw-literal
throw "Error while dispatching connectAndPollControllers!";
});
const getState = jest.fn(() => state);
const action = connectAndStartPolling();
await action(dispatch, getState, null);
expect(dispatch).toHaveBeenCalledWith(
appActions.connectAndPollControllers({
controllers: [["wss://controller.example.com", undefined, false]],
isJuju: true,
}),
);
expect(console.error).toHaveBeenCalledWith(
"Error while triggering the connection and polling of models.",
"Error while dispatching connectAndPollControllers!",
);
expect(dispatch).toHaveBeenCalledWith(
generalActions.storeConnectionError(
"Unable to connect: Error while dispatching connectAndPollControllers!",
),
);
});

it("connectAndStartPolling should catch string error", async () => {
const dispatch = jest
.fn()
// Successfuly dispatch connectAndStartPolling/pending.
.mockImplementationOnce(() => {})
// Throw error when trying to dispatch connectAndPollControllers.
.mockImplementationOnce(() => {
throw new Error("Error while dispatching connectAndPollControllers!");
});
const getState = jest.fn(() => state);
const action = connectAndStartPolling();
await action(dispatch, getState, null);
expect(dispatch).toHaveBeenCalledWith(
appActions.connectAndPollControllers({
controllers: [["wss://controller.example.com", undefined, false]],
isJuju: true,
}),
);
const action = connectAndListModels();
expect(console.error).toHaveBeenCalledWith(
"Error while triggering the connection and polling of models.",
new Error("Error while dispatching connectAndPollControllers!"),
);
expect(dispatch).toHaveBeenCalledWith(
generalActions.storeConnectionError(
"Unable to connect: Error while dispatching connectAndPollControllers!",
),
);
});

it("connectAndStartPolling should catch non-standard type of error", async () => {
const dispatch = jest
.fn()
// Successfuly dispatch connectAndStartPolling/pending.
.mockImplementationOnce(() => {})
// Throw error when trying to dispatch connectAndPollControllers.
.mockImplementationOnce(() => {
// eslint-disable-next-line no-throw-literal
throw ["Unknown error."];
});
const getState = jest.fn(() => state);
const action = connectAndStartPolling();
await action(dispatch, getState, null);
expect(dispatch).toHaveBeenCalledWith(
appActions.connectAndPollControllers({
controllers: [["wss://controller.example.com", undefined, false]],
isJuju: true,
}),
);
expect(console.error).toHaveBeenCalledWith(
"Error while triggering the connection and polling of models.",
["Unknown error."],
);
expect(dispatch).toHaveBeenCalledWith(
generalActions.storeConnectionError(
"Unable to connect: Something went wrong. View the console log for more details.",
),
);
});
});
38 changes: 18 additions & 20 deletions src/store/app/thunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,6 @@ export const connectAndStartPolling = createAsyncThunk<
state: RootState;
}
>("app/connectAndStartPolling", async (_, thunkAPI) => {
try {
await thunkAPI.dispatch(connectAndListModels());
} catch (error) {
// XXX Add to Sentry.
console.error("Error while trying to connect and list models.", error);
}
});

export const connectAndListModels = createAsyncThunk<
void,
void,
{
state: RootState;
}
>("app/connectAndListModels", async (_, thunkAPI) => {
try {
const storeState = thunkAPI.getState();
const config = getConfig(storeState);
Expand Down Expand Up @@ -93,10 +78,23 @@ export const connectAndListModels = createAsyncThunk<
}),
);
} catch (error) {
// XXX Surface error to UI.
// XXX Send to sentry if it's an error that's not connection related
// a common error returned by this is:
// Something went wrong: cannot send request {"type":"ModelManager","request":"ListModels","version":5,"params":...}: connection state 3 is not open
console.error("Something went wrong: ", error);
// a common error logged to the console by this is:
// Error while triggering the connection and polling of models. cannot send request {"type":"ModelManager","request":"ListModels","version":5,"params":...}: connection state 3 is not open
console.error(
"Error while triggering the connection and polling of models.",
error,
);
let errorMessage;
if (error instanceof Error) {
errorMessage = error.message;
} else if (typeof error === "string") {
errorMessage = error;
} else {
errorMessage =
"Something went wrong. View the console log for more details.";
}
thunkAPI.dispatch(
generalActions.storeConnectionError(`Unable to connect: ${errorMessage}`),
);
}
});
3 changes: 0 additions & 3 deletions src/store/middleware/check-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ export const checkAuthMiddleware: Middleware<
];

const thunkAllowlist = [
appThunks.connectAndListModels.fulfilled.type,
appThunks.connectAndListModels.pending.type,
appThunks.connectAndListModels.rejected.type,
appThunks.connectAndStartPolling.fulfilled.type,
appThunks.connectAndStartPolling.pending.type,
appThunks.connectAndStartPolling.rejected.type,
Expand Down