Skip to content

Commit c4a6f34

Browse files
committed
test(plugin-security,plugin-auth,plugin-approvals): narrow the blanket pre-18 tenant-column relax rigs to the tables they write organization-less
Each blanket `relaxEveryRegisteredTenantColumn` / `relaxEveryTenantColumn` call now names the tables its rig writes organization-less, measured per call site with a NULL-write trigger probe, through `relaxTenantColumns`. Two sites wrote no organization-less row at all, so their relax is gone. The blanket exports leave the three packages' testkits. Test files only. No behavior change. Claude-Session: https://claude.ai/code/session_014aRYTwvuSQoDgWzLdw1nc9 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9dc1f42 commit c4a6f34

41 files changed

Lines changed: 148 additions & 253 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
4545
import { bindSecurityCatalogReader, type SecurityCatalogEntry, type SecurityCatalogReader } from '@objectstack/core';
4646
import { ApprovalService } from './approval-service.js';
4747
import { ApprovalsServicePlugin } from './approvals-plugin.js';
48-
import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
48+
import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
4949

5050
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
5151

@@ -186,10 +186,9 @@ describe('an empty admin_rescue slate opens on the organization\'s administrator
186186
];
187187
for (const def of defs) engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
188188
await engine.syncSchemas();
189-
// The organization-less rows below (positions, holdings) exist only on a
190-
// store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
191-
// NULL): this is that store.
192-
await relaxEveryRegisteredTenantColumn(engine);
189+
// The organization-less rows below exist only on a store built before 18.x
190+
// (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store.
191+
await relaxTenantColumns(engine, ['sys_user', 'sys_user_permission_set', 'crm_deal', 'sys_approval_delegation']);
193192
bindSecurityCatalogReader(engine, catalogServing([
194193
'organization_admin', 'organization_admin_no_bypass', 'admin_full_access',
195194
]));

‎packages/plugins/plugin-approvals/src/approval-notification-body.integration.test.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ import { SqlDriver } from '@objectstack/driver-sql';
7676
import { APPROVAL_REVISE_NODE_TYPE } from '@objectstack/spec/automation';
7777
import { ApprovalService } from './approval-service.js';
7878
import { ApprovalsServicePlugin } from './approvals-plugin.js';
79-
import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
79+
import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
8080

