Skip to content

Commit 3f68ac0

Browse files
fix(web): remove logout route (#1794)
* fix(web): remove logout route * test(web): isolate logout hook tests --------- Co-authored-by: GHX5T-SOL <200635707+GHX5T-SOL@users.noreply.github.com> Co-authored-by: Tyler Dane <tyler@switchback.tech>
1 parent 7c8aea3 commit 3f68ac0

17 files changed

Lines changed: 304 additions & 186 deletions

File tree

docs/acceptance/auth.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ Logout should end the server session, but the local rollout gate should still re
274274
### Steps
275275

276276
1. Log in with email/password or Google.
277-
2. Use the app logout path.
277+
2. Use the `Z` shortcut or the command-palette logout action.
278278
3. Confirm you are redirected back into the app.
279279
4. Reload `/day`.
280280
5. Open the command palette.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { act, renderHook } from "@testing-library/react";
2+
import {
3+
afterAll,
4+
beforeEach,
5+
describe,
6+
expect,
7+
it,
8+
mock,
9+
spyOn,
10+
} from "bun:test";
11+
12+
const clearAuthenticationState = mock();
13+
const setAuthenticated = mock();
14+
const signOut = mock();
15+
const mockUseSession = mock();
16+
17+
mock.module("@web/auth/compass/session/useSession", () => ({
18+
useSession: mockUseSession,
19+
}));
20+
21+
mock.module("@web/auth/compass/state/auth.state.util", () => ({
22+
clearAuthenticationState,
23+
}));
24+
25+
mock.module("@web/common/classes/Session", () => ({
26+
session: {
27+
signOut,
28+
},
29+
}));
30+
31+
const { useLogout } = await import("./useLogout");
32+
33+
describe("useLogout", () => {
34+
beforeEach(() => {
35+
clearAuthenticationState.mockClear();
36+
setAuthenticated.mockClear();
37+
signOut.mockReset();
38+
mockUseSession.mockReset();
39+
mockUseSession.mockReturnValue({
40+
authenticated: true,
41+
setAuthenticated,
42+
});
43+
signOut.mockResolvedValue(undefined);
44+
});
45+
46+
it("signs out and clears authenticated state immediately", () => {
47+
const { result } = renderHook(() => useLogout());
48+
49+
act(() => {
50+
result.current();
51+
});
52+
53+
expect(signOut).toHaveBeenCalledTimes(1);
54+
expect(clearAuthenticationState).toHaveBeenCalledTimes(1);
55+
expect(setAuthenticated).toHaveBeenCalledWith(false);
56+
});
57+
58+
it("does not wait for backend sign-out before clearing local state", () => {
59+
signOut.mockReturnValue(new Promise(() => undefined));
60+
const { result } = renderHook(() => useLogout());
61+
62+
act(() => {
63+
result.current();
64+
});
65+
66+
expect(clearAuthenticationState).toHaveBeenCalledTimes(1);
67+
expect(setAuthenticated).toHaveBeenCalledWith(false);
68+
});
69+
70+
it("logs backend sign-out failures after local logout completes", async () => {
71+
const consoleWarn = spyOn(console, "warn").mockImplementation(() => {});
72+
const error = new Error("network");
73+
signOut.mockRejectedValue(error);
74+
const { result } = renderHook(() => useLogout());
75+
76+
act(() => {
77+
result.current();
78+
});
79+
80+
await Promise.resolve();
81+
82+
expect(clearAuthenticationState).toHaveBeenCalledTimes(1);
83+
expect(setAuthenticated).toHaveBeenCalledWith(false);
84+
expect(consoleWarn).toHaveBeenCalledWith(
85+
"Failed to complete backend sign-out:",
86+
error,
87+
);
88+
89+
consoleWarn.mockRestore();
90+
});
91+
});
92+
93+
afterAll(() => {
94+
mock.restore();
95+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useCallback } from "react";
2+
import { useSession } from "@web/auth/compass/session/useSession";
3+
import { clearAuthenticationState } from "@web/auth/compass/state/auth.state.util";
4+
import { session } from "@web/common/classes/Session";
5+
6+
export function useLogout() {
7+
const { setAuthenticated } = useSession();
8+
9+
return useCallback(() => {
10+
void session.signOut().catch((error) => {
11+
console.warn("Failed to complete backend sign-out:", error);
12+
});
13+
14+
clearAuthenticationState();
15+
setAuthenticated(false);
16+
}, [setAuthenticated]);
17+
}

packages/web/src/common/constants/routes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
export const ROOT_ROUTES = {
22
API: "/api",
3-
LOGOUT: "/logout",
43
CLEANUP: "/cleanup",
54
GOOGLE_AUTH_CALLBACK: "/auth/google/callback",
65
ROOT: "/",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { renderHook } from "@testing-library/react";
2+
import { act, type MouseEvent } from "react";
3+
import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test";
4+
5+
const clearAuthenticationState = mock();
6+
const signOut = mock();
7+
const mockUseSession = mock();
8+
9+
mock.module("@web/auth/compass/session/useSession", () => ({
10+
useSession: mockUseSession,
11+
}));
12+
13+
mock.module("@web/auth/compass/state/auth.state.util", () => ({
14+
clearAuthenticationState,
15+
}));
16+
17+
mock.module("@web/common/classes/Session", () => ({
18+
session: {
19+
signOut,
20+
},
21+
}));
22+
23+
const { useLogoutCmdItems } = await import("./useLogoutCmdItems");
24+
25+
describe("useLogoutCmdItems", () => {
26+
beforeEach(() => {
27+
clearAuthenticationState.mockClear();
28+
signOut.mockReset();
29+
mockUseSession.mockReset();
30+
mockUseSession.mockReturnValue({
31+
authenticated: true,
32+
setAuthenticated: mock(),
33+
});
34+
signOut.mockResolvedValue(undefined);
35+
});
36+
37+
it("returns no items when logged out", () => {
38+
mockUseSession.mockReturnValue({
39+
authenticated: false,
40+
setAuthenticated: mock(),
41+
});
42+
43+
const { result } = renderHook(() => useLogoutCmdItems());
44+
45+
expect(result.current).toEqual([]);
46+
});
47+
48+
it("logs out from the command palette item", () => {
49+
const { result } = renderHook(() => useLogoutCmdItems());
50+
const logoutItem = result.current[0];
51+
52+
act(() => {
53+
logoutItem.onClick?.({} as MouseEvent<HTMLButtonElement>);
54+
});
55+
56+
expect(signOut).toHaveBeenCalledTimes(1);
57+
expect(clearAuthenticationState).toHaveBeenCalledTimes(1);
58+
});
59+
});
60+
61+
afterAll(() => {
62+
mock.restore();
63+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { type JsonStructureItem } from "react-cmdk";
2+
import { useLogout } from "@web/auth/compass/hooks/useLogout";
3+
import { useSession } from "@web/auth/compass/session/useSession";
4+
5+
export const useLogoutCmdItems = (): JsonStructureItem[] => {
6+
const { authenticated } = useSession();
7+
const logout = useLogout();
8+
9+
if (!authenticated) {
10+
return [];
11+
}
12+
13+
return [
14+
{
15+
id: "log-out",
16+
children: "Log Out [z]",
17+
icon: "ArrowRightOnRectangleIcon",
18+
onClick: logout,
19+
},
20+
];
21+
};

packages/web/src/routers/index.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,6 @@ export const router = createBrowserRouter(
7070
},
7171
],
7272
},
73-
{
74-
path: ROOT_ROUTES.LOGOUT,
75-
lazy: async () =>
76-
import(/* webpackChunkName: "logout" */ "@web/views/Logout").then(
77-
(module) => ({
78-
Component: module.LogoutView,
79-
}),
80-
),
81-
},
8273
{
8374
path: ROOT_ROUTES.WEEK,
8475
lazy: async () =>

packages/web/src/routers/loaders.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,6 @@ export async function loadAuthenticated() {
2323
return { authenticated };
2424
}
2525

26-
export async function loadLogoutData() {
27-
const { authenticated } = await loadAuthenticated();
28-
29-
return { authenticated };
30-
}
31-
3226
export function loadTodayData(): DayLoaderData {
3327
const dateInView = dayjs();
3428
const dateFormat = dayjs.DateFormat.YEAR_MONTH_DAY_FORMAT;

packages/web/src/views/CmdPalette/CmdPalette.tsx

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Categories_Event } from "@core/types/event.types";
99
import { moreCommandPaletteItems } from "@web/common/constants/more.cmd.constants";
1010
import { useAuthCmdItems } from "@web/common/hooks/useAuthCmdItems";
1111
import { useGoogleCmdItems } from "@web/common/hooks/useGoogleCmdItems";
12-
import { pressKey } from "@web/common/utils/dom/event-emitter.util";
12+
import { useLogoutCmdItems } from "@web/common/hooks/useLogoutCmdItems";
1313
import { onEventTargetVisibility } from "@web/common/utils/dom/event-target-visibility.util";
1414
import {
1515
createAlldayDraft,
@@ -46,6 +46,7 @@ const CmdPalette = ({
4646
const [search, setSearch] = useState("");
4747
const authCmdItems = useAuthCmdItems();
4848
const googleCmdItems = useGoogleCmdItems();
49+
const logoutCmdItems = useLogoutCmdItems();
4950

5051
const handleCreateSomedayDraft = async (
5152
category: Categories_Event.SOMEDAY_WEEK | Categories_Event.SOMEDAY_MONTH,
@@ -137,16 +138,7 @@ const CmdPalette = ({
137138
{
138139
heading: "Settings",
139140
id: "settings",
140-
items: [
141-
...googleCmdItems,
142-
...authCmdItems,
143-
{
144-
id: "log-out",
145-
children: "Log Out [z]",
146-
icon: "ArrowRightOnRectangleIcon",
147-
onClick: () => pressKey("z"),
148-
},
149-
],
141+
items: [...googleCmdItems, ...authCmdItems, ...logoutCmdItems],
150142
},
151143
...moreCommandPaletteItems,
152144
],

packages/web/src/views/Day/components/DayCmdPalette.tsx

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { moreCommandPaletteItems } from "@web/common/constants/more.cmd.constant
66
import { VIEW_SHORTCUTS } from "@web/common/constants/shortcuts.constants";
77
import { useAuthCmdItems } from "@web/common/hooks/useAuthCmdItems";
88
import { useGoogleCmdItems } from "@web/common/hooks/useGoogleCmdItems";
9+
import { useLogoutCmdItems } from "@web/common/hooks/useLogoutCmdItems";
910
import { pressKey } from "@web/common/utils/dom/event-emitter.util";
1011
import {
1112
openEventFormCreateEvent,
@@ -30,6 +31,7 @@ export const DayCmdPalette = ({ onGoToToday }: DayCmdPaletteProps) => {
3031
const today = dayjs();
3132
const authCmdItems = useAuthCmdItems();
3233
const googleCmdItems = useGoogleCmdItems();
34+
const logoutCmdItems = useLogoutCmdItems();
3335

3436
const filteredItems = filterItems(
3537
[
@@ -74,16 +76,7 @@ export const DayCmdPalette = ({ onGoToToday }: DayCmdPaletteProps) => {
7476
{
7577
heading: "Settings",
7678
id: "settings",
77-
items: [
78-
...googleCmdItems,
79-
...authCmdItems,
80-
{
81-
id: "log-out",
82-
children: "Log Out [z]",
83-
icon: "ArrowRightOnRectangleIcon",
84-
onClick: () => pressKey("z"),
85-
},
86-
],
79+
items: [...googleCmdItems, ...authCmdItems, ...logoutCmdItems],
8780
},
8881
...moreCommandPaletteItems,
8982
],

0 commit comments

Comments
 (0)