Add logout functionality and Garmin sync structured logging
- Add POST /api/auth/logout endpoint with tests (5 tests) - Add logout button to settings page (5 tests) - Add structured logging to garmin-sync cron (sync start/complete/failure) - Update IMPLEMENTATION_PLAN.md with spec gap analysis findings - Total: 835 tests passing across 44 test files Closes spec gaps from authentication.md (logout) and observability.md (logging) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -296,7 +296,7 @@ describe("SettingsPage", () => {
|
||||
expect(cycleLengthInput).toBeDisabled();
|
||||
expect(screen.getByLabelText(/notification time/i)).toBeDisabled();
|
||||
expect(screen.getByLabelText(/timezone/i)).toBeDisabled();
|
||||
expect(screen.getByRole("button")).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: /saving/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
resolveSave(mockUser);
|
||||
@@ -525,4 +525,150 @@ describe("SettingsPage", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("logout", () => {
|
||||
it("renders a logout button", async () => {
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: /log out/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("calls POST /api/auth/logout when logout button clicked", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockUser),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
success: true,
|
||||
message: "Logged out successfully",
|
||||
redirectTo: "/login",
|
||||
}),
|
||||
});
|
||||
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: /log out/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const logoutButton = screen.getByRole("button", { name: /log out/i });
|
||||
fireEvent.click(logoutButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledWith("/api/auth/logout", {
|
||||
method: "POST",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("redirects to login page after logout", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockUser),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
success: true,
|
||||
message: "Logged out successfully",
|
||||
redirectTo: "/login",
|
||||
}),
|
||||
});
|
||||
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: /log out/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const logoutButton = screen.getByRole("button", { name: /log out/i });
|
||||
fireEvent.click(logoutButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPush).toHaveBeenCalledWith("/login");
|
||||
});
|
||||
});
|
||||
|
||||
it("shows loading state while logging out", async () => {
|
||||
let resolveLogout: (value: unknown) => void = () => {};
|
||||
const logoutPromise = new Promise((resolve) => {
|
||||
resolveLogout = resolve;
|
||||
});
|
||||
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockUser),
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
ok: true,
|
||||
json: () => logoutPromise,
|
||||
});
|
||||
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: /log out/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const logoutButton = screen.getByRole("button", { name: /log out/i });
|
||||
fireEvent.click(logoutButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: /logging out/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
resolveLogout({
|
||||
success: true,
|
||||
message: "Logged out successfully",
|
||||
redirectTo: "/login",
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error if logout fails", async () => {
|
||||
mockFetch
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockUser),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: () => Promise.resolve({ error: "Logout failed" }),
|
||||
});
|
||||
|
||||
render(<SettingsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: /log out/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const logoutButton = screen.getByRole("button", { name: /log out/i });
|
||||
fireEvent.click(logoutButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("alert")).toBeInTheDocument();
|
||||
expect(screen.getByText(/logout failed/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user