8181
const SYSTEM = { isSystem: true, positions: [], permissions: [] } as any;
8282
const asUser = (userId: string, positions: string[] = []) =>
@@ -214,10 +214,9 @@ describe('every approval notification carries its text in body', () => {
214214
engine.registry.registerObject(def as any, 'approvals-test', 'approvals-test');
215215
}
216216
await engine.syncSchemas();
217-
// The organization-less rows below (positions, holdings) exist only on a
218-
// store built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT
219-
// NULL): this is that store.
220-
await relaxEveryRegisteredTenantColumn(engine);
217+
// The organization-less rows below exist only on a store built before 18.x
218+
// (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store.
219+
await relaxTenantColumns(engine, ['sys_user', 'sys_approval_delegation']);
221220
for (const u of USERS) await engine.insert('sys_user', u, { context: SYSTEM } as any);
222221

223222
svc = new ApprovalService({

‎packages/plugins/plugin-approvals/src/pre-18-tenant-column.testkit.ts‎

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
* attributed them. A suite whose subject is those rows calls this after
1212
* `syncSchemas()` and before seeding them. Done through each table's driver
1313
* and its own drift apply door (`relax_not_null`), never a hand `ALTER`.
14+
*
15+
* Name only the tables the suite writes organization-less: a relaxed table
16+
* also admits an unstamped production write, which an 18.x store refuses.
1417
*/
1518
export async function relaxTenantColumns(
1619
engine: { getDriverForObject(object: string): unknown },
@@ -28,29 +31,3 @@ export async function relaxTenantColumns(
2831
if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table}.${column} was not relaxed`);
2932
}
3033
}
31-
32-
/**
33-
* {@link relaxTenantColumns} over every object of an app that carries a tenant
34-
* column as the engine registered it — the shape a whole pre-18 store has.
35-
*/
36-
export async function relaxEveryTenantColumn(
37-
engine: { getDriverForObject(object: string): unknown; getSchema(object: string): unknown },
38-
objects: readonly { name: string }[],
39-
): Promise<void> {
40-
const tenant = objects
41-
.map((o) => o.name)
42-
.filter((name) => {
43-
const schema = engine.getSchema(name) as { fields?: Record<string, unknown>; tenancy?: { enabled?: boolean } } | undefined;
44-
return !!schema?.fields?.organization_id && schema.tenancy?.enabled !== false;
45-
});
46-
await relaxTenantColumns(engine, tenant);
47-
}
48-
49-
/** {@link relaxEveryTenantColumn} over every object the engine's registry holds. */
50-
export async function relaxEveryRegisteredTenantColumn(engine: {
51-
getDriverForObject(object: string): unknown;
52-
getSchema(object: string): unknown;
53-
registry: { getAllObjects(): readonly { name: string }[] };
54-
}): Promise<void> {
55-
await relaxEveryTenantColumn(engine, engine.registry.getAllObjects());
56-
}

‎packages/plugins/plugin-auth/src/account-identity-preflight.test.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import {
4646
SYS_ACCOUNT_OBJECT,
4747
} from './account-identity-preflight.js';
4848
import { authIdentityObjects } from './manifest.js';
49-
import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
49+
import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
5050

5151
const SYSTEM = { context: { isSystem: true } } as never;
5252

@@ -106,10 +106,9 @@ async function bootEngine(objects: unknown[]): Promise<ObjectQL> {
106106
engine.registry.registerObject(object as never, '@objectstack/plugin-auth');
107107
}
108108
await engine.syncSchemas();
109-
// The organization-less rows below (global sets, grants, identities) exist
110-
// only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column
111-
// NOT NULL): this is that store.
112-
await relaxEveryRegisteredTenantColumn(engine);
109+
// The organization-less rows below exist only on a store built before 18.x
110+
// (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store.
111+
await relaxTenantColumns(engine, ['sys_account', 'sys_sso_provider']);
113112
return engine;
114113
}
115114

‎packages/plugins/plugin-auth/src/driver-fault-auth-log-carriers.test.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import { ObjectQL } from '@objectstack/objectql';
4747
import { SqlDriver } from '@objectstack/driver-sql';
4848
import { AuthManager } from './auth-manager.js';
4949
import { authIdentityObjects } from './manifest.js';
50-
import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
50+
import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
5151

5252
/** The caller's value. Synthetic; asserted ABSENT from every carrier. */
5353
const S = 'SENTINEL-21274-BOUND-VALUE';
@@ -253,10 +253,9 @@ for (const cell of CELLS) {
253253
engine.registry.registerObject(sysPermissionSet as never, '@objectstack/plugin-security');
254254
engine.registry.registerObject(sysUserPermissionSet as never, '@objectstack/plugin-security');
255255
await engine.syncSchemas();
256-
// The organization-less rows below (global sets, grants, identities) exist
257-
// only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column
258-
// NOT NULL): this is that store.
259-
await relaxEveryRegisteredTenantColumn(engine);
256+
// The organization-less rows below exist only on a store built before 18.x
257+
// (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store.
258+
await relaxTenantColumns(engine, ['sys_permission_set']);
260259

261260
// The seeded holder of the sentinel, then the constraint the two writes
262261
// below will meet in the DATABASE — not in any check of ours.

‎packages/plugins/plugin-auth/src/grant-readers-by-name.golden.test.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import { registerLastAdminGuard, type LastAdminGuardEngine } from './last-admin-
4242
import { ensureDefaultOrganization } from './ensure-default-organization.js';
4343
import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testkit.js';
4444
import { withLegacyPermissionSetTable } from './__tests__/legacy-catalog-objects.testkit.js';
45-
import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
45+
import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
4646

4747
const SYS = { isSystem: true } as const;
4848
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
@@ -135,10 +135,9 @@ async function standingByReader(posture: Posture): Promise<Record<string, unknow
135135
objects: withLegacyPermissionSetTable([SysUser, SysAccount, SysMember, SysOrganization, SysMetadataActivation, ...securityObjects]),
136136
} as any);
137137
await engine.syncSchemas();
138-
// The organization-less rows below (global sets, grants, identities) exist
139-
// only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column
140-
// NOT NULL): this is that store.
141-
await relaxEveryRegisteredTenantColumn(engine);
138+
// The organization-less rows below exist only on a store built before 18.x
139+
// (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store.
140+
await relaxTenantColumns(engine, ['sys_permission_set']);
142141
await plugin.start(ctx);
143142
vi.spyOn((engine as any).logger, 'warn').mockImplementation(() => undefined);
144143
registerLastAdminGuard(engine as unknown as LastAdminGuardEngine, {

‎packages/plugins/plugin-auth/src/grant-readers-unnamed-grant.test.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { registerLastAdminGuard, type LastAdminGuardEngine } from './last-admin-
3636
import { ensureDefaultOrganization } from './ensure-default-organization.js';
3737
import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testkit.js';
3838
import { withLegacyPermissionSetTable } from './__tests__/legacy-catalog-objects.testkit.js';
39-
import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
39+
import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
4040

4141
const SYS = { context: { isSystem: true } } as any;
4242
const GUARD_PACKAGE = 'test.grant-readers-unnamed';
@@ -114,10 +114,9 @@ async function soleGrantAnchoredAdmin(): Promise<Rig> {
114114
]),
115115
} as any);
116116
await engine.syncSchemas();
117-
// The organization-less rows below (global sets, grants, identities) exist
118-
// only on a store built before 18.x (ADR-0131 D1 makes a fresh tenant column
119-
// NOT NULL): this is that store.
120-
await relaxEveryRegisteredTenantColumn(engine);
117+
// The organization-less rows below exist only on a store built before 18.x
118+
// (ADR-0131 D1 makes a fresh tenant column NOT NULL): this is that store.
119+
await relaxTenantColumns(engine, ['sys_permission_set']);
121120
// [ADR-0131 D3/D5] The Default Organization a `single` boot creates before
122121
// `kernel:ready` (AuthPlugin's invariant); it owns the promotion's grant.
123122
await engine.insert('sys_organization', { id: 'org_default', name: 'Default Organization', slug: 'default' }, SYS);

‎packages/plugins/plugin-auth/src/last-admin-guard.anchor-parity.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import { ADMIN_FULL_ACCESS } from '@objectstack/spec/identity';
4444

4545
import { registerLastAdminGuard, type LastAdminGuardEngine } from './last-admin-guard.js';
4646
import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit.js';
47-
import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
47+
import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
4848

4949
const SYSTEM = { context: { isSystem: true } } as const;
5050
const OWNER = 'owner@corp.example';
@@ -132,10 +132,10 @@ async function boot(opts: { organizationObject: boolean; nullableTenantColumn?:
132132
const objects = [sysUser, sysMember, sysUserPermissionSet, sysUserPosition, ...(opts.organizationObject ? [sysOrganization] : [])];
133133
for (const o of objects) engine.registry.registerObject(o as never);
134134
await engine.syncSchemas();
135-
// A tenant column that is NOT NULL nowhere: a store built before 18.x has one
136-
// (ADR-0131 D1 makes a fresh one NOT NULL), and so does a driver that
137-
// enforces no NOT NULL.
138-
if (opts.nullableTenantColumn) await relaxEveryRegisteredTenantColumn(engine);
135+
// A grant tenant column that is NOT NULL nowhere: a store built before 18.x
136+
// has one (ADR-0131 D1 makes a fresh one NOT NULL), and so does a driver
137+
// that enforces no NOT NULL.
138+
if (opts.nullableTenantColumn) await relaxTenantColumns(engine, ['sys_user_permission_set']);
139139
bindTestSecurityCatalog(engine, { permissions: [{ name: ADMIN_FULL_ACCESS }] });
140140
registerLastAdminGuard(engine as unknown as LastAdminGuardEngine, { packageId: 'test.last-admin-guard-parity' });
141141
return { engine, driver: driver as unknown as Rig['driver'] };

‎packages/plugins/plugin-auth/src/last-admin-guard.config-anchor.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import { resetPlatformAdminEmailMemo } from '@objectstack/core';
5252

5353
import { registerLastAdminGuard, USER_STANDING_KEYS, type LastAdminGuardEngine } from './last-admin-guard.js';
5454
import { bindTestSecurityCatalog } from './__tests__/security-catalog.testkit.js';
55-
import { relaxEveryRegisteredTenantColumn } from './pre-18-tenant-column.testkit.js';
55+
import { relaxTenantColumns } from './pre-18-tenant-column.testkit.js';
5656

5757
const ENV = 'OS_PLATFORM_OWNER_EMAIL';
5858
/**
@@ -224,7 +224,7 @@ async function boot(opts: { unguarded?: boolean; legacyStore?: boolean } = {}):
224224
// An organization-less row ({@link seedLegacyRow}) exists only on a store
225225
// built before 18.x (ADR-0131 D1 makes a fresh tenant column NOT NULL): the
226226
// cases that seed one ask for that store.
227-
if (opts.legacyStore) await relaxEveryRegisteredTenantColumn(engine);
227+
if (opts.legacyStore) await relaxTenantColumns(engine, ['sys_permission_set', 'sys_user_permission_set']);
228228
// [ADR-0131 D3/D4] The set a grant names is the catalog's definition.
229229
bindTestSecurityCatalog(engine, { permissions: [{ name: ADMIN_FULL_ACCESS }] });
230230
if (!opts.unguarded) {

‎packages/plugins/plugin-auth/src/pre-18-tenant-column.testkit.ts‎

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
* attributed them. A suite whose subject is those rows calls this after
1212
* `syncSchemas()` and before seeding them. Done through each table's driver
1313
* and its own drift apply door (`relax_not_null`), never a hand `ALTER`.
14+
*
15+
* Name only the tables the suite writes organization-less: a relaxed table
16+
* also admits an unstamped production write, which an 18.x store refuses.
1417
*/
1518
export async function relaxTenantColumns(
1619
engine: { getDriverForObject(object: string): unknown },
@@ -28,29 +31,3 @@ export async function relaxTenantColumns(
2831
if (applied.length !== 1) throw new Error(`relaxTenantColumns: ${table}.${column} was not relaxed`);
2932
}
3033
}
31-
32-
/**
33-
* {@link relaxTenantColumns} over every object of an app that carries a tenant
34-
* column as the engine registered it — the shape a whole pre-18 store has.
35-
*/
36-
export async function relaxEveryTenantColumn(
37-
engine: { getDriverForObject(object: string): unknown; getSchema(object: string): unknown },
38-
objects: readonly { name: string }[],
39-
): Promise<void> {
40-
const tenant = objects
41-
.map((o) => o.name)
42-
.filter((name) => {
43-
const schema = engine.getSchema(name) as { fields?: Record<string, unknown>; tenancy?: { enabled?: boolean } } | undefined;
44-
return !!schema?.fields?.organization_id && schema.tenancy?.enabled !== false;
45-
});
46-
await relaxTenantColumns(engine, tenant);
47-
}
48-
49-
/** {@link relaxEveryTenantColumn} over every object the engine's registry holds. */
50-
export async function relaxEveryRegisteredTenantColumn(engine: {
51-
getDriverForObject(object: string): unknown;
52-
getSchema(object: string): unknown;
53-
registry: { getAllObjects(): readonly { name: string }[] };
54-
}): Promise<void> {
55-
await relaxEveryTenantColumn(engine, engine.registry.getAllObjects());
56-
}

0 commit comments

Comments
 (0)