Add tests for @reviq/db package
- token.test.ts: Unit tests for generateToken, parseToken, hashToken - client.test.ts: Tests for createDb validation and e2e connectivity - execute-bootstrap.test.ts: Comprehensive e2e tests for bootstrap operation including overwrite mode and related record cleanup Coverage: client.ts 100%, token.ts 100%, execute-bootstrap.ts 98.69% Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
697
packages/db/src/helpers/execute-bootstrap.test.ts
Normal file
697
packages/db/src/helpers/execute-bootstrap.test.ts
Normal file
@@ -0,0 +1,697 @@
|
||||
/**
|
||||
* Tests for the bootstrap operation
|
||||
*
|
||||
* These tests use a real PostgreSQL database to test the executeBootstrap function.
|
||||
*/
|
||||
|
||||
import type { Database } from "@reviq/db-schema";
|
||||
import type { Kysely } from "kysely";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { sql } from "kysely";
|
||||
import { createDb } from "../client.js";
|
||||
import { executeBootstrap } from "./execute-bootstrap.js";
|
||||
import { hashToken, parseToken, TOKEN_PREFIX } from "./token.js";
|
||||
|
||||
/**
|
||||
* Skip flag for database-dependent tests.
|
||||
* Tests are skipped when TEST_DATABASE_URL is not configured.
|
||||
*/
|
||||
const SKIP_DB_TESTS = !process.env.TEST_DATABASE_URL;
|
||||
|
||||
const describeE2E = describe.skipIf(SKIP_DB_TESTS);
|
||||
|
||||
/** Tables to truncate between tests */
|
||||
const TABLES_TO_TRUNCATE = [
|
||||
"sessions",
|
||||
"api_tokens",
|
||||
"login_requests",
|
||||
"passkeys",
|
||||
"user_devices",
|
||||
"webauthn_challenges",
|
||||
"email_verifications",
|
||||
"password_resets",
|
||||
"org_invites",
|
||||
"org_sites",
|
||||
"org_members",
|
||||
"orgs",
|
||||
"users",
|
||||
] as const;
|
||||
|
||||
/** Generate unique test ID */
|
||||
let testCounter = 0;
|
||||
const uniqueTestId = (): string => {
|
||||
const timestamp = Date.now();
|
||||
testCounter++;
|
||||
return `${timestamp}-${testCounter.toString()}`;
|
||||
};
|
||||
|
||||
/** Truncate all tables */
|
||||
async function truncateAllTables(db: Kysely<Database>): Promise<void> {
|
||||
const tableList = TABLES_TO_TRUNCATE.join(", ");
|
||||
await sql`TRUNCATE ${sql.raw(tableList)} RESTART IDENTITY CASCADE`.execute(
|
||||
db,
|
||||
);
|
||||
}
|
||||
|
||||
/** Signal for transaction rollback */
|
||||
class RollbackSignal extends Error {
|
||||
constructor() {
|
||||
super("RollbackSignal");
|
||||
this.name = "RollbackSignal";
|
||||
}
|
||||
}
|
||||
|
||||
/** Run test in transaction that auto-rollbacks */
|
||||
async function withTestTransaction<T>(
|
||||
db: Kysely<Database>,
|
||||
testFn: (trx: Kysely<Database>) => Promise<T>,
|
||||
): Promise<T | undefined> {
|
||||
let result: T | undefined;
|
||||
|
||||
try {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
result = await testFn(trx);
|
||||
throw new RollbackSignal();
|
||||
});
|
||||
} catch (e) {
|
||||
if (!(e instanceof RollbackSignal)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
describeE2E("[e2e] executeBootstrap", () => {
|
||||
let db: Kysely<Database>;
|
||||
|
||||
beforeAll(async () => {
|
||||
const testUrl = process.env.TEST_DATABASE_URL;
|
||||
if (!testUrl) {
|
||||
throw new Error("TEST_DATABASE_URL not set");
|
||||
}
|
||||
db = createDb(testUrl);
|
||||
await truncateAllTables(db);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (db) {
|
||||
await truncateAllTables(db);
|
||||
await db.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
test("creates superuser with correct email and password", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
const result = await executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
expect(result.user.email).toBe("admin@example.com");
|
||||
|
||||
// Verify user in database
|
||||
const user = await trx
|
||||
.selectFrom("users")
|
||||
.where("id", "=", result.user.id)
|
||||
.selectAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(user.email).toBe("admin@example.com");
|
||||
expect(user.is_superuser).toBe(true);
|
||||
expect(user.email_verified_at).not.toBeNull();
|
||||
expect(user.password_hash).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizes email to lowercase", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
const result = await executeBootstrap(trx, {
|
||||
email: "ADMIN@EXAMPLE.COM",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
expect(result.user.email).toBe("admin@example.com");
|
||||
});
|
||||
});
|
||||
|
||||
test("creates organization with default slug and name", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
const result = await executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
expect(result.org.slug).toBe("reviq");
|
||||
|
||||
// Verify org in database
|
||||
const org = await trx
|
||||
.selectFrom("orgs")
|
||||
.where("id", "=", result.org.id)
|
||||
.selectAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(org.slug).toBe("reviq");
|
||||
expect(org.display_name).toBe("RevIQ");
|
||||
});
|
||||
});
|
||||
|
||||
test("creates organization with custom slug and name", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
const result = await executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "password123",
|
||||
orgSlug: "custom-org",
|
||||
orgDisplayName: "Custom Organization",
|
||||
});
|
||||
|
||||
expect(result.org.slug).toBe("custom-org");
|
||||
|
||||
const org = await trx
|
||||
.selectFrom("orgs")
|
||||
.where("id", "=", result.org.id)
|
||||
.selectAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(org.slug).toBe("custom-org");
|
||||
expect(org.display_name).toBe("Custom Organization");
|
||||
});
|
||||
});
|
||||
|
||||
test("adds user as owner of organization", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
const result = await executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const membership = await trx
|
||||
.selectFrom("org_members")
|
||||
.where("user_id", "=", result.user.id)
|
||||
.where("org_id", "=", result.org.id)
|
||||
.selectAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(membership.role).toBe("owner");
|
||||
});
|
||||
});
|
||||
|
||||
test("creates API token with correct properties", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
const result = await executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
// Token should be parseable
|
||||
expect(result.token.startsWith(TOKEN_PREFIX)).toBe(true);
|
||||
expect(parseToken(result.token)).not.toBeNull();
|
||||
|
||||
// Token should be stored as hash in database
|
||||
const tokenRecord = await trx
|
||||
.selectFrom("api_tokens")
|
||||
.where("user_id", "=", result.user.id)
|
||||
.selectAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(tokenRecord.token_hash).toBe(hashToken(result.token));
|
||||
expect(tokenRecord.name).toBe("CLI bootstrap token");
|
||||
});
|
||||
});
|
||||
|
||||
test("creates API token with custom name", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
const result = await executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "password123",
|
||||
tokenName: "Custom Token Name",
|
||||
});
|
||||
|
||||
const tokenRecord = await trx
|
||||
.selectFrom("api_tokens")
|
||||
.where("user_id", "=", result.user.id)
|
||||
.selectAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(tokenRecord.name).toBe("Custom Token Name");
|
||||
});
|
||||
});
|
||||
|
||||
test("creates API token with custom expiration", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
const beforeCreate = Date.now();
|
||||
|
||||
const result = await executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "password123",
|
||||
tokenExpirationDays: 30,
|
||||
});
|
||||
|
||||
const tokenRecord = await trx
|
||||
.selectFrom("api_tokens")
|
||||
.where("user_id", "=", result.user.id)
|
||||
.selectAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const expectedMin = beforeCreate + 30 * 24 * 60 * 60 * 1000 - 1000;
|
||||
const expectedMax = beforeCreate + 30 * 24 * 60 * 60 * 1000 + 5000;
|
||||
|
||||
expect(tokenRecord.expires_at.getTime()).toBeGreaterThan(expectedMin);
|
||||
expect(tokenRecord.expires_at.getTime()).toBeLessThan(expectedMax);
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error for password less than 8 characters", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
await expect(
|
||||
executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "short",
|
||||
}),
|
||||
).rejects.toThrow("Password must be at least 8 characters");
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error for password exactly 7 characters", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
await expect(
|
||||
executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "1234567",
|
||||
}),
|
||||
).rejects.toThrow("Password must be at least 8 characters");
|
||||
});
|
||||
});
|
||||
|
||||
test("accepts password exactly 8 characters", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
const result = await executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "12345678",
|
||||
});
|
||||
|
||||
expect(result.user.email).toBe("admin@example.com");
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error for invalid email without @", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
await expect(
|
||||
executeBootstrap(trx, {
|
||||
email: "invalidemail",
|
||||
password: "password123",
|
||||
}),
|
||||
).rejects.toThrow("Invalid email address");
|
||||
});
|
||||
});
|
||||
|
||||
test("accepts email with @ symbol", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
const result = await executeBootstrap(trx, {
|
||||
email: "valid@email",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
expect(result.user.email).toBe("valid@email");
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error if user already exists (normal mode)", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
// Create the first user
|
||||
await executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "password123",
|
||||
orgSlug: "org1",
|
||||
});
|
||||
|
||||
// Attempt to create the same user again
|
||||
await expect(
|
||||
executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "password123",
|
||||
orgSlug: "org2",
|
||||
}),
|
||||
).rejects.toThrow("User with email admin@example.com already exists");
|
||||
});
|
||||
});
|
||||
|
||||
test("overwrites existing user in dangerouslyOverwriteExisting mode", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
// Create the first user
|
||||
const result1 = await executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "password123",
|
||||
orgSlug: "original-org",
|
||||
});
|
||||
|
||||
const originalUserId = result1.user.id;
|
||||
|
||||
// Overwrite the user
|
||||
const result2 = await executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "newpassword123",
|
||||
orgSlug: "new-org",
|
||||
dangerouslyOverwriteExisting: true,
|
||||
});
|
||||
|
||||
// Should be a different user ID
|
||||
expect(result2.user.id).not.toBe(originalUserId);
|
||||
|
||||
// Original user should be deleted
|
||||
const originalUser = await trx
|
||||
.selectFrom("users")
|
||||
.where("id", "=", originalUserId)
|
||||
.selectAll()
|
||||
.executeTakeFirst();
|
||||
|
||||
expect(originalUser).toBeUndefined();
|
||||
|
||||
// New user should exist
|
||||
const newUser = await trx
|
||||
.selectFrom("users")
|
||||
.where("id", "=", result2.user.id)
|
||||
.selectAll()
|
||||
.executeTakeFirst();
|
||||
|
||||
expect(newUser).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test("deletes existing org in dangerouslyOverwriteExisting mode", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
// Create the first bootstrap
|
||||
const result1 = await executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "password123",
|
||||
orgSlug: "test-org",
|
||||
});
|
||||
|
||||
const originalOrgId = result1.org.id;
|
||||
|
||||
// Overwrite with a different email but same org slug
|
||||
const result2 = await executeBootstrap(trx, {
|
||||
email: "newadmin@example.com",
|
||||
password: "password123",
|
||||
orgSlug: "test-org",
|
||||
dangerouslyOverwriteExisting: true,
|
||||
});
|
||||
|
||||
// Should be a different org ID
|
||||
expect(result2.org.id).not.toBe(originalOrgId);
|
||||
|
||||
// Original org should be deleted
|
||||
const originalOrg = await trx
|
||||
.selectFrom("orgs")
|
||||
.where("id", "=", originalOrgId)
|
||||
.selectAll()
|
||||
.executeTakeFirst();
|
||||
|
||||
expect(originalOrg).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
test("deletes related user records in overwrite mode", async () => {
|
||||
const uniqueId = uniqueTestId();
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
// Create the first bootstrap
|
||||
const result1 = await executeBootstrap(trx, {
|
||||
email: `admin-${uniqueId}@example.com`,
|
||||
password: "password123",
|
||||
orgSlug: `org-${uniqueId}`,
|
||||
});
|
||||
|
||||
// Manually add some related records
|
||||
await trx
|
||||
.insertInto("sessions")
|
||||
.values({
|
||||
user_id: result1.user.id,
|
||||
token_hash: "test-hash",
|
||||
ip_address: "127.0.0.1",
|
||||
user_agent: "test",
|
||||
expires_at: new Date(Date.now() + 86400000),
|
||||
trusted_mode: false,
|
||||
})
|
||||
.execute();
|
||||
|
||||
await trx
|
||||
.insertInto("email_verifications")
|
||||
.values({
|
||||
user_id: result1.user.id,
|
||||
token: "test-token",
|
||||
expires_at: new Date(Date.now() + 86400000),
|
||||
})
|
||||
.execute();
|
||||
|
||||
await trx
|
||||
.insertInto("login_requests")
|
||||
.values({
|
||||
user_id: result1.user.id,
|
||||
email: `admin-${uniqueId}@example.com`,
|
||||
token: "login-token",
|
||||
device_fingerprint: "fingerprint",
|
||||
expires_at: new Date(Date.now() + 86400000),
|
||||
})
|
||||
.execute();
|
||||
|
||||
await trx
|
||||
.insertInto("passkeys")
|
||||
.values({
|
||||
user_id: result1.user.id,
|
||||
credential_id: Buffer.from("credential"),
|
||||
public_key: Buffer.from("publickey"),
|
||||
counter: 0,
|
||||
backup_eligible: false,
|
||||
backup_status: false,
|
||||
device_type: "singleDevice",
|
||||
name: "Test Passkey",
|
||||
rpid: "localhost",
|
||||
webauthn_user_id: "test-user-id",
|
||||
})
|
||||
.execute();
|
||||
|
||||
await trx
|
||||
.insertInto("password_resets")
|
||||
.values({
|
||||
user_id: result1.user.id,
|
||||
token: "reset-token",
|
||||
expires_at: new Date(Date.now() + 86400000),
|
||||
})
|
||||
.execute();
|
||||
|
||||
await trx
|
||||
.insertInto("user_devices")
|
||||
.values({
|
||||
user_id: result1.user.id,
|
||||
device_fingerprint: "device-fingerprint",
|
||||
user_agent: "test-agent",
|
||||
})
|
||||
.execute();
|
||||
|
||||
// Overwrite the user
|
||||
await executeBootstrap(trx, {
|
||||
email: `admin-${uniqueId}@example.com`,
|
||||
password: "newpassword123",
|
||||
orgSlug: `org-${uniqueId}`,
|
||||
dangerouslyOverwriteExisting: true,
|
||||
});
|
||||
|
||||
// All related records should be deleted
|
||||
const sessions = await trx
|
||||
.selectFrom("sessions")
|
||||
.where("user_id", "=", result1.user.id)
|
||||
.selectAll()
|
||||
.execute();
|
||||
expect(sessions).toHaveLength(0);
|
||||
|
||||
const emailVerifications = await trx
|
||||
.selectFrom("email_verifications")
|
||||
.where("user_id", "=", result1.user.id)
|
||||
.selectAll()
|
||||
.execute();
|
||||
expect(emailVerifications).toHaveLength(0);
|
||||
|
||||
const loginRequests = await trx
|
||||
.selectFrom("login_requests")
|
||||
.where("user_id", "=", result1.user.id)
|
||||
.selectAll()
|
||||
.execute();
|
||||
expect(loginRequests).toHaveLength(0);
|
||||
|
||||
const passkeys = await trx
|
||||
.selectFrom("passkeys")
|
||||
.where("user_id", "=", result1.user.id)
|
||||
.selectAll()
|
||||
.execute();
|
||||
expect(passkeys).toHaveLength(0);
|
||||
|
||||
const passwordResets = await trx
|
||||
.selectFrom("password_resets")
|
||||
.where("user_id", "=", result1.user.id)
|
||||
.selectAll()
|
||||
.execute();
|
||||
expect(passwordResets).toHaveLength(0);
|
||||
|
||||
const userDevices = await trx
|
||||
.selectFrom("user_devices")
|
||||
.where("user_id", "=", result1.user.id)
|
||||
.selectAll()
|
||||
.execute();
|
||||
expect(userDevices).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test("deletes org invites created by user in overwrite mode", async () => {
|
||||
const uniqueId = uniqueTestId();
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
// Create the first bootstrap
|
||||
const result1 = await executeBootstrap(trx, {
|
||||
email: `admin-${uniqueId}@example.com`,
|
||||
password: "password123",
|
||||
orgSlug: `org-${uniqueId}`,
|
||||
});
|
||||
|
||||
// Create another org and invite
|
||||
const [otherOrg] = await trx
|
||||
.insertInto("orgs")
|
||||
.values({
|
||||
slug: `other-org-${uniqueId}`,
|
||||
display_name: "Other Org",
|
||||
})
|
||||
.returning(["id"])
|
||||
.execute();
|
||||
|
||||
await trx
|
||||
.insertInto("org_invites")
|
||||
.values({
|
||||
org_id: otherOrg!.id,
|
||||
email: "invitee@example.com",
|
||||
role: "member",
|
||||
invited_by: result1.user.id,
|
||||
token: "invite-token",
|
||||
expires_at: new Date(Date.now() + 86400000),
|
||||
})
|
||||
.execute();
|
||||
|
||||
// Overwrite the user
|
||||
await executeBootstrap(trx, {
|
||||
email: `admin-${uniqueId}@example.com`,
|
||||
password: "newpassword123",
|
||||
orgSlug: `new-org-${uniqueId}`,
|
||||
dangerouslyOverwriteExisting: true,
|
||||
});
|
||||
|
||||
// Invite created by the user should be deleted
|
||||
const invites = await trx
|
||||
.selectFrom("org_invites")
|
||||
.where("invited_by", "=", result1.user.id)
|
||||
.selectAll()
|
||||
.execute();
|
||||
expect(invites).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test("deletes org related records in overwrite mode", async () => {
|
||||
const uniqueId = uniqueTestId();
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
// Create the first bootstrap
|
||||
const result1 = await executeBootstrap(trx, {
|
||||
email: `admin-${uniqueId}@example.com`,
|
||||
password: "password123",
|
||||
orgSlug: `org-${uniqueId}`,
|
||||
});
|
||||
|
||||
// Add org sites
|
||||
await trx
|
||||
.insertInto("org_sites")
|
||||
.values({
|
||||
org_id: result1.org.id,
|
||||
domain: "example.com",
|
||||
})
|
||||
.execute();
|
||||
|
||||
// Add org invites (to the org, not by the user)
|
||||
const [anotherUser] = await trx
|
||||
.insertInto("users")
|
||||
.values({
|
||||
email: `other-${uniqueId}@example.com`,
|
||||
display_name: "Other User",
|
||||
})
|
||||
.returning(["id"])
|
||||
.execute();
|
||||
|
||||
await trx
|
||||
.insertInto("org_invites")
|
||||
.values({
|
||||
org_id: result1.org.id,
|
||||
email: "invitee@example.com",
|
||||
role: "member",
|
||||
invited_by: anotherUser!.id,
|
||||
token: "invite-token-2",
|
||||
expires_at: new Date(Date.now() + 86400000),
|
||||
})
|
||||
.execute();
|
||||
|
||||
// Overwrite with the same org slug
|
||||
await executeBootstrap(trx, {
|
||||
email: `newadmin-${uniqueId}@example.com`,
|
||||
password: "password123",
|
||||
orgSlug: `org-${uniqueId}`,
|
||||
dangerouslyOverwriteExisting: true,
|
||||
});
|
||||
|
||||
// Org sites should be deleted
|
||||
const sites = await trx
|
||||
.selectFrom("org_sites")
|
||||
.where("org_id", "=", result1.org.id)
|
||||
.selectAll()
|
||||
.execute();
|
||||
expect(sites).toHaveLength(0);
|
||||
|
||||
// Org invites should be deleted
|
||||
const invites = await trx
|
||||
.selectFrom("org_invites")
|
||||
.where("org_id", "=", result1.org.id)
|
||||
.selectAll()
|
||||
.execute();
|
||||
expect(invites).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test("succeeds when no existing user/org in overwrite mode", async () => {
|
||||
const uniqueId = uniqueTestId();
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
// Should not throw even when nothing exists to overwrite
|
||||
const result = await executeBootstrap(trx, {
|
||||
email: `fresh-${uniqueId}@example.com`,
|
||||
password: "password123",
|
||||
orgSlug: `fresh-org-${uniqueId}`,
|
||||
dangerouslyOverwriteExisting: true,
|
||||
});
|
||||
|
||||
expect(result.user.email).toBe(`fresh-${uniqueId}@example.com`);
|
||||
expect(result.org.slug).toBe(`fresh-org-${uniqueId}`);
|
||||
});
|
||||
});
|
||||
|
||||
test("returns all expected fields", async () => {
|
||||
await withTestTransaction(db, async (trx) => {
|
||||
const result = await executeBootstrap(trx, {
|
||||
email: "admin@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
// Check user fields
|
||||
expect(typeof result.user.id).toBe("number");
|
||||
expect(typeof result.user.email).toBe("string");
|
||||
|
||||
// Check org fields
|
||||
expect(typeof result.org.id).toBe("number");
|
||||
expect(typeof result.org.slug).toBe("string");
|
||||
|
||||
// Check token
|
||||
expect(typeof result.token).toBe("string");
|
||||
expect(result.token.startsWith(TOKEN_PREFIX)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user