Compare commits
2 Commits
42d2054532
...
2a1caa197b
Author | SHA1 | Date | |
---|---|---|---|
2a1caa197b | |||
cc63918ae8 |
@ -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,
|
@ -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",
|
||||
|
@ -5,8 +5,8 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1752200400975,
|
||||
"tag": "0000_long_puff_adder",
|
||||
"when": 1752443645228,
|
||||
"tag": "0000_hard_violations",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
@ -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"),
|
||||
|
15
api/src/db/schema/helpers.ts
Normal file
15
api/src/db/schema/helpers.ts
Normal 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()`);
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
|
||||
|
@ -10,6 +10,11 @@ export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
}),
|
||||
advanced: {
|
||||
database: {
|
||||
generateId: false,
|
||||
},
|
||||
},
|
||||
plugins: [username(), apiKey()],
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
|
301
api/src/test/auth/apikey.test.ts
Normal file
301
api/src/test/auth/apikey.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
121
api/src/test/auth/integration.test.ts
Normal file
121
api/src/test/auth/integration.test.ts
Normal 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);
|
||||
});
|
||||
});
|
125
api/src/test/auth/user.test.ts
Normal file
125
api/src/test/auth/user.test.ts
Normal 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
17
api/src/test/setup.ts
Normal 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();
|
||||
});
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user