1- import { eq } from 'drizzle-orm' ;
1+ import { eq , sql } from 'drizzle-orm' ;
22import { afterAll , beforeAll , beforeEach , describe , expect , it } from 'vite-plus/test' ;
33import {
44 createGitHubConnection ,
@@ -14,6 +14,7 @@ import {
1414 respondToProjectInvitation ,
1515 suspendUser ,
1616} from '../../src/db.ts' ;
17+ import { proxyTokenRotationTarget } from '../../src/proxy-token-rotation-request.ts' ;
1718import {
1819 projectAccessEvents ,
1920 projectCleanupJobs ,
@@ -196,6 +197,75 @@ describe('project team lifecycle', () => {
196197 }
197198 } ) ;
198199
200+ it ( 'queues the rotations of an admin suspension in one statement, keeping queued jobs' , async ( ) => {
201+ const owner = await createTestUser ( env . db , 'many-owner' ) ;
202+ const member = await createTestUser ( env . db , 'many-member' ) ;
203+ const collaborated = await createProject ( env . db , owner . id , 'many-collaborator' ) ;
204+ const administered = await createProject ( env . db , owner . id , 'many-admin' ) ;
205+ const read = await createProject ( env . db , owner . id , 'many-reader' ) ;
206+ const queued = await createProject ( env . db , owner . id , 'many-queued' ) ;
207+ const started = await createProject ( env . db , owner . id , 'many-started' ) ;
208+ await env . db . insert ( projectMembers ) . values ( [
209+ { project_id : collaborated . id , user_id : member . id , role : 'collaborator' } ,
210+ { project_id : administered . id , user_id : member . id , role : 'admin' } ,
211+ { project_id : read . id , user_id : member . id , role : 'reader' } ,
212+ { project_id : queued . id , user_id : member . id , role : 'collaborator' } ,
213+ { project_id : started . id , user_id : member . id , role : 'admin' } ,
214+ ] ) ;
215+ const later = sql `now() + interval '1 hour'` ;
216+ await env . db . insert ( projectCleanupJobs ) . values ( [
217+ {
218+ target : proxyTokenRotationTarget ( queued . id ) ,
219+ project_id : queued . id ,
220+ kind : 'proxy-token-rotation' ,
221+ payload : { phase : 'requested' , reason : 'member-role-changed' } ,
222+ next_attempt_at : later ,
223+ } ,
224+ {
225+ target : proxyTokenRotationTarget ( started . id ) ,
226+ project_id : started . id ,
227+ kind : 'proxy-token-rotation' ,
228+ payload : { phase : 'patch' , reason : 'operator' , gen : '1' , cursor : '' } ,
229+ attempts : 2 ,
230+ next_attempt_at : later ,
231+ } ,
232+ ] ) ;
233+ const before = new Map (
234+ ( await env . db . select ( ) . from ( projectCleanupJobs ) ) . map ( ( job ) => [ job . project_id , job ] ) ,
235+ ) ;
236+
237+ const { queries } = await captureQueries ( env . db , ( ) =>
238+ suspendUser ( env . db , member . id , 'admin_suspended' ) ,
239+ ) ;
240+
241+ const jobs = new Map (
242+ ( await env . db . select ( ) . from ( projectCleanupJobs ) ) . map ( ( job ) => [ job . project_id , job ] ) ,
243+ ) ;
244+ expect ( [ ...jobs . keys ( ) ] . sort ( ) ) . toEqual (
245+ [ collaborated . id , administered . id , queued . id , started . id ] . sort ( ) ,
246+ ) ;
247+ for ( const projectId of [ collaborated . id , administered . id ] ) {
248+ expect ( jobs . get ( projectId ) ) . toMatchObject ( {
249+ target : proxyTokenRotationTarget ( projectId ) ,
250+ kind : 'proxy-token-rotation' ,
251+ payload : { phase : 'requested' , reason : 'user-suspended' } ,
252+ attempts : 0 ,
253+ last_error : null ,
254+ } ) ;
255+ }
256+ // A job that has not started covers the request as it is.
257+ expect ( jobs . get ( queued . id ) ) . toEqual ( before . get ( queued . id ) ) ;
258+ // A started job finishes and runs once more, on its own schedule.
259+ const startedBefore = before . get ( started . id ) ! ;
260+ expect ( jobs . get ( started . id ) ) . toEqual ( {
261+ ...startedBefore ,
262+ payload : { ...startedBefore . payload , again : '1' } ,
263+ } ) ;
264+ expect (
265+ queries . filter ( ( query ) => / ^ i n s e r t i n t o " p r o j e c t _ c l e a n u p _ j o b s " / i. test ( query . trim ( ) ) ) ,
266+ ) . toHaveLength ( 1 ) ;
267+ } ) ;
268+
199269 it ( 'atomically discovers and locks pending-invitation projects before user deletion' , async ( ) => {
200270 const owner = await createTestUser ( env . db , 'owner' ) ;
201271 const invitee = await createTestUser ( env . db , 'invitee' ) ;
0 commit comments