diff --git a/functions/db/deck-access/get-deck-access-by-share-id-db.ts b/functions/db/deck-access/get-deck-access-by-share-id-db.ts index d442eb14..38e60cd8 100644 --- a/functions/db/deck-access/get-deck-access-by-share-id-db.ts +++ b/functions/db/deck-access/get-deck-access-by-share-id-db.ts @@ -9,21 +9,25 @@ const resultSchema = z.object({ used_by: z.number().nullable(), }); +type GetDeckAccessByShareIdDbResultType = z.infer; + export const getDeckAccessByShareIdDb = async ( envSafe: EnvSafe, shareId: string, -) => { +): Promise => { const db = getDatabase(envSafe); const oneTimeShareLinkResult = await db .from("deck_access") .select("deck_id, author_id, used_by") .eq("share_id", shareId) - .single(); + .maybeSingle(); if (oneTimeShareLinkResult.error) { throw new DatabaseException(oneTimeShareLinkResult.error); } - return resultSchema.parse(oneTimeShareLinkResult.data); + return oneTimeShareLinkResult.data + ? resultSchema.parse(oneTimeShareLinkResult.data) + : null; }; diff --git a/functions/db/deck/get-deck-with-cards-by-id-db.ts b/functions/db/deck/get-deck-with-cards-by-id-db.ts index 5df99575..958478a1 100644 --- a/functions/db/deck/get-deck-with-cards-by-id-db.ts +++ b/functions/db/deck/get-deck-with-cards-by-id-db.ts @@ -1,9 +1,15 @@ import { DatabaseException } from "../database-exception.ts"; -import { deckWithCardsSchema } from "./decks-with-cards-schema.ts"; +import { + DeckWithCardsDbType, + deckWithCardsSchema, +} from "./decks-with-cards-schema.ts"; import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; -export const getDeckWithCardsById = async (env: EnvSafe, deckId: number) => { +export const getDeckWithCardsById = async ( + env: EnvSafe, + deckId: number, +): Promise => { const db = getDatabase(env); const stableShareLinkResult = await db diff --git a/functions/get-shared-deck.test.ts b/functions/get-shared-deck.test.ts new file mode 100644 index 00000000..36363b80 --- /dev/null +++ b/functions/get-shared-deck.test.ts @@ -0,0 +1,139 @@ +import { afterEach, describe, expect, test, vi } from "vitest"; +import { onRequest as getSharedDeckRequest } from "./get-shared-deck.ts"; +import { DeckWithCardsDbType } from "./db/deck/decks-with-cards-schema.ts"; +import { createMockRequest } from "./lib/cloudflare/create-mock-request.ts"; + +vi.mock("./services/get-user.ts", () => ({ + getUser: async () => ({ id: 1 }), +})); + +const getDeckAccessByShareIdMock = vi.hoisted(() => vi.fn()); +vi.mock("./db/deck-access/get-deck-access-by-share-id-db.ts", () => ({ + getDeckAccessByShareIdDb: async () => getDeckAccessByShareIdMock(), +})); + +const createJsonResponseMock = vi.hoisted(() => vi.fn()); +vi.mock("./lib/json-response/create-json-response.ts", () => ({ + createJsonResponse: async () => createJsonResponseMock(), +})); + +const getDeckWithCardsByIdMock = vi.hoisted(() => vi.fn()); +vi.mock("./db/deck/get-deck-with-cards-by-id-db.ts", () => ({ + getDeckWithCardsById: async () => getDeckWithCardsByIdMock(), +})); + +const getDeckWithCardsByShareIdDbMock = vi.hoisted(() => vi.fn()); +vi.mock("./db/deck/get-deck-with-cards-by-share-id-db.ts", () => ({ + getDeckWithCardsByShareIdDb: async () => getDeckWithCardsByShareIdDbMock(), +})); + +const createBadRequestResponseMock = vi.hoisted(() => vi.fn()); +vi.mock("./lib/json-response/create-bad-request-response.ts", () => ({ + createBadRequestResponse: async () => createBadRequestResponseMock(), +})); + +const startUsingDeckAccessDbMock = vi.hoisted(() => vi.fn()); +vi.mock("./db/deck-access/start-using-deck-access-db.ts", () => ({ + startUsingDeckAccessDb: async () => startUsingDeckAccessDbMock(), +})); + +const mockDeckOfUser1: DeckWithCardsDbType = { + id: 1, + name: "name", + description: "description", + share_id: "share_id", + author_id: 1, + created_at: new Date().toISOString(), + deck_category: null, + deck_card: [], + is_public: false, + speak_field: null, + available_in: null, + category_id: null, + speak_locale: null, +}; + +const mockDeckOfUser2: DeckWithCardsDbType = { + id: 2, + name: "name", + description: "description", + share_id: "share_id", + author_id: 2, + created_at: new Date().toISOString(), + deck_category: null, + deck_card: [], + is_public: false, + speak_field: null, + available_in: null, + category_id: null, + speak_locale: null, +}; + +describe("get shared deck", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + test("deck author equals deck", async () => { + getDeckAccessByShareIdMock.mockReturnValue({ + author_id: 1, + used_by: null, + deck_id: 1, + }); + getDeckWithCardsByIdMock.mockReturnValue(mockDeckOfUser1); + + await getSharedDeckRequest( + createMockRequest(`https://example.com?share_id=SHARE_ID1`), + ); + + expect(getDeckWithCardsByIdMock).toBeCalled(); + expect(getDeckWithCardsByShareIdDbMock).not.toBeCalled(); + expect(createBadRequestResponseMock).not.toBeCalled(); + expect(startUsingDeckAccessDbMock).not.toBeCalled(); + }); + + test("no one time access found", async () => { + getDeckAccessByShareIdMock.mockReturnValue(null); + getDeckWithCardsByShareIdDbMock.mockReturnValue(mockDeckOfUser1); + + await getSharedDeckRequest( + createMockRequest(`https://example.com?share_id=SHARE_ID1`), + ); + + expect(getDeckWithCardsByIdMock).not.toBeCalled(); + expect(getDeckWithCardsByShareIdDbMock).toBeCalled(); + expect(createBadRequestResponseMock).not.toBeCalled(); + expect(startUsingDeckAccessDbMock).not.toBeCalled(); + }); + + test("one time access found, but already used by another user", async () => { + getDeckAccessByShareIdMock.mockReturnValue({ + author_id: 2, + used_by: 2, + deck_id: 2, + }); + getDeckWithCardsByIdMock.mockReturnValue(mockDeckOfUser2); + + await getSharedDeckRequest( + createMockRequest(`https://example.com?share_id=SHARE_ID1`), + ); + + expect(createBadRequestResponseMock).toBeCalled(); + }); + + test("one time access found and it is not used", async () => { + getDeckAccessByShareIdMock.mockReturnValue({ + author_id: 2, + used_by: null, + deck_id: 2, + }); + getDeckWithCardsByIdMock.mockReturnValue(mockDeckOfUser2); + + await getSharedDeckRequest( + createMockRequest(`https://example.com?share_id=SHARE_ID1`), + ); + + expect(getDeckWithCardsByIdMock).toBeCalled(); + expect(startUsingDeckAccessDbMock).toBeCalled(); + }); +}); diff --git a/functions/lib/cloudflare/create-mock-request.ts b/functions/lib/cloudflare/create-mock-request.ts new file mode 100644 index 00000000..ed9b7e55 --- /dev/null +++ b/functions/lib/cloudflare/create-mock-request.ts @@ -0,0 +1,13 @@ +export const createMockRequest = (url: string) => { + const mockEnv = { + BOT_TOKEN: "BOT_TOKEN", + SUPABASE_KEY: "SUPABASE_KEY", + SUPABASE_URL: "SUPABASE_URL", + VITE_BOT_APP_URL: "VITE_BOT_APP_URL", + }; + + return { + request: { url: url }, + env: mockEnv, + } as any; +};