Compare commits

...

2 Commits

Author SHA1 Message Date
2a1caa197b
feat: update schema for auth 2025-07-13 18:03:29 -04:00
cc63918ae8
feat: update tests for auth 2025-07-13 18:03:12 -04:00
13 changed files with 720 additions and 105 deletions

View File

@ -1,10 +1,10 @@
CREATE SCHEMA "shared";
--> statement-breakpoint
CREATE TABLE "shared"."account" (
"id" text PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
"user_id" text NOT NULL,
"user_id" uuid NOT NULL,
"access_token" text,
"refresh_token" text,
"id_token" text,
@ -17,12 +17,12 @@ CREATE TABLE "shared"."account" (
);
--> statement-breakpoint
CREATE TABLE "shared"."apikey" (
"id" text PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text,
"start" text,
"prefix" text,
"key" text NOT NULL,
"user_id" text NOT NULL,
"user_id" uuid NOT NULL,
"refill_interval" integer,
"refill_amount" integer,
"last_refill_at" timestamp,
@ -41,19 +41,19 @@ CREATE TABLE "shared"."apikey" (
);
--> statement-breakpoint
CREATE TABLE "shared"."session" (
"id" text PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"expires_at" timestamp NOT NULL,
"token" text NOT NULL,
"created_at" timestamp NOT NULL,
"updated_at" timestamp NOT NULL,
"ip_address" text,
"user_agent" text,
"user_id" text NOT NULL,
"user_id" uuid NOT NULL,
CONSTRAINT "session_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "shared"."user" (
"id" text PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"email_verified" boolean NOT NULL,
@ -67,7 +67,7 @@ CREATE TABLE "shared"."user" (
);
--> statement-breakpoint
CREATE TABLE "shared"."verification" (
"id" text PRIMARY KEY NOT NULL,
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp NOT NULL,

View File

@ -1,5 +1,5 @@
{
"id": "033383bb-5508-4523-b1f5-d7b7f0c00a33",
"id": "6f6d04f6-92de-4c28-8736-75fafbfa3aef",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
@ -10,9 +10,10 @@
"columns": {
"id": {
"name": "id",
"type": "text",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"account_id": {
"name": "account_id",
@ -28,7 +29,7 @@
},
"user_id": {
"name": "user_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
@ -116,9 +117,10 @@
"columns": {
"id": {
"name": "id",
"type": "text",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
@ -146,7 +148,7 @@
},
"user_id": {
"name": "user_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
@ -274,9 +276,10 @@
"columns": {
"id": {
"name": "id",
"type": "text",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"expires_at": {
"name": "expires_at",
@ -316,7 +319,7 @@
},
"user_id": {
"name": "user_id",
"type": "text",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
@ -358,9 +361,10 @@
"columns": {
"id": {
"name": "id",
"type": "text",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
@ -440,9 +444,10 @@
"columns": {
"id": {
"name": "id",
"type": "text",
"type": "uuid",
"primaryKey": true,
"notNull": true
"notNull": true,
"default": "gen_random_uuid()"
},
"identifier": {
"name": "identifier",

View File

@ -5,8 +5,8 @@
{
"idx": 0,
"version": "7",
"when": 1752200400975,
"tag": "0000_long_puff_adder",
"when": 1752443645228,
"tag": "0000_hard_violations",
"breakpoints": true
}
]

View File

@ -4,12 +4,14 @@ import {
pgSchema,
text,
timestamp,
uuid,
} from "drizzle-orm/pg-core";
import { idPrimaryKey } from "./helpers";
export const shared = pgSchema("shared");
export const user = shared.table("user", {
id: text("id").primaryKey(),
id: idPrimaryKey,
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified")
@ -27,23 +29,23 @@ export const user = shared.table("user", {
});
export const session = shared.table("session", {
id: text("id").primaryKey(),
id: idPrimaryKey,
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
userId: uuid("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
});
export const account = shared.table("account", {
id: text("id").primaryKey(),
id: idPrimaryKey,
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
userId: uuid("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
@ -58,7 +60,7 @@ export const account = shared.table("account", {
});
export const verification = shared.table("verification", {
id: text("id").primaryKey(),
id: idPrimaryKey,
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
@ -67,12 +69,12 @@ export const verification = shared.table("verification", {
});
export const apikey = shared.table("apikey", {
id: text("id").primaryKey(),
id: idPrimaryKey,
name: text("name"),
start: text("start"),
prefix: text("prefix"),
key: text("key").notNull(),
userId: text("user_id")
userId: uuid("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
refillInterval: integer("refill_interval"),

View File

@ -0,0 +1,15 @@
import { sql } from "drizzle-orm";
import { timestamp, uuid } from "drizzle-orm/pg-core";
export const timestampSchema = {
updated_at: timestamp()
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
created_at: timestamp().defaultNow().notNull(),
deleted_at: timestamp(),
};
export const idPrimaryKey = uuid("id")
.primaryKey()
.default(sql`gen_random_uuid()`);

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

@ -10,6 +10,11 @@ export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
}),
advanced: {
database: {
generateId: false,
},
},
plugins: [username(), apiKey()],
emailAndPassword: {
enabled: true,

View File

@ -0,0 +1,301 @@
import {
afterAll,
beforeAll,
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;
beforeAll(async () => {
// Create user and sign in for each test
authHelper = new AuthTestHelper();
const authUser = await authHelper.createAuthenticatedUser();
authHeaders = authUser.authHeaders;
userId = authUser.userId;
});
afterAll(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(4);
expect(res.some((key) => key.name === "Key 1")).toBe(true);
expect(res.some((key) => key.name === "Key 2")).toBe(true);
});
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();
}
}