From 5f08e9c1f4189e12ee8a32234d8f81ae55ec8c1d Mon Sep 17 00:00:00 2001 From: mina Date: Tue, 10 Nov 2026 22:06:55 +0100 Subject: [PATCH 1/2] test: add authentication backend tests --- backend/tests/auth.test.ts | 313 +++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 backend/tests/auth.test.ts diff --git a/backend/tests/auth.test.ts b/backend/tests/auth.test.ts new file mode 100644 index 0000000..26ccd4b --- /dev/null +++ b/backend/tests/auth.test.ts @@ -0,0 +1,313 @@ +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { ApolloServer } from "@apollo/server"; +import { MongoClient } from "mongodb"; +import { randomUUID } from "node:crypto"; +import { typeDefs } from "../src/graphql/typeDefs.js"; +import { createResolvers } from "../src/graphql/resolvers/index.js"; + +process.env.JWT_SECRET = "test-secret"; + +type TestContext = { + userId: string | null; +}; + +const client = new MongoClient( + process.env.TEST_MONGODB_URI ?? "mongodb://127.0.0.1:27018", + { + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }, +); + +const database = client.db(`artex_auth_test_${randomUUID()}`); + +const server = new ApolloServer({ + typeDefs, + resolvers: createResolvers(database), +}); + +beforeAll(async () => { + await client.connect(); + await server.start(); +}, 15000); + +beforeEach(async () => { + // Start every auth test with an empty users collection. + await database.collection("users").deleteMany({}); +}); + +afterAll(async () => { + try { + await server.stop(); + } finally { + try { + await database.dropDatabase(); + } finally { + await client.close(); + } + } +}, 15000); + +const registerMutation = ` + mutation Register($username: String!, $password: String!) { + register(username: $username, password: $password) { + id + username + } + } +`; + +describe("Authentication API", () => { + it("registers a new user", async () => { + const response = await server.executeOperation( + { + query: registerMutation, + variables: { + username: "TestUser", + password: "Password123!", + }, + }, + { + contextValue: { userId: null }, + }, + ); + + if (response.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(response.body.singleResult.errors).toBeUndefined(); + + expect(response.body.singleResult.data?.register).toEqual({ + id: expect.any(String), + username: "testuser", + }); + + // Verify that registration also persists the user in MongoDB. + const storedUser = await database.collection("users").findOne({ + username: "testuser", + }); + + expect(storedUser).not.toBeNull(); + expect(storedUser?.username).toBe("testuser"); + expect(typeof storedUser?.passwordHash).toBe("string"); + expect(storedUser?.passwordHash).not.toBe("Password123!"); + }); + + it("rejects duplicate usernames", async () => { + const firstResponse = await server.executeOperation( + { + query: registerMutation, + variables: { + username: "TestUser", + password: "Password123!", + }, + }, + { + contextValue: { userId: null }, + }, + ); + + if (firstResponse.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(firstResponse.body.singleResult.errors).toBeUndefined(); + + const secondResponse = await server.executeOperation( + { + query: registerMutation, + variables: { + username: "testuser", + password: "AnotherPassword123!", + }, + }, + { + contextValue: { userId: null }, + }, + ); + + if (secondResponse.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(secondResponse.body.singleResult.errors).toEqual([ + expect.objectContaining({ + message: "Username is already taken", + }), + ]); + + // Only the first registration should exist. + expect(await database.collection("users").countDocuments()).toBe(1); + }); + + it("logs in with valid credentials", async () => { + const registerResponse = await server.executeOperation( + { + query: registerMutation, + variables: { + username: "TestUser", + password: "Password123!", + }, + }, + { + contextValue: { userId: null }, + }, + ); + + if (registerResponse.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(registerResponse.body.singleResult.errors).toBeUndefined(); + + const loginMutation = ` + mutation Login($username: String!, $password: String!) { + login(username: $username, password: $password) { + user { + id + username + } + token + } + } + `; + + const loginResponse = await server.executeOperation( + { + query: loginMutation, + variables: { + username: "TESTUSER", + password: "Password123!", + }, + }, + { + contextValue: { userId: null }, + }, + ); + + if (loginResponse.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(loginResponse.body.singleResult.errors).toBeUndefined(); + + const login = loginResponse.body.singleResult.data?.login as + | { + user: { + id: string; + username: string; + }; + token: string; + } + | undefined; + + expect(login).toEqual({ + user: { + id: expect.any(String), + username: "testuser", + }, + token: expect.any(String), + }); + + expect(login?.token.length).toBeGreaterThan(0); + }); + + it("rejects login with invalid credentials", async () => { + const registerResponse = await server.executeOperation( + { + query: registerMutation, + variables: { + username: "TestUser", + password: "Password123!", + }, + }, + { + contextValue: { userId: null }, + }, + ); + + if (registerResponse.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(registerResponse.body.singleResult.errors).toBeUndefined(); + + const loginMutation = ` + mutation Login($username: String!, $password: String!) { + login(username: $username, password: $password) { + user { + id + username + } + token + } + } + `; + + const loginResponse = await server.executeOperation( + { + query: loginMutation, + variables: { + username: "testuser", + password: "WrongPassword123!", + }, + }, + { + contextValue: { userId: null }, + }, + ); + + if (loginResponse.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(loginResponse.body.singleResult.errors).toEqual([ + expect.objectContaining({ + message: "Incorrect username or password", + }), + ]); + }); + + it.each([ + { + username: "ab", + password: "Password123!", + expectedError: "Username must be between 3 and 20 characters", + }, + { + username: "validuser", + password: "short", + expectedError: "Password must be between 8 and 100 characters", + }, + ])( + "rejects invalid registration input", + async ({ username, password, expectedError }) => { + const response = await server.executeOperation( + { + query: registerMutation, + variables: { + username, + password, + }, + }, + { + contextValue: { userId: null }, + }, + ); + + if (response.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(response.body.singleResult.errors).toEqual([ + expect.objectContaining({ + message: expectedError, + }), + ]); + + // Invalid registration attempts should not create users. + expect(await database.collection("users").countDocuments()).toBe(0); + }, + ); +}); From a4b73e9067b5ec3a6db02d6f2083ed4a13c87f84 Mon Sep 17 00:00:00 2001 From: mina Date: Tue, 10 Nov 2026 22:07:09 +0100 Subject: [PATCH 2/2] test: add gallery creation backend tests --- backend/tests/galleryCreation.test.ts | 324 ++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 backend/tests/galleryCreation.test.ts diff --git a/backend/tests/galleryCreation.test.ts b/backend/tests/galleryCreation.test.ts new file mode 100644 index 0000000..898bd91 --- /dev/null +++ b/backend/tests/galleryCreation.test.ts @@ -0,0 +1,324 @@ +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { ApolloServer } from "@apollo/server"; +import { MongoClient, ObjectId } from "mongodb"; +import { randomUUID } from "node:crypto"; +import { typeDefs } from "../src/graphql/typeDefs.js"; +import { createResolvers } from "../src/graphql/resolvers/index.js"; + +type TestContext = { + userId: string | null; +}; + +const client = new MongoClient( + process.env.TEST_MONGODB_URI ?? "mongodb://127.0.0.1:27018", + { + serverSelectionTimeoutMS: 5000, + connectTimeoutMS: 5000, + }, +); + +const database = client.db(`artex_gallery_create_${randomUUID()}`); + +const ownerId = new ObjectId(); +const otherUserId = new ObjectId(); +const galleryId = new ObjectId(); +const otherGalleryId = new ObjectId(); + +const server = new ApolloServer({ + typeDefs, + resolvers: createResolvers(database), +}); + +beforeAll(async () => { + await client.connect(); + await server.start(); +}, 15000); + +beforeEach(async () => { + await database.collection("galleries").deleteMany({}); + await database.collection("met_artworks").deleteMany({}); + + // Seed artworks that can be added to galleries. + await database.collection("met_artworks").insertMany([ + { + id: 10, + title: "First artwork", + }, + { + id: 20, + title: "Second artwork", + }, + ]); + + // Seed galleries used by the add-artwork tests. + await database.collection("galleries").insertMany([ + { + _id: galleryId, + userId: ownerId, + title: "Owner gallery", + artworkIds: [], + }, + { + _id: otherGalleryId, + userId: otherUserId, + title: "Other user's gallery", + artworkIds: [], + }, + ]); +}, 15000); + +afterAll(async () => { + try { + await server.stop(); + } finally { + try { + await database.dropDatabase(); + } finally { + await client.close(); + } + } +}, 15000); + +const createGalleryMutation = ` + mutation CreateGallery($title: String!) { + createGallery(title: $title) { + id + userId + title + artworkIds + } + } +`; + +const addArtworkMutation = ` + mutation AddArtworkToGallery($galleryId: ID!, $artworkId: Int!) { + addArtworkToGallery( + galleryId: $galleryId + artworkId: $artworkId + ) { + id + userId + title + artworkIds + } + } +`; + +describe("Gallery creation API", () => { + it("creates a gallery for an authenticated user", async () => { + const response = await server.executeOperation( + { + query: createGalleryMutation, + variables: { + title: " My new gallery ", + }, + }, + { + contextValue: { + userId: ownerId.toString(), + }, + }, + ); + + if (response.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(response.body.singleResult.errors).toBeUndefined(); + + const createdGallery = response.body.singleResult.data?.createGallery as + | { + id: string; + userId: string; + title: string; + artworkIds: number[]; + } + | undefined; + + expect(createdGallery).toEqual({ + id: expect.any(String), + userId: ownerId.toString(), + title: "My new gallery", + artworkIds: [], + }); + + // Verify that the gallery is also persisted correctly in MongoDB. + const storedGallery = await database.collection("galleries").findOne({ + _id: new ObjectId(createdGallery!.id), + }); + + expect(storedGallery).not.toBeNull(); + expect(storedGallery?.userId).toEqual(ownerId); + expect(storedGallery?.title).toBe("My new gallery"); + expect(storedGallery?.artworkIds).toEqual([]); + expect(storedGallery?.createdAt).toBeInstanceOf(Date); + }); + + it("requires authentication to create a gallery", async () => { + const response = await server.executeOperation( + { + query: createGalleryMutation, + variables: { + title: "My gallery", + }, + }, + { + contextValue: { + userId: null, + }, + }, + ); + + if (response.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(response.body.singleResult.errors).toEqual([ + expect.objectContaining({ + message: "You must be logged in to create a gallery", + }), + ]); + + // Only the two seeded galleries should still exist. + expect(await database.collection("galleries").countDocuments()).toBe(2); + }); + + it.each(["", " "])("rejects an empty gallery title: %j", async (title) => { + const response = await server.executeOperation( + { + query: createGalleryMutation, + variables: { + title, + }, + }, + { + contextValue: { + userId: ownerId.toString(), + }, + }, + ); + + if (response.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(response.body.singleResult.errors).toEqual([ + expect.objectContaining({ + message: "Gallery title cannot be empty", + }), + ]); + + // Invalid gallery creation should not persist anything new. + expect(await database.collection("galleries").countDocuments()).toBe(2); + }); + + it("adds an artwork to the user's gallery", async () => { + const response = await server.executeOperation( + { + query: addArtworkMutation, + variables: { + galleryId: galleryId.toString(), + artworkId: 10, + }, + }, + { + contextValue: { + userId: ownerId.toString(), + }, + }, + ); + + if (response.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(response.body.singleResult.errors).toBeUndefined(); + + expect(response.body.singleResult.data?.addArtworkToGallery).toEqual({ + id: galleryId.toString(), + userId: ownerId.toString(), + title: "Owner gallery", + artworkIds: [10], + }); + + // Verify that the new artwork ID was persisted. + const storedGallery = await database.collection("galleries").findOne({ + _id: galleryId, + }); + + expect(storedGallery?.artworkIds).toEqual([10]); + }); + + it("prevents another user from adding artwork to the gallery", async () => { + const response = await server.executeOperation( + { + query: addArtworkMutation, + variables: { + galleryId: galleryId.toString(), + artworkId: 10, + }, + }, + { + contextValue: { + userId: otherUserId.toString(), + }, + }, + ); + + if (response.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(response.body.singleResult.errors).toEqual([ + expect.objectContaining({ + message: "Gallery not found", + }), + ]); + + // The owner's gallery must remain unchanged. + const storedGallery = await database.collection("galleries").findOne({ + _id: galleryId, + }); + + expect(storedGallery?.artworkIds).toEqual([]); + }); + + it("does not add the same artwork more than once", async () => { + for (let attempt = 0; attempt < 2; attempt++) { + const response = await server.executeOperation( + { + query: addArtworkMutation, + variables: { + galleryId: galleryId.toString(), + artworkId: 10, + }, + }, + { + contextValue: { + userId: ownerId.toString(), + }, + }, + ); + + if (response.body.kind !== "single") { + throw new Error("Expected a single GraphQL response"); + } + + expect(response.body.singleResult.errors).toBeUndefined(); + + expect(response.body.singleResult.data?.addArtworkToGallery).toEqual({ + id: galleryId.toString(), + userId: ownerId.toString(), + title: "Owner gallery", + artworkIds: [10], + }); + } + + // $addToSet should ensure that duplicate artwork IDs are not stored. + const storedGallery = await database.collection("galleries").findOne({ + _id: galleryId, + }); + + expect(storedGallery?.artworkIds).toEqual([10]); + }); +});