Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
313 changes: 313 additions & 0 deletions backend/tests/auth.test.ts
Original file line number Diff line number Diff line change
@@ -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<TestContext>({
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);
},
);
});
Loading