mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 05:55:34 +09:00
fix(api): sync stripe customer email on change (#3243)
This commit is contained in:
1 parent
6e28092cdc
commit
cc5545c333
17 files changed
+509
-125
No files matched your search
@@ -9,8 +9,9 @@ import {
|
||||
type TestAccount,
|
||||
} from '@app/api/auth/tests/AuthTestUtils';
|
||||
import type {ApiTestHarness} from '@app/api/test/ApiTestHarness';
|
||||
import {NoopWorkerService} from '@app/api/test/NoopWorkerService';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
interface BouncedEmailRequestNewResponse {
|
||||
ticket: string;
|
||||
@@ -51,12 +52,21 @@ describe('Bounced email recovery flow', () => {
|
||||
await harness.reset();
|
||||
await clearTestEmails(harness);
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
it('allows bounced users to replace email without original-email verification', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
await markEmailAsBounced(harness, account);
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post(`/test/users/${account.userId}/premium`)
|
||||
.body({stripe_customer_id: 'cus_bounced_email_sync'})
|
||||
.expect(200)
|
||||
.execute();
|
||||
const addJob = vi.spyOn(NoopWorkerService.prototype, 'addJob');
|
||||
const initialMe = await createBuilder<UserPrivateResponse>(harness, account.token)
|
||||
.get('/users/@me')
|
||||
.expect(200)
|
||||
@@ -94,6 +104,7 @@ describe('Bounced email recovery flow', () => {
|
||||
const finalMe = await createBuilder<UserPrivateResponse>(harness, account.token).get('/users/@me').execute();
|
||||
expect(finalMe.email).toBe(replacementEmail);
|
||||
expect(finalMe.email_bounced).toBe(false);
|
||||
expect(addJob).toHaveBeenCalledWith('syncStripeCustomerEmail', {userId: account.userId});
|
||||
});
|
||||
it('rejects bounced-email recovery for accounts that are not marked as bounced', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
|
||||
@@ -12,8 +12,9 @@ import {
|
||||
type TestAccount,
|
||||
} from '@app/api/auth/tests/AuthTestUtils';
|
||||
import type {ApiTestHarness} from '@app/api/test/ApiTestHarness';
|
||||
import {NoopWorkerService} from '@app/api/test/NoopWorkerService';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
interface EmailChangeStartResponse {
|
||||
ticket: string;
|
||||
@@ -129,6 +130,9 @@ describe('Email change flow', () => {
|
||||
await harness.reset();
|
||||
await clearTestEmails(harness);
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
@@ -341,13 +345,14 @@ describe('Email change flow', () => {
|
||||
.execute();
|
||||
expect(updated.email).toBe(newEmail);
|
||||
});
|
||||
it('applies email changes for users who have ever purchased', async () => {
|
||||
it('applies email changes for users who have ever purchased and syncs their Stripe customer', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post(`/test/users/${account.userId}/premium`)
|
||||
.body({has_ever_purchased: true})
|
||||
.body({has_ever_purchased: true, stripe_customer_id: 'cus_email_change_sync'})
|
||||
.expect(200)
|
||||
.execute();
|
||||
const addJob = vi.spyOn(NoopWorkerService.prototype, 'addJob');
|
||||
const startResp = await startEmailChange(harness, account, account.password);
|
||||
let originalProof: string;
|
||||
if (startResp.require_original) {
|
||||
@@ -387,9 +392,11 @@ describe('Email change flow', () => {
|
||||
.execute();
|
||||
expect(updated.email).toBe(newEmail);
|
||||
expect(updated.has_ever_purchased).toBe(true);
|
||||
expect(addJob).toHaveBeenCalledWith('syncStripeCustomerEmail', {userId: account.userId});
|
||||
});
|
||||
it('applies ordinary claimed email changes', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const addJob = vi.spyOn(NoopWorkerService.prototype, 'addJob');
|
||||
const startResp = await startEmailChange(harness, account, account.password);
|
||||
const emails = await listTestEmails(harness, {recipient: account.email});
|
||||
const originalEmail = findLastTestEmail(emails, 'email_change_original');
|
||||
@@ -425,6 +432,7 @@ describe('Email change flow', () => {
|
||||
.execute();
|
||||
expect(updated.email).toBe(newEmail);
|
||||
expect(updated.verified).toBe(true);
|
||||
expect(addJob).not.toHaveBeenCalledWith('syncStripeCustomerEmail', expect.anything());
|
||||
});
|
||||
it('requires MFA (not password) for email_token apply when user has TOTP enabled', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
type TestAccount,
|
||||
} from '@app/api/auth/tests/AuthTestUtils';
|
||||
import type {ApiTestHarness} from '@app/api/test/ApiTestHarness';
|
||||
import {NoopWorkerService} from '@app/api/test/NoopWorkerService';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
interface EmailChangeStartResponse {
|
||||
ticket: string;
|
||||
@@ -130,11 +131,20 @@ describe('Email revert flow', () => {
|
||||
await harness.reset();
|
||||
await clearTestEmails(harness);
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
it('restores original email and clears mfa', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post(`/test/users/${account.userId}/premium`)
|
||||
.body({stripe_customer_id: 'cus_email_revert_sync'})
|
||||
.expect(200)
|
||||
.execute();
|
||||
const addJob = vi.spyOn(NoopWorkerService.prototype, 'addJob');
|
||||
const startResp = await startEmailChange(harness, account, account.password);
|
||||
let originalProof: string;
|
||||
if (startResp.require_original) {
|
||||
@@ -178,6 +188,7 @@ describe('Email revert flow', () => {
|
||||
expect(revertEmail?.metadata?.token).toBeDefined();
|
||||
const revertToken = revertEmail!.metadata!.token!;
|
||||
const newPassword = uniquePassword();
|
||||
addJob.mockClear();
|
||||
const revertResp = await createBuilderWithoutAuth<EmailRevertResponse>(harness)
|
||||
.post('/auth/email-revert')
|
||||
.body({
|
||||
@@ -186,6 +197,7 @@ describe('Email revert flow', () => {
|
||||
})
|
||||
.execute();
|
||||
expect(revertResp.token.length).toBeGreaterThan(0);
|
||||
expect(addJob).toHaveBeenCalledWith('syncStripeCustomerEmail', {userId: account.userId});
|
||||
await createBuilder(harness, account.token).get('/users/@me').expect(401).execute();
|
||||
const user = await createBuilder<UserPrivateResponse>(harness, revertResp.token).get('/users/@me').execute();
|
||||
expect(user.email).toBe(account.email);
|
||||
|
||||
Reference in new issue
Block a user