-
Notifications
You must be signed in to change notification settings - Fork 50
feat(auth): implement session user synchronization and related tests #1469
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { useContext } from "react"; | ||
| import { fireEvent, render, screen, waitFor } from "@testing-library/react"; | ||
| import { syncLocalTaskUsersToAuthenticatedUser } from "@web/common/utils/sync/local-task-user-sync.util"; | ||
| import { SessionContext, SessionProvider } from "./SessionProvider"; | ||
|
|
||
| jest.mock("@web/common/utils/sync/local-task-user-sync.util"); | ||
|
|
||
| function SessionTestHarness() { | ||
| const { setAuthenticated } = useContext(SessionContext); | ||
|
|
||
| return ( | ||
| <div> | ||
| <button onClick={() => setAuthenticated(true)} type="button"> | ||
| Set Authenticated | ||
| </button> | ||
| <button onClick={() => setAuthenticated(false)} type="button"> | ||
| Set Unauthenticated | ||
| </button> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| describe("SessionProvider", () => { | ||
| const mockSyncLocalTaskUsersToAuthenticatedUser = | ||
| syncLocalTaskUsersToAuthenticatedUser as jest.MockedFunction< | ||
| typeof syncLocalTaskUsersToAuthenticatedUser | ||
| >; | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| mockSyncLocalTaskUsersToAuthenticatedUser.mockResolvedValue(0); | ||
| }); | ||
|
|
||
| it("does not sync task users when authenticated is false", () => { | ||
| render( | ||
| <SessionProvider> | ||
| <SessionTestHarness /> | ||
| </SessionProvider>, | ||
| ); | ||
|
|
||
| expect(mockSyncLocalTaskUsersToAuthenticatedUser).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("syncs task users when authenticated becomes true", async () => { | ||
| render( | ||
| <SessionProvider> | ||
| <SessionTestHarness /> | ||
| </SessionProvider>, | ||
| ); | ||
|
|
||
| fireEvent.click(screen.getByRole("button", { name: "Set Authenticated" })); | ||
|
|
||
| await waitFor(() => { | ||
| expect(mockSyncLocalTaskUsersToAuthenticatedUser).toHaveBeenCalledTimes( | ||
| 1, | ||
| ); | ||
| }); | ||
|
|
||
| fireEvent.click( | ||
| screen.getByRole("button", { name: "Set Unauthenticated" }), | ||
| ); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,14 +2,18 @@ import { | |||||||||||||||||||||||||||||||||||||||||||||||
| createTestTask, | ||||||||||||||||||||||||||||||||||||||||||||||||
| createTestTasks, | ||||||||||||||||||||||||||||||||||||||||||||||||
| } from "@web/__tests__/utils/repositories/repository.test.factory"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { getUserId } from "@web/auth/auth.util"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { UNAUTHENTICATED_USER } from "@web/common/constants/auth.constants"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import * as storageAdapter from "@web/common/storage/adapter/adapter"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { LocalTaskRepository } from "./local.task.repository"; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Mock the storage adapter module | ||||||||||||||||||||||||||||||||||||||||||||||||
| jest.mock("@web/common/storage/adapter/adapter"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| jest.mock("@web/auth/auth.util"); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| describe("LocalTaskRepository", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| let repository: LocalTaskRepository; | ||||||||||||||||||||||||||||||||||||||||||||||||
| let mockGetUserId: jest.MockedFunction<typeof getUserId>; | ||||||||||||||||||||||||||||||||||||||||||||||||
| let mockAdapter: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| getTasks: jest.Mock; | ||||||||||||||||||||||||||||||||||||||||||||||||
| putTasks: jest.Mock; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -19,6 +23,7 @@ describe("LocalTaskRepository", () => { | |||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| beforeEach(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| mockGetUserId = getUserId as jest.MockedFunction<typeof getUserId>; | ||||||||||||||||||||||||||||||||||||||||||||||||
| mockAdapter = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| getTasks: jest.fn().mockResolvedValue([]), | ||||||||||||||||||||||||||||||||||||||||||||||||
| putTasks: jest.fn().mockResolvedValue(undefined), | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -33,6 +38,7 @@ describe("LocalTaskRepository", () => { | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| repository = new LocalTaskRepository(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| jest.clearAllMocks(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| mockGetUserId.mockResolvedValue(UNAUTHENTICATED_USER); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Re-mock after clearing | ||||||||||||||||||||||||||||||||||||||||||||||||
| (storageAdapter.getStorageAdapter as jest.Mock).mockReturnValue( | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -108,6 +114,70 @@ describe("LocalTaskRepository", () => { | |||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockAdapter.putTask).toHaveBeenCalledTimes(1); | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockAdapter.putTasks).not.toHaveBeenCalled(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| it("rewrites placeholder task users when authenticated", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const dateKey = "2024-01-01"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| mockGetUserId.mockResolvedValue("mongo-user-id"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const tasks = [ | ||||||||||||||||||||||||||||||||||||||||||||||||
| createTestTask({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| _id: "task-1", | ||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Task One", | ||||||||||||||||||||||||||||||||||||||||||||||||
| user: UNAUTHENTICATED_USER, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| await repository.save(dateKey, tasks); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockAdapter.putTasks).toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| dateKey, | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect.arrayContaining([ | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect.objectContaining({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| _id: "task-1", | ||||||||||||||||||||||||||||||||||||||||||||||||
| user: "mongo-user-id", | ||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ]), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| it("does not rewrite non-placeholder task users", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const dateKey = "2024-01-01"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| mockGetUserId.mockResolvedValue("mongo-user-id"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const task = createTestTask({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| _id: "task-1", | ||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Task One", | ||||||||||||||||||||||||||||||||||||||||||||||||
| user: "already-authenticated-user", | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| await repository.save(dateKey, task); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockAdapter.putTask).toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| dateKey, | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect.objectContaining({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| _id: "task-1", | ||||||||||||||||||||||||||||||||||||||||||||||||
| user: "already-authenticated-user", | ||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| it("keeps placeholder task users when unauthenticated", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const dateKey = "2024-01-01"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| mockGetUserId.mockResolvedValue(UNAUTHENTICATED_USER); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const task = createTestTask({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| _id: "task-1", | ||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Task One", | ||||||||||||||||||||||||||||||||||||||||||||||||
| user: UNAUTHENTICATED_USER, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| await repository.save(dateKey, task); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockAdapter.putTask).toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||
| dateKey, | ||||||||||||||||||||||||||||||||||||||||||||||||
| expect.objectContaining({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| _id: "task-1", | ||||||||||||||||||||||||||||||||||||||||||||||||
| user: UNAUTHENTICATED_USER, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
| }); | |
| }); | |
| it("keeps original task user when getUserId fails", async () => { | |
| const dateKey = "2024-01-01"; | |
| const originalUser = UNAUTHENTICATED_USER; | |
| mockGetUserId.mockRejectedValue(new Error("getUserId failed")); | |
| const task = createTestTask({ | |
| _id: "task-1", | |
| title: "Task One", | |
| user: originalUser, | |
| }); | |
| await repository.save(dateKey, task); | |
| expect(mockAdapter.putTask).toHaveBeenCalledWith( | |
| dateKey, | |
| expect.objectContaining({ | |
| _id: "task-1", | |
| user: originalUser, | |
| }), | |
| ); | |
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import { createTestTask } from "@web/__tests__/utils/repositories/repository.test.factory"; | ||
| import { prepareEmptyStorageForTests } from "@web/__tests__/utils/storage/indexeddb.test.util"; | ||
| import { getUserId } from "@web/auth/auth.util"; | ||
| import { UNAUTHENTICATED_USER } from "@web/common/constants/auth.constants"; | ||
| import { | ||
| ensureStorageReady, | ||
| getStorageAdapter, | ||
| resetStorage, | ||
| } from "@web/common/storage/adapter/adapter"; | ||
| import { syncLocalTaskUsersToAuthenticatedUser } from "./local-task-user-sync.util"; | ||
|
|
||
| jest.mock("@web/auth/auth.util"); | ||
|
|
||
| describe("syncLocalTaskUsersToAuthenticatedUser", () => { | ||
| const mockGetUserId = getUserId as jest.MockedFunction<typeof getUserId>; | ||
|
|
||
| beforeEach(async () => { | ||
| jest.clearAllMocks(); | ||
| resetStorage(); | ||
| await prepareEmptyStorageForTests(); | ||
| await ensureStorageReady(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| resetStorage(); | ||
| }); | ||
|
|
||
| it("rewrites unauthenticated users across multiple dates", async () => { | ||
| mockGetUserId.mockResolvedValue("mongo-user-id"); | ||
| const adapter = getStorageAdapter(); | ||
|
|
||
| await adapter.putTasks("2024-01-01", [ | ||
| createTestTask({ _id: "task-1", user: UNAUTHENTICATED_USER }), | ||
| createTestTask({ _id: "task-2", user: "already-authenticated-user" }), | ||
| ]); | ||
| await adapter.putTasks("2024-01-02", [ | ||
| createTestTask({ _id: "task-3", user: UNAUTHENTICATED_USER }), | ||
| ]); | ||
|
|
||
| const syncedCount = await syncLocalTaskUsersToAuthenticatedUser(); | ||
|
|
||
| expect(syncedCount).toBe(2); | ||
| await expect(adapter.getTasks("2024-01-01")).resolves.toEqual( | ||
| expect.arrayContaining([ | ||
| expect.objectContaining({ _id: "task-1", user: "mongo-user-id" }), | ||
| expect.objectContaining({ | ||
| _id: "task-2", | ||
| user: "already-authenticated-user", | ||
| }), | ||
| ]), | ||
| ); | ||
| await expect(adapter.getTasks("2024-01-02")).resolves.toEqual( | ||
| expect.arrayContaining([ | ||
| expect.objectContaining({ _id: "task-3", user: "mongo-user-id" }), | ||
| ]), | ||
| ); | ||
| }); | ||
|
|
||
| it("returns 0 when there are no tasks", async () => { | ||
| mockGetUserId.mockResolvedValue("mongo-user-id"); | ||
|
|
||
| await expect(syncLocalTaskUsersToAuthenticatedUser()).resolves.toBe(0); | ||
| }); | ||
|
|
||
| it("returns 0 and does not rewrite when user is unauthenticated", async () => { | ||
| mockGetUserId.mockResolvedValue(UNAUTHENTICATED_USER); | ||
| const adapter = getStorageAdapter(); | ||
| await adapter.putTasks("2024-01-01", [ | ||
| createTestTask({ _id: "task-1", user: UNAUTHENTICATED_USER }), | ||
| ]); | ||
|
|
||
| const syncedCount = await syncLocalTaskUsersToAuthenticatedUser(); | ||
|
|
||
| expect(syncedCount).toBe(0); | ||
| await expect(adapter.getTasks("2024-01-01")).resolves.toEqual( | ||
| expect.arrayContaining([ | ||
| expect.objectContaining({ _id: "task-1", user: UNAUTHENTICATED_USER }), | ||
| ]), | ||
| ); | ||
| }); | ||
|
|
||
| it("returns 0 when all tasks already have authenticated users", async () => { | ||
| mockGetUserId.mockResolvedValue("mongo-user-id"); | ||
| const adapter = getStorageAdapter(); | ||
| await adapter.putTasks("2024-01-01", [ | ||
| createTestTask({ _id: "task-1", user: "already-authenticated-user" }), | ||
| ]); | ||
|
|
||
| await expect(syncLocalTaskUsersToAuthenticatedUser()).resolves.toBe(0); | ||
| await expect(adapter.getTasks("2024-01-01")).resolves.toEqual( | ||
| expect.arrayContaining([ | ||
| expect.objectContaining({ | ||
| _id: "task-1", | ||
| user: "already-authenticated-user", | ||
| }), | ||
| ]), | ||
| ); | ||
| }); | ||
|
|
||
| it("throws when user id lookup fails", async () => { | ||
| mockGetUserId.mockRejectedValue(new Error("Failed to resolve user id")); | ||
|
|
||
| await expect(syncLocalTaskUsersToAuthenticatedUser()).rejects.toThrow( | ||
| "Failed to resolve user id", | ||
| ); | ||
| }); | ||
|
|
||
| it("throws when loading tasks fails", async () => { | ||
| mockGetUserId.mockResolvedValue("mongo-user-id"); | ||
| const adapter = getStorageAdapter(); | ||
| jest | ||
| .spyOn(adapter, "getAllTasks") | ||
| .mockRejectedValueOnce(new Error("Failed to load tasks")); | ||
|
|
||
| await expect(syncLocalTaskUsersToAuthenticatedUser()).rejects.toThrow( | ||
| "Failed to load tasks", | ||
| ); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing test coverage for error handling when syncLocalTaskUsersToAuthenticatedUser fails. While the SessionProvider correctly catches and logs errors from the sync function, there should be a test verifying that errors are properly handled and don't crash the component. This would ensure the error handling path is tested and maintainable.