diff --git a/.env.test b/.env.test index a8c805e0..6d03fac1 100644 --- a/.env.test +++ b/.env.test @@ -1 +1,2 @@ DATABASE_URL=:memory: +ZEROBYTE_APP_SECRET=8b9acd4456dd5db0a4a3c4f4e1240b2c3ae08bb59690167197425e4a25dd9a69 diff --git a/AGENTS.md b/AGENTS.md index 9d09ac2c..62a639f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,14 +75,13 @@ bun run gen:api-client ### Code Quality ```bash -# Format and lint (Biome) -bunx biome check --write . +# Format and lint -# Format only -bunx biome format --write . +# Format +bunx oxfmt format --write -# Lint only -bunx biome lint . +# Lint +bun run lint ``` ## Architecture diff --git a/app/server/modules/backups/__tests__/backups.patterns.test.ts b/app/server/modules/backups/__tests__/backups.patterns.test.ts index fafb9e6d..2a39d54e 100644 --- a/app/server/modules/backups/__tests__/backups.patterns.test.ts +++ b/app/server/modules/backups/__tests__/backups.patterns.test.ts @@ -7,6 +7,7 @@ import { generateBackupOutput } from "~/test/helpers/restic"; import { getVolumePath } from "../../volumes/helpers"; import { restic } from "~/server/utils/restic"; import path from "node:path"; +import { TEST_ORG_ID } from "~/test/helpers/organization"; const backupMock = mock(() => Promise.resolve({ exitCode: 0, result: JSON.parse(generateBackupOutput()) })); @@ -36,7 +37,7 @@ describe("executeBackup - include / exclude patterns", () => { }); // act - await backupsService.executeBackup(schedule.id); + await backupsService.executeBackup(schedule.id, TEST_ORG_ID); // assert expect(backupMock).toHaveBeenCalledWith( @@ -67,7 +68,7 @@ describe("executeBackup - include / exclude patterns", () => { }); // act - await backupsService.executeBackup(schedule.id); + await backupsService.executeBackup(schedule.id, TEST_ORG_ID); // assert expect(backupMock).toHaveBeenCalledWith( @@ -97,7 +98,7 @@ describe("executeBackup - include / exclude patterns", () => { }); // act - await backupsService.executeBackup(schedule.id); + await backupsService.executeBackup(schedule.id, TEST_ORG_ID); // assert expect(backupMock).toHaveBeenCalledWith( @@ -121,7 +122,7 @@ describe("executeBackup - include / exclude patterns", () => { }); // act - await backupsService.executeBackup(schedule.id); + await backupsService.executeBackup(schedule.id, TEST_ORG_ID); // assert expect(backupMock).toHaveBeenCalledWith( diff --git a/app/server/modules/backups/__tests__/backups.service.test.ts b/app/server/modules/backups/__tests__/backups.service.test.ts index d99c5819..e41b7e47 100644 --- a/app/server/modules/backups/__tests__/backups.service.test.ts +++ b/app/server/modules/backups/__tests__/backups.service.test.ts @@ -6,6 +6,7 @@ import { createTestRepository } from "~/test/helpers/repository"; import { generateBackupOutput } from "~/test/helpers/restic"; import { faker } from "@faker-js/faker"; import * as spawnModule from "~/server/utils/spawn"; +import { TEST_ORG_ID } from "~/test/helpers/organization"; const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: "", error: "" })); @@ -35,10 +36,10 @@ describe("execute backup", () => { ); // act - await backupsService.executeBackup(schedule.id); + await backupsService.executeBackup(schedule.id, TEST_ORG_ID); // assert - const updatedSchedule = await backupsService.getSchedule(schedule.id); + const updatedSchedule = await backupsService.getSchedule(schedule.id, TEST_ORG_ID); expect(updatedSchedule.nextBackupAt).not.toBeNull(); const nextBackupAt = new Date(updatedSchedule.nextBackupAt ?? 0); @@ -59,7 +60,7 @@ describe("execute backup", () => { }); // act - await backupsService.executeBackup(schedule.id); + await backupsService.executeBackup(schedule.id, TEST_ORG_ID); // assert expect(resticBackupMock).not.toHaveBeenCalled(); @@ -80,7 +81,7 @@ describe("execute backup", () => { ); // act - await backupsService.executeBackup(schedule.id, true); + await backupsService.executeBackup(schedule.id, TEST_ORG_ID, true); // assert expect(resticBackupMock).toHaveBeenCalled(); @@ -101,9 +102,9 @@ describe("execute backup", () => { }); // act - void backupsService.executeBackup(schedule.id); + void backupsService.executeBackup(schedule.id, TEST_ORG_ID); await new Promise((resolve) => setTimeout(resolve, 10)); - await backupsService.executeBackup(schedule.id); + await backupsService.executeBackup(schedule.id, TEST_ORG_ID); // assert expect(resticBackupMock).toHaveBeenCalledTimes(1); @@ -123,10 +124,10 @@ describe("execute backup", () => { ); // act - await backupsService.executeBackup(schedule.id); + await backupsService.executeBackup(schedule.id, TEST_ORG_ID); // assert - const updatedSchedule = await backupsService.getSchedule(schedule.id); + const updatedSchedule = await backupsService.getSchedule(schedule.id, TEST_ORG_ID); expect(updatedSchedule.lastBackupStatus).toBe("warning"); }); @@ -144,10 +145,10 @@ describe("execute backup", () => { ); // act - await backupsService.executeBackup(schedule.id); + await backupsService.executeBackup(schedule.id, TEST_ORG_ID); // assert - const updatedSchedule = await backupsService.getSchedule(schedule.id); + const updatedSchedule = await backupsService.getSchedule(schedule.id, TEST_ORG_ID); expect(updatedSchedule.lastBackupStatus).toBe("error"); }); }); @@ -168,7 +169,7 @@ describe("getSchedulesToExecute", () => { }); // act - const schedulesToExecute = await backupsService.getSchedulesToExecute(); + const schedulesToExecute = await backupsService.getSchedulesToExecute(TEST_ORG_ID); // assert expect(schedulesToExecute).toContain(schedule.id); diff --git a/app/test/helpers/auth.ts b/app/test/helpers/auth.ts index b8e7dadd..46479ffc 100644 --- a/app/test/helpers/auth.ts +++ b/app/test/helpers/auth.ts @@ -1,7 +1,8 @@ import { db } from "~/server/db/db"; -import { sessionsTable, usersTable, account } from "~/server/db/schema"; +import { sessionsTable, usersTable, account, organization, member } from "~/server/db/schema"; import { hashPassword } from "better-auth/crypto"; import { createHmac } from "node:crypto"; +import { eq } from "drizzle-orm"; export async function createTestSession() { const [existingUser] = await db.select().from(usersTable); @@ -21,6 +22,34 @@ export async function createTestSession() { const sessionId = token; const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); + const [existingOrg] = await db.select().from(organization); + + let orgId: string; + + if (!existingOrg) { + orgId = crypto.randomUUID(); + await db.insert(organization).values({ + id: orgId, + name: "Test Org", + slug: "test-org", + createdAt: new Date(), + }); + } else { + orgId = existingOrg.id; + } + + const [existingMember] = await db.select().from(member).where(eq(member.userId, user.id)); + + if (!existingMember) { + await db.insert(member).values({ + id: crypto.randomUUID(), + userId: user.id, + organizationId: orgId, + role: "owner", + createdAt: new Date(), + }); + } + await db.insert(sessionsTable).values({ id: sessionId, userId: user.id, @@ -28,12 +57,10 @@ export async function createTestSession() { token: token, createdAt: new Date(), updatedAt: new Date(), + activeOrganizationId: orgId, }); - // Better Auth signs the token using HMAC-SHA256 with the secret - // The secret is "test-secret" because we mocked cryptoUtils.deriveSecret const signature = createHmac("sha256", "test-secret").update(token).digest("base64"); - const signedToken = `${token}.${signature}`; await db @@ -49,5 +76,5 @@ export async function createTestSession() { }) .onConflictDoNothing(); - return { token: encodeURIComponent(signedToken), user }; + return { token: encodeURIComponent(signedToken), user, organizationId: orgId }; } diff --git a/app/test/helpers/backup.ts b/app/test/helpers/backup.ts index 67cae6f0..59256086 100644 --- a/app/test/helpers/backup.ts +++ b/app/test/helpers/backup.ts @@ -1,14 +1,18 @@ import { db } from "~/server/db/db"; import { faker } from "@faker-js/faker"; import { backupSchedulesTable, type BackupScheduleInsert } from "~/server/db/schema"; +import { ensureTestOrganization, TEST_ORG_ID } from "./organization"; export const createTestBackupSchedule = async (overrides: Partial = {}) => { + await ensureTestOrganization(); + const backup: BackupScheduleInsert = { name: faker.system.fileName(), cronExpression: "0 0 * * *", repositoryId: "repo_123", volumeId: 1, shortId: faker.string.uuid(), + organizationId: TEST_ORG_ID, ...overrides, }; diff --git a/app/test/helpers/organization.ts b/app/test/helpers/organization.ts new file mode 100644 index 00000000..0a356c18 --- /dev/null +++ b/app/test/helpers/organization.ts @@ -0,0 +1,35 @@ +import { db } from "~/server/db/db"; +import { organization, type OrganizationMetadata } from "~/server/db/schema"; +import { faker } from "@faker-js/faker"; + +export const TEST_ORG_ID = "test-org-00000001"; + +export const createTestOrganization = async (overrides: Partial = {}) => { + const metadata: OrganizationMetadata = { + resticPassword: "test-encrypted-restic-password", + }; + + const org: typeof organization.$inferInsert = { + id: TEST_ORG_ID, + name: "Test Organization", + slug: `test-org-${faker.string.alphanumeric(6)}`, + createdAt: new Date(), + metadata, + ...overrides, + }; + + const existing = await db.query.organization.findFirst({ + where: (o, { eq }) => eq(o.id, org.id ?? TEST_ORG_ID), + }); + + if (existing) { + return existing; + } + + const data = await db.insert(organization).values(org).returning(); + return data[0]; +}; + +export const ensureTestOrganization = async () => { + return createTestOrganization(); +}; diff --git a/app/test/helpers/repository.ts b/app/test/helpers/repository.ts index 0a15c59e..dd8592cd 100644 --- a/app/test/helpers/repository.ts +++ b/app/test/helpers/repository.ts @@ -1,8 +1,11 @@ import { db } from "~/server/db/db"; import { faker } from "@faker-js/faker"; import { repositoriesTable, type RepositoryInsert } from "~/server/db/schema"; +import { ensureTestOrganization, TEST_ORG_ID } from "./organization"; export const createTestRepository = async (overrides: Partial = {}) => { + await ensureTestOrganization(); + const repository: RepositoryInsert = { id: faker.string.alphanumeric(6), name: faker.string.alphanumeric(10), @@ -12,6 +15,7 @@ export const createTestRepository = async (overrides: Partial backend: "local", }, type: "local", + organizationId: TEST_ORG_ID, ...overrides, }; diff --git a/app/test/helpers/volume.ts b/app/test/helpers/volume.ts index 0d63740d..a60f137d 100644 --- a/app/test/helpers/volume.ts +++ b/app/test/helpers/volume.ts @@ -1,8 +1,11 @@ import { db } from "~/server/db/db"; import { faker } from "@faker-js/faker"; import { volumesTable, type VolumeInsert } from "~/server/db/schema"; +import { ensureTestOrganization, TEST_ORG_ID } from "./organization"; export const createTestVolume = async (overrides: Partial = {}) => { + await ensureTestOrganization(); + const volume: VolumeInsert = { name: faker.system.fileName(), config: { @@ -13,6 +16,7 @@ export const createTestVolume = async (overrides: Partial = {}) => autoRemount: true, shortId: faker.string.alphanumeric(6), type: "directory", + organizationId: TEST_ORG_ID, ...overrides, }; diff --git a/app/test/setup.ts b/app/test/setup.ts index ea73e5b0..e986c495 100644 --- a/app/test/setup.ts +++ b/app/test/setup.ts @@ -22,6 +22,7 @@ void mock.module("~/server/utils/crypto", () => ({ deriveSecret: async () => "test-secret", sealSecret: async (v: string) => v, resolveSecret: async (v: string) => v, + generateResticPassword: () => "test-restic-password", }, }));