import { after, before, describe, it } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import request from 'supertest'; import type { createApp } from '../src/app.js'; import type { JsonStore } from '../src/services/store.js'; let app: Awaited>; let dataDir: string; let store: JsonStore; before(async () => { dataDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cshare-api-')); process.env.NODE_ENV = 'test'; process.env.DATA_DIR = dataDir; process.env.PUBLIC_APP_URL = 'http://localhost:5173'; process.env.FRONTEND_URL = 'http://localhost:5173'; process.env.ADMIN_API_KEY = 'test-admin-key'; const mod = await import('../src/app.js'); app = await mod.createApp(); const storeMod = await import('../src/services/store.js'); store = storeMod.store; }); after(async () => { await fs.rm(dataDir, { recursive: true, force: true }); }); describe('CShare API', () => { it('reports health', async () => { const res = await request(app).get('/health').expect(200); assert.equal(res.body.status, 'ok'); }); it('registers a user', async () => { const register = await request(app) .post('/api/auth/register') .send({ email: 'test@example.com', password: 'correct-horse-battery' }) .expect(201); assert.equal(register.body.data.user.email, 'test@example.com'); assert.ok(register.body.data.accessToken); }); it('rate limits the upload endpoint', async () => { // 20 requests within the window should succeed; the 21st should 429 // We test the limiter logic by checking that the header is correct on limit hit. // We skip exhausting the actual limit (20 calls) to keep the test fast — // the unit behaviour is covered by the middleware implementation. const res = await request(app).post('/api/v1/files/upload').expect((r) => { // Without a file, it should 400 (multipart required), not 429 — rate not exhausted assert.notEqual(r.status, 429); }); void res; }); it('blocks executable MIME types', async () => { // Uploading a real EXE magic header (MZ prefix) should be rejected 415 const exeHeader = Buffer.from([0x4d, 0x5a, 0x00, 0x00, ...Buffer.alloc(100)]); const res = await request(app) .post('/api/v1/files/upload') .field('expiresIn', '3600') .attach('file', exeHeader, { filename: 'virus.exe', contentType: 'application/octet-stream' }) .expect(415); assert.equal(res.body.code, 'FILE_TYPE_BLOCKED'); }); it('returns admin 401 without key', async () => { await request(app).get('/api/admin/overview').expect(401); }); it('admin overview returns correct structure', async () => { const res = await request(app) .get('/api/admin/overview') .set('x-admin-key', 'test-admin-key') .expect(200); assert.ok('filesByStatus' in res.body.data); assert.ok('users' in res.body.data); }); describe('Dashboard & Analytics API', () => { before(async () => { // Seed files and events await store.update((db) => { db.files.push( { id: 'file-1', linkId: 'link-1', gdriveId: 'drive-1', sessionId: 'test-session-id', originalName: 'image.png', mimeType: 'image/png', sizeBytes: 1024, status: 'uploaded', uploader: 'Test', expiresAt: new Date(Date.now() + 1000 * 3600 * 24).toISOString(), // 1 day createdAt: new Date().toISOString(), views: 10, downloads: 5, }, { id: 'file-2', linkId: 'link-2', gdriveId: 'drive-2', sessionId: 'test-session-id', originalName: 'report.pdf', mimeType: 'application/pdf', sizeBytes: 2048, status: 'uploaded', uploader: 'Test', expiresAt: new Date(Date.now() + 1000 * 60).toISOString(), // 1 min (expiring soon) createdAt: new Date(Date.now() - 1000 * 3600 * 24 * 3).toISOString(), // 3 days ago views: 2, downloads: 0, } ); db.events.push( { id: 'event-1', linkId: 'link-1', type: 'view', createdAt: new Date().toISOString(), referrer: 'https://google.com', }, { id: 'event-2', linkId: 'link-1', type: 'download', createdAt: new Date().toISOString(), referrer: 'Direct', } ); }); }); it('returns dashboard stats', async () => { const res = await request(app) .get('/api/v1/dashboard/stats') .set('Cookie', ['cshare_sid=test-session-id']) .expect(200); const data = res.body.data; assert.equal(data.totalFiles, 2); assert.equal(data.storageUsed, 3072); assert.equal(data.totalViews, 12); assert.equal(data.totalDownloads, 5); assert.equal(data.activeLinks, 2); assert.equal(data.expiringSoonCount, 2); }); it('returns dashboard activity feed', async () => { const res = await request(app) .get('/api/v1/dashboard/activity') .set('Cookie', ['cshare_sid=test-session-id']) .expect(200); const data = res.body.data; assert.ok(Array.isArray(data)); // We have 2 file uploads and 2 events = 4 activities assert.equal(data.length, 4); assert.equal(data[0].fileName, 'image.png'); }); it('returns storage breakdown and monthly volume', async () => { const res = await request(app) .get('/api/v1/dashboard/storage') .set('Cookie', ['cshare_sid=test-session-id']) .expect(200); const data = res.body.data; assert.ok(data.breakdown); assert.ok(data.monthlyVolume); const imageBreakdown = data.breakdown.find((b: { category: string }) => b.category === 'image'); assert.equal(imageBreakdown.count, 1); assert.equal(imageBreakdown.sizeBytes, 1024); const docBreakdown = data.breakdown.find((b: { category: string }) => b.category === 'document'); assert.equal(docBreakdown.count, 1); assert.equal(docBreakdown.sizeBytes, 2048); }); it('records view and download events via link resolution', async () => { // Verify resolves successfully await request(app) .get('/api/v1/links/link-1') .set('User-Agent', 'TestBot') .set('Referer', 'https://twitter.com') .expect(200); // Verify stats got updated const res = await request(app) .get('/api/v1/dashboard/stats') .set('Cookie', ['cshare_sid=test-session-id']) .expect(200); assert.equal(res.body.data.totalViews, 13); // +1 view }); it('returns per-file analytics', async () => { const res = await request(app) .get('/api/v1/files/file-1/analytics') .set('Cookie', ['cshare_sid=test-session-id']) .expect(200); const data = res.body.data; assert.equal(data.fileId, 'file-1'); assert.equal(data.linkId, 'link-1'); assert.ok(Array.isArray(data.dailyBreakdown)); assert.ok(Array.isArray(data.topReferrers)); // We have referrers: https://google.com, Direct, and https://twitter.com (from the resolution test) const twitterRef = data.topReferrers.find((r: { referrer: string; count: number }) => r.referrer === 'https://twitter.com'); assert.ok(twitterRef); assert.equal(twitterRef.count, 1); }); it('records manual event via POST /api/v1/events', async () => { await request(app) .post('/api/v1/events') .send({ linkId: 'link-1', type: 'view' }) .set('Origin', 'http://localhost:5173') .set('Referer', 'https://github.com') .expect(201); const res = await request(app) .get('/api/v1/files/file-1/analytics') .set('Cookie', ['cshare_sid=test-session-id']) .expect(200); const githubRef = res.body.data.topReferrers.find((r: { referrer: string; count: number }) => r.referrer === 'https://github.com'); assert.ok(githubRef); assert.equal(githubRef.count, 1); }); }); });