feat: bootstrap commercial AI drama platform
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const base = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787";
|
||||
const scope = {
|
||||
"x-organization-id": "org-studio-lab",
|
||||
"x-workspace-id": "ws-local-aidrama",
|
||||
"x-project-id": "thunder-mouth"
|
||||
};
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const response = await fetch(`${base}${path}`, {
|
||||
...options,
|
||||
headers: { "content-type": "application/json", ...(options.headers || {}) }
|
||||
});
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
return { response, payload };
|
||||
}
|
||||
|
||||
function assertOk(result, label) {
|
||||
assert.equal(result.response.ok, true, `${label}: ${result.response.status} ${result.payload.detail || result.payload.error || ""}`);
|
||||
return result.payload;
|
||||
}
|
||||
|
||||
async function login(email) {
|
||||
const result = await request("/api/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password: "Demo@123456" })
|
||||
});
|
||||
assertOk(result, `${email} login`);
|
||||
return { authorization: `Bearer ${result.payload.session.token}` };
|
||||
}
|
||||
|
||||
const ownerHeaders = { ...(await login("producer@local.test")), ...scope };
|
||||
const writerHeaders = { ...(await login("writer@local.test")), ...scope };
|
||||
const inviteEmail = `invitation-smoke-${Date.now()}@local.test`;
|
||||
let invitationId = "";
|
||||
let invitationState = "";
|
||||
|
||||
try {
|
||||
const writerAttempt = await request("/api/organizations/org-studio-lab/invitations", {
|
||||
method: "POST",
|
||||
headers: writerHeaders,
|
||||
body: JSON.stringify({ email: inviteEmail, roleKey: "writer", workspaceId: "ws-local-aidrama" })
|
||||
});
|
||||
assert.equal(writerAttempt.response.status, 403, "普通编剧不应创建组织邀请");
|
||||
assert.equal(writerAttempt.payload.error, "permission_denied", "普通编剧越权邀请需要稳定错误码");
|
||||
|
||||
const before = assertOk(await request("/api/organizations/org-studio-lab/commercial", { headers: ownerHeaders }), "读取邀请前商业席位");
|
||||
const created = assertOk(await request("/api/organizations/org-studio-lab/invitations", {
|
||||
method: "POST",
|
||||
headers: ownerHeaders,
|
||||
body: JSON.stringify({ email: inviteEmail, roleKey: "writer", workspaceId: "ws-local-aidrama" })
|
||||
}), "管理员创建邀请");
|
||||
const firstInvitation = created.invitation;
|
||||
invitationId = firstInvitation.id;
|
||||
invitationState = firstInvitation.status;
|
||||
assert.ok(firstInvitation.inviteToken, "创建邀请必须返回一次性令牌");
|
||||
assert.ok(firstInvitation.acceptUrl?.startsWith("/register?invite="), "创建邀请必须返回注册链接");
|
||||
|
||||
const afterCreate = assertOk(await request("/api/organizations/org-studio-lab/commercial", { headers: ownerHeaders }), "读取创建后商业席位");
|
||||
assert.equal(Number(afterCreate.seat.pending), Number(before.seat.pending) + 1, "创建邀请后必须预留一个待入组席位");
|
||||
|
||||
const firstPreview = assertOk(await request(`/api/invitations/preview?token=${encodeURIComponent(firstInvitation.inviteToken)}`), "预览首次注册链接");
|
||||
assert.equal(firstPreview.invitation.email, inviteEmail, "首次注册链接邮箱不匹配");
|
||||
|
||||
const duplicate = await request("/api/organizations/org-studio-lab/invitations", {
|
||||
method: "POST",
|
||||
headers: ownerHeaders,
|
||||
body: JSON.stringify({ email: inviteEmail, roleKey: "writer", workspaceId: "ws-local-aidrama" })
|
||||
});
|
||||
assert.equal(duplicate.response.status, 409, "重复邀请必须被阻止");
|
||||
assert.equal(duplicate.payload.error, "invitation_already_pending", "重复邀请错误码不稳定");
|
||||
|
||||
const resent = assertOk(await request(`/api/organizations/org-studio-lab/invitations/${encodeURIComponent(invitationId)}/resend`, {
|
||||
method: "POST",
|
||||
headers: ownerHeaders,
|
||||
body: "{}"
|
||||
}), "管理员重发邀请");
|
||||
const secondInvitation = resent.invitation;
|
||||
invitationState = secondInvitation.status;
|
||||
assert.ok(secondInvitation.inviteToken, "重发邀请必须返回新令牌");
|
||||
assert.notEqual(secondInvitation.inviteToken, firstInvitation.inviteToken, "重发不能复用旧令牌");
|
||||
|
||||
const oldPreview = await request(`/api/invitations/preview?token=${encodeURIComponent(firstInvitation.inviteToken)}`);
|
||||
assert.equal(oldPreview.response.status, 404, "重发后旧注册链接必须失效");
|
||||
const secondPreview = assertOk(await request(`/api/invitations/preview?token=${encodeURIComponent(secondInvitation.inviteToken)}`), "预览新注册链接");
|
||||
assert.equal(secondPreview.invitation.email, inviteEmail, "新注册链接邮箱不匹配");
|
||||
|
||||
const revoked = assertOk(await request(`/api/organizations/org-studio-lab/invitations/${encodeURIComponent(invitationId)}/revoke`, {
|
||||
method: "POST",
|
||||
headers: ownerHeaders,
|
||||
body: "{}"
|
||||
}), "管理员撤销邀请");
|
||||
invitationState = revoked.invitation.status;
|
||||
assert.equal(invitationState, "revoked", "撤销后邀请状态必须为 revoked");
|
||||
|
||||
const revokedPreview = await request(`/api/invitations/preview?token=${encodeURIComponent(secondInvitation.inviteToken)}`);
|
||||
assert.equal(revokedPreview.response.status, 404, "撤销后注册链接必须失效");
|
||||
const afterRevoke = assertOk(await request("/api/organizations/org-studio-lab/commercial", { headers: ownerHeaders }), "读取撤销后商业席位");
|
||||
assert.equal(Number(afterRevoke.seat.pending), Number(before.seat.pending), "撤销邀请后必须释放待入组席位");
|
||||
|
||||
const listed = assertOk(await request("/api/organizations/org-studio-lab", { headers: ownerHeaders }), "读取组织邀请列表");
|
||||
assert.ok(!listed.invitations.some((item) => item.id === invitationId), "撤销邀请不能继续出现在待处理列表");
|
||||
} finally {
|
||||
if (invitationId && invitationState === "pending") {
|
||||
await request(`/api/organizations/org-studio-lab/invitations/${encodeURIComponent(invitationId)}/revoke`, {
|
||||
method: "POST",
|
||||
headers: ownerHeaders,
|
||||
body: "{}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`invitations smoke passed: ${base}`);
|
||||
Reference in New Issue
Block a user