Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions packages/verify/src/harness.app-default-profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
// fixture object is denied unless the app's declared profile is actually wired.
// A name-only assertion could not tell "wired" from "wired but inert".

import { describe, it, expect, afterAll } from 'vitest';
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { defineStack } from '@objectstack/spec';
import { ObjectSchema, Field } from '@objectstack/spec/data';
import { PermissionSetSchema } from '@objectstack/spec/security';
Expand All @@ -48,10 +48,16 @@ import {
// in `DEBT`, `TEST_DEBT` or `EXEMPT`. The layer is at zero with no ledger, so
// an error a new file introduces is red on arrival.
import { bootStack, type VerifyStack } from './harness.js';
import { warmUpBoot } from './boot-warm-up.testkit.js';

// Booting the full in-process stack runs well past vitest's 5s default.
const BOOT_TIMEOUT = 120_000;

// The boot's first load, paid during collection rather than in the first
// boot's clock (see `boot-warm-up.testkit.ts`). Every boot below that is not
// the opt-out case uses the default options.
await warmUpBoot();

const APP_DEFAULT_SET = 'appdefault_member_default';

// `defineStack` enforces `${manifest.namespace}_${shortName}` on every object
Expand Down Expand Up @@ -142,10 +148,20 @@ const wiredBaseline = (stack: VerifyStack): Promise<string | null> =>
stack.kernel.getServiceAsync<string | null>('security.fallbackPermissionSet');

describe('bootStack honours the app-declared default permission set (#7001)', () => {
// The first two cases read ONE boot of the declared-default configuration
// with the default options: the first reads the profile the boot wired, a
// fixed answer of the boot, and the second signs a member up and reads as
// them, which changes no wiring. Neither reads what the other writes, so
// they hold in either order.
let declaredDefault: VerifyStack;
beforeAll(async () => {
declaredDefault = await boot(buildAppWithDeclaredDefault());
}, BOOT_TIMEOUT);

it(
'wires the SAME profile `serve` wires for the same config',
async () => {
const stack = await boot(buildAppWithDeclaredDefault());
const stack = declaredDefault;

// The exact expression `serve.ts` evaluates for its `fallbackPermissionSet`.
const servesChoice = appDefaultPermissionSetName(appWithDeclaredDefault.permissions);
Expand All @@ -162,7 +178,7 @@ describe('bootStack honours the app-declared default permission set (#7001)', ()
it(
'and that profile is load-bearing: a fresh member holds the declared grants',
async () => {
const stack = await boot(buildAppWithDeclaredDefault());
const stack = declaredDefault;
await stack.signIn(); // first user is the seeded dev admin
const memberToken = await stack.signUp('appdefault-member@verify.test');

Expand Down
140 changes: 80 additions & 60 deletions packages/verify/src/harness.org-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
// lives in the dogfood suite, against a real app:
// `packages/qa/dogfood/test/org-scoped-sharing-rule-listing.dogfood.test.ts`.

import { describe, it, expect, afterEach } from 'vitest';
import { describe, it, expect, beforeAll, afterEach } from 'vitest';
// `.js` extension deliberately: this package resolves NodeNext, so an
// extensionless relative import is a TS2835 the type-check-debt ratchet counts
// (the sibling test files predate the gate and carry theirs in the ledger — a
// NEW one would raise it, which is not something a test file gets to do).
import { bootStack, type VerifyStack } from './harness.js';
import { bootStack, type BootOptions, type VerifyStack } from './harness.js';
import { warmUpBoot } from './boot-warm-up.testkit.js';

const app = {
manifest: {
Expand All @@ -56,6 +57,11 @@ const SYS = { isSystem: true } as const;
// Booting the full in-process stack runs well past vitest's 5s default.
const BOOT_TIMEOUT = 120_000;

// The boot's first load, paid during collection rather than in the first
// boot's clock (see `boot-warm-up.testkit.ts`). The first boot below uses the
// default options.
await warmUpBoot();

afterEach(() => {
delete process.env.OS_TENANCY_POSTURE;
});
Expand Down Expand Up @@ -96,7 +102,58 @@ async function sessionOrgId(stack: VerifyStack): Promise<string | null> {
return null;
}

/** What one boot of `app` answered, read while it was live. */
interface BootReading {
posture: TenancyShape;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
orgs: any[];
sessionOrgId: string | null;
/** The roles of the admin's `sys_member` rows in the first organization. */
adminRoles: unknown[];
}

/**
* Boot `app` with `opts`, read the posture, the organizations, the org id the
* admin's session carries and the admin's membership roles, and stop. Each
* read is one the cases below used to make on a boot of their own with the
* same options.
*/
async function bootAndRead(opts: BootOptions): Promise<BootReading> {
const stack = await bootStack(app, opts);
try {
const shape = await posture(stack);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ql = await stack.kernel.getServiceAsync<any>('objectql');
const orgs = rowsOf(await ql.find('sys_organization', { limit: 10, context: SYS }));
const orgId = await sessionOrgId(stack);
const users = rowsOf(
await ql.find('sys_user', { where: { email: 'admin@objectos.ai' }, limit: 1, context: SYS }),
);
const memberships = rowsOf(
await ql.find('sys_member', {
where: { user_id: users[0]?.id, organization_id: orgs[0]?.id },
limit: 10,
context: SYS,
}),
);
return { posture: shape, orgs, sessionOrgId: orgId, adminRoles: memberships.map((m) => m.role) };
} finally {
await stack.stop();
}
}

describe('bootStack orgContext (#7762)', () => {
// Two boots for the three cases that read one: the flag off (the default
// boot) and on, in that order, each stopped before the next starts — the
// order the ruling case always booted them in. Every case only reads what a
// boot answered, so no case depends on another's run or on their order.
let off: BootReading;
let on: BootReading;
beforeAll(async () => {
off = await bootAndRead({});
on = await bootAndRead({ orgContext: true });
}, BOOT_TIMEOUT * 2);

it(
'THE RULING: the tenancy posture and `degraded` are IDENTICAL with the flag off vs on',
async () => {
Expand All @@ -106,21 +163,8 @@ describe('bootStack orgContext (#7762)', () => {
// so binding the admin to an organization must not move it. If this ever
// fails, `orgContext` is making the open core claim a wall it does not
// have and the option must not ship in that shape.
const off = await bootStack(app, {});
let baseline: TenancyShape;
try {
baseline = await posture(off);
} finally {
await off.stop();
}

const on = await bootStack(app, { orgContext: true });
let withOrg: TenancyShape;
try {
withOrg = await posture(on);
} finally {
await on.stop();
}
const baseline = off.posture;
const withOrg = on.posture;

expect(withOrg).toEqual(baseline);
// Spelled out too, so a future refactor of `posture()` cannot make the
Expand All @@ -137,23 +181,16 @@ describe('bootStack orgContext (#7762)', () => {
it(
'binds the admin to a real organization, and their session carries it',
async () => {
const stack = await bootStack(app, { orgContext: true });
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ql = await stack.kernel.getServiceAsync<any>('objectql');
const orgs = rowsOf(await ql.find('sys_organization', { limit: 10, context: SYS }));
expect(orgs.length, 'the default-org bootstrap minted an organization').toBeGreaterThan(0);

// `session.activeOrganizationId` is the ONE field `resolveAuthzContext`
// reads into `tenantId` → `ExecutionContext`. Without it on the session
// row, every org-scoped read downstream is org-less no matter how many
// organizations exist in the table.
const orgId = await sessionOrgId(stack);
expect(orgId, "the admin's session carries an active organization").toBeTruthy();
expect(orgs.map((o) => o.id)).toContain(orgId);
} finally {
await stack.stop();
}
const { orgs } = on;
expect(orgs.length, 'the default-org bootstrap minted an organization').toBeGreaterThan(0);

// `session.activeOrganizationId` is the ONE field `resolveAuthzContext`
// reads into `tenantId` → `ExecutionContext`. Without it on the session
// row, every org-scoped read downstream is org-less no matter how many
// organizations exist in the table.
const orgId = on.sessionOrgId;
expect(orgId, "the admin's session carries an active organization").toBeTruthy();
expect(orgs.map((o) => o.id)).toContain(orgId);
},
BOOT_TIMEOUT,
);
Expand All @@ -165,32 +202,15 @@ describe('bootStack orgContext (#7762)', () => {
// Default Organization (a boot invariant), and the admin is bound to it
// without asking — the shape `objectstack dev` / `serve` boot. There is
// no org-less `single` boot left to keep a fixture on (ADR-0131 D11).
const stack = await bootStack(app, {});
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ql = await stack.kernel.getServiceAsync<any>('objectql');
const orgs = rowsOf(await ql.find('sys_organization', { limit: 10, context: SYS }));
expect(orgs.map((o) => o.slug)).toEqual(['default']);
expect(await sessionOrgId(stack)).toBe(orgs[0].id);
// The harness line this case pins is the owner bind (ADR-0093 D7), on
// for every boot: the admin OWNS the Default Organization. The two
// assertions above hold without it (the boot invariant creates the
// organization and the membership reconciler binds the admin), but as
// `member` — which is what an `objectstack dev` admin never is.
const users = rowsOf(
await ql.find('sys_user', { where: { email: 'admin@objectos.ai' }, limit: 1, context: SYS }),
);
const memberships = rowsOf(
await ql.find('sys_member', {
where: { user_id: users[0]?.id, organization_id: orgs[0].id },
limit: 10,
context: SYS,
}),
);
expect(memberships.map((m) => m.role)).toEqual(['owner']);
} finally {
await stack.stop();
}
const { orgs } = off;
expect(orgs.map((o) => o.slug)).toEqual(['default']);
expect(off.sessionOrgId).toBe(orgs[0].id);
// The harness line this case pins is the owner bind (ADR-0093 D7), on
// for every boot: the admin OWNS the Default Organization. The two
// assertions above hold without it (the boot invariant creates the
// organization and the membership reconciler binds the admin), but as
// `member` — which is what an `objectstack dev` admin never is.
expect(off.adminRoles).toEqual(['owner']);
},
BOOT_TIMEOUT,
);
Expand Down
Loading