11/**
22 * A dot, `+tag`, or `googlemail.com` variant of an existing Gmail address must not get a second
3- * free allowance. Runs against real PostgreSQL through the production signup-stats and
4- * fallback-stats paths, and checks the lookup is served by `user_gmail_mailbox_idx`.
3+ * free allowance, even when two signups on one inbox race. Runs against real PostgreSQL through
4+ * the production signup-stats and fallback-stats paths, and checks the lookup is served by
5+ * `user_gmail_mailbox_idx`.
56 */
67import { db } from '@sim/db'
78import { gmailMailbox , user , userStats } from '@sim/db/schema'
9+ import { createDeferred } from '@sim/testing'
810import { authMock } from '@sim/testing/mocks/auth.mock'
911import { envFlagsMock } from '@sim/testing/mocks/env-flags.mock'
1012import { generateId } from '@sim/utils/id'
1113import { eq , inArray , sql } from 'drizzle-orm'
14+ import { alias } from 'drizzle-orm/pg-core'
1215import { afterAll , describe , expect , it , vi } from 'vitest'
1316import { ensureUserStatsExists , handleNewUser } from '@/lib/billing/core/usage'
1417import { getFreeTierLimit } from '@/lib/billing/subscriptions/utils'
18+ import { acquireGmailInboxLock } from '@/lib/users/gmail-alias'
1519
1620vi . mock ( '@/lib/core/config/env-flags' , ( ) => ( {
1721 ...envFlagsMock ,
@@ -60,6 +64,11 @@ async function statsOf(userId: string) {
6064 return { freeTierEligible : row ?. freeTierEligible , limit : Number ( row ?. currentUsageLimit ) }
6165}
6266
67+ /** An account created a minute ago, so any account inserted after it counts as later. */
68+ function insertEarlierUser ( email : string ) : Promise < string > {
69+ return insertUser ( email , { createdAt : new Date ( Date . now ( ) - 60_000 ) } )
70+ }
71+
6372/** A Gmail local part no other test run can collide with. */
6473function uniqueLocalPart ( ) : string {
6574 return `alias${ generateId ( ) . replaceAll ( '-' , '' ) } `
@@ -127,18 +136,16 @@ describe('Gmail inboxes and the free allowance', () => {
127136 it ( 'withholds it from a stats row the fallback creates after signup failed to' , async ( ) => {
128137 const local = uniqueLocalPart ( )
129138 await signUp ( `${ local } @gmail.com` )
130- const alias = await insertUser ( `${ local . slice ( 0 , 3 ) } .${ local . slice ( 3 ) } @gmail.com` )
139+ const later = await insertUser ( `${ local . slice ( 0 , 3 ) } .${ local . slice ( 3 ) } @gmail.com` )
131140
132- await ensureUserStatsExists ( alias )
141+ await ensureUserStatsExists ( later )
133142
134- expect ( await statsOf ( alias ) ) . toEqual ( { freeTierEligible : false , limit : 0 } )
143+ expect ( await statsOf ( later ) ) . toEqual ( { freeTierEligible : false , limit : 0 } )
135144 } )
136145
137146 it ( 'keeps the allowance with the inbox’s first account when its stats row comes after an alias’s' , async ( ) => {
138147 const local = uniqueLocalPart ( )
139- const original = await insertUser ( `${ local } @gmail.com` , {
140- createdAt : new Date ( Date . now ( ) - 60_000 ) ,
141- } )
148+ const original = await insertEarlierUser ( `${ local } @gmail.com` )
142149 const alias = await signUp ( `${ local } +later@gmail.com` )
143150
144151 await ensureUserStatsExists ( original )
@@ -164,11 +171,78 @@ describe('Gmail inboxes and the free allowance', () => {
164171 expect ( eligible . sort ( ) ) . toEqual ( [ false , true ] )
165172 } )
166173
174+ it ( 'withdraws it from an alias whose signup decided before the earlier account was visible' , async ( ) => {
175+ const local = uniqueLocalPart ( )
176+ const later = await signUp ( `${ local } +fast@gmail.com` )
177+ const original = await insertEarlierUser ( `${ local } @gmail.com` )
178+
179+ await handleNewUser ( original , { } )
180+
181+ expect ( await statsOf ( original ) ) . toEqual ( { freeTierEligible : true , limit : getFreeTierLimit ( ) } )
182+ expect ( await statsOf ( later ) ) . toEqual ( { freeTierEligible : false , limit : 0 } )
183+ } )
184+
185+ it ( 'never withdraws it from a later alias when the fallback creates the earlier account’s row' , async ( ) => {
186+ const local = uniqueLocalPart ( )
187+ const original = await insertEarlierUser ( `${ local } @gmail.com` )
188+ const later = await signUp ( `${ local } +later@gmail.com` )
189+ await db . update ( userStats ) . set ( { freeTierEligible : true } ) . where ( eq ( userStats . userId , later ) )
190+
191+ await ensureUserStatsExists ( original )
192+
193+ expect ( await statsOf ( original ) ) . toEqual ( { freeTierEligible : true , limit : getFreeTierLimit ( ) } )
194+ expect ( ( await statsOf ( later ) ) . freeTierEligible ) . toBe ( true )
195+ } )
196+
197+ it ( 'makes a racing signup on the same inbox wait for the inbox lock' , async ( ) => {
198+ const local = uniqueLocalPart ( )
199+ const original = await insertEarlierUser ( `${ local } @gmail.com` )
200+ const racer = await insertUser ( `${ local . slice ( 0 , 4 ) } .${ local . slice ( 4 ) } @gmail.com` )
201+ const held = createDeferred < number > ( )
202+ const release = createDeferred < void > ( )
203+ const pending : Promise < unknown > [ ] = [ ]
204+ try {
205+ const holder = db . transaction ( async ( tx ) => {
206+ await acquireGmailInboxLock ( tx , original )
207+ const [ connection ] = await tx . execute < { pid : number } > ( sql `SELECT pg_backend_pid() AS pid` )
208+ held . resolve ( connection . pid )
209+ await release . promise
210+ } )
211+ pending . push ( holder )
212+ void holder . catch ( ( error : unknown ) => held . reject ( error ) )
213+ const blockerPid = await held . promise
214+ const signup = handleNewUser ( racer , { } )
215+ pending . push ( signup )
216+
217+ await vi . waitFor (
218+ async ( ) => {
219+ const [ waiting ] = await db . execute < { pid : number } > ( sql `
220+ SELECT pid FROM pg_stat_activity
221+ WHERE datname = current_database()
222+ AND ${ blockerPid } ::int = ANY(pg_blocking_pids(pid))
223+ ` )
224+ expect ( waiting ) . toBeDefined ( )
225+ } ,
226+ { timeout : 5_000 , interval : 25 }
227+ )
228+ expect ( ( await statsOf ( racer ) ) . freeTierEligible ) . toBeUndefined ( )
229+
230+ release . resolve ( )
231+ await holder
232+ await signup
233+ expect ( await statsOf ( racer ) ) . toEqual ( { freeTierEligible : false , limit : 0 } )
234+ } finally {
235+ release . resolve ( )
236+ await Promise . allSettled ( pending )
237+ }
238+ } )
239+
167240 it ( 'finds the inbox through user_gmail_mailbox_idx rather than a scan of user' , async ( ) => {
241+ const subject = alias ( user , 'subject' )
168242 const plan = await db . transaction ( async ( tx ) => {
169243 await tx . execute ( sql `SET LOCAL enable_seqscan = off` )
170244 const rows = await tx . execute < { 'QUERY PLAN' : string } > (
171- sql `EXPLAIN SELECT ${ user . id } FROM ${ user } WHERE ${ gmailMailbox ( user . email ) } = ${ ` ${ uniqueLocalPart ( ) } @gmail.com` } LIMIT 1 `
245+ sql `EXPLAIN SELECT ${ user . id } FROM ${ user } AS "subject" JOIN ${ user } ON ${ gmailMailbox ( user . email ) } = ${ gmailMailbox ( subject . email ) } WHERE ${ subject . id } = ${ generateId ( ) } `
172246 )
173247 return rows . map ( ( row ) => row [ 'QUERY PLAN' ] ) . join ( '\n' )
174248 } )
0 commit comments