|
| 1 | +/** |
| 2 | + * 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`. |
| 5 | + */ |
| 6 | +import { db } from '@sim/db' |
| 7 | +import { gmailMailbox, user, userStats } from '@sim/db/schema' |
| 8 | +import { authMock } from '@sim/testing/mocks/auth.mock' |
| 9 | +import { envFlagsMock } from '@sim/testing/mocks/env-flags.mock' |
| 10 | +import { generateId } from '@sim/utils/id' |
| 11 | +import { eq, inArray, sql } from 'drizzle-orm' |
| 12 | +import { afterAll, describe, expect, it, vi } from 'vitest' |
| 13 | +import { ensureUserStatsExists, handleNewUser } from '@/lib/billing/core/usage' |
| 14 | +import { getFreeTierLimit } from '@/lib/billing/subscriptions/utils' |
| 15 | + |
| 16 | +vi.mock('@/lib/core/config/env-flags', () => ({ |
| 17 | + ...envFlagsMock, |
| 18 | + isHosted: true, |
| 19 | + isBillingEnabled: true, |
| 20 | +})) |
| 21 | +vi.mock('@/lib/auth', () => authMock) |
| 22 | + |
| 23 | +const userIds: string[] = [] |
| 24 | + |
| 25 | +async function insertUser( |
| 26 | + email: string, |
| 27 | + { banned = false, emailVerified = true, createdAt = new Date() } = {} |
| 28 | +): Promise<string> { |
| 29 | + const id = generateId() |
| 30 | + userIds.push(id) |
| 31 | + await db.insert(user).values({ |
| 32 | + id, |
| 33 | + name: 'Gmail alias fixture', |
| 34 | + email, |
| 35 | + emailVerified, |
| 36 | + banned, |
| 37 | + createdAt, |
| 38 | + updatedAt: createdAt, |
| 39 | + }) |
| 40 | + return id |
| 41 | +} |
| 42 | + |
| 43 | +async function signUp( |
| 44 | + email: string, |
| 45 | + options?: { banned?: boolean; emailVerified?: boolean } |
| 46 | +): Promise<string> { |
| 47 | + const id = await insertUser(email, options) |
| 48 | + await handleNewUser(id, {}) |
| 49 | + return id |
| 50 | +} |
| 51 | + |
| 52 | +async function statsOf(userId: string) { |
| 53 | + const [row] = await db |
| 54 | + .select({ |
| 55 | + freeTierEligible: userStats.freeTierEligible, |
| 56 | + currentUsageLimit: userStats.currentUsageLimit, |
| 57 | + }) |
| 58 | + .from(userStats) |
| 59 | + .where(eq(userStats.userId, userId)) |
| 60 | + return { freeTierEligible: row?.freeTierEligible, limit: Number(row?.currentUsageLimit) } |
| 61 | +} |
| 62 | + |
| 63 | +/** A Gmail local part no other test run can collide with. */ |
| 64 | +function uniqueLocalPart(): string { |
| 65 | + return `alias${generateId().replaceAll('-', '')}` |
| 66 | +} |
| 67 | + |
| 68 | +afterAll(async () => { |
| 69 | + if (userIds.length) await db.delete(user).where(inArray(user.id, userIds)) |
| 70 | +}) |
| 71 | + |
| 72 | +describe('Gmail inboxes and the free allowance', () => { |
| 73 | + it('withholds the allowance from dot, +tag, and googlemail variants of a live account', async () => { |
| 74 | + const local = uniqueLocalPart() |
| 75 | + const original = await signUp(`${local}@gmail.com`) |
| 76 | + |
| 77 | + const dotted = await signUp(`${local.slice(0, 4)}.${local.slice(4)}@gmail.com`) |
| 78 | + const tagged = await signUp(`${local}+farm@gmail.com`) |
| 79 | + const googlemail = await signUp(`${local.toUpperCase()}@GoogleMail.com`) |
| 80 | + |
| 81 | + expect(await statsOf(original)).toEqual({ freeTierEligible: true, limit: getFreeTierLimit() }) |
| 82 | + for (const variant of [dotted, tagged, googlemail]) { |
| 83 | + expect(await statsOf(variant)).toEqual({ freeTierEligible: false, limit: 0 }) |
| 84 | + } |
| 85 | + }) |
| 86 | + |
| 87 | + it('withholds it when the inbox belongs to a banned account', async () => { |
| 88 | + const local = uniqueLocalPart() |
| 89 | + await signUp(`${local}@gmail.com`, { banned: true }) |
| 90 | + |
| 91 | + const returning = await signUp(`${local}+again@gmail.com`) |
| 92 | + |
| 93 | + expect(await statsOf(returning)).toEqual({ freeTierEligible: false, limit: 0 }) |
| 94 | + }) |
| 95 | + |
| 96 | + it('withholds it from aliases registered before any of them verified the inbox', async () => { |
| 97 | + const local = uniqueLocalPart() |
| 98 | + const first = await signUp(`${local}@gmail.com`, { emailVerified: false }) |
| 99 | + const second = await signUp(`${local}+two@gmail.com`, { emailVerified: false }) |
| 100 | + await db |
| 101 | + .update(user) |
| 102 | + .set({ emailVerified: true }) |
| 103 | + .where(inArray(user.id, [first, second])) |
| 104 | + |
| 105 | + const third = await signUp(`${local.slice(0, 5)}.${local.slice(5)}@gmail.com`) |
| 106 | + |
| 107 | + expect(await statsOf(first)).toEqual({ freeTierEligible: true, limit: getFreeTierLimit() }) |
| 108 | + expect(await statsOf(second)).toEqual({ freeTierEligible: false, limit: 0 }) |
| 109 | + expect(await statsOf(third)).toEqual({ freeTierEligible: false, limit: 0 }) |
| 110 | + }) |
| 111 | + |
| 112 | + it('grants it to a different Gmail inbox and never folds addresses outside Gmail', async () => { |
| 113 | + const local = uniqueLocalPart() |
| 114 | + await signUp(`${local}@gmail.com`) |
| 115 | + await signUp(`${local}+one@gmail-alias.test`) |
| 116 | + |
| 117 | + const otherInbox = await signUp(`${local}x@gmail.com`) |
| 118 | + const otherDomain = await signUp(`${local}@gmail-alias.test`) |
| 119 | + |
| 120 | + expect(await statsOf(otherInbox)).toEqual({ freeTierEligible: true, limit: getFreeTierLimit() }) |
| 121 | + expect(await statsOf(otherDomain)).toEqual({ |
| 122 | + freeTierEligible: true, |
| 123 | + limit: getFreeTierLimit(), |
| 124 | + }) |
| 125 | + }) |
| 126 | + |
| 127 | + it('withholds it from a stats row the fallback creates after signup failed to', async () => { |
| 128 | + const local = uniqueLocalPart() |
| 129 | + await signUp(`${local}@gmail.com`) |
| 130 | + const alias = await insertUser(`${local.slice(0, 3)}.${local.slice(3)}@gmail.com`) |
| 131 | + |
| 132 | + await ensureUserStatsExists(alias) |
| 133 | + |
| 134 | + expect(await statsOf(alias)).toEqual({ freeTierEligible: false, limit: 0 }) |
| 135 | + }) |
| 136 | + |
| 137 | + it('keeps the allowance with the inbox’s first account when its stats row comes after an alias’s', async () => { |
| 138 | + const local = uniqueLocalPart() |
| 139 | + const original = await insertUser(`${local}@gmail.com`, { |
| 140 | + createdAt: new Date(Date.now() - 60_000), |
| 141 | + }) |
| 142 | + const alias = await signUp(`${local}+later@gmail.com`) |
| 143 | + |
| 144 | + await ensureUserStatsExists(original) |
| 145 | + |
| 146 | + expect(await statsOf(original)).toEqual({ freeTierEligible: true, limit: getFreeTierLimit() }) |
| 147 | + expect(await statsOf(alias)).toEqual({ freeTierEligible: false, limit: 0 }) |
| 148 | + }) |
| 149 | + |
| 150 | + it('withholds from exactly one of two accounts created at the same instant', async () => { |
| 151 | + const local = uniqueLocalPart() |
| 152 | + const createdAt = new Date() |
| 153 | + const first = await insertUser(`${local}@gmail.com`, { createdAt }) |
| 154 | + const second = await insertUser(`${local.slice(0, 2)}.${local.slice(2)}@gmail.com`, { |
| 155 | + createdAt, |
| 156 | + }) |
| 157 | + |
| 158 | + await ensureUserStatsExists(first) |
| 159 | + await ensureUserStatsExists(second) |
| 160 | + |
| 161 | + const eligible = [await statsOf(first), await statsOf(second)].map( |
| 162 | + (stats) => stats.freeTierEligible |
| 163 | + ) |
| 164 | + expect(eligible.sort()).toEqual([false, true]) |
| 165 | + }) |
| 166 | + |
| 167 | + it('finds the inbox through user_gmail_mailbox_idx rather than a scan of user', async () => { |
| 168 | + const plan = await db.transaction(async (tx) => { |
| 169 | + await tx.execute(sql`SET LOCAL enable_seqscan = off`) |
| 170 | + 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` |
| 172 | + ) |
| 173 | + return rows.map((row) => row['QUERY PLAN']).join('\n') |
| 174 | + }) |
| 175 | + |
| 176 | + expect(plan).toContain('user_gmail_mailbox_idx') |
| 177 | + }) |
| 178 | +}) |
0 commit comments