@@ -44,11 +44,14 @@ const MANIFEST = {
4444 ] ,
4545} as any ;
4646
47- // The service's stored row shape, per rung.
48- const userRow = ( userId : string , value : string ) : Row => ( {
47+ // The service's stored row shape, per rung. A user row with a `null` owner is
48+ // the shape an ownerless write stored before the write half refused it; a
49+ // database may still hold one.
50+ const userRow = ( userId : string | null , value : string ) : Row => ( {
4951 namespace : NS , key : 'theme' , scope : 'user' , user_id : userId , value,
5052 value_enc : null , encrypted : false , locked : false , locked_reason : null ,
5153} ) ;
54+ const PEOPLE = [ userRow ( null , 'ownerless' ) , userRow ( USER_A , 'dark' ) , userRow ( USER_B , 'light' ) ] ;
5255const tenantRow = ( key : string , value : string ) : Row => ( {
5356 namespace : NS , key, scope : 'tenant' , user_id : null , value,
5457 value_enc : null , encrypted : false , locked : false , locked_reason : null ,
@@ -74,17 +77,20 @@ function matches(row: Row, where: Row): boolean {
7477 * tenant and user rungs. Every user id resolves in `sys_user`, so the
7578 * service's own user-reference refusal never decides a case here.
7679 */
77- function engineStack ( seed : Row [ ] ) {
80+ function engineStack ( seed : Row [ ] , opts : { overAnswer ?: boolean } = { } ) {
7881 const stores : Record < string , Row [ ] > = {
7982 sys_platform_setting : seed
8083 . filter ( ( r ) => r . scope === 'global' )
8184 . map ( ( { scope : _scope , user_id : _userId , ...stored } ) => ( { ...stored } ) ) ,
8285 sys_setting : seed . filter ( ( r ) => r . scope !== 'global' ) . map ( ( r ) => ( { ...r } ) ) ,
8386 } ;
84- const find = vi . fn ( async ( object : string , opts : any ) => {
85- if ( object === 'sys_user' ) return [ { id : opts ?. where ?. id } ] ;
86- const hits = ( stores [ object ] ?? [ ] ) . filter ( ( r ) => matches ( r , opts ?. where ?? { } ) ) ;
87- return typeof opts ?. limit === 'number' ? hits . slice ( 0 , opts . limit ) : hits ;
87+ const find = vi . fn ( async ( object : string , query : any ) => {
88+ if ( object === 'sys_user' ) return [ { id : query ?. where ?. id } ] ;
89+ // `overAnswer`: a store that hands back every row of the namespace to any
90+ // load, whatever else the predicate asked for.
91+ const where = opts . overAnswer ? { namespace : query ?. where ?. namespace } : query ?. where ?? { } ;
92+ const hits = ( stores [ object ] ?? [ ] ) . filter ( ( r ) => matches ( r , where ) ) ;
93+ return typeof query ?. limit === 'number' ? hits . slice ( 0 , query . limit ) : hits ;
8894 } ) ;
8995 const insert = vi . fn ( async ( object : string , data : Row ) => {
9096 ( stores [ object ] ??= [ ] ) . push ( { ...data } ) ;
@@ -150,20 +156,20 @@ describe('the user rung answers only its owner', () => {
150156 describe . each ( STACKS ) ( '%s' , ( _label , stack ) => {
151157 describe . each ( READ_PATHS ) ( '%s' , ( _path , read ) => {
152158 it . each ( NO_USER ) ( 'a resolve with no user id never answers a user-scoped row (%s)' , async ( _c , ctx ) => {
153- const { svc } = stack ( [ userRow ( USER_A , 'dark' ) , userRow ( USER_B , 'light' ) ] ) ;
159+ const { svc } = stack ( PEOPLE ) ;
154160 expect ( await read ( svc , ctx ) ) . toBe ( 'system' ) ;
155161 } ) ;
156162
157163 it ( 'control: a caller with a user id reads only their own row' , async ( ) => {
158- const { svc } = stack ( [ userRow ( USER_A , 'dark' ) , userRow ( USER_B , 'light' ) ] ) ;
164+ const { svc } = stack ( PEOPLE ) ;
159165 expect ( await read ( svc , { userId : USER_A } ) ) . toBe ( 'dark' ) ;
160166 expect ( await read ( svc , { userId : USER_B } ) ) . toBe ( 'light' ) ;
161167 expect ( await read ( svc , { userId : 'usr_owner_c' } ) ) . toBe ( 'system' ) ;
162168 } ) ;
163169 } ) ;
164170
165171 it ( 'with no user id, a user-scoped key falls through to the tenant rung, then global, then the default' , async ( ) => {
166- const users = [ userRow ( USER_A , 'dark' ) , userRow ( USER_B , 'light' ) ] ;
172+ const users = PEOPLE ;
167173
168174 const withTenant = await stack ( [ ...users , tenantRow ( 'theme' , 'tenant-theme' ) ] ) . svc . get ( NS , 'theme' ) ;
169175 expect ( withTenant ) . toMatchObject ( { value : 'tenant-theme' , source : 'tenant' } ) ;
@@ -186,7 +192,7 @@ describe('the user rung answers only its owner', () => {
186192 } ) ;
187193
188194 it ( 'a reset with no user id counts no user row and writes nothing' , async ( ) => {
189- const { svc, wrote, snapshot } = stack ( [ userRow ( USER_A , 'dark' ) , userRow ( USER_B , 'light' ) ] ) ;
195+ const { svc, wrote, snapshot } = stack ( PEOPLE ) ;
190196 const before = snapshot ( ) ;
191197 expect ( await svc . resetNamespace ( NS ) ) . toBe ( 0 ) ;
192198 expect ( wrote ( ) ) . toBe ( 0 ) ;
@@ -203,6 +209,16 @@ describe('the user rung answers only its owner', () => {
203209 } ) ;
204210} ) ;
205211
212+ describe ( 'the user rung compares the owner itself, whatever rows the store answers' , ( ) => {
213+ it . each ( READ_PATHS ) ( '%s' , async ( _path , read ) => {
214+ const { svc } = engineStack ( PEOPLE , { overAnswer : true } ) ;
215+ expect ( await read ( svc , { userId : USER_B } ) ) . toBe ( 'light' ) ;
216+ expect ( await read ( svc , { userId : USER_A } ) ) . toBe ( 'dark' ) ;
217+ expect ( await read ( svc , { userId : 'usr_owner_c' } ) ) . toBe ( 'system' ) ;
218+ expect ( await read ( svc , { } ) ) . toBe ( 'system' ) ;
219+ } ) ;
220+ } ) ;
221+
206222describe ( 'a user-scoped write names its owner' , ( ) => {
207223 describe . each ( STACKS ) ( '%s' , ( _label , stack ) => {
208224 it . each ( NO_USER ) ( 'a write of a user-scoped key with no user id is refused and stores nothing (%s)' , async ( _c , ctx ) => {
0 commit comments