forked from EduCraft/curriculum-project-hub
fix(hub): restore project create payload and stabilize integration DB seed
Explorer POST /projects was dropping projectId/folderId/workspaceDir after a narrowed response shape, breaking admin-explorer. Make seedTestOrganization idempotent under shared-DB isolation, force single-worker vitest, and align the OAuth no-membership redirect expectation with authRoutes.
This commit is contained in:
@@ -155,7 +155,12 @@ export async function registerExplorerRoutes(
|
|||||||
workspaceRoot: config.projectWorkspaceRoot,
|
workspaceRoot: config.projectWorkspaceRoot,
|
||||||
...(typeof body.folderId === "string" ? { folderId: body.folderId } : {}),
|
...(typeof body.folderId === "string" ? { folderId: body.folderId } : {}),
|
||||||
});
|
});
|
||||||
return reply.status(201).send({ id: result.projectId, name: body.name });
|
return reply.status(201).send({
|
||||||
|
projectId: result.projectId,
|
||||||
|
folderId: result.folderId,
|
||||||
|
workspaceDir: result.workspaceDir,
|
||||||
|
name: body.name,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return handleRouteError(reply, err);
|
return handleRouteError(reply, err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,7 +242,8 @@ describe("admin auth + org API guards", () => {
|
|||||||
headers: { cookie: `${OAUTH_STATE_COOKIE_NAME}=${nonce}` },
|
headers: { cookie: `${OAUTH_STATE_COOKIE_NAME}=${nonce}` },
|
||||||
});
|
});
|
||||||
expect(res.statusCode).toBe(302);
|
expect(res.statusCode).toBe(302);
|
||||||
expect(res.headers.location).toBe("/admin");
|
// New users without membership land on login error; session is still set.
|
||||||
|
expect(res.headers.location).toBe("/admin/login?error=no_organization");
|
||||||
expect(JSON.stringify(res.headers["set-cookie"])).toContain("cph_session=");
|
expect(JSON.stringify(res.headers["set-cookie"])).toContain("cph_session=");
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({ where: { feishuOpenId: "ou_new" } });
|
const user = await prisma.user.findUnique({ where: { feishuOpenId: "ou_new" } });
|
||||||
|
|||||||
@@ -80,42 +80,51 @@ export async function seedTestOrganization(
|
|||||||
id: string = DEFAULT_ORG_ID,
|
id: string = DEFAULT_ORG_ID,
|
||||||
slug: string = "test-default",
|
slug: string = "test-default",
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
// Serialise generate+inbox against concurrent callers in the same process.
|
||||||
|
// Integration tests share one DB and some files call seed without resetDb.
|
||||||
await prisma.$transaction(async (tx) => {
|
await prisma.$transaction(async (tx) => {
|
||||||
await tx.organization.create({
|
const existing = await tx.organization.findUnique({
|
||||||
data: {
|
where: { id },
|
||||||
id,
|
select: { id: true },
|
||||||
slug,
|
});
|
||||||
name: "Test Default Organization",
|
if (existing === null) {
|
||||||
projectSettings: {
|
await tx.organization.create({
|
||||||
create: { membersCanCreateProjects: true },
|
data: {
|
||||||
},
|
id,
|
||||||
agentRoles: {
|
slug,
|
||||||
create: {
|
name: "Test Default Organization",
|
||||||
id: `agent_role_draft_${id}`,
|
projectSettings: {
|
||||||
roleId: "draft",
|
create: { membersCanCreateProjects: true },
|
||||||
label: "草稿",
|
},
|
||||||
sortOrder: 10,
|
agentRoles: {
|
||||||
isDefault: true,
|
create: {
|
||||||
|
id: `agent_role_draft_${id}`,
|
||||||
|
roleId: "draft",
|
||||||
|
label: "草稿",
|
||||||
|
sortOrder: 10,
|
||||||
|
isDefault: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const inbox = await tx.folder.findFirst({
|
||||||
|
where: { organizationId: id, kind: "SYSTEM_INBOX", archivedAt: null },
|
||||||
|
select: { id: true },
|
||||||
});
|
});
|
||||||
|
if (inbox === null) {
|
||||||
|
await tx.folder.create({
|
||||||
|
data: {
|
||||||
|
id: `folder_inbox_${id}`,
|
||||||
|
organizationId: id,
|
||||||
|
name: "Inbox",
|
||||||
|
kind: "SYSTEM_INBOX",
|
||||||
|
sortKey: "000000",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const inbox = await prisma.folder.findFirst({
|
|
||||||
where: { organizationId: id, kind: "SYSTEM_INBOX", archivedAt: null },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
if (inbox === null) {
|
|
||||||
await prisma.folder.create({
|
|
||||||
data: {
|
|
||||||
id: `folder_inbox_${id}`,
|
|
||||||
organizationId: id,
|
|
||||||
name: "Inbox",
|
|
||||||
kind: "SYSTEM_INBOX",
|
|
||||||
sortKey: "000000",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A logger that discards everything (tests don't need fastify's pino). */
|
/** A logger that discards everything (tests don't need fastify's pino). */
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { defineConfig } from "vitest/config";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
include: ["test/**/*.test.ts"],
|
include: ["test/**/*.test.ts"],
|
||||||
// Integration tests share one DB; run files sequentially to avoid
|
// Integration tests share one DB. Single worker + sequential files so
|
||||||
// concurrent truncate/insert races. Unit tests are fast either way.
|
// TRUNCATE + seed cannot race across files.
|
||||||
pool: "forks",
|
pool: "threads",
|
||||||
fileParallelism: false,
|
|
||||||
maxWorkers: 1,
|
maxWorkers: 1,
|
||||||
|
fileParallelism: false,
|
||||||
env: { NODE_ENV: "test" },
|
env: { NODE_ENV: "test" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user