test: ensure organization is created
This commit is contained in:
parent
207c88ef7e
commit
203df04214
10 changed files with 103 additions and 26 deletions
|
|
@ -1 +1,2 @@
|
||||||
DATABASE_URL=:memory:
|
DATABASE_URL=:memory:
|
||||||
|
ZEROBYTE_APP_SECRET=8b9acd4456dd5db0a4a3c4f4e1240b2c3ae08bb59690167197425e4a25dd9a69
|
||||||
|
|
|
||||||
11
AGENTS.md
11
AGENTS.md
|
|
@ -75,14 +75,13 @@ bun run gen:api-client
|
||||||
### Code Quality
|
### Code Quality
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Format and lint (Biome)
|
# Format and lint
|
||||||
bunx biome check --write .
|
|
||||||
|
|
||||||
# Format only
|
# Format
|
||||||
bunx biome format --write .
|
bunx oxfmt format --write <path>
|
||||||
|
|
||||||
# Lint only
|
# Lint
|
||||||
bunx biome lint .
|
bun run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { generateBackupOutput } from "~/test/helpers/restic";
|
||||||
import { getVolumePath } from "../../volumes/helpers";
|
import { getVolumePath } from "../../volumes/helpers";
|
||||||
import { restic } from "~/server/utils/restic";
|
import { restic } from "~/server/utils/restic";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { TEST_ORG_ID } from "~/test/helpers/organization";
|
||||||
|
|
||||||
const backupMock = mock(() => Promise.resolve({ exitCode: 0, result: JSON.parse(generateBackupOutput()) }));
|
const backupMock = mock(() => Promise.resolve({ exitCode: 0, result: JSON.parse(generateBackupOutput()) }));
|
||||||
|
|
||||||
|
|
@ -36,7 +37,7 @@ describe("executeBackup - include / exclude patterns", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id);
|
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(backupMock).toHaveBeenCalledWith(
|
expect(backupMock).toHaveBeenCalledWith(
|
||||||
|
|
@ -67,7 +68,7 @@ describe("executeBackup - include / exclude patterns", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id);
|
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(backupMock).toHaveBeenCalledWith(
|
expect(backupMock).toHaveBeenCalledWith(
|
||||||
|
|
@ -97,7 +98,7 @@ describe("executeBackup - include / exclude patterns", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id);
|
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(backupMock).toHaveBeenCalledWith(
|
expect(backupMock).toHaveBeenCalledWith(
|
||||||
|
|
@ -121,7 +122,7 @@ describe("executeBackup - include / exclude patterns", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id);
|
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(backupMock).toHaveBeenCalledWith(
|
expect(backupMock).toHaveBeenCalledWith(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { createTestRepository } from "~/test/helpers/repository";
|
||||||
import { generateBackupOutput } from "~/test/helpers/restic";
|
import { generateBackupOutput } from "~/test/helpers/restic";
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
import * as spawnModule from "~/server/utils/spawn";
|
import * as spawnModule from "~/server/utils/spawn";
|
||||||
|
import { TEST_ORG_ID } from "~/test/helpers/organization";
|
||||||
|
|
||||||
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: "", error: "" }));
|
const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: "", error: "" }));
|
||||||
|
|
||||||
|
|
@ -35,10 +36,10 @@ describe("execute backup", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id);
|
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
const updatedSchedule = await backupsService.getSchedule(schedule.id);
|
const updatedSchedule = await backupsService.getSchedule(schedule.id, TEST_ORG_ID);
|
||||||
expect(updatedSchedule.nextBackupAt).not.toBeNull();
|
expect(updatedSchedule.nextBackupAt).not.toBeNull();
|
||||||
|
|
||||||
const nextBackupAt = new Date(updatedSchedule.nextBackupAt ?? 0);
|
const nextBackupAt = new Date(updatedSchedule.nextBackupAt ?? 0);
|
||||||
|
|
@ -59,7 +60,7 @@ describe("execute backup", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id);
|
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(resticBackupMock).not.toHaveBeenCalled();
|
expect(resticBackupMock).not.toHaveBeenCalled();
|
||||||
|
|
@ -80,7 +81,7 @@ describe("execute backup", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id, true);
|
await backupsService.executeBackup(schedule.id, TEST_ORG_ID, true);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(resticBackupMock).toHaveBeenCalled();
|
expect(resticBackupMock).toHaveBeenCalled();
|
||||||
|
|
@ -101,9 +102,9 @@ describe("execute backup", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
void backupsService.executeBackup(schedule.id);
|
void backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
await backupsService.executeBackup(schedule.id);
|
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(resticBackupMock).toHaveBeenCalledTimes(1);
|
expect(resticBackupMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
@ -123,10 +124,10 @@ describe("execute backup", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id);
|
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
const updatedSchedule = await backupsService.getSchedule(schedule.id);
|
const updatedSchedule = await backupsService.getSchedule(schedule.id, TEST_ORG_ID);
|
||||||
expect(updatedSchedule.lastBackupStatus).toBe("warning");
|
expect(updatedSchedule.lastBackupStatus).toBe("warning");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -144,10 +145,10 @@ describe("execute backup", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
await backupsService.executeBackup(schedule.id);
|
await backupsService.executeBackup(schedule.id, TEST_ORG_ID);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
const updatedSchedule = await backupsService.getSchedule(schedule.id);
|
const updatedSchedule = await backupsService.getSchedule(schedule.id, TEST_ORG_ID);
|
||||||
expect(updatedSchedule.lastBackupStatus).toBe("error");
|
expect(updatedSchedule.lastBackupStatus).toBe("error");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -168,7 +169,7 @@ describe("getSchedulesToExecute", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// act
|
// act
|
||||||
const schedulesToExecute = await backupsService.getSchedulesToExecute();
|
const schedulesToExecute = await backupsService.getSchedulesToExecute(TEST_ORG_ID);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
expect(schedulesToExecute).toContain(schedule.id);
|
expect(schedulesToExecute).toContain(schedule.id);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { db } from "~/server/db/db";
|
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 { hashPassword } from "better-auth/crypto";
|
||||||
import { createHmac } from "node:crypto";
|
import { createHmac } from "node:crypto";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export async function createTestSession() {
|
export async function createTestSession() {
|
||||||
const [existingUser] = await db.select().from(usersTable);
|
const [existingUser] = await db.select().from(usersTable);
|
||||||
|
|
@ -21,6 +22,34 @@ export async function createTestSession() {
|
||||||
const sessionId = token;
|
const sessionId = token;
|
||||||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
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({
|
await db.insert(sessionsTable).values({
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|
@ -28,12 +57,10 @@ export async function createTestSession() {
|
||||||
token: token,
|
token: token,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: 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 signature = createHmac("sha256", "test-secret").update(token).digest("base64");
|
||||||
|
|
||||||
const signedToken = `${token}.${signature}`;
|
const signedToken = `${token}.${signature}`;
|
||||||
|
|
||||||
await db
|
await db
|
||||||
|
|
@ -49,5 +76,5 @@ export async function createTestSession() {
|
||||||
})
|
})
|
||||||
.onConflictDoNothing();
|
.onConflictDoNothing();
|
||||||
|
|
||||||
return { token: encodeURIComponent(signedToken), user };
|
return { token: encodeURIComponent(signedToken), user, organizationId: orgId };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
import { db } from "~/server/db/db";
|
import { db } from "~/server/db/db";
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
import { backupSchedulesTable, type BackupScheduleInsert } from "~/server/db/schema";
|
import { backupSchedulesTable, type BackupScheduleInsert } from "~/server/db/schema";
|
||||||
|
import { ensureTestOrganization, TEST_ORG_ID } from "./organization";
|
||||||
|
|
||||||
export const createTestBackupSchedule = async (overrides: Partial<BackupScheduleInsert> = {}) => {
|
export const createTestBackupSchedule = async (overrides: Partial<BackupScheduleInsert> = {}) => {
|
||||||
|
await ensureTestOrganization();
|
||||||
|
|
||||||
const backup: BackupScheduleInsert = {
|
const backup: BackupScheduleInsert = {
|
||||||
name: faker.system.fileName(),
|
name: faker.system.fileName(),
|
||||||
cronExpression: "0 0 * * *",
|
cronExpression: "0 0 * * *",
|
||||||
repositoryId: "repo_123",
|
repositoryId: "repo_123",
|
||||||
volumeId: 1,
|
volumeId: 1,
|
||||||
shortId: faker.string.uuid(),
|
shortId: faker.string.uuid(),
|
||||||
|
organizationId: TEST_ORG_ID,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
35
app/test/helpers/organization.ts
Normal file
35
app/test/helpers/organization.ts
Normal file
|
|
@ -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<typeof organization.$inferInsert> = {}) => {
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import { db } from "~/server/db/db";
|
import { db } from "~/server/db/db";
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
import { repositoriesTable, type RepositoryInsert } from "~/server/db/schema";
|
import { repositoriesTable, type RepositoryInsert } from "~/server/db/schema";
|
||||||
|
import { ensureTestOrganization, TEST_ORG_ID } from "./organization";
|
||||||
|
|
||||||
export const createTestRepository = async (overrides: Partial<RepositoryInsert> = {}) => {
|
export const createTestRepository = async (overrides: Partial<RepositoryInsert> = {}) => {
|
||||||
|
await ensureTestOrganization();
|
||||||
|
|
||||||
const repository: RepositoryInsert = {
|
const repository: RepositoryInsert = {
|
||||||
id: faker.string.alphanumeric(6),
|
id: faker.string.alphanumeric(6),
|
||||||
name: faker.string.alphanumeric(10),
|
name: faker.string.alphanumeric(10),
|
||||||
|
|
@ -12,6 +15,7 @@ export const createTestRepository = async (overrides: Partial<RepositoryInsert>
|
||||||
backend: "local",
|
backend: "local",
|
||||||
},
|
},
|
||||||
type: "local",
|
type: "local",
|
||||||
|
organizationId: TEST_ORG_ID,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import { db } from "~/server/db/db";
|
import { db } from "~/server/db/db";
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
import { volumesTable, type VolumeInsert } from "~/server/db/schema";
|
import { volumesTable, type VolumeInsert } from "~/server/db/schema";
|
||||||
|
import { ensureTestOrganization, TEST_ORG_ID } from "./organization";
|
||||||
|
|
||||||
export const createTestVolume = async (overrides: Partial<VolumeInsert> = {}) => {
|
export const createTestVolume = async (overrides: Partial<VolumeInsert> = {}) => {
|
||||||
|
await ensureTestOrganization();
|
||||||
|
|
||||||
const volume: VolumeInsert = {
|
const volume: VolumeInsert = {
|
||||||
name: faker.system.fileName(),
|
name: faker.system.fileName(),
|
||||||
config: {
|
config: {
|
||||||
|
|
@ -13,6 +16,7 @@ export const createTestVolume = async (overrides: Partial<VolumeInsert> = {}) =>
|
||||||
autoRemount: true,
|
autoRemount: true,
|
||||||
shortId: faker.string.alphanumeric(6),
|
shortId: faker.string.alphanumeric(6),
|
||||||
type: "directory",
|
type: "directory",
|
||||||
|
organizationId: TEST_ORG_ID,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ void mock.module("~/server/utils/crypto", () => ({
|
||||||
deriveSecret: async () => "test-secret",
|
deriveSecret: async () => "test-secret",
|
||||||
sealSecret: async (v: string) => v,
|
sealSecret: async (v: string) => v,
|
||||||
resolveSecret: async (v: string) => v,
|
resolveSecret: async (v: string) => v,
|
||||||
|
generateResticPassword: () => "test-restic-password",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue