Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions libs/accounts/passkey/src/lib/passkey.manager.in.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
AccountDbProvider,
PasskeyFactory,
testAccountDatabaseSetup,
testAccountDatabaseTeardown,
} from '@fxa/shared/db/mysql/account';
import { AccountManager } from '@fxa/shared/account/account';
import { LOGGER_PROVIDER } from '@fxa/shared/log';
Expand Down Expand Up @@ -85,9 +86,7 @@ describe('PasskeyManager (Integration)', () => {
});

afterAll(async () => {
if (db) {
await db.destroy();
}
await testAccountDatabaseTeardown(db);
});

async function createTestAccount(): Promise<string> {
Expand Down
5 changes: 2 additions & 3 deletions libs/accounts/passkey/src/lib/passkey.repository.in.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { faker } from '@faker-js/faker';
import {
AccountDatabase,
testAccountDatabaseSetup,
testAccountDatabaseTeardown,
PasskeyFactory,
} from '@fxa/shared/db/mysql/account';
import { AccountManager } from '@fxa/shared/account/account';
Expand Down Expand Up @@ -51,9 +52,7 @@ describe('PasskeyRepository (Integration)', () => {
}

afterAll(async () => {
if (db) {
await db.destroy();
}
await testAccountDatabaseTeardown(db);
});

describe('insert and find operations', () => {
Expand Down
5 changes: 2 additions & 3 deletions libs/accounts/passkey/src/lib/passkey.security.in.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
AccountDbProvider,
PasskeyFactory,
testAccountDatabaseSetup,
testAccountDatabaseTeardown,
} from '@fxa/shared/db/mysql/account';
import { AccountManager } from '@fxa/shared/account/account';
import { LOGGER_PROVIDER } from '@fxa/shared/log';
Expand Down Expand Up @@ -82,9 +83,7 @@ describe('Passkey Security Tests', () => {
});

afterAll(async () => {
if (db) {
await db.destroy();
}
await testAccountDatabaseTeardown(db);
});

async function createTestAccount(): Promise<string> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AccountDatabase,
AccountDbProvider,
testAccountDatabaseSetup,
testAccountDatabaseTeardown,
RecoveryPhoneFactory,
} from '@fxa/shared/db/mysql/account';
import { Test } from '@nestjs/testing';
Expand Down Expand Up @@ -82,9 +83,10 @@ describe('RecoveryPhoneManager', () => {
});

afterAll(async () => {
await clearRedisSmsKeys();
await db.destroy();
dateMock.mockReset();
// Drop the schema first, so a failed Redis cleanup cannot leak it.
await testAccountDatabaseTeardown(db);
await clearRedisSmsKeys();
});

it('should get a recovery phone', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
AccountDatabase,
AccountDbProvider,
testAccountDatabaseSetup,
testAccountDatabaseTeardown,
} from '@fxa/shared/db/mysql/account';
import { RecoveryCodeFactory } from './backup-code.factories';
import { faker } from '@faker-js/faker';
Expand Down Expand Up @@ -46,8 +47,8 @@ describe('BackupCodeManager', () => {
backupCodeManager = moduleRef.get(BackupCodeManager);
});

afterAll(async () => {
await db.destroy();
afterEach(async () => {
await testAccountDatabaseTeardown(db);
});

it('should return that the user has backup codes and count them', async () => {
Expand Down
3 changes: 2 additions & 1 deletion libs/payments/cart/src/lib/cart.manager.in.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
CartFactory,
CartState,
testAccountDatabaseSetup,
testAccountDatabaseTeardown,
AccountDatabase,
CartUpdate,
CartErrorReasonId,
Expand Down Expand Up @@ -87,7 +88,7 @@ describe('CartManager', () => {
});

afterAll(async () => {
await db.destroy();
await testAccountDatabaseTeardown(db);
});

beforeEach(async () => {
Expand Down
3 changes: 2 additions & 1 deletion libs/payments/cart/src/lib/checkout.service.in.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CartErrorReasonId,
CartState,
testAccountDatabaseSetup,
testAccountDatabaseTeardown,
AccountDatabase,
} from '@fxa/shared/db/mysql/account';

Expand Down Expand Up @@ -65,7 +66,7 @@ describe('CheckoutService', () => {
});

afterAll(async () => {
await db.destroy();
await testAccountDatabaseTeardown(db);
});

describe('full flow: cart state transitions in DB', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Kysely } from 'kysely';

import { DB, testAccountDatabaseSetup } from '@fxa/shared/db/mysql/account';
import {
DB,
testAccountDatabaseSetup,
testAccountDatabaseTeardown,
} from '@fxa/shared/db/mysql/account';

import { CreatePaypalCustomerFactory } from './paypalCustomer.factories';
import { PaypalCustomerManager } from './paypalCustomer.manager';
Expand All @@ -25,18 +29,15 @@ describe('PaypalCustomerManager', () => {
});

afterAll(async () => {
if (kyselyDb) {
await kyselyDb.destroy();
}
await testAccountDatabaseTeardown(kyselyDb);
});

describe('createPaypalCustomer', () => {
it('creates a paypalCustomer successfully', async () => {
const paypalCustomer = CreatePaypalCustomerFactory();

const result = await paypalCustomerManager.createPaypalCustomer(
paypalCustomer
);
const result =
await paypalCustomerManager.createPaypalCustomer(paypalCustomer);

expect(result).toEqual({
...paypalCustomer,
Expand Down Expand Up @@ -184,9 +185,8 @@ describe('PaypalCustomerManager', () => {
const resultPaypalCustomer =
await paypalCustomerManager.createPaypalCustomer(paypalCustomer);

const result = await paypalCustomerManager.deletePaypalCustomer(
resultPaypalCustomer
);
const result =
await paypalCustomerManager.deletePaypalCustomer(resultPaypalCustomer);

expect(result).toEqual(true);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
DB,
PaypalCustomerFactory,
testAccountDatabaseSetup,
testAccountDatabaseTeardown,
} from '@fxa/shared/db/mysql/account';
import { PaypalCustomerNoRowsUpdatedError } from './paypalCustomer.error';

Expand All @@ -27,9 +28,7 @@ describe('PaypalCustomer Repository', () => {
});

afterAll(async () => {
if (kyselyDb) {
await kyselyDb.destroy();
}
await testAccountDatabaseTeardown(kyselyDb);
});

describe('createPaypalCustomer', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
import { faker } from '@faker-js/faker';
import { Kysely } from 'kysely';

import { DB, testAccountDatabaseSetup } from '@fxa/shared/db/mysql/account';
import {
DB,
testAccountDatabaseSetup,
testAccountDatabaseTeardown,
} from '@fxa/shared/db/mysql/account';

import {
AccountCustomerDeleteAccountError,
Expand All @@ -26,18 +30,15 @@ describe('AccountCustomer Manager', () => {
});

afterAll(async () => {
if (kyselyDb) {
await kyselyDb.destroy();
}
await testAccountDatabaseTeardown(kyselyDb);
});

describe('createAccountCustomer', () => {
it('creates an accountCustomer successfully', async () => {
const mockAccountCustomer = CreateAccountCustomerFactory();

const result = await accountCustomerManager.createAccountCustomer(
mockAccountCustomer
);
const result =
await accountCustomerManager.createAccountCustomer(mockAccountCustomer);

expect(result).toEqual({
...mockAccountCustomer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
AccountCustomerFactory,
DB,
testAccountDatabaseSetup,
testAccountDatabaseTeardown,
} from '@fxa/shared/db/mysql/account';

import { AccountCustomerUpdatedNoEffectError } from './accountCustomer.error';
Expand All @@ -27,9 +28,7 @@ describe('AccountCustomer Repository', () => {
});

afterAll(async () => {
if (kyselyDb) {
await kyselyDb.destroy();
}
await testAccountDatabaseTeardown(kyselyDb);
});

describe('createAccountCustomer', () => {
Expand Down
10 changes: 6 additions & 4 deletions libs/shared/account/account/src/lib/account.manager.in.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
import { Kysely } from 'kysely';

import { faker } from '@faker-js/faker';
import { DB, testAccountDatabaseSetup } from '@fxa/shared/db/mysql/account';
import {
DB,
testAccountDatabaseSetup,
testAccountDatabaseTeardown,
} from '@fxa/shared/db/mysql/account';

import { AccountAlreadyExistsError } from './account.error';
import { AccountManager } from './account.manager';
Expand All @@ -19,9 +23,7 @@ describe('accountManager', () => {
});

afterAll(async () => {
if (kyselyDb) {
await kyselyDb.destroy();
}
await testAccountDatabaseTeardown(kyselyDb);
});

describe('createAccountStub', () => {
Expand Down
5 changes: 4 additions & 1 deletion libs/shared/db/mysql/account/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ export {
RecoveryPhoneFactory,
} from './lib/factories';
export { setupAccountDatabase, AccountDbProvider } from './lib/setup';
export { testAccountDatabaseSetup } from './lib/tests';
export {
testAccountDatabaseSetup,
testAccountDatabaseTeardown,
} from './lib/tests';
export type { ACCOUNT_TABLES } from './lib/tests';
export type { AccountDatabase } from './lib/setup';
export {
Expand Down
99 changes: 99 additions & 0 deletions libs/shared/db/mysql/account/src/lib/tests.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import type { AccountDatabase } from './setup';
import { testAccountDatabaseTeardown } from './tests';

const mockExecute = jest.fn();

// Replaces the sql tag so the helper runs without MySQL. sql.table becomes the
// identity, so a schema name arrives as a plain string.
jest.mock('kysely', () => {
const actual = jest.requireActual('kysely');
const sql = Object.assign(
(strings: TemplateStringsArray, ...values: unknown[]) => ({
execute: () => mockExecute(strings.join('?'), values),
}),
actual.sql,
{ table: (name: string) => name }
);
return { ...actual, sql };
});

describe('testAccountDatabaseTeardown', () => {
const REAL_SCHEMA = 'fxa';
const TEST_SCHEMA = 'testAccount-2e0b1c4a';

let destroy: jest.MockedFunction<AccountDatabase['destroy']>;
let db: AccountDatabase;

function mockCurrentSchema(name: string | null) {
mockExecute.mockResolvedValueOnce({ rows: [{ name }] });
}

function dropCalls() {
return mockExecute.mock.calls.filter(([query]) =>
query.startsWith('DROP DATABASE')
);
}

beforeEach(() => {
mockExecute.mockReset();
destroy = jest.fn();
db = { destroy } as unknown as AccountDatabase;
});

it('does nothing when the setup never returned a database', async () => {
await testAccountDatabaseTeardown(undefined);

expect(mockExecute).not.toHaveBeenCalled();
});

// The prefix must be anchored, and a pool opened with no database reports a
// null schema.
it.each([
{ label: 'lacks the test prefix', name: REAL_SCHEMA },
{ label: 'only contains the test prefix', name: `fxa_${TEST_SCHEMA}` },
{ label: 'is null', name: null },
])('does not drop a schema whose name $label', async ({ name }) => {
mockCurrentSchema(name);

await testAccountDatabaseTeardown(db);

expect(dropCalls()).toEqual([]);
});

it('closes the pool when it leaves a schema in place', async () => {
mockCurrentSchema(REAL_SCHEMA);

await testAccountDatabaseTeardown(db);

expect(destroy).toHaveBeenCalledTimes(1);
});

it('drops a schema whose name carries the test prefix', async () => {
mockCurrentSchema(TEST_SCHEMA);

await testAccountDatabaseTeardown(db);

expect(dropCalls()).toEqual([['DROP DATABASE IF EXISTS ?', [TEST_SCHEMA]]]);
});

it('closes the pool after it drops a schema', async () => {
mockCurrentSchema(TEST_SCHEMA);

await testAccountDatabaseTeardown(db);

expect(destroy).toHaveBeenCalledTimes(1);
});

it('closes the pool when the drop fails', async () => {
mockCurrentSchema(TEST_SCHEMA);
mockExecute.mockRejectedValueOnce(new Error('ER_DBACCESS_DENIED_ERROR'));

await expect(testAccountDatabaseTeardown(db)).rejects.toThrow(
'ER_DBACCESS_DENIED_ERROR'
);
expect(destroy).toHaveBeenCalledTimes(1);
});
});
Loading