Skip to content

Commit b656298

Browse files
committed
test: add comprehensive tests for project data availability and session handling
1 parent f14bcd0 commit b656298

File tree

9 files changed

+1826
-1
lines changed

9 files changed

+1826
-1
lines changed

packages/backend/src/tests/database/storage-config.test.ts

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2-
import { getClickHouseConfig, validateStorageConfig, STORAGE_ENGINE } from '../../database/storage-config.js';
2+
import { getClickHouseConfig, getMongoDBConfig, validateStorageConfig, STORAGE_ENGINE } from '../../database/storage-config.js';
33

44
describe('storage-config', () => {
55
describe('STORAGE_ENGINE', () => {
@@ -91,5 +91,132 @@ describe('storage-config', () => {
9191
// STORAGE_ENGINE is 'timescale' in test env by default
9292
expect(() => validateStorageConfig()).not.toThrow();
9393
});
94+
95+
// Dynamic import tests for non-default engine values
96+
it('should throw for invalid engine value', async () => {
97+
vi.resetModules();
98+
vi.stubEnv('STORAGE_ENGINE', 'oracle');
99+
const { validateStorageConfig: validate } = await import('../../database/storage-config.js');
100+
expect(() => validate()).toThrow('Invalid STORAGE_ENGINE');
101+
vi.unstubAllEnvs();
102+
vi.resetModules();
103+
});
104+
105+
it('should throw for clickhouse without CLICKHOUSE_HOST', async () => {
106+
vi.resetModules();
107+
vi.stubEnv('STORAGE_ENGINE', 'clickhouse');
108+
const { validateStorageConfig: validate } = await import('../../database/storage-config.js');
109+
expect(() => validate()).toThrow('Missing CLICKHOUSE_HOST');
110+
vi.unstubAllEnvs();
111+
vi.resetModules();
112+
});
113+
114+
it('should throw for clickhouse with HOST but without CLICKHOUSE_DATABASE', async () => {
115+
vi.resetModules();
116+
vi.stubEnv('STORAGE_ENGINE', 'clickhouse');
117+
vi.stubEnv('CLICKHOUSE_HOST', 'localhost');
118+
const { validateStorageConfig: validate } = await import('../../database/storage-config.js');
119+
expect(() => validate()).toThrow('Missing CLICKHOUSE_DATABASE');
120+
vi.unstubAllEnvs();
121+
vi.resetModules();
122+
});
123+
124+
it('should not throw for clickhouse with both required vars', async () => {
125+
vi.resetModules();
126+
vi.stubEnv('STORAGE_ENGINE', 'clickhouse');
127+
vi.stubEnv('CLICKHOUSE_HOST', 'localhost');
128+
vi.stubEnv('CLICKHOUSE_DATABASE', 'logtide');
129+
const { validateStorageConfig: validate } = await import('../../database/storage-config.js');
130+
expect(() => validate()).not.toThrow();
131+
vi.unstubAllEnvs();
132+
vi.resetModules();
133+
});
134+
135+
it('should throw for mongodb without MONGODB_URI or MONGODB_HOST', async () => {
136+
vi.resetModules();
137+
vi.stubEnv('STORAGE_ENGINE', 'mongodb');
138+
const { validateStorageConfig: validate } = await import('../../database/storage-config.js');
139+
expect(() => validate()).toThrow('Missing MONGODB_URI or MONGODB_HOST');
140+
vi.unstubAllEnvs();
141+
vi.resetModules();
142+
});
143+
144+
it('should not throw for mongodb with MONGODB_URI', async () => {
145+
vi.resetModules();
146+
vi.stubEnv('STORAGE_ENGINE', 'mongodb');
147+
vi.stubEnv('MONGODB_URI', 'mongodb://localhost:27017/test');
148+
const { validateStorageConfig: validate } = await import('../../database/storage-config.js');
149+
expect(() => validate()).not.toThrow();
150+
vi.unstubAllEnvs();
151+
vi.resetModules();
152+
});
153+
154+
it('should not throw for mongodb with MONGODB_HOST', async () => {
155+
vi.resetModules();
156+
vi.stubEnv('STORAGE_ENGINE', 'mongodb');
157+
vi.stubEnv('MONGODB_HOST', 'localhost');
158+
const { validateStorageConfig: validate } = await import('../../database/storage-config.js');
159+
expect(() => validate()).not.toThrow();
160+
vi.unstubAllEnvs();
161+
vi.resetModules();
162+
});
163+
});
164+
165+
describe('getMongoDBConfig()', () => {
166+
afterEach(() => {
167+
vi.unstubAllEnvs();
168+
});
169+
170+
it('should return defaults when no env vars are set', () => {
171+
const config = getMongoDBConfig();
172+
expect(config.host).toBe('localhost');
173+
expect(config.port).toBe(27017);
174+
expect(config.database).toBe('logtide');
175+
expect(config.username).toBe('');
176+
expect(config.password).toBe('');
177+
});
178+
179+
it('should parse MONGODB_URI for host, port, database, credentials', () => {
180+
vi.stubEnv('MONGODB_URI', 'mongodb://admin:secret@mongo.example.com:27018/mydb');
181+
const config = getMongoDBConfig();
182+
expect(config.host).toBe('mongo.example.com');
183+
expect(config.port).toBe(27018);
184+
expect(config.database).toBe('mydb');
185+
expect(config.username).toBe('admin');
186+
expect(config.password).toBe('secret');
187+
});
188+
189+
it('should parse authSource from MONGODB_URI query string', () => {
190+
vi.stubEnv('MONGODB_URI', 'mongodb://user:pass@localhost:27017/mydb?authSource=admin');
191+
const config = getMongoDBConfig();
192+
expect((config as any).options?.authSource).toBe('admin');
193+
});
194+
195+
it('should fall back to individual env vars when MONGODB_URI is not set', () => {
196+
vi.stubEnv('MONGODB_HOST', 'mongo.internal');
197+
vi.stubEnv('MONGODB_PORT', '27019');
198+
vi.stubEnv('MONGODB_DATABASE', 'proddb');
199+
vi.stubEnv('MONGODB_USERNAME', 'dbuser');
200+
vi.stubEnv('MONGODB_PASSWORD', 'dbpass');
201+
const config = getMongoDBConfig();
202+
expect(config.host).toBe('mongo.internal');
203+
expect(config.port).toBe(27019);
204+
expect(config.database).toBe('proddb');
205+
expect(config.username).toBe('dbuser');
206+
expect(config.password).toBe('dbpass');
207+
});
208+
209+
it('should include authSource in options when MONGODB_AUTH_SOURCE is set', () => {
210+
vi.stubEnv('MONGODB_HOST', 'localhost');
211+
vi.stubEnv('MONGODB_AUTH_SOURCE', 'admin');
212+
const config = getMongoDBConfig();
213+
expect((config as any).options?.authSource).toBe('admin');
214+
});
215+
216+
it('should not include options when MONGODB_AUTH_SOURCE is not set', () => {
217+
vi.stubEnv('MONGODB_HOST', 'localhost');
218+
const config = getMongoDBConfig();
219+
expect((config as any).options).toBeUndefined();
220+
});
94221
});
95222
});

packages/backend/src/tests/modules/dashboard/routes.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,4 +404,91 @@ describe('Dashboard Routes', () => {
404404
expect(body.events.length).toBe(0);
405405
});
406406
});
407+
408+
// =========================================================================
409+
// projectId scoping — verifyProjectBelongsToOrg branch
410+
// =========================================================================
411+
412+
describe('projectId parameter scoping', () => {
413+
it('stats: returns 200 with projectId scoped to the org', async () => {
414+
const res = await app.inject({
415+
method: 'GET',
416+
url: `/api/v1/dashboard/stats?organizationId=${testOrganization.id}&projectId=${testProject.id}`,
417+
headers: authHeaders(),
418+
});
419+
expect(res.statusCode).toBe(200);
420+
});
421+
422+
it('stats: returns 404 when projectId does not belong to the org', async () => {
423+
const res = await app.inject({
424+
method: 'GET',
425+
url: `/api/v1/dashboard/stats?organizationId=${testOrganization.id}&projectId=00000000-0000-0000-0000-000000000000`,
426+
headers: authHeaders(),
427+
});
428+
expect(res.statusCode).toBe(404);
429+
});
430+
431+
it('timeseries: returns 200 with projectId scoped to the org', async () => {
432+
const res = await app.inject({
433+
method: 'GET',
434+
url: `/api/v1/dashboard/timeseries?organizationId=${testOrganization.id}&projectId=${testProject.id}`,
435+
headers: authHeaders(),
436+
});
437+
expect(res.statusCode).toBe(200);
438+
});
439+
440+
it('timeseries: returns 404 when projectId does not belong to the org', async () => {
441+
const res = await app.inject({
442+
method: 'GET',
443+
url: `/api/v1/dashboard/timeseries?organizationId=${testOrganization.id}&projectId=00000000-0000-0000-0000-000000000000`,
444+
headers: authHeaders(),
445+
});
446+
expect(res.statusCode).toBe(404);
447+
});
448+
449+
it('top-services: returns 200 with projectId scoped to the org', async () => {
450+
const res = await app.inject({
451+
method: 'GET',
452+
url: `/api/v1/dashboard/top-services?organizationId=${testOrganization.id}&projectId=${testProject.id}`,
453+
headers: authHeaders(),
454+
});
455+
expect(res.statusCode).toBe(200);
456+
});
457+
458+
it('top-services: returns 404 when projectId does not belong to the org', async () => {
459+
const res = await app.inject({
460+
method: 'GET',
461+
url: `/api/v1/dashboard/top-services?organizationId=${testOrganization.id}&projectId=00000000-0000-0000-0000-000000000000`,
462+
headers: authHeaders(),
463+
});
464+
expect(res.statusCode).toBe(404);
465+
});
466+
467+
it('timeline-events: returns 200 with projectId scoped to the org', async () => {
468+
const res = await app.inject({
469+
method: 'GET',
470+
url: `/api/v1/dashboard/timeline-events?organizationId=${testOrganization.id}&projectId=${testProject.id}`,
471+
headers: authHeaders(),
472+
});
473+
expect(res.statusCode).toBe(200);
474+
});
475+
476+
it('recent-errors: returns 200 with projectId scoped to the org', async () => {
477+
const res = await app.inject({
478+
method: 'GET',
479+
url: `/api/v1/dashboard/recent-errors?organizationId=${testOrganization.id}&projectId=${testProject.id}`,
480+
headers: authHeaders(),
481+
});
482+
expect(res.statusCode).toBe(200);
483+
});
484+
485+
it('recent-errors: returns 404 when projectId does not belong to the org', async () => {
486+
const res = await app.inject({
487+
method: 'GET',
488+
url: `/api/v1/dashboard/recent-errors?organizationId=${testOrganization.id}&projectId=00000000-0000-0000-0000-000000000000`,
489+
headers: authHeaders(),
490+
});
491+
expect(res.statusCode).toBe(404);
492+
});
493+
});
407494
});

packages/backend/src/tests/modules/projects/projects-service.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,4 +389,64 @@ describe('ProjectsService', () => {
389389
expect(result?.name).toBe('Project 2');
390390
});
391391
});
392+
393+
describe('getProjectDataAvailability', () => {
394+
it('should return empty arrays when no data exists', async () => {
395+
const { user, organization } = await createTestContext();
396+
397+
const result = await projectsService.getProjectDataAvailability(organization.id, user.id);
398+
399+
// Shape: { logs: string[], traces: string[], metrics: string[] }
400+
expect(result.logs).toBeDefined();
401+
expect(result.traces).toBeDefined();
402+
expect(result.metrics).toBeDefined();
403+
expect(Array.isArray(result.logs)).toBe(true);
404+
});
405+
406+
it('should throw when user is not a member of the organization', async () => {
407+
const { organization } = await createTestContext();
408+
const outsider = await createTestUser({ email: 'outsider-da@test.com' });
409+
410+
await expect(
411+
projectsService.getProjectDataAvailability(organization.id, outsider.id)
412+
).rejects.toThrow('do not have access');
413+
});
414+
415+
it('should include project in logs array when logs exist', async () => {
416+
const { user, organization, project } = await createTestContext();
417+
418+
await db.insertInto('logs').values({
419+
project_id: project.id,
420+
service: 'api',
421+
level: 'info',
422+
message: 'test log',
423+
time: new Date(),
424+
}).execute();
425+
426+
const result = await projectsService.getProjectDataAvailability(organization.id, user.id);
427+
expect(result.logs).toContain(project.id);
428+
});
429+
430+
it('should not include project in logs array when no logs exist', async () => {
431+
const { user, organization, project } = await createTestContext();
432+
433+
const result = await projectsService.getProjectDataAvailability(organization.id, user.id);
434+
expect(result.logs).not.toContain(project.id);
435+
});
436+
437+
it('should include project in traces array when traces exist', async () => {
438+
const { user, organization } = await createTestContext();
439+
const { createTestTrace } = await import('../../helpers/factories.js');
440+
const traceProject = await projectsService.createProject({
441+
organizationId: organization.id,
442+
userId: user.id,
443+
name: 'Trace Project',
444+
});
445+
446+
await createTestTrace({ projectId: traceProject.id, organizationId: organization.id });
447+
448+
const result = await projectsService.getProjectDataAvailability(organization.id, user.id);
449+
expect(result.traces).toContain(traceProject.id);
450+
});
451+
});
392452
});

0 commit comments

Comments
 (0)