fix: harden onboarding completion
This commit is contained in:
1 parent
e41605640b
commit
dc71084323
44 files changed
+3003
-1000
No files matched your search
@@ -330,7 +330,11 @@ describe("getManageOverview", () => {
|
||||
const baseLayoutId = createId();
|
||||
const mobileLayoutId = createId();
|
||||
const rootSectionId = createId();
|
||||
const containerSectionId = createId();
|
||||
const itemId = createId();
|
||||
const mobileItemId = createId();
|
||||
const offscreenItemId = createId();
|
||||
const nestedItemId = createId();
|
||||
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
await db.insert(boards).values([
|
||||
@@ -356,23 +360,75 @@ describe("getManageOverview", () => {
|
||||
role: "base",
|
||||
},
|
||||
]);
|
||||
await db.insert(sections).values({
|
||||
id: rootSectionId,
|
||||
boardId,
|
||||
kind: "empty",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
});
|
||||
await db.insert(items).values({ id: itemId, boardId, kind: "clock" });
|
||||
await db.insert(itemLayouts).values({
|
||||
itemId,
|
||||
sectionId: rootSectionId,
|
||||
await db.insert(sections).values([
|
||||
{
|
||||
id: rootSectionId,
|
||||
boardId,
|
||||
kind: "empty",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
},
|
||||
{
|
||||
id: containerSectionId,
|
||||
boardId,
|
||||
kind: "container",
|
||||
xOffset: null,
|
||||
yOffset: null,
|
||||
},
|
||||
]);
|
||||
await db.insert(sectionLayouts).values({
|
||||
sectionId: containerSectionId,
|
||||
parentSectionId: rootSectionId,
|
||||
layoutId: baseLayoutId,
|
||||
xOffset: 2,
|
||||
yOffset: 1,
|
||||
width: 4,
|
||||
height: 2,
|
||||
xOffset: 0,
|
||||
yOffset: 4,
|
||||
width: 6,
|
||||
height: 3,
|
||||
});
|
||||
await db.insert(items).values([
|
||||
{ id: itemId, boardId, kind: "clock" },
|
||||
{ id: mobileItemId, boardId, kind: "clock" },
|
||||
{ id: offscreenItemId, boardId, kind: "clock" },
|
||||
{ id: nestedItemId, boardId, kind: "clock" },
|
||||
]);
|
||||
await db.insert(itemLayouts).values([
|
||||
{
|
||||
itemId,
|
||||
sectionId: rootSectionId,
|
||||
layoutId: baseLayoutId,
|
||||
xOffset: 2,
|
||||
yOffset: 1,
|
||||
width: 4,
|
||||
height: 2,
|
||||
},
|
||||
{
|
||||
itemId: mobileItemId,
|
||||
sectionId: rootSectionId,
|
||||
layoutId: mobileLayoutId,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 3,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
itemId: offscreenItemId,
|
||||
sectionId: rootSectionId,
|
||||
layoutId: baseLayoutId,
|
||||
xOffset: 0,
|
||||
yOffset: 12,
|
||||
width: 2,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
itemId: nestedItemId,
|
||||
sectionId: containerSectionId,
|
||||
layoutId: baseLayoutId,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 2,
|
||||
height: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await caller.getManageOverview();
|
||||
|
||||
@@ -388,9 +444,89 @@ describe("getManageOverview", () => {
|
||||
layouts: [{ layoutId: baseLayoutId, sectionId: rootSectionId, width: 4, height: 2 }],
|
||||
},
|
||||
],
|
||||
sections: [
|
||||
{ id: rootSectionId, kind: "empty" },
|
||||
{
|
||||
id: containerSectionId,
|
||||
kind: "container",
|
||||
layouts: [{ layoutId: baseLayoutId, parentSectionId: rootSectionId, yOffset: 4 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(result[0]?.preview?.layouts).toHaveLength(1);
|
||||
expect(result[0]?.preview?.items.map((item) => item.id)).toEqual([itemId]);
|
||||
expect(result[0]?.preview?.sections).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("uses the highest-breakpoint layout when a board has no base layout", async () => {
|
||||
const db = createDb();
|
||||
const caller = boardRouter.createCaller({ db, deviceType: undefined, session: defaultSession });
|
||||
const boardId = createId();
|
||||
const mobileLayoutId = createId();
|
||||
const tabletLayoutId = createId();
|
||||
const rootSectionId = createId();
|
||||
const mobileItemId = createId();
|
||||
const tabletItemId = createId();
|
||||
|
||||
await db.insert(users).values({ id: defaultCreatorId });
|
||||
await db.insert(boards).values({ id: boardId, name: "fallback-overview", creatorId: defaultCreatorId });
|
||||
await db.insert(layouts).values([
|
||||
{
|
||||
id: mobileLayoutId,
|
||||
name: "Mobile",
|
||||
boardId,
|
||||
columnCount: 3,
|
||||
breakpoint: 0,
|
||||
role: "mobile",
|
||||
},
|
||||
{
|
||||
id: tabletLayoutId,
|
||||
name: "Tablet",
|
||||
boardId,
|
||||
columnCount: 8,
|
||||
breakpoint: 600,
|
||||
role: "custom",
|
||||
},
|
||||
]);
|
||||
await db.insert(sections).values({
|
||||
id: rootSectionId,
|
||||
boardId,
|
||||
kind: "empty",
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
});
|
||||
await db.insert(items).values([
|
||||
{ id: mobileItemId, boardId, kind: "clock" },
|
||||
{ id: tabletItemId, boardId, kind: "clock" },
|
||||
]);
|
||||
await db.insert(itemLayouts).values([
|
||||
{
|
||||
itemId: mobileItemId,
|
||||
sectionId: rootSectionId,
|
||||
layoutId: mobileLayoutId,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 3,
|
||||
height: 1,
|
||||
},
|
||||
{
|
||||
itemId: tabletItemId,
|
||||
sectionId: rootSectionId,
|
||||
layoutId: tabletLayoutId,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await caller.getManageOverview();
|
||||
|
||||
expect(result[0]?.preview?.layouts).toEqual([
|
||||
expect.objectContaining({ id: tabletLayoutId, breakpoint: 600, role: "custom" }),
|
||||
]);
|
||||
expect(result[0]?.preview?.items.map((item) => item.id)).toEqual([tabletItemId]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { groups, groupPermissions, onboarding } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
|
||||
import { groupRouter } from "../group";
|
||||
|
||||
const adminSession = {
|
||||
user: { id: "admin", permissions: ["admin"], colorScheme: "light" },
|
||||
expires: new Date().toISOString(),
|
||||
} satisfies Session;
|
||||
|
||||
describe("group.createInitialExternalGroup", () => {
|
||||
it("commits exactly one concurrent group-to-setup transition", async () => {
|
||||
const db = createDb();
|
||||
await db.insert(onboarding).values({ id: "onboarding", step: "group", previousStep: "start" });
|
||||
const caller = groupRouter.createCaller({ db, deviceType: undefined, session: adminSession });
|
||||
|
||||
const results = await Promise.allSettled([
|
||||
caller.createInitialExternalGroup({ name: "LDAP admins" }),
|
||||
caller.createInitialExternalGroup({ name: "OIDC admins" }),
|
||||
]);
|
||||
|
||||
expect(results.map((result) => result.status).toSorted()).toEqual(["fulfilled", "rejected"]);
|
||||
expect(results.find((result) => result.status === "rejected")).toMatchObject({
|
||||
reason: expect.objectContaining({ message: "The initial external group was already created." }),
|
||||
});
|
||||
const createdGroups = await db.select().from(groups);
|
||||
expect(createdGroups).toHaveLength(1);
|
||||
expect(["LDAP admins", "OIDC admins"]).toContain(createdGroups[0]?.name);
|
||||
expect(await db.select().from(groupPermissions)).toEqual([
|
||||
expect.objectContaining({ groupId: createdGroups[0]?.id, permission: "admin" }),
|
||||
]);
|
||||
expect(await db.query.onboarding.findFirst()).toMatchObject({ step: "setup", previousStep: "group" });
|
||||
});
|
||||
});
|
||||
@@ -20,12 +20,14 @@ import {
|
||||
} from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import type { OnboardingCompleteSetupInput } from "@homarr/validation/onboarding";
|
||||
|
||||
import { claimOnboardingAsync, isOnboardingClaimValidAsync } from "../../onboarding-claim";
|
||||
import * as integrationConnection from "../integration/integration-test-connection";
|
||||
|
||||
const listDiscoveredContainersAsync = vi.hoisted(() => vi.fn());
|
||||
const createIdMock = vi.hoisted(() => vi.fn());
|
||||
const isProviderEnabled = vi.hoisted(() => vi.fn<(provider: string) => boolean>(() => false));
|
||||
const stableAppIdForSource = (sourceId: string) => {
|
||||
const digest = createHash("sha256").update(sourceId).digest("hex").slice(0, 32);
|
||||
return `onboarding_app_${digest}`;
|
||||
@@ -35,7 +37,7 @@ const stableIntegrationIdForSource = (sourceId: string) => {
|
||||
return `onboarding_integration_${digest}`;
|
||||
};
|
||||
|
||||
vi.mock("@homarr/auth/server", () => ({ isProviderEnabled: () => false }));
|
||||
vi.mock("@homarr/auth/server", () => ({ isProviderEnabled }));
|
||||
vi.mock("@homarr/common", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("@homarr/common")>()),
|
||||
createId: createIdMock,
|
||||
@@ -51,7 +53,7 @@ import { normalizeOnboardingStep } from "../onboard/onboard-queries";
|
||||
const createCaller = (db: ReturnType<typeof createDb>, onboardingClaimToken?: string) =>
|
||||
onboardRouter.createCaller({ db, deviceType: undefined, session: null, onboardingClaimToken });
|
||||
|
||||
const completionInput = (boardId: string, name = "Configured-board") => ({
|
||||
const completionInput = (boardId: string, name = "Configured-board"): OnboardingCompleteSetupInput => ({
|
||||
server: {
|
||||
defaultLocale: "en" as const,
|
||||
defaultColorScheme: "dark" as const,
|
||||
@@ -69,6 +71,8 @@ const completionInput = (boardId: string, name = "Configured-board") => ({
|
||||
},
|
||||
selectedIntegrationIds: [] as string[],
|
||||
selectedAppIds: [] as string[],
|
||||
integrations: [],
|
||||
apps: [],
|
||||
selectedDockerSourceIds: [] as string[],
|
||||
selectedWidgetKinds: [] as WidgetKind[],
|
||||
});
|
||||
@@ -188,6 +192,63 @@ describe("onboard.completeSetup", () => {
|
||||
expect(await isOnboardingClaimValidAsync(db, claimToken)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a stale completion after another submission claims the setup transition", async () => {
|
||||
const db = createDb();
|
||||
const claimToken = await seedOnboardingAsync(db);
|
||||
await seedBoardAsync(db, { boardId: "target" });
|
||||
const input = completionInput("target");
|
||||
input.selectedDockerSourceIds = ["docker:missing"];
|
||||
input.selectedWidgetKinds = ["clock"];
|
||||
|
||||
let releaseDiscovery: () => void = () => undefined;
|
||||
const discoveryGate = new Promise<void>((resolve) => {
|
||||
releaseDiscovery = resolve;
|
||||
});
|
||||
listDiscoveredContainersAsync.mockImplementation(async () => {
|
||||
await discoveryGate;
|
||||
return { hosts: [], services: [] };
|
||||
});
|
||||
|
||||
const submission = Promise.allSettled([createCaller(db, claimToken).completeSetup(input)]);
|
||||
try {
|
||||
await vi.waitFor(() => expect(listDiscoveredContainersAsync).toHaveBeenCalledOnce());
|
||||
await db.update(onboarding).set({ previousStep: "setup", step: "finish" });
|
||||
} finally {
|
||||
releaseDiscovery();
|
||||
}
|
||||
const [result] = await submission;
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: "rejected",
|
||||
reason: expect.objectContaining({ message: "Onboarding setup was already completed." }),
|
||||
});
|
||||
expect(await db.query.items.findMany({ where: eq(items.kind, "clock") })).toHaveLength(0);
|
||||
expect(await db.query.boards.findFirst({ where: eq(boards.id, "target") })).toMatchObject({ name: "target" });
|
||||
expect(await db.query.onboarding.findFirst()).toMatchObject({ step: "finish", previousStep: "setup" });
|
||||
});
|
||||
|
||||
it("uses canonical widget sizes for automatically generated integration widgets", async () => {
|
||||
const db = createDb();
|
||||
const claimToken = await seedOnboardingAsync(db);
|
||||
await seedBoardAsync(db, { boardId: "target" });
|
||||
await db.insert(integrations).values({
|
||||
id: "sonarr",
|
||||
name: "Sonarr",
|
||||
url: "http://sonarr:8989",
|
||||
kind: "sonarr",
|
||||
});
|
||||
const input = completionInput("target");
|
||||
input.selectedIntegrationIds = ["sonarr"];
|
||||
|
||||
await createCaller(db, claimToken).completeSetup(input);
|
||||
|
||||
const mediaMissing = await db.query.items.findFirst({ where: eq(items.kind, "mediaMissing") });
|
||||
expect(mediaMissing).toBeDefined();
|
||||
expect(
|
||||
await db.query.itemLayouts.findFirst({ where: eq(itemLayouts.itemId, mediaMissing?.id ?? "missing") }),
|
||||
).toMatchObject({ width: 4, height: 3 });
|
||||
});
|
||||
|
||||
it("requires an exact board id when more than one board exists", async () => {
|
||||
const db = createDb();
|
||||
const claimToken = await seedOnboardingAsync(db);
|
||||
@@ -206,6 +267,21 @@ describe("onboard.completeSetup", () => {
|
||||
expect(await db.query.onboarding.findFirst()).toMatchObject({ step: "setup", previousStep: "group" });
|
||||
});
|
||||
|
||||
it("rejects a board rename that only differs by case from another board", async () => {
|
||||
const db = createDb();
|
||||
const claimToken = await seedOnboardingAsync(db);
|
||||
await seedBoardAsync(db, { boardId: "target" });
|
||||
await seedBoardAsync(db, { boardId: "existing" });
|
||||
await db.update(boards).set({ name: "Media" }).where(eq(boards.id, "existing"));
|
||||
|
||||
await expect(createCaller(db, claimToken).completeSetup(completionInput("target", "media"))).rejects.toThrow(
|
||||
"A board with this name already exists.",
|
||||
);
|
||||
|
||||
expect(await db.query.boards.findFirst({ where: eq(boards.id, "target") })).toMatchObject({ name: "target" });
|
||||
expect(await db.query.onboarding.findFirst()).toMatchObject({ step: "setup", previousStep: "group" });
|
||||
});
|
||||
|
||||
it("creates a complete first board when setup resumes without any boards", async () => {
|
||||
const db = createDb();
|
||||
const claimToken = await seedOnboardingAsync(db);
|
||||
@@ -334,11 +410,12 @@ describe("onboard.completeSetup", () => {
|
||||
expect(await db.query.items.findMany()).toEqual([expect.objectContaining({ boardId: "target", kind: "clock" })]);
|
||||
});
|
||||
|
||||
it("persists discovery metadata on created apps and integration companion apps", async () => {
|
||||
it("creates selected apps, integrations, secrets, and companion apps in completeSetup", async () => {
|
||||
const db = createDb();
|
||||
const claimToken = await seedOnboardingAsync(db);
|
||||
const caller = createCaller(db, claimToken);
|
||||
const appResult = await caller.createAppsFromDiscovery([
|
||||
await seedBoardAsync(db, { boardId: "target" });
|
||||
const input = completionInput("target");
|
||||
input.apps = [
|
||||
{
|
||||
sourceId: "docker:good:status",
|
||||
name: "Status",
|
||||
@@ -347,18 +424,9 @@ describe("onboard.completeSetup", () => {
|
||||
iconUrl: "https://icons.example/status.svg",
|
||||
description: "Internal status page",
|
||||
},
|
||||
]);
|
||||
const createdApp = appResult.apps[0];
|
||||
assert(createdApp);
|
||||
expect(await db.query.apps.findFirst({ where: eq(apps.id, createdApp.id) })).toMatchObject({
|
||||
pingUrl: "http://status:3001/health",
|
||||
iconUrl: "https://icons.example/status.svg",
|
||||
description: "Internal status page",
|
||||
});
|
||||
|
||||
const testConnection = vi.spyOn(integrationConnection, "testConnectionAsync").mockResolvedValue({ success: true });
|
||||
try {
|
||||
const integrationResult = await caller.createIntegration({
|
||||
];
|
||||
input.integrations = [
|
||||
{
|
||||
sourceId: "docker:good:sonarr",
|
||||
name: "Sonarr",
|
||||
url: "http://sonarr:8989",
|
||||
@@ -367,45 +435,85 @@ describe("onboard.completeSetup", () => {
|
||||
pingUrl: "http://sonarr:8989/ping",
|
||||
iconUrl: "https://icons.example/sonarr.svg",
|
||||
description: "TV automation",
|
||||
},
|
||||
];
|
||||
|
||||
const testConnection = vi.spyOn(integrationConnection, "testConnectionAsync").mockResolvedValue({ success: true });
|
||||
try {
|
||||
await createCaller(db, claimToken).completeSetup(input);
|
||||
|
||||
expect(
|
||||
await db.query.apps.findFirst({ where: eq(apps.id, stableAppIdForSource("docker:good:status")) }),
|
||||
).toMatchObject({
|
||||
href: "http://status:3001",
|
||||
pingUrl: "http://status:3001/health",
|
||||
iconUrl: "https://icons.example/status.svg",
|
||||
description: "Internal status page",
|
||||
});
|
||||
const createdAppId = "appId" in integrationResult ? integrationResult.appId : undefined;
|
||||
expect(createdAppId).toBeDefined();
|
||||
if (!createdAppId) throw new Error("Expected integration creation to succeed");
|
||||
expect(await db.query.apps.findFirst({ where: eq(apps.id, createdAppId) })).toMatchObject({
|
||||
expect(
|
||||
await db.query.apps.findFirst({ where: eq(apps.id, stableAppIdForSource("docker:good:sonarr")) }),
|
||||
).toMatchObject({
|
||||
href: "http://sonarr:8989",
|
||||
pingUrl: "http://sonarr:8989/ping",
|
||||
iconUrl: "https://icons.example/sonarr.svg",
|
||||
description: "TV automation",
|
||||
});
|
||||
expect(
|
||||
await db.query.integrations.findFirst({
|
||||
where: eq(integrations.id, stableIntegrationIdForSource("docker:good:sonarr")),
|
||||
}),
|
||||
).toMatchObject({
|
||||
appId: stableAppIdForSource("docker:good:sonarr"),
|
||||
name: "Sonarr",
|
||||
url: "http://sonarr:8989",
|
||||
kind: "sonarr",
|
||||
});
|
||||
const savedSecrets = await db.query.integrationSecrets.findMany({
|
||||
where: eq(integrationSecrets.integrationId, stableIntegrationIdForSource("docker:good:sonarr")),
|
||||
});
|
||||
expect(savedSecrets).toEqual([expect.objectContaining({ kind: "apiKey" })]);
|
||||
expect(savedSecrets[0]?.value).not.toBe("secret");
|
||||
expect(await db.query.items.findMany({ where: eq(items.kind, "app") })).toHaveLength(2);
|
||||
} finally {
|
||||
testConnection.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("reuses a manual integration and companion app across retries", async () => {
|
||||
it("rolls draft records back with board writes and reuses stable IDs on retry", async () => {
|
||||
const db = createDb();
|
||||
const claimToken = await seedOnboardingAsync(db);
|
||||
const caller = createCaller(db, claimToken);
|
||||
await seedBoardAsync(db, { boardId: "target", itemId: "generated-id" });
|
||||
const sourceId = "manual:sonarr";
|
||||
const createInput = {
|
||||
sourceId,
|
||||
name: "Sonarr",
|
||||
url: "http://sonarr:8989",
|
||||
kind: "sonarr" as const,
|
||||
secrets: [{ kind: "apiKey" as const, value: "secret" }],
|
||||
};
|
||||
const input = completionInput("target", "Retried-board");
|
||||
input.integrations = [
|
||||
{
|
||||
sourceId,
|
||||
name: "Sonarr",
|
||||
url: "http://sonarr:8989",
|
||||
kind: "sonarr",
|
||||
secrets: [{ kind: "apiKey", value: "secret" }],
|
||||
},
|
||||
];
|
||||
createIdMock.mockReturnValue("generated-id");
|
||||
const testConnection = vi.spyOn(integrationConnection, "testConnectionAsync").mockResolvedValue({ success: true });
|
||||
|
||||
try {
|
||||
const first = await caller.createIntegration(createInput);
|
||||
const second = await caller.createIntegration(createInput);
|
||||
await expect(createCaller(db, claimToken).completeSetup(input)).rejects.toThrow();
|
||||
expect(await db.query.integrations.findMany()).toHaveLength(0);
|
||||
expect(await db.query.apps.findMany()).toHaveLength(0);
|
||||
expect(await db.query.integrationSecrets.findMany()).toHaveLength(0);
|
||||
expect(await db.query.onboarding.findFirst()).toMatchObject({ step: "setup", previousStep: "group" });
|
||||
|
||||
expect(first).toEqual(second);
|
||||
expect(first).toEqual({
|
||||
let nextRetryId = 0;
|
||||
createIdMock.mockImplementation(() => `retry-${++nextRetryId}`);
|
||||
await createCaller(db, claimToken).completeSetup(input);
|
||||
|
||||
expect(await db.query.integrations.findMany()).toHaveLength(1);
|
||||
expect(await db.query.apps.findMany()).toHaveLength(1);
|
||||
expect(await db.query.integrations.findFirst()).toMatchObject({
|
||||
id: stableIntegrationIdForSource(sourceId),
|
||||
appId: stableAppIdForSource(sourceId),
|
||||
});
|
||||
expect(await db.query.integrations.findMany()).toHaveLength(1);
|
||||
expect(await db.query.apps.findMany()).toHaveLength(1);
|
||||
expect(
|
||||
await db.query.integrationSecrets.findMany({
|
||||
where: eq(integrationSecrets.integrationId, stableIntegrationIdForSource(sourceId)),
|
||||
@@ -938,6 +1046,10 @@ describe("onboard.completeSetup", () => {
|
||||
});
|
||||
|
||||
describe("onboard.nextStep", () => {
|
||||
beforeEach(() => {
|
||||
isProviderEnabled.mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("advances welcome exactly once", async () => {
|
||||
const db = createDb();
|
||||
await db.insert(onboarding).values({ id: "onboarding", step: "start" });
|
||||
@@ -950,6 +1062,29 @@ describe("onboard.nextStep", () => {
|
||||
await expect(caller.nextStep()).rejects.toThrow("The welcome step is already complete.");
|
||||
expect(await db.query.onboarding.findFirst()).toMatchObject({ step: "setup", previousStep: "start" });
|
||||
});
|
||||
|
||||
it("skips duplicate credential account creation when an administrator resumes a legacy step", async () => {
|
||||
isProviderEnabled.mockImplementation((provider: string) => provider === "credentials");
|
||||
const db = createDb();
|
||||
await db.insert(onboarding).values({ id: "onboarding", step: "import" as never });
|
||||
await db.insert(users).values({ id: "existing-admin", name: "admin", provider: "credentials" });
|
||||
|
||||
const { nextOnboardingStepAsync } = await import("../onboard/onboard-queries");
|
||||
await nextOnboardingStepAsync(db);
|
||||
|
||||
expect(await db.query.onboarding.findFirst()).toMatchObject({ step: "setup", previousStep: "start" });
|
||||
});
|
||||
|
||||
it("still requires credential account creation on a fresh installation", async () => {
|
||||
isProviderEnabled.mockImplementation((provider: string) => provider === "credentials");
|
||||
const db = createDb();
|
||||
await db.insert(onboarding).values({ id: "onboarding", step: "start" });
|
||||
|
||||
const { nextOnboardingStepAsync } = await import("../onboard/onboard-queries");
|
||||
await nextOnboardingStepAsync(db);
|
||||
|
||||
expect(await db.query.onboarding.findFirst()).toMatchObject({ step: "user", previousStep: "start" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeOnboardingStep", () => {
|
||||
|
||||
Reference in new issue
Block a user