100 lines
3.7 KiB
TypeScript
100 lines
3.7 KiB
TypeScript
import { expect } from "vitest";
|
|
import { denFetch } from "@openwork/behaviors";
|
|
import type { DenSession } from "@openwork/behaviors";
|
|
import {
|
|
createAdmin,
|
|
createOrg,
|
|
defaultReuseAdmin,
|
|
inviteMember,
|
|
localMysqlIsRunning,
|
|
localRedisIsRunning,
|
|
needs,
|
|
server,
|
|
SkipError,
|
|
test,
|
|
} from "@openwork/testkit";
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
async function organizations(session: DenSession): Promise<Map<string, string>> {
|
|
const result = await denFetch(session, "/v1/me/orgs", {
|
|
headers: { authorization: `Bearer ${session.token}` },
|
|
signal: AbortSignal.timeout(10_000),
|
|
});
|
|
if (!result.response.ok || !isRecord(result.body) || !Array.isArray(result.body.orgs)) {
|
|
throw new Error(`Organization list failed: HTTP ${result.response.status} ${result.text.slice(0, 500)}`);
|
|
}
|
|
const listed = new Map<string, string>();
|
|
for (const organization of result.body.orgs) {
|
|
if (!isRecord(organization) || typeof organization.name !== "string" || typeof organization.id !== "string") continue;
|
|
listed.set(organization.name, organization.id);
|
|
}
|
|
return listed;
|
|
}
|
|
|
|
test("a script world attaches to an existing Den without owning it", { timeout: 300_000 }, async ({ evidence, place }) => {
|
|
needs({ placement: "local" });
|
|
if (!await localMysqlIsRunning()) {
|
|
throw new SkipError("local MySQL on 127.0.0.1:3306");
|
|
}
|
|
if (!await localRedisIsRunning()) {
|
|
throw new SkipError("local Redis on 127.0.0.1:6379");
|
|
}
|
|
|
|
const nonce = `${Date.now().toString(36)}-${process.pid.toString(36)}`;
|
|
const preexistingOrgName = `Attach Outer ${nonce}`;
|
|
const attachedOrgName = `Attach World ${nonce}`;
|
|
const resolvedEmail = `attach-member+${nonce}@openwork.test`;
|
|
const resolvedPassword = `AttachSecret-${nonce}!`;
|
|
|
|
await using stack = new AsyncDisposableStack();
|
|
const outer = stack.use(await server({ place, provision: false, web: false }));
|
|
await createAdmin(outer, defaultReuseAdmin());
|
|
const preexistingOrg = stack.use(await createOrg(outer, preexistingOrgName));
|
|
const before = await organizations(outer.admin);
|
|
expect(before.get(preexistingOrgName)).toBe(preexistingOrg.id);
|
|
|
|
{
|
|
await using attachedStack = new AsyncDisposableStack();
|
|
const attached = attachedStack.use(await server({
|
|
place,
|
|
provision: false,
|
|
reuse: outer.ref,
|
|
web: false,
|
|
}));
|
|
await createAdmin(attached, defaultReuseAdmin());
|
|
attachedStack.use(await createOrg(attached, attachedOrgName));
|
|
const jordan = await inviteMember(attached, "jordan", {
|
|
email: resolvedEmail,
|
|
name: "Jordan",
|
|
password: resolvedPassword,
|
|
});
|
|
const attachedOrganizations = await organizations(attached.admin);
|
|
expect(attachedOrganizations.has(attachedOrgName)).toBe(true);
|
|
expect(jordan.email).toBe(resolvedEmail);
|
|
evidence.recordAssertionEvidence(
|
|
"The script used an authenticated admin session on the attached Den",
|
|
[...attachedOrganizations.keys()].join(", "),
|
|
attachedOrganizations.has(attachedOrgName),
|
|
);
|
|
}
|
|
|
|
const health = await fetch(`${outer.ref.apiUrl}/health`, { signal: AbortSignal.timeout(10_000) });
|
|
expect(health.ok).toBe(true);
|
|
const after = await organizations(outer.admin);
|
|
expect(after.get(preexistingOrgName)).toBe(preexistingOrg.id);
|
|
expect(after.has(attachedOrgName)).toBe(false);
|
|
evidence.recordAssertionEvidence(
|
|
"Disposing the attached script left the outer Den healthy",
|
|
`HTTP ${health.status}`,
|
|
health.ok,
|
|
);
|
|
evidence.recordAssertionEvidence(
|
|
"Disposal deleted only the script-created organization",
|
|
[...after.keys()].join(", "),
|
|
after.get(preexistingOrgName) === preexistingOrg.id && !after.has(attachedOrgName),
|
|
);
|
|
});
|