Skip to content

Commit 10d2b85

Browse files
committed
test(dogfood): a sign-up's verification mail lands in sys_identity_email, not sys_email
Claude-Session: https://claude.ai/code/session_016CvQEdxxYYVa8C8AjKZBtL Co-authored-by: Claude <noreply@anthropic.com>
1 parent 28fcf4c commit 10d2b85

1 file changed

Lines changed: 37 additions & 12 deletions

File tree

‎packages/qa/dogfood/test/signup-organization-census.dogfood.test.ts‎

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22
//
33
// [ADR-0131 D9] The organization census of one self-registration, end to end
4-
// on the real stack: a sign-up through `POST /auth/sign-up/email` lands three
4+
// on the real stack: a sign-up through `POST /auth/sign-up/email` lands two
55
// framework writes on tenant-column objects — the declared self-registration
6-
// grant (`sys_user_permission_set`), the verification mail (`sys_email`) and
7-
// the activity about the records the sign-up creates (`sys_activity`). Each
8-
// producer names the organization it can derive, so on a `single` boot —
9-
// where the Default Organization is the only organization there is — the
10-
// sign-up leaves no row without one on any of the three tables.
6+
// grant (`sys_user_permission_set`) and the activity about the records the
7+
// sign-up creates (`sys_activity`). Each producer names the organization it
8+
// can derive, so on a `single` boot — where the Default Organization is the
9+
// only organization there is — the sign-up leaves no row without one on
10+
// either table, nor on `sys_email`.
11+
//
12+
// [ADR-0131 D7, ruling of record 6257510973, Q2 A] The verification mail is
13+
// identity mail — about a person, not an organization — so it is written to
14+
// `sys_identity_email`, `sys_email`'s deployment-level sibling with no
15+
// organization column, and not to the tenant mail log.
1116
//
1217
// Under a wall the same sign-up has no organization to derive (the registrant
1318
// belongs to none yet); those rows' fate is not settled by this file.
@@ -19,7 +24,9 @@ import { EmailServicePlugin } from '@objectstack/plugin-email';
1924
import { AuditPlugin } from '@objectstack/plugin-audit';
2025

2126
const SYS = { context: { isSystem: true } };
22-
const CENSUS_TABLES = ['sys_user_permission_set', 'sys_email', 'sys_activity'] as const;
27+
const CENSUS_TABLES = ['sys_user_permission_set', 'sys_email', 'sys_activity', 'sys_identity_email'] as const;
28+
/** The tenant-column tables the sign-up must write to — the mail is no longer one of them. */
29+
const WRITTEN_TENANT_TABLES = ['sys_user_permission_set', 'sys_activity'] as const;
2330
const REGISTRANT = 'census-registrant@dogfood.test';
2431

2532
/** A transport that accepts every message, so the verification mail persists as sent. */
@@ -87,26 +94,44 @@ describe('[ADR-0131 D9] a self-registration on a single boot leaves no organizat
8794
expect(registrant?.id, 'the registrant exists').toBeTruthy();
8895
const grants = await ql.find('sys_user_permission_set', { where: { user_id: registrant.id }, ...SYS });
8996
expect(grants.map((g: any) => g.permission_set)).toEqual(['member_default']);
90-
const mails = await ql.find('sys_email', { where: { related_object: 'sys_user', related_id: registrant.id }, ...SYS });
91-
expect(mails.length, 'the verification mail persisted').toBeGreaterThan(0);
97+
const mails = await ql.find('sys_identity_email', { where: { related_object: 'sys_user', related_id: registrant.id }, ...SYS });
98+
expect(mails.length, 'the verification mail persisted, in the identity-mail log').toBeGreaterThan(0);
99+
const tenantMails = await ql.find('sys_email', { where: { related_object: 'sys_user', related_id: registrant.id }, ...SYS });
100+
expect(tenantMails, 'no verification mail in the tenant mail log').toEqual([]);
92101
const activity = await ql.find('sys_activity', { where: { object_name: 'sys_user', record_id: registrant.id }, ...SYS });
93102
expect(activity.length, 'the registrant\'s creation is on the activity stream').toBeGreaterThan(0);
94103
});
95104

96-
it('every row the sign-up wrote on the three tables names the Default Organization', async () => {
105+
it('every row the sign-up wrote on a tenant-column table names the Default Organization', async () => {
97106
const census: Record<string, { written: number; orgLess: unknown[]; foreign: unknown[] }> = {};
98-
for (const table of CENSUS_TABLES) {
107+
for (const table of CENSUS_TABLES.filter((t) => t !== 'sys_identity_email')) {
99108
const written = (await rowsOf(table)).filter((r) => !beforeSignUp.get(table)!.has(r.id));
100109
census[table] = {
101110
written: written.length,
102111
orgLess: written.filter((r) => r.organization_id == null || r.organization_id === '').map((r) => r.id),
103112
foreign: written.filter((r) => r.organization_id != null && r.organization_id !== defaultOrgId).map((r) => r.id),
104113
};
105114
}
106-
for (const table of CENSUS_TABLES) {
115+
for (const table of WRITTEN_TENANT_TABLES) {
107116
expect(census[table].written, `${table}: the sign-up wrote rows here`).toBeGreaterThan(0);
117+
}
118+
for (const table of Object.keys(census)) {
108119
expect(census[table].orgLess, `${table}: rows without an organization`).toEqual([]);
109120
expect(census[table].foreign, `${table}: rows owned by another organization`).toEqual([]);
110121
}
111122
});
123+
124+
// [ADR-0131 D7, Q2 A] The verification mail is identity mail.
125+
it('sys_identity_email carries no organization column, and the sign-up\'s mail row names none', async () => {
126+
// Read through the registry's schema, which carries the injected columns.
127+
const schema = ql.getSchema?.('sys_identity_email');
128+
expect(schema, 'sys_identity_email is registered').toBeTruthy();
129+
expect(Object.keys(schema.fields ?? {})).not.toContain('organization_id');
130+
// Non-vacuity: the same read sees the injected column on the tenant mail log.
131+
expect(Object.keys(ql.getSchema('sys_email')?.fields ?? {})).toContain('organization_id');
132+
const registrant = await ql.findOne('sys_user', { where: { email: REGISTRANT }, ...SYS });
133+
const [mail] = await ql.find('sys_identity_email', { where: { related_id: registrant.id }, ...SYS });
134+
expect(mail).toMatchObject({ related_object: 'sys_user', subject: expect.any(String) });
135+
expect('organization_id' in mail).toBe(false);
136+
});
112137
});

0 commit comments

Comments
 (0)