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';
1924import { AuditPlugin } from '@objectstack/plugin-audit' ;
2025
2126const 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 ;
2330const 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