refactor: extract shared test fixtures and secret helpers
This commit is contained in:
parent
26203cca59
commit
c64862f604
10 changed files with 174 additions and 394 deletions
|
|
@ -15,27 +15,33 @@ vi.mock("@tanstack/react-router", async (importOriginal) => {
|
||||||
|
|
||||||
import { EditBackupPage } from "../edit-backup";
|
import { EditBackupPage } from "../edit-backup";
|
||||||
|
|
||||||
afterEach(() => {
|
const repository = { shortId: "repo-1", name: "Repo 1", type: "local" };
|
||||||
navigateMock.mockClear();
|
const volume = {
|
||||||
cleanup();
|
id: "volume-1",
|
||||||
});
|
shortId: "vol-1",
|
||||||
|
name: "Volume 1",
|
||||||
|
config: { backend: "directory", path: "/mnt" },
|
||||||
|
};
|
||||||
|
const volumeFilesResponse = {
|
||||||
|
files: [{ name: "project", path: "/project", type: "directory" }],
|
||||||
|
path: "/",
|
||||||
|
offset: 0,
|
||||||
|
limit: 100,
|
||||||
|
total: 1,
|
||||||
|
hasMore: false,
|
||||||
|
};
|
||||||
|
|
||||||
test("submits the computed cron expression when saving a daily schedule", async () => {
|
const renderEditBackupPage = ({ enabled, cronExpression }: { enabled: boolean; cronExpression: string }) => {
|
||||||
const submittedBody = new Promise<Record<string, unknown>>((resolve) => {
|
const submittedBody = new Promise<Record<string, unknown>>((resolve) => {
|
||||||
server.use(
|
server.use(
|
||||||
http.get("/api/v1/backups/:shortId", () => {
|
http.get("/api/v1/backups/:shortId", () => {
|
||||||
return HttpResponse.json({
|
return HttpResponse.json({
|
||||||
shortId: "backup-1",
|
shortId: "backup-1",
|
||||||
name: "Backup 1",
|
name: "Backup 1",
|
||||||
enabled: true,
|
enabled,
|
||||||
repository: { shortId: "repo-1", name: "Repo 1", type: "local" },
|
repository,
|
||||||
volume: {
|
volume,
|
||||||
id: "volume-1",
|
cronExpression,
|
||||||
shortId: "vol-1",
|
|
||||||
name: "Volume 1",
|
|
||||||
config: { backend: "directory", path: "/mnt" },
|
|
||||||
},
|
|
||||||
cronExpression: "0 2 * * *",
|
|
||||||
retentionPolicy: null,
|
retentionPolicy: null,
|
||||||
includePaths: ["/project"],
|
includePaths: ["/project"],
|
||||||
includePatterns: [],
|
includePatterns: [],
|
||||||
|
|
@ -46,17 +52,10 @@ test("submits the computed cron expression when saving a daily schedule", async
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
http.get("/api/v1/repositories", () => {
|
http.get("/api/v1/repositories", () => {
|
||||||
return HttpResponse.json([{ shortId: "repo-1", name: "Repo 1", type: "local" }]);
|
return HttpResponse.json([repository]);
|
||||||
}),
|
}),
|
||||||
http.get("/api/v1/volumes/:shortId/files", () => {
|
http.get("/api/v1/volumes/:shortId/files", () => {
|
||||||
return HttpResponse.json({
|
return HttpResponse.json(volumeFilesResponse);
|
||||||
files: [{ name: "project", path: "/project", type: "directory" }],
|
|
||||||
path: "/",
|
|
||||||
offset: 0,
|
|
||||||
limit: 100,
|
|
||||||
total: 1,
|
|
||||||
hasMore: false,
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
http.patch("/api/v1/backups/:shortId", async ({ request }) => {
|
http.patch("/api/v1/backups/:shortId", async ({ request }) => {
|
||||||
const body = (await request.json()) as Record<string, unknown>;
|
const body = (await request.json()) as Record<string, unknown>;
|
||||||
|
|
@ -64,13 +63,8 @@ test("submits the computed cron expression when saving a daily schedule", async
|
||||||
|
|
||||||
return HttpResponse.json({
|
return HttpResponse.json({
|
||||||
shortId: "backup-1",
|
shortId: "backup-1",
|
||||||
volume: {
|
volume,
|
||||||
id: "volume-1",
|
repository,
|
||||||
shortId: "vol-1",
|
|
||||||
name: "Volume 1",
|
|
||||||
config: { backend: "directory", path: "/mnt" },
|
|
||||||
},
|
|
||||||
repository: { shortId: "repo-1", name: "Repo 1", type: "local" },
|
|
||||||
...body,
|
...body,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
@ -79,6 +73,17 @@ test("submits the computed cron expression when saving a daily schedule", async
|
||||||
|
|
||||||
render(<EditBackupPage backupId="backup-1" />, { withSuspense: true });
|
render(<EditBackupPage backupId="backup-1" />, { withSuspense: true });
|
||||||
|
|
||||||
|
return { submittedBody };
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
navigateMock.mockClear();
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("submits the computed cron expression when saving a daily schedule", async () => {
|
||||||
|
const { submittedBody } = renderEditBackupPage({ enabled: true, cronExpression: "0 2 * * *" });
|
||||||
|
|
||||||
await userEvent.click(await screen.findByRole("button", { name: "Update schedule" }));
|
await userEvent.click(await screen.findByRole("button", { name: "Update schedule" }));
|
||||||
|
|
||||||
await expect(submittedBody).resolves.toMatchObject({
|
await expect(submittedBody).resolves.toMatchObject({
|
||||||
|
|
@ -89,63 +94,7 @@ test("submits the computed cron expression when saving a daily schedule", async
|
||||||
});
|
});
|
||||||
|
|
||||||
test("disables the schedule when switching an enabled custom cron schedule to manual only", async () => {
|
test("disables the schedule when switching an enabled custom cron schedule to manual only", async () => {
|
||||||
const submittedBody = new Promise<Record<string, unknown>>((resolve) => {
|
const { submittedBody } = renderEditBackupPage({ enabled: true, cronExpression: "*/13 * * * *" });
|
||||||
server.use(
|
|
||||||
http.get("/api/v1/backups/:shortId", () => {
|
|
||||||
return HttpResponse.json({
|
|
||||||
shortId: "backup-1",
|
|
||||||
name: "Backup 1",
|
|
||||||
enabled: true,
|
|
||||||
repository: { shortId: "repo-1", name: "Repo 1", type: "local" },
|
|
||||||
volume: {
|
|
||||||
id: "volume-1",
|
|
||||||
shortId: "vol-1",
|
|
||||||
name: "Volume 1",
|
|
||||||
config: { backend: "directory", path: "/mnt" },
|
|
||||||
},
|
|
||||||
cronExpression: "*/13 * * * *",
|
|
||||||
retentionPolicy: null,
|
|
||||||
includePaths: ["/project"],
|
|
||||||
includePatterns: [],
|
|
||||||
excludePatterns: [],
|
|
||||||
excludeIfPresent: [],
|
|
||||||
oneFileSystem: false,
|
|
||||||
customResticParams: [],
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
http.get("/api/v1/repositories", () => {
|
|
||||||
return HttpResponse.json([{ shortId: "repo-1", name: "Repo 1", type: "local" }]);
|
|
||||||
}),
|
|
||||||
http.get("/api/v1/volumes/:shortId/files", () => {
|
|
||||||
return HttpResponse.json({
|
|
||||||
files: [{ name: "project", path: "/project", type: "directory" }],
|
|
||||||
path: "/",
|
|
||||||
offset: 0,
|
|
||||||
limit: 100,
|
|
||||||
total: 1,
|
|
||||||
hasMore: false,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
http.patch("/api/v1/backups/:shortId", async ({ request }) => {
|
|
||||||
const body = (await request.json()) as Record<string, unknown>;
|
|
||||||
resolve(body);
|
|
||||||
|
|
||||||
return HttpResponse.json({
|
|
||||||
shortId: "backup-1",
|
|
||||||
volume: {
|
|
||||||
id: "volume-1",
|
|
||||||
shortId: "vol-1",
|
|
||||||
name: "Volume 1",
|
|
||||||
config: { backend: "directory", path: "/mnt" },
|
|
||||||
},
|
|
||||||
repository: { shortId: "repo-1", name: "Repo 1", type: "local" },
|
|
||||||
...body,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<EditBackupPage backupId="backup-1" />, { withSuspense: true });
|
|
||||||
|
|
||||||
const nativeFrequencySelect = (await screen.findAllByRole("combobox"))
|
const nativeFrequencySelect = (await screen.findAllByRole("combobox"))
|
||||||
.at(1)
|
.at(1)
|
||||||
|
|
@ -165,63 +114,7 @@ test("disables the schedule when switching an enabled custom cron schedule to ma
|
||||||
});
|
});
|
||||||
|
|
||||||
test("preserves a disabled schedule when saving a non-manual frequency", async () => {
|
test("preserves a disabled schedule when saving a non-manual frequency", async () => {
|
||||||
const submittedBody = new Promise<Record<string, unknown>>((resolve) => {
|
const { submittedBody } = renderEditBackupPage({ enabled: false, cronExpression: "0 2 * * *" });
|
||||||
server.use(
|
|
||||||
http.get("/api/v1/backups/:shortId", () => {
|
|
||||||
return HttpResponse.json({
|
|
||||||
shortId: "backup-1",
|
|
||||||
name: "Backup 1",
|
|
||||||
enabled: false,
|
|
||||||
repository: { shortId: "repo-1", name: "Repo 1", type: "local" },
|
|
||||||
volume: {
|
|
||||||
id: "volume-1",
|
|
||||||
shortId: "vol-1",
|
|
||||||
name: "Volume 1",
|
|
||||||
config: { backend: "directory", path: "/mnt" },
|
|
||||||
},
|
|
||||||
cronExpression: "0 2 * * *",
|
|
||||||
retentionPolicy: null,
|
|
||||||
includePaths: ["/project"],
|
|
||||||
includePatterns: [],
|
|
||||||
excludePatterns: [],
|
|
||||||
excludeIfPresent: [],
|
|
||||||
oneFileSystem: false,
|
|
||||||
customResticParams: [],
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
http.get("/api/v1/repositories", () => {
|
|
||||||
return HttpResponse.json([{ shortId: "repo-1", name: "Repo 1", type: "local" }]);
|
|
||||||
}),
|
|
||||||
http.get("/api/v1/volumes/:shortId/files", () => {
|
|
||||||
return HttpResponse.json({
|
|
||||||
files: [{ name: "project", path: "/project", type: "directory" }],
|
|
||||||
path: "/",
|
|
||||||
offset: 0,
|
|
||||||
limit: 100,
|
|
||||||
total: 1,
|
|
||||||
hasMore: false,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
http.patch("/api/v1/backups/:shortId", async ({ request }) => {
|
|
||||||
const body = (await request.json()) as Record<string, unknown>;
|
|
||||||
resolve(body);
|
|
||||||
|
|
||||||
return HttpResponse.json({
|
|
||||||
shortId: "backup-1",
|
|
||||||
volume: {
|
|
||||||
id: "volume-1",
|
|
||||||
shortId: "vol-1",
|
|
||||||
name: "Volume 1",
|
|
||||||
config: { backend: "directory", path: "/mnt" },
|
|
||||||
},
|
|
||||||
repository: { shortId: "repo-1", name: "Repo 1", type: "local" },
|
|
||||||
...body,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
render(<EditBackupPage backupId="backup-1" />, { withSuspense: true });
|
|
||||||
|
|
||||||
await userEvent.click(await screen.findByRole("button", { name: "Update schedule" }));
|
await userEvent.click(await screen.findByRole("button", { name: "Update schedule" }));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,21 @@
|
||||||
import { beforeEach, describe, expect, test } from "vitest";
|
import { beforeEach, describe, expect, test } from "vitest";
|
||||||
import { db, sqlite } from "~/server/db/db";
|
import { db, sqlite } from "~/server/db/db";
|
||||||
import { account, member, organization, sessionsTable, usersTable } from "~/server/db/schema";
|
import { account, member, organization, sessionsTable, usersTable } from "~/server/db/schema";
|
||||||
|
import {
|
||||||
|
createMembership,
|
||||||
|
createOrganization,
|
||||||
|
createSession,
|
||||||
|
createUser,
|
||||||
|
dropTrigger,
|
||||||
|
escapeSqlLiteral,
|
||||||
|
randomId,
|
||||||
|
randomSlug,
|
||||||
|
} from "~/test/helpers/user-org";
|
||||||
import { authService } from "../auth.service";
|
import { authService } from "../auth.service";
|
||||||
|
|
||||||
const DELETE_USER_ACCOUNT_ROLLBACK_TRIGGER = "delete_user_account_abort";
|
const DELETE_USER_ACCOUNT_ROLLBACK_TRIGGER = "delete_user_account_abort";
|
||||||
const REMOVE_ORG_MEMBER_ROLLBACK_TRIGGER = "remove_org_member_reassign_abort";
|
const REMOVE_ORG_MEMBER_ROLLBACK_TRIGGER = "remove_org_member_reassign_abort";
|
||||||
|
|
||||||
function randomId() {
|
|
||||||
return Bun.randomUUIDv7();
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomSlug(prefix: string) {
|
|
||||||
return `${prefix}-${Math.random().toString(36).slice(2, 8)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeSqlLiteral(value: string) {
|
|
||||||
return value.replaceAll("'", "''");
|
|
||||||
}
|
|
||||||
|
|
||||||
function dropTrigger(name: string) {
|
|
||||||
sqlite.exec(`DROP TRIGGER IF EXISTS ${name};`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createUser(email: string) {
|
|
||||||
const id = randomId();
|
|
||||||
|
|
||||||
await db.insert(usersTable).values({
|
|
||||||
id,
|
|
||||||
email,
|
|
||||||
name: email.split("@")[0],
|
|
||||||
username: randomSlug("user"),
|
|
||||||
});
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createOrganization(name: string) {
|
|
||||||
const id = randomId();
|
|
||||||
|
|
||||||
await db.insert(organization).values({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
slug: randomSlug("org"),
|
|
||||||
createdAt: new Date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createMembership({
|
|
||||||
userId,
|
|
||||||
organizationId,
|
|
||||||
role,
|
|
||||||
}: {
|
|
||||||
userId: string;
|
|
||||||
organizationId: string;
|
|
||||||
role: "owner" | "admin" | "member";
|
|
||||||
}) {
|
|
||||||
const id = randomId();
|
|
||||||
await db.insert(member).values({ id, userId, organizationId, role, createdAt: new Date() });
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createSession({
|
|
||||||
userId,
|
|
||||||
activeOrganizationId,
|
|
||||||
}: {
|
|
||||||
userId: string;
|
|
||||||
activeOrganizationId: string | null;
|
|
||||||
}) {
|
|
||||||
await db.insert(sessionsTable).values({
|
|
||||||
id: randomId(),
|
|
||||||
userId,
|
|
||||||
token: randomSlug("token"),
|
|
||||||
expiresAt: new Date(Date.now() + 60_000),
|
|
||||||
activeOrganizationId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createAccount({ userId, providerId }: { userId: string; providerId: string }) {
|
async function createAccount({ userId, providerId }: { userId: string; providerId: string }) {
|
||||||
const id = randomId();
|
const id = randomId();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,19 @@
|
||||||
import { beforeEach, describe, expect, test } from "vitest";
|
import { beforeEach, describe, expect, test } from "vitest";
|
||||||
import { db, sqlite } from "~/server/db/db";
|
import { db, sqlite } from "~/server/db/db";
|
||||||
import { member, organization, sessionsTable, usersTable } from "~/server/db/schema";
|
import { member, organization, sessionsTable, usersTable } from "~/server/db/schema";
|
||||||
|
import {
|
||||||
|
createMembership,
|
||||||
|
createOrganization,
|
||||||
|
createSession,
|
||||||
|
createUser,
|
||||||
|
dropTrigger,
|
||||||
|
escapeSqlLiteral,
|
||||||
|
randomSlug,
|
||||||
|
} from "~/test/helpers/user-org";
|
||||||
import { authService } from "../auth.service";
|
import { authService } from "../auth.service";
|
||||||
|
|
||||||
const CLEANUP_USER_ORGS_ROLLBACK_TRIGGER = "cleanup_user_orgs_final_session_abort";
|
const CLEANUP_USER_ORGS_ROLLBACK_TRIGGER = "cleanup_user_orgs_final_session_abort";
|
||||||
|
|
||||||
function randomId() {
|
|
||||||
return Bun.randomUUIDv7();
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomSlug(prefix: string) {
|
|
||||||
return `${prefix}-${Math.random().toString(36).slice(2, 8)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeSqlLiteral(value: string) {
|
|
||||||
return value.replaceAll("'", "''");
|
|
||||||
}
|
|
||||||
|
|
||||||
function dropTrigger(name: string) {
|
|
||||||
sqlite.exec(`DROP TRIGGER IF EXISTS ${name};`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createUser(email: string) {
|
|
||||||
const id = randomId();
|
|
||||||
|
|
||||||
await db.insert(usersTable).values({
|
|
||||||
id,
|
|
||||||
email,
|
|
||||||
name: email.split("@")[0],
|
|
||||||
username: randomSlug("user"),
|
|
||||||
});
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createOrganization(name: string) {
|
|
||||||
const id = randomId();
|
|
||||||
|
|
||||||
await db.insert(organization).values({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
slug: randomSlug("org"),
|
|
||||||
createdAt: new Date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createMembership({
|
|
||||||
userId,
|
|
||||||
organizationId,
|
|
||||||
role,
|
|
||||||
}: {
|
|
||||||
userId: string;
|
|
||||||
organizationId: string;
|
|
||||||
role: "owner" | "admin" | "member";
|
|
||||||
}) {
|
|
||||||
await db.insert(member).values({
|
|
||||||
id: randomId(),
|
|
||||||
userId,
|
|
||||||
organizationId,
|
|
||||||
role,
|
|
||||||
createdAt: new Date(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createSession({
|
|
||||||
userId,
|
|
||||||
activeOrganizationId,
|
|
||||||
}: {
|
|
||||||
userId: string;
|
|
||||||
activeOrganizationId: string | null;
|
|
||||||
}) {
|
|
||||||
const id = randomId();
|
|
||||||
|
|
||||||
await db.insert(sessionsTable).values({
|
|
||||||
id,
|
|
||||||
userId,
|
|
||||||
token: randomSlug("token"),
|
|
||||||
expiresAt: new Date(Date.now() + 60_000),
|
|
||||||
activeOrganizationId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("authService.cleanupUserOrganizations", () => {
|
describe("authService.cleanupUserOrganizations", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
dropTrigger(CLEANUP_USER_ORGS_ROLLBACK_TRIGGER);
|
dropTrigger(CLEANUP_USER_ORGS_ROLLBACK_TRIGGER);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,6 @@
|
||||||
import { cryptoUtils } from "~/server/utils/crypto";
|
import { cryptoUtils, transformOptionalSecret, type SecretTransformer } from "~/server/utils/crypto";
|
||||||
import type { NotificationConfig } from "~/schemas/notifications";
|
import type { NotificationConfig } from "~/schemas/notifications";
|
||||||
|
|
||||||
type SecretTransformer = (value: string) => Promise<string>;
|
|
||||||
|
|
||||||
const transformOptionalSecret = async (value: string | undefined, transformSecret: SecretTransformer) => {
|
|
||||||
if (!value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await transformSecret(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapNotificationConfigSecrets = async (config: NotificationConfig, transformSecret: SecretTransformer) => {
|
export const mapNotificationConfigSecrets = async (config: NotificationConfig, transformSecret: SecretTransformer) => {
|
||||||
switch (config.type) {
|
switch (config.type) {
|
||||||
case "email":
|
case "email":
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,5 @@
|
||||||
import type { RepositoryConfig } from "@zerobyte/core/restic";
|
import type { RepositoryConfig } from "@zerobyte/core/restic";
|
||||||
import { cryptoUtils } from "~/server/utils/crypto";
|
import { cryptoUtils, transformOptionalSecret, type SecretTransformer } from "~/server/utils/crypto";
|
||||||
|
|
||||||
type SecretTransformer = (value: string) => Promise<string>;
|
|
||||||
|
|
||||||
const transformOptionalSecret = async (
|
|
||||||
value: string | undefined,
|
|
||||||
transformSecret: SecretTransformer,
|
|
||||||
): Promise<string | undefined> => {
|
|
||||||
if (!value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await transformSecret(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapRepositoryConfigSecrets = async (
|
export const mapRepositoryConfigSecrets = async (
|
||||||
config: RepositoryConfig,
|
config: RepositoryConfig,
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,19 @@
|
||||||
import { beforeEach, describe, expect, test } from "vitest";
|
import { beforeEach, describe, expect, test } from "vitest";
|
||||||
import { db, sqlite } from "~/server/db/db";
|
import { db, sqlite } from "~/server/db/db";
|
||||||
import { account, invitation, member, organization, sessionsTable, ssoProvider, usersTable } from "~/server/db/schema";
|
import { account, invitation, member, organization, sessionsTable, ssoProvider, usersTable } from "~/server/db/schema";
|
||||||
|
import {
|
||||||
|
createOrganization,
|
||||||
|
createSession,
|
||||||
|
createUser,
|
||||||
|
dropTrigger,
|
||||||
|
escapeSqlLiteral,
|
||||||
|
randomId,
|
||||||
|
randomSlug,
|
||||||
|
} from "~/test/helpers/user-org";
|
||||||
import { ssoService } from "../sso.service";
|
import { ssoService } from "../sso.service";
|
||||||
|
|
||||||
const DELETE_SSO_PROVIDER_ROLLBACK_TRIGGER = "delete_sso_provider_sessions_abort";
|
const DELETE_SSO_PROVIDER_ROLLBACK_TRIGGER = "delete_sso_provider_sessions_abort";
|
||||||
|
|
||||||
function randomId() {
|
|
||||||
return Bun.randomUUIDv7();
|
|
||||||
}
|
|
||||||
|
|
||||||
function randomSlug(prefix: string) {
|
|
||||||
return `${prefix}-${Math.random().toString(36).slice(2, 8)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeSqlLiteral(value: string) {
|
|
||||||
return value.replaceAll("'", "''");
|
|
||||||
}
|
|
||||||
|
|
||||||
function dropTrigger(name: string) {
|
|
||||||
sqlite.exec(`DROP TRIGGER IF EXISTS ${name};`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createUser(email: string) {
|
|
||||||
const id = randomId();
|
|
||||||
|
|
||||||
await db.insert(usersTable).values({
|
|
||||||
id,
|
|
||||||
email,
|
|
||||||
name: email.split("@")[0],
|
|
||||||
username: randomSlug("user"),
|
|
||||||
});
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createOrganization(name: string) {
|
|
||||||
const id = randomId();
|
|
||||||
|
|
||||||
await db.insert(organization).values({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
slug: randomSlug("org"),
|
|
||||||
createdAt: new Date(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createSession(userId: string) {
|
|
||||||
await db.insert(sessionsTable).values({
|
|
||||||
id: randomId(),
|
|
||||||
userId,
|
|
||||||
token: randomSlug("token"),
|
|
||||||
expiresAt: new Date(Date.now() + 60_000),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("ssoService.deleteSsoProvider", () => {
|
describe("ssoService.deleteSsoProvider", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
dropTrigger(DELETE_SSO_PROVIDER_ROLLBACK_TRIGGER);
|
dropTrigger(DELETE_SSO_PROVIDER_ROLLBACK_TRIGGER);
|
||||||
|
|
@ -140,8 +98,8 @@ describe("ssoService.deleteSsoProvider", () => {
|
||||||
userId: accountUserB,
|
userId: accountUserB,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
await createSession(accountUserA);
|
await createSession({ userId: accountUserA });
|
||||||
await createSession(accountUserB);
|
await createSession({ userId: accountUserB });
|
||||||
|
|
||||||
const deleted = await ssoService.deleteSsoProvider(providerId, org);
|
const deleted = await ssoService.deleteSsoProvider(providerId, org);
|
||||||
|
|
||||||
|
|
@ -225,7 +183,7 @@ describe("ssoService.deleteSsoProvider", () => {
|
||||||
providerId,
|
providerId,
|
||||||
userId: accountUser,
|
userId: accountUser,
|
||||||
});
|
});
|
||||||
await createSession(accountUser);
|
await createSession({ userId: accountUser });
|
||||||
|
|
||||||
sqlite.exec(`
|
sqlite.exec(`
|
||||||
CREATE TRIGGER ${DELETE_SSO_PROVIDER_ROLLBACK_TRIGGER}
|
CREATE TRIGGER ${DELETE_SSO_PROVIDER_ROLLBACK_TRIGGER}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,5 @@
|
||||||
import type { BackendConfig } from "~/schemas/volumes";
|
import type { BackendConfig } from "~/schemas/volumes";
|
||||||
import { cryptoUtils } from "~/server/utils/crypto";
|
import { cryptoUtils, transformOptionalSecret, type SecretTransformer } from "~/server/utils/crypto";
|
||||||
|
|
||||||
type SecretTransformer = (value: string) => Promise<string>;
|
|
||||||
|
|
||||||
const transformOptionalSecret = async (
|
|
||||||
value: string | undefined,
|
|
||||||
transformSecret: SecretTransformer,
|
|
||||||
): Promise<string | undefined> => {
|
|
||||||
if (!value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await transformSecret(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mapVolumeConfigSecrets = async (
|
export const mapVolumeConfigSecrets = async (
|
||||||
config: BackendConfig,
|
config: BackendConfig,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,19 @@ const algorithm = "aes-256-gcm" as const;
|
||||||
const keyLength = 32;
|
const keyLength = 32;
|
||||||
const encryptionPrefix = "encv1:";
|
const encryptionPrefix = "encv1:";
|
||||||
|
|
||||||
|
export type SecretTransformer = (value: string) => Promise<string>;
|
||||||
|
|
||||||
|
export const transformOptionalSecret = async (
|
||||||
|
value: string | undefined,
|
||||||
|
transformSecret: SecretTransformer,
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
if (!value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await transformSecret(value);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a given string is encrypted by looking for the encryption prefix.
|
* Checks if a given string is encrypted by looking for the encryption prefix.
|
||||||
*/
|
*/
|
||||||
|
|
@ -90,11 +103,7 @@ const sealSecret = async (value: string): Promise<string> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const sealOptionalSecret = async (value?: string): Promise<string | undefined> => {
|
const sealOptionalSecret = async (value?: string): Promise<string | undefined> => {
|
||||||
if (typeof value === "undefined") {
|
return transformOptionalSecret(value, sealSecret);
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return sealSecret(value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function deriveSecret(label: string) {
|
async function deriveSecret(label: string) {
|
||||||
|
|
|
||||||
88
app/test/helpers/user-org.ts
Normal file
88
app/test/helpers/user-org.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { db, sqlite } from "~/server/db/db";
|
||||||
|
import { member, organization, sessionsTable, usersTable } from "~/server/db/schema";
|
||||||
|
|
||||||
|
type MembershipRole = "owner" | "admin" | "member";
|
||||||
|
|
||||||
|
export function randomId() {
|
||||||
|
return Bun.randomUUIDv7();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randomSlug(prefix: string) {
|
||||||
|
return `${prefix}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeSqlLiteral(value: string) {
|
||||||
|
return value.replaceAll("'", "''");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dropTrigger(name: string) {
|
||||||
|
sqlite.exec(`DROP TRIGGER IF EXISTS ${name};`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createUser(email: string) {
|
||||||
|
const id = randomId();
|
||||||
|
|
||||||
|
await db.insert(usersTable).values({
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
name: email.split("@")[0],
|
||||||
|
username: randomSlug("user"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createOrganization(name: string) {
|
||||||
|
const id = randomId();
|
||||||
|
|
||||||
|
await db.insert(organization).values({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
slug: randomSlug("org"),
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMembership({
|
||||||
|
userId,
|
||||||
|
organizationId,
|
||||||
|
role,
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
organizationId: string;
|
||||||
|
role: MembershipRole;
|
||||||
|
}) {
|
||||||
|
const id = randomId();
|
||||||
|
|
||||||
|
await db.insert(member).values({
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
organizationId,
|
||||||
|
role,
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSession({
|
||||||
|
userId,
|
||||||
|
activeOrganizationId = null,
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
activeOrganizationId?: string | null;
|
||||||
|
}) {
|
||||||
|
const id = randomId();
|
||||||
|
|
||||||
|
await db.insert(sessionsTable).values({
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
token: randomSlug("token"),
|
||||||
|
expiresAt: new Date(Date.now() + 60_000),
|
||||||
|
activeOrganizationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ vi.mock(import("~/server/utils/crypto"), async () => {
|
||||||
const cryptoModule = await vi.importActual<typeof import("~/server/utils/crypto")>("~/server/utils/crypto");
|
const cryptoModule = await vi.importActual<typeof import("~/server/utils/crypto")>("~/server/utils/crypto");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...cryptoModule,
|
||||||
cryptoUtils: {
|
cryptoUtils: {
|
||||||
...cryptoModule.cryptoUtils,
|
...cryptoModule.cryptoUtils,
|
||||||
deriveSecret: async () => "test-secret",
|
deriveSecret: async () => "test-secret",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue