Skip to content

Commit 1a2a2cd

Browse files
chore: audit polish — migration test scoping, dead fallback, OAuth guard test, failure counters (#3960)
* chore: audit polish — migration test scoping, dead fallback, OAuth guard test, failure counters Batches 4 small audit findings: scope the signup-grant backfill migration integration test to its own seeded fixture ids (was crediting any leftover grant-plan org in the shared collection), drop the unreachable `result.plan || 'free'` fallback plus the unit test that forced it via an impossible repository return shape, add a regression test that registers a REAL passport strategy so a passport upgrade renaming the private `_strategy` API fails CI instead of silently 404-ing OAuth logins, and differentiate the migration's `failed` counter into planNotFound/guardStripped/error buckets. Closes #3954 Claude-Session: https://claude.ai/code/session_01WfNC8bt1TgL4AsiYgCEGup * test(billing): use a Proxy for the scoped-migration collection mock Preserve every method/property of the real Mongoose Collection instance instead of a duck-typed { find } object — only find() is intercepted to intersect in the fixture id scope. Addresses CodeRabbit nit on #3960 (PR #3960 / issue #3954).
1 parent 2edb8e4 commit 1a2a2cd

6 files changed

Lines changed: 157 additions & 27 deletions
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Module dependencies.
3+
*/
4+
import { jest, describe, test, expect } from '@jest/globals';
5+
import { setupAuthControllerMocks } from './fixtures/auth-controller.mock-setup.js';
6+
7+
/**
8+
* Regression guard (issue #3954): every test in auth.oauthCall.controller.unit.tests.js
9+
* replaces `passport` wholesale via `jest.unstable_mockModule` and stubs `_strategy` as
10+
* a `jest.fn()` — so if a future passport upgrade renames or removes the private
11+
* `_strategy` API that isEnabledOAuthProvider() relies on (see auth.controller.js), none
12+
* of those mocks would notice; production would silently 404 every OAuth login while
13+
* that whole suite kept passing green.
14+
*
15+
* This test uses the REAL `passport` package, registers a trivial strategy via the real
16+
* `passport.use()`, and asserts the guard recognizes it through the real `_strategy`
17+
* lookup (only `passport.authenticate` itself is stubbed, since the trivial strategy's
18+
* `authenticate()` is never meant to run a full handshake).
19+
*
20+
* Deliberately kept in its OWN file rather than added to
21+
* auth.oauthCall.controller.unit.tests.js: `jest.unstable_mockModule('passport', ...)`
22+
* mock-factory registrations are sticky for the lifetime of a test FILE — they survive
23+
* `jest.resetModules()` — so a real-passport test placed after any mocked-passport test
24+
* in the same file would still resolve `import('passport')` to the stale mock. A
25+
* dedicated file gives this test a clean module registry with no prior passport mock.
26+
*/
27+
describe('auth.controller oauthCall — isEnabledOAuthProvider against a REAL passport strategy:', () => {
28+
test('a strategy registered via the real passport.use() is recognized by the guard and delegated to', async () => {
29+
setupAuthControllerMocks({ realPassport: true });
30+
31+
const { default: passport } = await import('passport');
32+
33+
class TrivialStrategy {
34+
constructor(name) {
35+
this.name = name;
36+
}
37+
38+
// Never actually invoked in this test — passport.authenticate() is stubbed
39+
// below, so the guard-focused assertions never reach a real handshake.
40+
authenticate() {
41+
this.fail({ message: 'trivial strategy stub — not exercised by this guard test' });
42+
}
43+
}
44+
passport.use(new TrivialStrategy('google'));
45+
const authenticateMiddleware = jest.fn();
46+
jest.spyOn(passport, 'authenticate').mockReturnValue(authenticateMiddleware);
47+
48+
const { default: AuthController } = await import('../../../modules/auth/controllers/auth.controller.js');
49+
50+
const req = { params: { strategy: 'google' } };
51+
const res = {};
52+
const next = jest.fn();
53+
54+
AuthController.oauthCall(req, res, next);
55+
56+
expect(passport.authenticate).toHaveBeenCalledWith('google', { session: false });
57+
expect(authenticateMiddleware).toHaveBeenCalledWith(req, res, next);
58+
expect(next).not.toHaveBeenCalled();
59+
});
60+
});

modules/auth/tests/fixtures/auth-controller.mock-setup.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,29 @@ import { jest } from '@jest/globals';
1313
* dependency graph needs, mirroring what each test's dynamic import expects.
1414
* Call this from `beforeEach` (not `beforeAll`) so the mocks are registered
1515
* before that test's `await import('.../auth.controller.js')`.
16-
* @returns {{authenticate: import('@jest/globals').Mock, _strategy: import('@jest/globals').Mock}} mockPassport - the passport mock, so callers can further customize per-test behavior (e.g. `mockPassport._strategy.mockReturnValue(...)`)
16+
* @param {Object} [opts]
17+
* @param {boolean} [opts.realPassport=false] - When true, skip mocking `passport` entirely
18+
* so the caller's own `await import('passport')` resolves the REAL package (issue #3954:
19+
* a regression test registers an actual strategy via `passport.use()` and asserts
20+
* `isEnabledOAuthProvider` sees it through the real `_strategy` API, so a passport
21+
* upgrade renaming that private API fails here instead of silently 404-ing OAuth
22+
* logins in prod — every other test in this suite mocks `_strategy` away and can't
23+
* catch that).
24+
* @returns {{authenticate: import('@jest/globals').Mock, _strategy: import('@jest/globals').Mock}|undefined} mockPassport - the passport mock, so callers can further customize per-test behavior (e.g. `mockPassport._strategy.mockReturnValue(...)`). `undefined` when `realPassport` is true.
1725
*/
18-
export function setupAuthControllerMocks() {
26+
export function setupAuthControllerMocks({ realPassport = false } = {}) {
1927
jest.resetModules();
2028

21-
const mockPassport = {
22-
authenticate: jest.fn().mockReturnValue(jest.fn()),
23-
_strategy: jest.fn().mockReturnValue(undefined),
24-
};
25-
jest.unstable_mockModule('passport', () => ({
26-
default: mockPassport,
27-
}));
29+
let mockPassport;
30+
if (!realPassport) {
31+
mockPassport = {
32+
authenticate: jest.fn().mockReturnValue(jest.fn()),
33+
_strategy: jest.fn().mockReturnValue(undefined),
34+
};
35+
jest.unstable_mockModule('passport', () => ({
36+
default: mockPassport,
37+
}));
38+
}
2839

2940
jest.unstable_mockModule('../../../../lib/services/logger.js', () => ({
3041
default: { warn: jest.fn(), error: jest.fn(), info: jest.fn() },

modules/billing/migrations/20260707100000-backfill-missing-signup-grant-credits.js

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import mongoose from 'mongoose';
3535
import config from '../../../config/index.js';
3636
import BillingSignupGrantService from '../services/billing.signupGrant.service.js';
37+
import BillingPlanService from '../services/billing.plan.service.js';
3738

3839
/**
3940
* @returns {Promise<void>}
@@ -57,9 +58,22 @@ export async function up() {
5758

5859
let granted = 0;
5960
let skipped = 0;
60-
let failed = 0;
61+
let failedPlanNotFound = 0;
62+
let failedGuardStripped = 0;
63+
let failedError = 0;
6164

6265
for await (const org of cursor) {
66+
// grantOnSignup swallows ALL failure modes to a bare `null` by design
67+
// (best-effort — never throws), so plan-not-found, co-presence-guard-stripped,
68+
// and a genuine runtime error are otherwise indistinguishable from the outside.
69+
// Pre-classify with the same read-only, config-static lookup grantOnSignup uses
70+
// internally BEFORE calling it, purely for the completion-log breakdown below —
71+
// this never short-circuits or skips the real call, so behavior is unchanged.
72+
const plan = BillingPlanService.getActivePlan(org.plan);
73+
let preclassifiedFailure = null;
74+
if (!plan) preclassifiedFailure = 'planNotFound';
75+
else if (!plan.signupGrant) preclassifiedFailure = 'guardStripped';
76+
6377
// grantOnSignup is idempotent (refId) and never throws — reuses the runtime
6478
// path so ObjectId casting + validation + co-presence guard all apply.
6579
const result = await BillingSignupGrantService.grantOnSignup({
@@ -68,8 +82,11 @@ export async function up() {
6882
});
6983

7084
if (result == null) {
71-
// Plan has no exposed grant (co-presence guard) or a swallowed error — logged by the service.
72-
failed += 1;
85+
if (preclassifiedFailure === 'planNotFound') failedPlanNotFound += 1;
86+
else if (preclassifiedFailure === 'guardStripped') failedGuardStripped += 1;
87+
// Plan found + grant configured, yet still null: the repository call itself
88+
// threw and grantOnSignup's catch swallowed it — a genuine runtime error.
89+
else failedError += 1;
7390
} else if (result.applied === false) {
7491
// Idempotent no-op — org already had a signup_grant entry.
7592
skipped += 1;
@@ -78,7 +95,11 @@ export async function up() {
7895
}
7996
}
8097

81-
console.info(`[migration] backfill-signup-grant: complete — granted=${granted} skipped=${skipped} failed=${failed}`);
98+
const failed = failedPlanNotFound + failedGuardStripped + failedError;
99+
console.info(
100+
`[migration] backfill-signup-grant: complete — granted=${granted} skipped=${skipped} failed=${failed} `
101+
+ `(planNotFound=${failedPlanNotFound} guardStripped=${failedGuardStripped} error=${failedError})`,
102+
);
82103
}
83104

84105
/**

modules/billing/tests/billing.migration.backfill-signup-grant.integration.tests.js

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Module dependencies.
33
*/
44
import mongoose from 'mongoose';
5-
import { describe, beforeAll, afterEach, afterAll, test, expect } from '@jest/globals';
5+
import { describe, beforeAll, afterEach, afterAll, test, expect, jest } from '@jest/globals';
66

77
import mongooseService from '../../../lib/services/mongoose.js';
88
import config from '../../../config/index.js';
@@ -15,6 +15,15 @@ import { up as backfillSignupGrants } from '../migrations/20260707100000-backfil
1515
* is Schema.ObjectId, so the migration MUST credit orgs through a path that casts to
1616
* ObjectId (it delegates to grantOnSignup → the Mongoose repository) — a raw-driver
1717
* write keyed on a string would create orphan, app-invisible ghost documents.
18+
*
19+
* The migration's `up()` takes no arguments — it scans the ENTIRE shared
20+
* `organizations` collection filtered only by plan. Run unscoped against the real
21+
* test DB, it would also silently credit any leftover grant-plan orgs left behind
22+
* by other integration test files (crashed runs, ordering quirks), mutating state
23+
* outside this test's ownership. `runScopedMigration` patches the `organizations`
24+
* collection's `find()` for the duration of a single `up()` call so it only ever
25+
* sees this test's own seeded fixture ids — the real migration logic (plan filter,
26+
* grantOnSignup delegation, idempotency) is untouched, only its blast radius is.
1827
*/
1928
describe('backfill-missing-signup-grant migration (integration):', () => {
2029
let organizations;
@@ -35,6 +44,39 @@ describe('backfill-missing-signup-grant migration (integration):', () => {
3544
return _id;
3645
};
3746

47+
/**
48+
* Run the real migration `up()` scoped to only the given org ids — intersects the
49+
* migration's own `{ plan: { $in: grantPlanIds } }` query with `_id: { $in: orgIds }`
50+
* for the duration of the call, so it can never reach an org outside this test's
51+
* fixtures, then restores the unpatched collection.
52+
* @param {import('mongoose').Types.ObjectId[]} orgIds - Fixture ids owned by this test.
53+
* @returns {Promise<void>}
54+
*/
55+
const runScopedMigration = async (orgIds) => {
56+
const realCollection = mongoose.connection.db.collection.bind(mongoose.connection.db);
57+
const spy = jest.spyOn(mongoose.connection.db, 'collection').mockImplementation((name) => {
58+
const coll = realCollection(name);
59+
if (name !== 'organizations') return coll;
60+
// Proxy (not a duck-typed `{ find }` object) so every other method/property
61+
// on the real Collection instance stays reachable — only `find()` is
62+
// intercepted to intersect in the fixture id scope.
63+
return new Proxy(coll, {
64+
get(target, prop) {
65+
if (prop === 'find') {
66+
return (filter, options) => target.find({ ...filter, _id: { $in: orgIds } }, options);
67+
}
68+
const val = target[prop];
69+
return typeof val === 'function' ? val.bind(target) : val;
70+
},
71+
});
72+
});
73+
try {
74+
await backfillSignupGrants();
75+
} finally {
76+
spy.mockRestore();
77+
}
78+
};
79+
3880
beforeAll(async () => {
3981
await mongooseService.loadModels();
4082
await mongooseService.connect();
@@ -62,7 +104,7 @@ describe('backfill-missing-signup-grant migration (integration):', () => {
62104
test('credits the configured grant to an ObjectId-keyed org missing it, readable by ObjectId', async () => {
63105
const orgId = await seedOrg(grantPlanId);
64106

65-
await backfillSignupGrants();
107+
await runScopedMigration(createdOrgIds);
66108

67109
// Stored + readable keyed on the ObjectId — the critical-fix assertion.
68110
const eb = await extraBalances.findOne({ organization: orgId });
@@ -79,8 +121,8 @@ describe('backfill-missing-signup-grant migration (integration):', () => {
79121
test('is idempotent — a second run does not double-credit', async () => {
80122
const orgId = await seedOrg(grantPlanId);
81123

82-
await backfillSignupGrants();
83-
await backfillSignupGrants();
124+
await runScopedMigration(createdOrgIds);
125+
await runScopedMigration(createdOrgIds);
84126

85127
const docs = await extraBalances.find({ organization: orgId }).toArray();
86128
expect(docs).toHaveLength(1);
@@ -94,7 +136,7 @@ describe('backfill-missing-signup-grant migration (integration):', () => {
94136
const repo = (await import('../repositories/billing.extraBalance.repository.js')).default;
95137
await repo.creditGrant(orgId.toString(), grantAmount, 'signup_grant');
96138

97-
await backfillSignupGrants();
139+
await runScopedMigration(createdOrgIds);
98140

99141
const docs = await extraBalances.find({ organization: orgId }).toArray();
100142
expect(docs).toHaveLength(1);

modules/organizations/services/organizations.crud.service.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,11 @@ const create = async (body, user) => {
128128
// idempotence (refId signup_grant-<orgId>) and never throws. Fire-and-forget, synchronous
129129
// emit — the try/catch here only guards a SYNCHRONOUS listener throw (see ../lib/events.js).
130130
try {
131-
organizationEvents.emit('organization.created', { orgId: result._id.toString(), planId: result.plan || 'free' });
131+
// planId is a literal 'free' (not `result.plan`), matching organizations.service.js::
132+
// createOrganizationForUser exactly — `plan: 'free'` is set unconditionally a few lines
133+
// above, so `result.plan` is always 'free' here too; a `|| 'free'` fallback would be
134+
// unreachable dead code (#3954 audit finding).
135+
organizationEvents.emit('organization.created', { orgId: result._id.toString(), planId: 'free' });
132136
} catch (err) {
133137
logger.warn('organizations: organization.created listener threw', { message: err?.message });
134138
}

modules/organizations/tests/organizations.crud.grant.unit.tests.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,6 @@ describe('organizations.crud.service.create organization.created emit:', () => {
8181
expect(mockEmit).toHaveBeenCalledWith('organization.created', { orgId: 'org1', planId: 'free' });
8282
});
8383

84-
test('defaults planId to free when the created org has no plan field', async () => {
85-
mockOrgCreate.mockResolvedValueOnce({ _id: 'org2' });
86-
const user = { id: 'u1', _id: 'u1', email: 'a@b.com', emailVerified: true };
87-
await OrgCrudService.create({ name: 'No Plan Org' }, user);
88-
89-
expect(mockEmit).toHaveBeenCalledWith('organization.created', { orgId: 'org2', planId: 'free' });
90-
});
91-
9284
test('does not emit when membership creation fails (rollback exits before the emit)', async () => {
9385
mockMembershipCreate.mockRejectedValueOnce(new Error('membership boom'));
9486
const user = { id: 'u1', _id: 'u1', email: 'a@b.com', emailVerified: true };

0 commit comments

Comments
 (0)