feat: update tests for auth

This commit is contained in:
Yadunand Prem 2025-07-12 22:42:12 -04:00
parent 9131a86262
commit 42d2054532
No known key found for this signature in database
7 changed files with 664 additions and 73 deletions

View File

@ -1,80 +1,10 @@
import { beforeAll, describe, expect, it } from "bun:test";
import { migrate } from "drizzle-orm/bun-sql/migrator";
import { describe, expect, it } from "bun:test";
import app from ".";
import { db } from "./db";
import { auth } from "./lib/auth";
import { buildHeaders } from "./test/utils";
beforeAll(async () => {
await migrate(db, { migrationsFolder: "./drizzle" });
});
describe("First Test", () => {
describe("API Health Check", () => {
it("should return 200 Response", async () => {
const req = new Request("http://localhost:3000/");
const res = await app.fetch(req);
expect(res.status).toBe(200);
});
});
describe("Auth", () => {
const user = {
email: "yadunand@yadunut.com",
password: "password123",
name: "Yadunand Prem",
username: "yadunut",
};
let authHeaders: Headers;
it("creates a new user", async () => {
const res = await auth.api.signUpEmail({
body: {
...user,
},
});
expect(res.user.email).toBe("yadunand@yadunut.com");
const foundUser = await db.query.user.findFirst();
expect(foundUser).not.toBeNull();
expect(foundUser?.email).toBe(user.email);
});
it("logs in user and stores auth headers", async () => {
const { headers: signInHeaders, response: signInRes } =
await auth.api.signInEmail({
body: {
email: user.email,
password: user.password,
},
returnHeaders: true,
});
expect(signInRes.token).not.toBeNull();
expect(signInRes.user.name).toBe(user.name);
// Store the auth headers for subsequent tests
authHeaders = buildHeaders(signInHeaders);
// Verify session works with stored headers
const res = await auth.api.getSession({ headers: authHeaders });
expect(res?.user.username).toBe(user.username);
});
it("creates auth token using stored headers", async () => {
const data = await auth.api.createApiKey({
headers: authHeaders,
body: {},
});
// Add expectations based on what createApiKey should return
expect(data).toBeDefined();
});
it("deletes user using stored headers", async () => {
const deleteRes = await auth.api.deleteUser({
headers: authHeaders,
body: {
password: user.password,
},
});
expect(deleteRes.success).toBe(true);
});
});

View File

@ -1,6 +1,6 @@
import { Hono } from "hono";
import { logger } from "hono/logger";
import { auth } from "./lib/auth";
import { auth } from "@/lib/auth";
const app = new Hono();

View File

@ -0,0 +1,304 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { auth } from "@/lib/auth";
import { AuthTestHelper, buildHeaders } from "../utils";
describe("API Key Management", () => {
const user = {
email: "apikey-test@example.com",
password: "password123",
name: "API Key Test User",
username: "apikeyuser",
};
let authHeaders: Headers;
let userId: string;
let authHelper: AuthTestHelper;
beforeEach(async () => {
// Create user and sign in for each test
authHelper = new AuthTestHelper();
const authUser = await authHelper.createAuthenticatedUser();
authHeaders = authUser.authHeaders;
userId = authUser.userId;
});
afterEach(async () => {
// Clean up: delete user and associated data
authHelper.cleanupUser(authHeaders, user.password);
});
describe("API Key Creation", () => {
it("creates an API key successfully", async () => {
const res = await auth.api.createApiKey({
headers: authHeaders,
body: {
name: "Test API Key",
},
});
expect(res).toBeDefined();
expect(res.id).toBeDefined();
expect(res.name).toBe("Test API Key");
expect(res.key).toBeDefined();
expect(res.userId).toBe(userId);
expect(res.enabled).toBe(true);
});
it("creates an API key with custom name", async () => {
const res = await auth.api.createApiKey({
headers: authHeaders,
body: {
name: "Custom API Key",
},
});
expect(res).toBeDefined();
expect(res.name).toBe("Custom API Key");
expect(res.key).toBeDefined();
expect(res.userId).toBe(userId);
});
it("rejects API key creation without authentication", async () => {
try {
await auth.api.createApiKey({
headers: new Headers(),
body: {
name: "Unauthorized Key",
},
});
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
}
});
});
describe("API Key Listing", () => {
it("lists user's API keys", async () => {
// Create multiple API keys
await auth.api.createApiKey({
headers: authHeaders,
body: { name: "Key 1" },
});
await auth.api.createApiKey({
headers: authHeaders,
body: { name: "Key 2" },
});
const res = await auth.api.listApiKeys({
headers: authHeaders,
});
expect(res).toBeDefined();
expect(Array.isArray(res)).toBe(true);
expect(res.length).toBe(2);
expect(res.some((key) => key.name === "Key 1")).toBe(true);
expect(res.some((key) => key.name === "Key 2")).toBe(true);
});
it("returns empty list for user with no API keys", async () => {
const res = await auth.api.listApiKeys({
headers: authHeaders,
});
expect(res).toBeDefined();
expect(Array.isArray(res)).toBe(true);
expect(res.length).toBe(0);
});
it("rejects listing without authentication", async () => {
try {
await auth.api.listApiKeys({
headers: new Headers(),
});
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
}
});
});
describe("API Key Deletion", () => {
let apiKeyId: string;
beforeEach(async () => {
const res = await auth.api.createApiKey({
headers: authHeaders,
body: { name: "Key to Revoke" },
});
apiKeyId = res.id;
});
it("deletes an API key successfully", async () => {
const res = await auth.api.deleteApiKey({
headers: authHeaders,
body: { keyId: apiKeyId },
});
expect(res.success).toBe(true);
// Verify the key is no longer in the list
const keys = await auth.api.listApiKeys({
headers: authHeaders,
});
expect(keys.find((key) => key.id === apiKeyId)).toBeUndefined();
});
it("rejects deleting non-existent API key", async () => {
try {
await auth.api.deleteApiKey({
headers: authHeaders,
body: { keyId: "non-existent-key-id" },
});
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
}
});
it("rejects deleting API key without authentication", async () => {
try {
await auth.api.deleteApiKey({
headers: new Headers(),
body: { keyId: apiKeyId },
});
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
}
});
it("rejects deleting another user's API key", async () => {
// Create another user
const otherUser = {
email: "other-user@example.com",
password: "password123",
name: "Other User",
username: "otheruser",
};
await auth.api.signUpEmail({
body: otherUser,
});
const { headers: otherHeaders } = await auth.api.signInEmail({
body: {
email: otherUser.email,
password: otherUser.password,
},
returnHeaders: true,
});
try {
await auth.api.deleteApiKey({
headers: buildHeaders(otherHeaders),
body: { keyId: apiKeyId },
});
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
}
// Clean up other user
await auth.api.deleteUser({
headers: buildHeaders(otherHeaders),
body: { password: otherUser.password },
});
});
});
describe("API Key Verification", () => {
let apiKey: string;
let apiKeyId: string;
beforeEach(async () => {
const res = await auth.api.createApiKey({
headers: authHeaders,
body: { name: "Test Auth Key" },
});
apiKey = res.key;
apiKeyId = res.id;
});
it("verifies valid API key", async () => {
const res = await auth.api.verifyApiKey({
body: { key: apiKey },
});
expect(res).toBeDefined();
expect(res.valid).toBe(true);
});
it("rejects invalid API key", async () => {
const res = await auth.api.verifyApiKey({
body: { key: "invalid-key" },
});
expect(res.valid).toBe(false);
});
it("gets API key details", async () => {
const res = await auth.api.getApiKey({
headers: authHeaders,
query: { id: apiKeyId },
});
expect(res).toBeDefined();
expect(res.id).toBe(apiKeyId);
expect(res.name).toBe("Test Auth Key");
expect(res.userId).toBe(userId);
});
it("updates API key", async () => {
const res = await auth.api.updateApiKey({
headers: authHeaders,
body: {
keyId: apiKeyId,
name: "Updated API Key Name",
},
});
expect(res).toBeDefined();
expect(res.name).toBe("Updated API Key Name");
});
});
describe("API Key Management", () => {
it("manages multiple API keys correctly", async () => {
// Create multiple keys
const key1 = await auth.api.createApiKey({
headers: authHeaders,
body: { name: "Key 1" },
});
const key2 = await auth.api.createApiKey({
headers: authHeaders,
body: { name: "Key 2" },
});
// List keys
const keys = await auth.api.listApiKeys({
headers: authHeaders,
});
expect(keys.length).toBeGreaterThanOrEqual(2);
expect(keys.find((k) => k.id === key1.id)).toBeDefined();
expect(keys.find((k) => k.id === key2.id)).toBeDefined();
// Delete one key
await auth.api.deleteApiKey({
headers: authHeaders,
body: { keyId: key1.id },
});
// Verify it's removed
const updatedKeys = await auth.api.listApiKeys({
headers: authHeaders,
});
expect(updatedKeys.find((k) => k.id === key1.id)).toBeUndefined();
expect(updatedKeys.find((k) => k.id === key2.id)).toBeDefined();
});
});
});

View File

@ -0,0 +1,121 @@
import { beforeEach, describe, expect, it } from "bun:test";
import { auth } from "@/lib/auth";
import { AuthTestHelper } from "../utils";
describe("Authentication Integration Flow", () => {
let authHelper: AuthTestHelper;
beforeEach(() => {
authHelper = new AuthTestHelper();
});
it("completes full auth flow: signup -> signin -> create API key -> use API key -> cleanup", async () => {
// 1. Create and authenticate user
const { user, userId, authHeaders } =
await authHelper.createAuthenticatedUser({
email: `integration-${Date.now()}@example.com`,
name: "Integration Test User",
username: `integrationuser${Date.now()}`,
});
// 2. Create API key using session auth
const apiKeyRes = await authHelper.createApiKey(authHeaders, {
name: "Integration Test Key",
});
expect(apiKeyRes.id).toBeDefined();
expect(apiKeyRes.key).toBeDefined();
expect(apiKeyRes.userId).toBe(userId);
expect(apiKeyRes.name).toBe("Integration Test Key");
// 3. Verify API key with verification endpoint
const verifyRes = await auth.api.verifyApiKey({
body: { key: apiKeyRes.key },
});
expect(verifyRes.valid).toBe(true);
// 4. List API keys to verify it appears
const keysRes = await auth.api.listApiKeys({
headers: authHeaders,
});
expect(keysRes).toBeDefined();
expect(Array.isArray(keysRes)).toBe(true);
expect(keysRes.length).toBe(1);
expect(keysRes?.[0]?.id).toBe(apiKeyRes.id);
// 5. Delete API key
const deleteRes = await auth.api.deleteApiKey({
headers: authHeaders,
body: { keyId: apiKeyRes.id },
});
expect(deleteRes.success).toBe(true);
// 6. Verify API key no longer works
const invalidVerifyRes = await auth.api.verifyApiKey({
body: { key: apiKeyRes.key },
});
expect(invalidVerifyRes.valid).toBe(false);
// 7. Cleanup user
await authHelper.cleanupUser(authHeaders, user.password);
});
it("handles multiple concurrent API keys per user", async () => {
const { user, authHeaders } = await authHelper.createAuthenticatedUser();
// Create multiple API keys
const [key1, key2, key3] = await Promise.all([
authHelper.createApiKey(authHeaders, { name: "Key 1" }),
authHelper.createApiKey(authHeaders, { name: "Key 2" }),
authHelper.createApiKey(authHeaders, { name: "Key 3" }),
]);
// Verify all keys work
const [verify1, verify2, verify3] = await Promise.all([
auth.api.verifyApiKey({ body: { key: key1.key } }),
auth.api.verifyApiKey({ body: { key: key2.key } }),
auth.api.verifyApiKey({ body: { key: key3.key } }),
]);
expect(verify1.valid).toBe(true);
expect(verify2.valid).toBe(true);
expect(verify3.valid).toBe(true);
// List all keys
const allKeys = await auth.api.listApiKeys({ headers: authHeaders });
expect(allKeys.length).toBe(3);
// Delete one key
await auth.api.deleteApiKey({
headers: authHeaders,
body: { keyId: key2.id },
});
// Verify only 2 keys remain
const remainingKeys = await auth.api.listApiKeys({ headers: authHeaders });
expect(remainingKeys.length).toBe(2);
expect(remainingKeys.find((k) => k.id === key2.id)).toBeUndefined();
// Verify deleted key doesn't work
const deletedVerify = await auth.api.verifyApiKey({
body: { key: key2.key },
});
expect(deletedVerify.valid).toBe(false);
// Verify other keys still work
const [stillWorking1, stillWorking3] = await Promise.all([
auth.api.verifyApiKey({ body: { key: key1.key } }),
auth.api.verifyApiKey({ body: { key: key3.key } }),
]);
expect(stillWorking1.valid).toBe(true);
expect(stillWorking3.valid).toBe(true);
// Cleanup
await authHelper.cleanupUser(authHeaders, user.password);
});
});

View File

@ -0,0 +1,125 @@
import { describe, expect, it } from "bun:test";
import { db } from "@/db";
import { auth } from "@/lib/auth";
import { buildHeaders } from "../utils";
describe("User Authentication", () => {
const user = {
email: "test-user@example.com",
password: "password123",
name: "Test User",
username: "testuser",
};
let authHeaders: Headers;
describe("User Registration", () => {
it("creates a new user successfully", async () => {
const res = await auth.api.signUpEmail({
body: {
...user,
},
});
expect(res.user.email).toBe(user.email);
expect(res.user.name).toBe(user.name);
// Note: Username field may not be returned in the response even if set
const foundUser = await db.query.user.findFirst({
where: (users, { eq }) => eq(users.email, user.email),
});
expect(foundUser).not.toBeNull();
expect(foundUser?.email).toBe(user.email);
});
it("prevents duplicate user registration", async () => {
try {
await auth.api.signUpEmail({
body: {
...user,
},
});
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
}
});
});
describe("User Login", () => {
it("logs in user with correct credentials", async () => {
const { headers: signInHeaders, response: signInRes } =
await auth.api.signInEmail({
body: {
email: user.email,
password: user.password,
},
returnHeaders: true,
});
expect(signInRes.token).not.toBeNull();
expect(signInRes.user.name).toBe(user.name);
expect(signInRes.user.email).toBe(user.email);
authHeaders = buildHeaders(signInHeaders);
});
it("rejects login with incorrect password", async () => {
try {
await auth.api.signInEmail({
body: {
email: user.email,
password: "wrongpassword",
},
});
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
}
});
it("rejects login with non-existent email", async () => {
try {
await auth.api.signInEmail({
body: {
email: "nonexistent@example.com",
password: user.password,
},
});
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
}
});
});
describe("Session Management", () => {
it("retrieves valid session with auth headers", async () => {
const res = await auth.api.getSession({ headers: authHeaders });
expect(res?.user.username).toBe(user.username);
expect(res?.user.email).toBe(user.email);
});
it("returns null for session without auth headers", async () => {
const res = await auth.api.getSession({ headers: new Headers() });
expect(res?.user).toBeUndefined();
});
});
describe("User Deletion", () => {
it("deletes user with correct password", async () => {
const deleteRes = await auth.api.deleteUser({
headers: authHeaders,
body: {
password: user.password,
},
});
expect(deleteRes.success).toBe(true);
// Verify user is actually deleted
const foundUser = await db.query.user.findFirst({
where: (users, { eq }) => eq(users.email, user.email),
});
expect(foundUser).toBeUndefined();
});
});
});

17
api/src/test/setup.ts Normal file
View File

@ -0,0 +1,17 @@
import { afterAll, beforeAll } from "bun:test";
import { migrate } from "drizzle-orm/bun-sql/migrator";
import { db } from "@/db";
beforeAll(async () => {
await migrate(db, { migrationsFolder: "./drizzle" });
});
afterAll(async () => {
// Clear all test data from database - note: order matters due to foreign keys
const { session, apikey, account, user } = await import("@/db/schema/auth");
await db.delete(session).execute();
await db.delete(apikey).execute();
await db.delete(account).execute();
await db.delete(user).execute();
});

View File

@ -1,3 +1,6 @@
import { db } from "@/db";
import { auth } from "@/lib/auth";
export function buildHeaders(signInHeaders: Headers) {
const headers = new Headers();
for (const cookie of signInHeaders.getSetCookie() ?? []) {
@ -5,3 +8,94 @@ export function buildHeaders(signInHeaders: Headers) {
}
return headers;
}
export interface TestUser {
email: string;
password: string;
name: string;
username: string;
}
export class AuthTestHelper {
createTestUser(userOverrides: Partial<TestUser> = {}): TestUser {
const defaultUser: TestUser = {
email: `test-${Date.now()}@example.com`,
password: "password123",
name: "Test User",
username: `testuser${Date.now()}`,
};
return { ...defaultUser, ...userOverrides };
}
async signUpUser(user: TestUser) {
return await auth.api.signUpEmail({
body: user,
});
}
async signInUser(user: Pick<TestUser, "email" | "password">) {
return await auth.api.signInEmail({
body: {
email: user.email,
password: user.password,
},
returnHeaders: true,
});
}
async createAuthenticatedUser(userOverrides: Partial<TestUser> = {}) {
const user = this.createTestUser(userOverrides);
const signUpRes = await this.signUpUser(user);
const { headers: signInHeaders } = await this.signInUser(user);
const authHeaders = buildHeaders(signInHeaders);
return {
user,
userId: signUpRes.user.id,
authHeaders,
};
}
async cleanupUser(authHeaders: Headers, password: string) {
try {
await auth.api.deleteUser({
headers: authHeaders,
body: { password },
});
} catch (_) {
// User might already be deleted, ignore error
}
}
async createApiKey(
authHeaders: Headers,
keyOptions: Partial<
Parameters<typeof auth.api.createApiKey>["0"]["body"]
> = {},
) {
return await auth.api.createApiKey({
headers: authHeaders,
body: {
name: `Test API Key ${Date.now()}`,
...keyOptions,
},
});
}
createApiKeyHeaders(apiKey: string) {
const headers = new Headers();
headers.set("Authorization", `Bearer ${apiKey}`);
return headers;
}
async clearDatabase() {
// Clear all test data from database - note: order matters due to foreign keys
const { session, apikey, account, user } = await import("@/db/schema/auth");
await db.delete(session).execute();
await db.delete(apikey).execute();
await db.delete(account).execute();
await db.delete(user).execute();
}
}