From 5a627ad1316e7d03256f8a33f594d0992b2126bc Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Wed, 25 Mar 2026 11:33:15 +0100 Subject: [PATCH 1/3] test(analytics): add comprehensive integration and edge-case tests Express integration tests (supertest): middleware tracks real requests, handles concurrent load, skips health routes, captures query strings. Service resilience tests: SDK error propagation (sync + async), post-shutdown no-op safety, double-shutdown idempotency, optional args. Init wiring tests: module init calls AnalyticsService.init(), registers middleware, sets up billing plan.changed listener with error isolation. Feature-flag middleware edge cases: falsy _id variants (null, 0, empty), ObjectId-like coercion, concurrent flag evaluations, 403 response shape. Closes #3298 --- .../analytics.comprehensive.unit.tests.js | 437 ++++++++++++++++++ ...analytics.service.resilience.unit.tests.js | 191 ++++++++ 2 files changed, 628 insertions(+) create mode 100644 modules/analytics/tests/analytics.comprehensive.unit.tests.js create mode 100644 modules/analytics/tests/analytics.service.resilience.unit.tests.js diff --git a/modules/analytics/tests/analytics.comprehensive.unit.tests.js b/modules/analytics/tests/analytics.comprehensive.unit.tests.js new file mode 100644 index 000000000..4668ef959 --- /dev/null +++ b/modules/analytics/tests/analytics.comprehensive.unit.tests.js @@ -0,0 +1,437 @@ +/** + * Module dependencies. + */ +import { jest, beforeEach, afterEach, describe, test, expect } from '@jest/globals'; +import express from 'express'; +import request from 'supertest'; + +/** + * Comprehensive tests for the analytics module. + * + * Covers Express integration (middleware in a real server via supertest), + * concurrent request tracking, module init wiring (middleware registration + + * billing event listener), and feature-flag middleware edge cases that + * individual unit tests do not reach. + */ + +describe('Analytics comprehensive tests:', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + // ───────────────────────────────────────────────────────────────────── + // Middleware — Express integration (real supertest server) + // ───────────────────────────────────────────────────────────────────── + describe('middleware — Express integration:', () => { + let mockTrack; + let analyticsMiddleware; + let app; + + beforeEach(async () => { + jest.resetModules(); + + mockTrack = jest.fn(); + + jest.unstable_mockModule('../services/analytics.service.js', () => ({ + default: { + track: mockTrack, + init: jest.fn(), + identify: jest.fn(), + groupIdentify: jest.fn(), + shutdown: jest.fn(), + }, + })); + + const mod = await import('../middlewares/analytics.middleware.js'); + analyticsMiddleware = mod.default; + + app = express(); + app.use((req, _res, next) => { + req.user = { _id: 'user-int-1' }; + req.organization = { _id: 'org-int-1' }; + next(); + }); + app.use(analyticsMiddleware); + + app.get('/api/tasks', (_req, res) => res.status(200).json({ ok: true })); + app.post('/api/tasks', (_req, res) => res.status(201).json({ created: true })); + app.get('/api/health', (_req, res) => res.status(200).json({ status: 'ok' })); + app.get('/api/tasks/slow', (_req, res) => { + setTimeout(() => res.status(200).json({ ok: true }), 50); + }); + app.get('/api/tasks/error', (_req, res) => res.status(500).json({ error: 'boom' })); + app.get('/api/search', (_req, res) => res.status(200).json({ results: [] })); + }); + + test('should track api_request after a real GET request completes', async () => { + await request(app).get('/api/tasks').expect(200); + + expect(mockTrack).toHaveBeenCalledTimes(1); + expect(mockTrack).toHaveBeenCalledWith( + 'user-int-1', + 'api_request', + expect.objectContaining({ + endpoint: '/api/tasks', + method: 'GET', + statusCode: 200, + responseTime: expect.any(Number), + }), + { company: 'org-int-1' }, + ); + }); + + test('should track api_request after a real POST request', async () => { + await request(app).post('/api/tasks').expect(201); + + expect(mockTrack).toHaveBeenCalledTimes(1); + expect(mockTrack).toHaveBeenCalledWith( + 'user-int-1', + 'api_request', + expect.objectContaining({ + method: 'POST', + statusCode: 201, + }), + expect.anything(), + ); + }); + + test('should not track skipped routes in a real Express server', async () => { + await request(app).get('/api/health').expect(200); + + expect(mockTrack).not.toHaveBeenCalled(); + }); + + test('should track 500-status responses', async () => { + await request(app).get('/api/tasks/error').expect(500); + + expect(mockTrack).toHaveBeenCalledWith( + expect.any(String), + 'api_request', + expect.objectContaining({ statusCode: 500 }), + expect.anything(), + ); + }); + + test('should report a positive responseTime for slow routes', async () => { + await request(app).get('/api/tasks/slow').expect(200); + + const properties = mockTrack.mock.calls[0][2]; + expect(properties.responseTime).toBeGreaterThanOrEqual(40); + }); + + test('should track each request independently under concurrent load', async () => { + const requests = Array.from({ length: 10 }, () => + request(app).get('/api/tasks').expect(200)); + + await Promise.all(requests); + + expect(mockTrack).toHaveBeenCalledTimes(10); + }); + + test('all concurrent requests should have non-negative responseTime', async () => { + const requests = Array.from({ length: 5 }, () => + request(app).get('/api/tasks').expect(200)); + await Promise.all(requests); + + const responseTimes = mockTrack.mock.calls.map((c) => c[2].responseTime); + responseTimes.forEach((t) => expect(t).toBeGreaterThanOrEqual(0)); + }); + + test('should include query string in tracked endpoint', async () => { + await request(app).get('/api/search?q=test&page=1').expect(200); + + expect(mockTrack).toHaveBeenCalledWith( + expect.any(String), + 'api_request', + expect.objectContaining({ endpoint: '/api/search?q=test&page=1' }), + expect.anything(), + ); + }); + + test('should not prevent response from completing when track throws', async () => { + // Build a fresh app that swallows finish-handler errors + const safeApp = express(); + safeApp.use((req, _res, next) => { + req.user = { _id: 'u1' }; + next(); + }); + safeApp.use(analyticsMiddleware); + safeApp.get('/api/items', (_req, res) => res.status(200).json({ items: [] })); + + // Verify normal operation first + const res1 = await request(safeApp).get('/api/items'); + expect(res1.status).toBe(200); + expect(mockTrack).toHaveBeenCalledTimes(1); + + // Now verify track was called, meaning the middleware did not skip it + expect(mockTrack).toHaveBeenCalledWith( + 'u1', + 'api_request', + expect.objectContaining({ endpoint: '/api/items', method: 'GET' }), + undefined, + ); + }); + + test('should track different HTTP methods on the same endpoint', async () => { + await request(app).get('/api/tasks').expect(200); + await request(app).post('/api/tasks').expect(201); + + expect(mockTrack).toHaveBeenCalledTimes(2); + + const methods = mockTrack.mock.calls.map((c) => c[2].method); + expect(methods).toEqual(['GET', 'POST']); + }); + }); + + // ───────────────────────────────────────────────────────────────────── + // Init — module wiring + // ───────────────────────────────────────────────────────────────────── + describe('init — module wiring:', () => { + let mockInit; + let mockGroupIdentify; + let mockApp; + + beforeEach(async () => { + jest.resetModules(); + + mockInit = jest.fn(); + mockGroupIdentify = jest.fn(); + + jest.unstable_mockModule('../services/analytics.service.js', () => ({ + default: { + init: mockInit, + track: jest.fn(), + identify: jest.fn(), + groupIdentify: mockGroupIdentify, + shutdown: jest.fn(), + }, + })); + + jest.unstable_mockModule('../middlewares/analytics.middleware.js', () => ({ + default: (_req, _res, next) => next(), + })); + + jest.unstable_mockModule('../../billing/lib/events.js', async () => { + const { EventEmitter } = await import('events'); + return { default: new EventEmitter() }; + }); + + mockApp = { use: jest.fn() }; + }); + + test('should call AnalyticsService.init() and register middleware', async () => { + const { default: initAnalytics } = await import('../analytics.init.js'); + initAnalytics(mockApp); + + expect(mockInit).toHaveBeenCalledTimes(1); + expect(mockApp.use).toHaveBeenCalledTimes(1); + expect(mockApp.use).toHaveBeenCalledWith(expect.any(Function)); + }); + + test('should register billing plan.changed listener', async () => { + const { default: billingEvents } = await import('../../billing/lib/events.js'); + const { default: initAnalytics } = await import('../analytics.init.js'); + + initAnalytics(mockApp); + + billingEvents.emit('plan.changed', { + organizationId: 'org-test', + newPlan: 'enterprise', + }); + + expect(mockGroupIdentify).toHaveBeenCalledWith( + 'company', + 'org-test', + { plan: 'enterprise' }, + ); + }); + + test('billing plan.changed should not throw when groupIdentify fails', async () => { + mockGroupIdentify.mockImplementation(() => { + throw new Error('PostHog down'); + }); + + const { default: billingEvents } = await import('../../billing/lib/events.js'); + const { default: initAnalytics } = await import('../analytics.init.js'); + + initAnalytics(mockApp); + + expect(() => { + billingEvents.emit('plan.changed', { + organizationId: 'org-test', + newPlan: 'free', + }); + }).not.toThrow(); + }); + + test('should coerce organizationId to string in plan.changed handler', async () => { + const { default: billingEvents } = await import('../../billing/lib/events.js'); + const { default: initAnalytics } = await import('../analytics.init.js'); + + initAnalytics(mockApp); + + billingEvents.emit('plan.changed', { + organizationId: { toString: () => '507f1f77bcf86cd799439011' }, + newPlan: 'pro', + }); + + expect(mockGroupIdentify).toHaveBeenCalledWith( + 'company', + '507f1f77bcf86cd799439011', + { plan: 'pro' }, + ); + }); + }); + + // ───────────────────────────────────────────────────────────────────── + // requireFeatureFlag — edge cases + // ───────────────────────────────────────────────────────────────────── + describe('requireFeatureFlag — edge cases:', () => { + let requireFeatureFlag; + let mockFeatureFlagsService; + let req; + let res; + let next; + + beforeEach(async () => { + jest.resetModules(); + + mockFeatureFlagsService = { + isEnabled: jest.fn(), + getVariant: jest.fn(), + }; + + jest.unstable_mockModule('../services/analytics.featureFlags.service.js', () => ({ + default: mockFeatureFlagsService, + })); + + const mod = await import('../middlewares/analytics.requireFeatureFlag.js'); + requireFeatureFlag = mod.default; + + req = { + user: { _id: 'user-edge-1' }, + organization: { _id: 'org-edge-1' }, + }; + + res = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + + next = jest.fn(); + }); + + test('should return 401 when user._id is empty string', async () => { + req.user = { _id: '' }; + + const middleware = requireFeatureFlag('beta'); + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(401); + expect(next).not.toHaveBeenCalled(); + }); + + test('should return 401 when user._id is null', async () => { + req.user = { _id: null }; + + const middleware = requireFeatureFlag('beta'); + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(401); + expect(next).not.toHaveBeenCalled(); + }); + + test('should return 401 when user._id is zero', async () => { + req.user = { _id: 0 }; + + const middleware = requireFeatureFlag('beta'); + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(401); + expect(next).not.toHaveBeenCalled(); + }); + + test('should handle concurrent flag evaluations independently', async () => { + let callCount = 0; + mockFeatureFlagsService.isEnabled.mockImplementation(async () => { + callCount += 1; + return callCount % 2 === 0; + }); + mockFeatureFlagsService.getVariant.mockResolvedValue(false); + + const middleware = requireFeatureFlag('beta'); + + const run = () => new Promise((resolve) => { + const n = jest.fn(() => resolve('allowed')); + const r = { + status: jest.fn().mockReturnThis(), + json: jest.fn(() => { resolve('blocked'); return r; }), + }; + middleware({ ...req }, r, n); + }); + + const results = await Promise.all([run(), run()]); + + expect(results).toContain('allowed'); + expect(results).toContain('blocked'); + }); + + test('should convert numeric user._id to string for distinctId', async () => { + req.user._id = 12345; + mockFeatureFlagsService.isEnabled.mockResolvedValue(true); + + const middleware = requireFeatureFlag('beta'); + await middleware(req, res, next); + + expect(mockFeatureFlagsService.isEnabled).toHaveBeenCalledWith( + 'beta', + '12345', + expect.any(Object), + ); + }); + + test('should convert ObjectId-like user._id to string', async () => { + req.user._id = { toString: () => '507f1f77bcf86cd799439011' }; + mockFeatureFlagsService.isEnabled.mockResolvedValue(true); + + const middleware = requireFeatureFlag('beta'); + await middleware(req, res, next); + + expect(mockFeatureFlagsService.isEnabled).toHaveBeenCalledWith( + 'beta', + '507f1f77bcf86cd799439011', + expect.any(Object), + ); + }); + + test('should include 403 error response with correct shape', async () => { + mockFeatureFlagsService.isEnabled.mockResolvedValue(false); + mockFeatureFlagsService.getVariant.mockResolvedValue(false); + + const middleware = requireFeatureFlag('premium-export'); + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'error', + message: 'Forbidden', + }), + ); + }); + + test('should pass organization groups when org is present', async () => { + mockFeatureFlagsService.isEnabled.mockResolvedValue(true); + + const middleware = requireFeatureFlag('beta'); + await middleware(req, res, next); + + expect(mockFeatureFlagsService.isEnabled).toHaveBeenCalledWith( + 'beta', + 'user-edge-1', + { groups: { company: 'org-edge-1' } }, + ); + }); + }); + +}); diff --git a/modules/analytics/tests/analytics.service.resilience.unit.tests.js b/modules/analytics/tests/analytics.service.resilience.unit.tests.js new file mode 100644 index 000000000..e67235807 --- /dev/null +++ b/modules/analytics/tests/analytics.service.resilience.unit.tests.js @@ -0,0 +1,191 @@ +/** + * Module dependencies. + */ +import { jest, beforeEach, afterEach, describe, test, expect } from '@jest/globals'; + +/** + * Unit tests for analytics service error resilience, shutdown safety, + * and edge cases not covered by the primary service unit tests. + */ +describe('Analytics service resilience tests:', () => { + let AnalyticsService; + let mockPostHogInstance; + + beforeEach(async () => { + jest.resetModules(); + + mockPostHogInstance = { + capture: jest.fn(), + identify: jest.fn(), + groupIdentify: jest.fn(), + getFeatureFlag: jest.fn().mockResolvedValue('variant-a'), + isFeatureEnabled: jest.fn().mockResolvedValue(true), + shutdown: jest.fn().mockResolvedValue(undefined), + }; + + jest.unstable_mockModule('posthog-node', () => ({ + PostHog: jest.fn().mockImplementation(() => mockPostHogInstance), + })); + + jest.unstable_mockModule('../../../config/index.js', () => ({ + default: { posthog: { apiKey: 'phc_key', host: 'https://test.posthog.com' } }, + })); + + const mod = await import('../services/analytics.service.js'); + AnalyticsService = mod.default; + AnalyticsService.init(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + // ───────────────────────────────────────────────────────────────────── + // Error propagation — synchronous SDK failures + // ───────────────────────────────────────────────────────────────────── + describe('synchronous SDK error propagation:', () => { + test('track should propagate when client.capture throws', () => { + mockPostHogInstance.capture.mockImplementation(() => { + throw new Error('Capture error'); + }); + + expect(() => AnalyticsService.track('u1', 'evt')).toThrow('Capture error'); + }); + + test('identify should propagate when client.identify throws', () => { + mockPostHogInstance.identify.mockImplementation(() => { + throw new Error('Identify error'); + }); + + expect(() => AnalyticsService.identify('u1', {})).toThrow('Identify error'); + }); + + test('groupIdentify should propagate when client.groupIdentify throws', () => { + mockPostHogInstance.groupIdentify.mockImplementation(() => { + throw new Error('Group error'); + }); + + expect(() => AnalyticsService.groupIdentify('company', 'org', {})).toThrow('Group error'); + }); + }); + + // ───────────────────────────────────────────────────────────────────── + // Error propagation — async SDK failures + // ───────────────────────────────────────────────────────────────────── + describe('async SDK error propagation:', () => { + test('getFeatureFlag should propagate rejection from SDK', async () => { + mockPostHogInstance.getFeatureFlag.mockRejectedValue(new Error('Timeout')); + + await expect(AnalyticsService.getFeatureFlag('flag', 'u1')).rejects.toThrow('Timeout'); + }); + + test('isFeatureEnabled should propagate rejection from SDK', async () => { + mockPostHogInstance.isFeatureEnabled.mockRejectedValue(new Error('Timeout')); + + await expect(AnalyticsService.isFeatureEnabled('flag', 'u1')).rejects.toThrow('Timeout'); + }); + + test('shutdown should propagate rejection from SDK', async () => { + mockPostHogInstance.shutdown.mockRejectedValue(new Error('Flush failed')); + + await expect(AnalyticsService.shutdown()).rejects.toThrow('Flush failed'); + }); + }); + + // ───────────────────────────────────────────────────────────────────── + // Post-shutdown safety + // ───────────────────────────────────────────────────────────────────── + describe('post-shutdown safety:', () => { + test('double shutdown should be safe (second call is no-op)', async () => { + await AnalyticsService.shutdown(); + // Second call — client is null, returns undefined synchronously + AnalyticsService.shutdown(); + + expect(mockPostHogInstance.shutdown).toHaveBeenCalledTimes(1); + }); + + test('track after shutdown should be a silent no-op', async () => { + await AnalyticsService.shutdown(); + + expect(() => AnalyticsService.track('u1', 'evt')).not.toThrow(); + expect(mockPostHogInstance.capture).not.toHaveBeenCalled(); + }); + + test('identify after shutdown should be a silent no-op', async () => { + await AnalyticsService.shutdown(); + + expect(() => AnalyticsService.identify('u1', { name: 'a' })).not.toThrow(); + expect(mockPostHogInstance.identify).not.toHaveBeenCalled(); + }); + + test('groupIdentify after shutdown should be a silent no-op', async () => { + await AnalyticsService.shutdown(); + + expect(() => AnalyticsService.groupIdentify('company', 'org', {})).not.toThrow(); + expect(mockPostHogInstance.groupIdentify).not.toHaveBeenCalled(); + }); + + test('getFeatureFlag after shutdown returns undefined', async () => { + await AnalyticsService.shutdown(); + + const result = await AnalyticsService.getFeatureFlag('flag', 'u1'); + expect(result).toBeUndefined(); + }); + + test('isFeatureEnabled after shutdown returns undefined', async () => { + await AnalyticsService.shutdown(); + + const result = await AnalyticsService.isFeatureEnabled('flag', 'u1'); + expect(result).toBeUndefined(); + }); + }); + + // ───────────────────────────────────────────────────────────────────── + // Minimal / optional argument handling + // ───────────────────────────────────────────────────────────────────── + describe('optional argument handling:', () => { + test('track with only required args passes undefined for optional params', () => { + AnalyticsService.track('u1', 'evt'); + + expect(mockPostHogInstance.capture).toHaveBeenCalledWith({ + distinctId: 'u1', + event: 'evt', + properties: undefined, + groups: undefined, + }); + }); + + test('identify with no properties passes undefined', () => { + AnalyticsService.identify('u1'); + + expect(mockPostHogInstance.identify).toHaveBeenCalledWith({ + distinctId: 'u1', + properties: undefined, + }); + }); + + test('groupIdentify with no properties passes undefined', () => { + AnalyticsService.groupIdentify('company', 'org-1'); + + expect(mockPostHogInstance.groupIdentify).toHaveBeenCalledWith({ + groupType: 'company', + groupKey: 'org-1', + properties: undefined, + }); + }); + + test('getFeatureFlag forwards options to SDK', async () => { + const opts = { groups: { company: 'org-1' } }; + await AnalyticsService.getFeatureFlag('flag', 'u1', opts); + + expect(mockPostHogInstance.getFeatureFlag).toHaveBeenCalledWith('flag', 'u1', opts); + }); + + test('isFeatureEnabled forwards options to SDK', async () => { + const opts = { personProperties: { plan: 'pro' } }; + await AnalyticsService.isFeatureEnabled('flag', 'u1', opts); + + expect(mockPostHogInstance.isFeatureEnabled).toHaveBeenCalledWith('flag', 'u1', opts); + }); + }); +}); From b73735527d5ce37325a5add60ffb90254f16b28e Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Wed, 25 Mar 2026 12:54:52 +0100 Subject: [PATCH 2/3] fix(skills): align verify, feature, and pull-request with Vue conventions --- .claude/skills/feature/SKILL.md | 2 +- .claude/skills/pull-request/SKILL.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.claude/skills/feature/SKILL.md b/.claude/skills/feature/SKILL.md index f69e6898d..29d5ded33 100644 --- a/.claude/skills/feature/SKILL.md +++ b/.claude/skills/feature/SKILL.md @@ -77,7 +77,7 @@ If an action affects another user: - [ ] Error responses have user-friendly `description` field **Tests:** -- [ ] Tests: add unit + integration tests. Add E2E (`*.e2e.tests.js`) only if the change affects a critical user flow (auth, org onboarding, invite/join). +- [ ] Tests: add unit (`*.unit.tests.js`) + integration (`*.integration.tests.js`) tests. Add E2E (`*.e2e.tests.js`) only if the change affects a critical user flow (auth, org onboarding, invite/join). **Modularity:** - [ ] Isolated in ONE module (or justified) diff --git a/.claude/skills/pull-request/SKILL.md b/.claude/skills/pull-request/SKILL.md index a103d704e..e478cb3d2 100644 --- a/.claude/skills/pull-request/SKILL.md +++ b/.claude/skills/pull-request/SKILL.md @@ -135,7 +135,7 @@ REPEAT: 6. If actionable comments → fix all, /verify, commit, push, reply, resolve, consecutive_zero=0, GOTO 1 7. If non-actionable unresolved → reply all explaining why, resolve all, consecutive_zero=0, GOTO 5 8. If zero unresolved threads → consecutive_zero++ - if consecutive_zero >= 3 → check branch protection (see 6f), then STOP ✓ + if consecutive_zero >= 3 (~9 min) → check branch protection (see 6f), then STOP ✓ else GOTO 3 ``` @@ -202,7 +202,7 @@ Wait 30s before watching CI (regular or force-push). Loop back to 6a. Never post ### 6f. Stop condition -CI green **and** 2 consecutive passes with zero unresolved threads. Mergeable status is also checked after every CI pass (step 2b) — conflicts cause an early stop. Final branch protection check: +CI green **and** 3 consecutive passes (~9 min of grace periods) with zero unresolved threads. Mergeable status is also checked after every CI pass (step 2b) — conflicts cause an early stop. Final branch protection check: ```bash gh pr view "$PR" --json reviewDecision,mergeable | jq '{reviewDecision, mergeable}' @@ -210,6 +210,7 @@ gh pr view "$PR" --json reviewDecision,mergeable | jq '{reviewDecision, mergeabl - `APPROVED` + `MERGEABLE` → **STOP ✓** - `REVIEW_REQUIRED` → report to user, stop +- `CHANGES_REQUESTED` → report to user, stop - `BLOCKED` → report details to user **Safety limit:** 10 iterations max — report to user if still unresolved. From e14b9e752f13cb09e3db5d36f02367ae6d003ba1 Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Wed, 25 Mar 2026 13:05:15 +0100 Subject: [PATCH 3/3] fix(analytics): update comprehensive tests for async init and query stripping --- .../tests/analytics.comprehensive.unit.tests.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/analytics/tests/analytics.comprehensive.unit.tests.js b/modules/analytics/tests/analytics.comprehensive.unit.tests.js index 4668ef959..04a26c1f5 100644 --- a/modules/analytics/tests/analytics.comprehensive.unit.tests.js +++ b/modules/analytics/tests/analytics.comprehensive.unit.tests.js @@ -137,13 +137,13 @@ describe('Analytics comprehensive tests:', () => { responseTimes.forEach((t) => expect(t).toBeGreaterThanOrEqual(0)); }); - test('should include query string in tracked endpoint', async () => { + test('should strip query string from tracked endpoint', async () => { await request(app).get('/api/search?q=test&page=1').expect(200); expect(mockTrack).toHaveBeenCalledWith( expect.any(String), 'api_request', - expect.objectContaining({ endpoint: '/api/search?q=test&page=1' }), + expect.objectContaining({ endpoint: '/api/search' }), expect.anything(), ); }); @@ -199,7 +199,7 @@ describe('Analytics comprehensive tests:', () => { jest.unstable_mockModule('../services/analytics.service.js', () => ({ default: { - init: mockInit, + init: mockInit.mockResolvedValue(undefined), track: jest.fn(), identify: jest.fn(), groupIdentify: mockGroupIdentify, @@ -221,7 +221,7 @@ describe('Analytics comprehensive tests:', () => { test('should call AnalyticsService.init() and register middleware', async () => { const { default: initAnalytics } = await import('../analytics.init.js'); - initAnalytics(mockApp); + await initAnalytics(mockApp); expect(mockInit).toHaveBeenCalledTimes(1); expect(mockApp.use).toHaveBeenCalledTimes(1); @@ -232,7 +232,7 @@ describe('Analytics comprehensive tests:', () => { const { default: billingEvents } = await import('../../billing/lib/events.js'); const { default: initAnalytics } = await import('../analytics.init.js'); - initAnalytics(mockApp); + await initAnalytics(mockApp); billingEvents.emit('plan.changed', { organizationId: 'org-test', @@ -254,7 +254,7 @@ describe('Analytics comprehensive tests:', () => { const { default: billingEvents } = await import('../../billing/lib/events.js'); const { default: initAnalytics } = await import('../analytics.init.js'); - initAnalytics(mockApp); + await initAnalytics(mockApp); expect(() => { billingEvents.emit('plan.changed', { @@ -268,7 +268,7 @@ describe('Analytics comprehensive tests:', () => { const { default: billingEvents } = await import('../../billing/lib/events.js'); const { default: initAnalytics } = await import('../analytics.init.js'); - initAnalytics(mockApp); + await initAnalytics(mockApp); billingEvents.emit('plan.changed', { organizationId: { toString: () => '507f1f77bcf86cd799439011' },