Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .changeset/unskip-integration-test-cluster.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

ci: un-skip 11 of 13 integration test files tracked in #3289. Auth-mock cluster moves to `vi.mock(path, async (importOriginal) => ({ ...await importOriginal(), <overrides> }))` so HTTPServer setup finds the real `optionalAuth` and friends; CSRF middleware is mocked alongside auth so POST/PUT/DELETE tests stop hitting `403 forbidden`. The two `--exclude`'d files (`admin-sync-revenue-backfill`, `self-service-delete`) get `vi.hoisted` for shared mock state and class-form WorkOS mocks, so they load and run instead of erroring at module-eval. `health.test.ts` drops `(server as any).registry.initialize()` (the property is gone). `schema-routing.test.ts` re-aligns with `semver.rcompare` ordering. `digest-email-recipients` and `member-db` reconcile drifted assertions. The `--exclude` flags are removed from `test:server-integration`, and `WORKOS_COOKIE_PASSWORD` is set in the test env so the WorkOS client constructs in tests that need it. **+131 tests now run in CI**; remaining skips are documented in #3289 with concrete next steps.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"test:unit": "vitest run --dir tests/ --pool=threads",
"test:redteam": "tsx server/src/addie/testing/redteam-cli.ts",
"test:server-unit": "vitest run server/tests/unit/",
"test:server-integration": "vitest run server/tests/integration --config server/vitest.config.ts --exclude '**/admin-sync-revenue-backfill.test.ts' --exclude '**/self-service-delete.test.ts'",
"test:server-integration": "vitest run server/tests/integration --config server/vitest.config.ts",
"test:openapi": "tsx scripts/generate-openapi.ts && git diff --exit-code static/openapi/registry.yaml || (echo 'OpenAPI spec is out of date. Run: npm run build:openapi' && exit 1)",
"test:registry": "vitest run server/tests --exclude 'server/tests/simulation/**'",
"test:simulation": "vitest run server/tests/simulation/scenarios --pool forks --poolOptions.forks.singleFork --testTimeout 120000",
Expand Down
22 changes: 15 additions & 7 deletions server/tests/integration/admin-endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,23 @@ import { getPool, initializeDatabase, closeDatabase } from '../../src/db/client.
import { runMigrations } from '../../src/db/migrate.js';
import type { Pool } from 'pg';

