@@ -122,6 +122,42 @@ const ORG_CONTEXT: StorageBillingContext = {
122122 customStorageLimitGB : null ,
123123}
124124
125+ const USER_CONTEXT : StorageBillingContext = {
126+ workspaceId : 'workspace-1' ,
127+ billedAccountUserId : 'workspace-owner' ,
128+ billingEntity : { type : 'user' , id : 'workspace-owner' } ,
129+ plan : 'pro' ,
130+ customStorageLimitGB : null ,
131+ }
132+
133+ /**
134+ * Both payer kinds. The workspace lock is shared, but the payer lock branches
135+ * to a different table per kind, so a lock-mode regression on only one of them
136+ * has to fail a test.
137+ */
138+ const PAYER_CASES = [
139+ {
140+ label : 'organization' ,
141+ context : ORG_CONTEXT ,
142+ workspaceRow : {
143+ billedAccountUserId : 'workspace-owner' ,
144+ organizationId : 'workspace-org' as string | null ,
145+ storageUsedBytes : 1_000 ,
146+ } ,
147+ payerLockRows : [ { id : 'workspace-org' , storageUsedBytes : 1_000 } ] ,
148+ } ,
149+ {
150+ label : 'user' ,
151+ context : USER_CONTEXT ,
152+ workspaceRow : {
153+ billedAccountUserId : 'workspace-owner' ,
154+ organizationId : null as string | null ,
155+ storageUsedBytes : 1_000 ,
156+ } ,
157+ payerLockRows : [ { id : 'workspace-owner' , storageUsedBytes : 1_000 } ] ,
158+ } ,
159+ ] as const
160+
125161beforeAll ( ( ) => {
126162 setEnvFlags ( { isBillingEnabled : true } )
127163} )
@@ -194,38 +230,30 @@ describe('workspace storage counter mutations', () => {
194230 * two concurrent uploads take on each other. `FOR NO KEY UPDATE` still
195231 * conflicts with itself, so the ledgers stay serialized.
196232 */
197- it ( 'takes every ledger lock as FOR NO KEY UPDATE so it never upgrades a key-share lock' , async ( ) => {
198- await incrementStorageUsageForBillingContextInTx ( mockTx as unknown as DbOrTx , ORG_CONTEXT , 100 )
233+ it . each ( PAYER_CASES ) (
234+ 'locks the workspace and its $label payer as FOR NO KEY UPDATE' ,
235+ async ( { context, workspaceRow } ) => {
236+ mockWorkspaceRow . current = { ...workspaceRow }
199237
200- expect ( mockTxFor ) . toHaveBeenCalled ( )
201- for ( const call of mockTxFor . mock . calls ) {
202- expect ( call ) . toEqual ( [ 'no key update' ] )
238+ await incrementStorageUsageForBillingContextInTx ( mockTx as unknown as DbOrTx , context , 100 )
239+
240+ expect ( mockTxFor . mock . calls ) . toEqual ( [ [ 'no key update' ] , [ 'no key update' ] ] )
203241 }
204- } )
242+ )
205243
206- it ( 'takes batch ledger locks as FOR NO KEY UPDATE' , async ( ) => {
207- mockOrderedLockRows . queue = [
208- [
209- {
210- id : 'workspace-1' ,
211- billedAccountUserId : 'workspace-owner' ,
212- organizationId : 'workspace-org' ,
213- storageUsedBytes : 1_000 ,
214- } ,
215- ] ,
216- [ { id : 'workspace-org' , storageUsedBytes : 1_000 } ] ,
217- ]
244+ it . each ( PAYER_CASES ) (
245+ 'locks batched workspace and $label payer ledgers as FOR NO KEY UPDATE' ,
246+ async ( { context, workspaceRow, payerLockRows } ) => {
247+ mockOrderedLockRows . queue = [ [ { id : 'workspace-1' , ...workspaceRow } ] , [ ...payerLockRows ] ]
218248
219- await applyStorageUsageDeltasInTx ( mockTx as unknown as DbOrTx , {
220- workspaceDeltas : [ { context : ORG_CONTEXT , deltaBytes : 100 } ] ,
221- legacyDeltas : [ ] ,
222- } )
249+ await applyStorageUsageDeltasInTx ( mockTx as unknown as DbOrTx , {
250+ workspaceDeltas : [ { context, deltaBytes : 100 } ] ,
251+ legacyDeltas : [ ] ,
252+ } )
223253
224- expect ( mockTxOrderedFor ) . toHaveBeenCalled ( )
225- for ( const call of mockTxOrderedFor . mock . calls ) {
226- expect ( call ) . toEqual ( [ 'no key update' ] )
254+ expect ( mockTxOrderedFor . mock . calls ) . toEqual ( [ [ 'no key update' ] , [ 'no key update' ] ] )
227255 }
228- } )
256+ )
229257
230258 it ( 'serializes quota admission on the locked payer ledger' , async ( ) => {
231259 mockGetStorageLimitForBillingContext . mockReturnValue ( 1_050 )
0 commit comments