// Mock auth middleware to bypass authentication in tests
vi.mock('../../src/middleware/auth.js', () => ({
requireAuth: (req: any, res: any, next: any) => {
// Override only the auth gates; spread the real module so HTTPServer setup
// still finds optionalAuth and other exports it imports.
vi.mock('../../src/middleware/auth.js', async (importOriginal) => ({
...(await importOriginal<typeof import('../../src/middleware/auth.js')>()),
requireAuth: (req: any, _res: any, next: any) => {
req.user = {
id: 'user_test_admin',
email: 'admin@test.com',
is_admin: true
};
next();
},
requireAdmin: (req: any, res: any, next: any) => next(),
requireAdmin: (_req: any, _res: any, next: any) => next(),
}));

vi.mock('../../src/middleware/csrf.js', () => ({
csrfProtection: (_req: any, _res: any, next: any) => next(),
}));

// Mock Stripe client to control subscription checks
Expand All @@ -27,8 +33,7 @@ vi.mock('../../src/billing/stripe-client.js', () => ({
createBillingPortalSession: vi.fn().mockResolvedValue(null),
}));

// Skipped: see #3289 — vi.mock('../../src/middleware/auth.js') doesn't return optionalAuth, so HTTPServer setup throws.
describe.skip('Admin Endpoints Integration Tests', () => {
describe('Admin Endpoints Integration Tests', () => {
let server: HTTPServer;
let app: any;
let pool: Pool;
Expand Down Expand Up @@ -355,7 +360,10 @@ describe.skip('Admin Endpoints Integration Tests', () => {
expect(afterResult.rows.length).toBe(0);
});

it('should prevent deletion of organization with active subscription', async () => {
// Skipped: see #3289 — handler now reads subscription state from OrganizationDatabase.getSubscriptionInfo
// (DB-backed) instead of the stripe-client import the test mocks. Test needs to seed
// subscription_status='active' on the org row, not vi.mock the stripe call.
it.skip('should prevent deletion of organization with active subscription', async () => {
// Create org with active subscription
const SUB_ORG_ID = 'org_delete_test_sub';
await pool.query(
Expand Down
136 changes: 74 additions & 62 deletions server/tests/integration/admin-sync-revenue-backfill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,80 +5,92 @@ import { getPool, initializeDatabase, closeDatabase } from '../../src/db/client.
import { runMigrations } from '../../src/db/migrate.js';
import type { Pool } from 'pg';

vi.mock('../../src/middleware/auth.js', () => ({
vi.mock('../../src/middleware/auth.js', async (importOriginal) => ({
...(await importOriginal<typeof import('../../src/middleware/auth.js')>()),
requireAuth: (req: any, _res: any, next: any) => {
req.user = { id: 'user_test_admin', email: 'admin@test.com', is_admin: true };
next();
},
requireAdmin: (_req: any, _res: any, next: any) => next(),
}));

async function* fakeInvoiceIterator(items: unknown[]) {
for (const item of items) yield item;
}

function makeStripeInvoiceList(items: unknown[]) {
return fakeInvoiceIterator(items);
}

const FAKE_INVOICE = {
id: 'in_backfill_test_001',
amount_paid: 250000,
currency: 'usd',
billing_reason: 'subscription_create',
subscription: 'sub_backfill_test_001',
payment_intent: 'pi_backfill_test_001',
charge: { id: 'ch_backfill_test_001' },
status_transitions: { paid_at: Math.floor(Date.now() / 1000) - 86400 },
created: Math.floor(Date.now() / 1000) - 86400,
period_start: Math.floor(Date.now() / 1000) - 86400,
period_end: Math.floor(Date.now() / 1000) + 86400 * 365,
description: null,
lines: {
data: [{
id: 'il_backfill_test_001',
price: {
id: 'price_backfill_test_001',
recurring: { interval: 'year' },
product: 'prod_backfill_test_001',
},
description: 'AgenticAdvertising.org Membership',
}],
},
};

const mockInvoicesList = vi.fn().mockImplementation(() => makeStripeInvoiceList([FAKE_INVOICE]));
const mockCustomersRetrieve = vi.fn().mockResolvedValue({
id: 'cus_test_backfill',
deleted: false,
subscriptions: {
data: [{
id: 'sub_backfill_test_001',
status: 'active',
current_period_end: Math.floor(Date.now() / 1000) + 86400 * 365,
canceled_at: null,
items: {
vi.mock('../../src/middleware/csrf.js', () => ({
csrfProtection: (_req: any, _res: any, next: any) => next(),
}));

// vi.mock factories are hoisted above all top-level statements, so any vars
// referenced inside have to be declared in vi.hoisted (which is also hoisted)
// to be live at factory-evaluation time. Bare `const x = ...` at top-level
// still hits "Cannot access 'x' before initialization".
const mocks = vi.hoisted(() => {
const FAKE_INVOICE = {
id: 'in_backfill_test_001',
amount_paid: 250000,
currency: 'usd',
billing_reason: 'subscription_create',
subscription: 'sub_backfill_test_001',
payment_intent: 'pi_backfill_test_001',
charge: { id: 'ch_backfill_test_001' },
status_transitions: { paid_at: Math.floor(Date.now() / 1000) - 86400 },
created: Math.floor(Date.now() / 1000) - 86400,
period_start: Math.floor(Date.now() / 1000) - 86400,
period_end: Math.floor(Date.now() / 1000) + 86400 * 365,
description: null,
lines: {
data: [{
id: 'il_backfill_test_001',
price: {
id: 'price_backfill_test_001',
recurring: { interval: 'year' },
product: 'prod_backfill_test_001',
},
description: 'AgenticAdvertising.org Membership',
}],
},
};

async function* fakeInvoiceIterator(items: unknown[]) {
for (const item of items) yield item;
}

return {
FAKE_INVOICE,
mockInvoicesList: vi.fn().mockImplementation(() => fakeInvoiceIterator([FAKE_INVOICE])),
mockCustomersRetrieve: vi.fn().mockResolvedValue({
id: 'cus_test_backfill',
deleted: false,
subscriptions: {
data: [{
price: {
unit_amount: 250000,
currency: 'usd',
recurring: { interval: 'year' },
id: 'sub_backfill_test_001',
status: 'active',
current_period_end: Math.floor(Date.now() / 1000) + 86400 * 365,
canceled_at: null,
items: {
data: [{
price: {
unit_amount: 250000,
currency: 'usd',
recurring: { interval: 'year' },
},
}],
},
}],
},
}],
},
});
const mockProductsRetrieve = vi.fn().mockResolvedValue({
id: 'prod_backfill_test_001',
name: 'Pinnacle Media Annual Plan',
}),
mockProductsRetrieve: vi.fn().mockResolvedValue({
id: 'prod_backfill_test_001',
name: 'Pinnacle Media Annual Plan',
}),
};
});

const { FAKE_INVOICE, mockInvoicesList, mockCustomersRetrieve, mockProductsRetrieve } = mocks;

vi.mock('../../src/billing/stripe-client.js', () => ({
stripe: {
customers: { retrieve: mockCustomersRetrieve },
invoices: { list: mockInvoicesList },
products: { retrieve: mockProductsRetrieve },
customers: { retrieve: mocks.mockCustomersRetrieve },
invoices: { list: mocks.mockInvoicesList },
products: { retrieve: mocks.mockProductsRetrieve },
webhooks: {
constructEvent: vi.fn().mockImplementation((body: any) => {
return typeof body === 'string' ? JSON.parse(body) : JSON.parse(body.toString());
Expand Down Expand Up @@ -131,7 +143,7 @@ describe('POST /api/admin/accounts/:orgId/sync — revenue_events backfill', ()

beforeEach(async () => {
await pool.query('DELETE FROM revenue_events WHERE workos_organization_id = $1', [TEST_ORG_ID]);
mockInvoicesList.mockImplementation(() => makeStripeInvoiceList([FAKE_INVOICE]));
mockInvoicesList.mockImplementation(async function* () { yield FAKE_INVOICE; });
});

it('inserts a revenue_events row for a missed invoice and returns revenue_events_synced: 1', async () => {
Expand All @@ -158,7 +170,7 @@ describe('POST /api/admin/accounts/:orgId/sync — revenue_events backfill', ()
expect(first.body.revenue_events_synced).toBe(1);

// Reset mock to return same invoice again
mockInvoicesList.mockImplementation(() => makeStripeInvoiceList([FAKE_INVOICE]));
mockInvoicesList.mockImplementation(async function* () { yield FAKE_INVOICE; });

const second = await request(app)
.post(`/api/admin/accounts/${TEST_ORG_ID}/sync`)
Expand Down Expand Up @@ -193,7 +205,7 @@ describe('POST /api/admin/accounts/:orgId/sync — revenue_events backfill', ()
});

it('returns 0 when the customer has no paid invoices', async () => {
mockInvoicesList.mockImplementation(() => makeStripeInvoiceList([]));
mockInvoicesList.mockImplementation(async function* () { /* no invoices */ });

const response = await request(app)
.post(`/api/admin/accounts/${TEST_ORG_ID}/sync`)
Expand Down
5 changes: 2 additions & 3 deletions server/tests/integration/agent-visibility-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,8 +418,7 @@ describe('Agent visibility E2E', () => {
expect(withApi.map((a) => a.url)).toContain('https://members.privp.example');
});

// Skipped: see #3289 — references `src/db/migrations/419_agent_visibility.sql`; path should be `server/src/db/migrations/...`.
it.skip('legacy is_public agents are normalized on read', async () => {
it('legacy is_public agents are normalized on read', async () => {
const orgId = `${TEST_PREFIX}_legacy`;
await seedOrg(pool, orgId, null);
// Bypass typed helper to store a legacy shape
Expand All @@ -440,7 +439,7 @@ describe('Agent visibility E2E', () => {
// Re-run migration 419 explicitly to simulate the transform on legacy rows
const fs = await import('fs/promises');
const path = await import('path');
const sqlPath = path.resolve(process.cwd(), 'src/db/migrations/419_agent_visibility.sql');
const sqlPath = path.resolve(process.cwd(), 'server/src/db/migrations/419_agent_visibility.sql');
const sql = await fs.readFile(sqlPath, 'utf-8');
await pool.query(sql);

Expand Down
22 changes: 15 additions & 7 deletions server/tests/integration/digest-email-recipients.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,7 @@ describe('getDigestEmailRecipients — active track scoping', () => {
expect(r.cert_total_modules).toBe(0);
});

// Skipped: see #3289 — counts return 4 instead of 3; scoping rule or fixture has drifted.
it.skip('scopes counts to the track most recently touched', async () => {
it('scopes counts to the track most recently touched', async () => {
// Track A: A1 + A2 completed, touched earlier (should NOT be the active track)
await query(
`INSERT INTO learner_progress (workos_user_id, module_id, status, updated_at)
Expand All @@ -87,10 +86,15 @@ describe('getDigestEmailRecipients — active track scoping', () => {
[TEST_USER]
);

const trackBTotal = (await query<{ count: string }>(
`SELECT COUNT(*)::text AS count FROM certification_modules WHERE track_id = 'B'`,
)).rows[0].count;

const r = await recipient();
// Track B has 3 seeded modules (B1, B2, B3); 1 is completed.
// Track B's module count comes from the live curriculum, not a hard-coded
// fixture; new modules land in main and would otherwise drift this test.
expect(r.cert_modules_completed).toBe(1);
expect(r.cert_total_modules).toBe(3);
expect(r.cert_total_modules).toBe(Number(trackBTotal));
});

it('counts tested_out the same as completed', async () => {
Expand All @@ -107,8 +111,7 @@ describe('getDigestEmailRecipients — active track scoping', () => {
expect(r.cert_total_modules).toBe(3);
});

// Skipped: see #3289 — same drift as the previous test.
it.skip('breaks updated_at ties deterministically via module_id DESC', async () => {
it('breaks updated_at ties deterministically via module_id DESC', async () => {
// Two rows with identical updated_at on different tracks. The ORDER BY
// tiebreak of module_id DESC should pick 'B1' > 'A1', so active track = B.
await query(
Expand All @@ -119,11 +122,16 @@ describe('getDigestEmailRecipients — active track scoping', () => {
[TEST_USER]
);

const trackBTotal = (await query<{ count: string }>(
`SELECT COUNT(*)::text AS count FROM certification_modules WHERE track_id = 'B'`,
)).rows[0].count;
const expectedTotal = Number(trackBTotal);

// Run several times to make sure the pick is stable (non-deterministic
// behavior would show up as flakes here).
for (let i = 0; i < 5; i++) {
const r = await recipient();
expect(r.cert_total_modules).toBe(3); // track B has 3 modules
expect(r.cert_total_modules).toBe(expectedTotal);
expect(r.cert_modules_completed).toBe(0);
}
});
Expand Down
10 changes: 7 additions & 3 deletions server/tests/integration/escalation-triage-endpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ vi.mock('../../src/addie/jobs/github-filer.js', () => ({
fileGitHubIssue: mocks.fileGitHubIssue,
}));

vi.mock('../../src/middleware/auth.js', () => ({
vi.mock('../../src/middleware/auth.js', async (importOriginal) => ({
...(await importOriginal<typeof import('../../src/middleware/auth.js')>()),
requireAuth: (req: { user?: unknown }, _res: unknown, next: () => void) => {
(req as { user: unknown }).user = {
id: 'user_test_admin',
Expand All @@ -34,6 +35,10 @@ vi.mock('../../src/middleware/auth.js', () => ({
requireAdmin: (_req: unknown, _res: unknown, next: () => void) => next(),
}));

vi.mock('../../src/middleware/csrf.js', () => ({
csrfProtection: (_req: unknown, _res: unknown, next: () => void) => next(),
}));

// Stripe is mocked in other integration tests to avoid the billing init path.
vi.mock('../../src/billing/stripe-client.js', () => ({
stripe: null,
Expand All @@ -43,8 +48,7 @@ vi.mock('../../src/billing/stripe-client.js', () => ({
createBillingPortalSession: vi.fn().mockResolvedValue(null),
}));

// Skipped: see #3289 — stale auth.js mock; HTTPServer setup throws on missing exports.
describe.skip('Escalation triage endpoints', () => {
describe('Escalation triage endpoints', () => {
let server: HTTPServer;
let app: unknown;

Expand Down
Loading
Loading