Prechádzať zdrojové kódy

Add automated tests (56 tests covering geo, GPX processor, and full API)

k4be 5 hodín pred
rodič
commit
69a7c74943

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 812 - 25
gpx-vis-backend/package-lock.json


+ 379 - 0
gpx-vis-backend/test/api.test.js

@@ -0,0 +1,379 @@
+const assert = require('assert');
+const supertest = require('supertest');
+
+let app, request, db;
+
+// Simple GPX with a track
+const TEST_GPX = `<?xml version="1.0"?>
+<gpx version="1.1">
+  <trk>
+    <name>Test Track</name>
+    <trkseg>
+      <trkpt lat="51.500" lon="-0.100"><ele>10</ele><time>2024-03-01T10:00:00Z</time></trkpt>
+      <trkpt lat="51.510" lon="-0.100"><ele>12</ele><time>2024-03-01T10:01:00Z</time></trkpt>
+      <trkpt lat="51.520" lon="-0.100"><ele>14</ele><time>2024-03-01T10:02:00Z</time></trkpt>
+    </trkseg>
+  </trk>
+</gpx>`;
+
+before(async () => {
+  // Reset sequelize singleton for in-memory SQLite
+  const dbModule = require('../src/database');
+  await dbModule.initDatabase();
+  app = require('../src/app').createApp(global.testConfig);
+  request = supertest(app);
+});
+
+// ─── Health ───────────────────────────────────────────────────────────────────
+describe('GET /health', () => {
+  it('returns ok', async () => {
+    const res = await request.get('/health').expect(200);
+    assert.strictEqual(res.body.ok, true);
+  });
+});
+
+// ─── Auth ─────────────────────────────────────────────────────────────────────
+describe('Auth', () => {
+  let adminToken, user2Token;
+
+  describe('POST /api/auth/register', () => {
+    it('first user becomes admin and is active', async () => {
+      const res = await request.post('/api/auth/register')
+        .send({ login: 'testadmin', password: 'admin123' })
+        .expect(201);
+      assert.strictEqual(res.body.login, 'testadmin');
+      assert.strictEqual(res.body.isAdmin, true);
+      assert.strictEqual(res.body.isActive, true);
+    });
+
+    it('rejects duplicate login', async () => {
+      await request.post('/api/auth/register')
+        .send({ login: 'testadmin', password: 'pass123456' })
+        .expect(409);
+    });
+
+    it('rejects invalid login format', async () => {
+      await request.post('/api/auth/register')
+        .send({ login: 'a b', password: 'pass123' })
+        .expect(400);
+    });
+
+    it('rejects short password', async () => {
+      await request.post('/api/auth/register')
+        .send({ login: 'validname', password: 'x' })
+        .expect(400);
+    });
+
+    it('second user is not active', async () => {
+      const res = await request.post('/api/auth/register')
+        .send({ login: 'testuser2', password: 'pass123' })
+        .expect(201);
+      assert.strictEqual(res.body.isAdmin, false);
+      assert.strictEqual(res.body.isActive, false);
+    });
+  });
+
+  describe('POST /api/auth/login', () => {
+    it('admin can login', async () => {
+      const res = await request.post('/api/auth/login')
+        .send({ login: 'testadmin', password: 'admin123' })
+        .expect(200);
+      assert.ok(res.body.token);
+      adminToken = res.body.token;
+    });
+
+    it('inactive user cannot login', async () => {
+      await request.post('/api/auth/login')
+        .send({ login: 'testuser2', password: 'pass123' })
+        .expect(403);
+    });
+
+    it('rejects wrong password', async () => {
+      await request.post('/api/auth/login')
+        .send({ login: 'testadmin', password: 'wrong' })
+        .expect(401);
+    });
+
+    it('rejects unknown user', async () => {
+      await request.post('/api/auth/login')
+        .send({ login: 'nobody', password: 'pass' })
+        .expect(401);
+    });
+  });
+
+  describe('GET /api/auth/me', () => {
+    it('returns current user when authenticated', async () => {
+      const res = await request.get('/api/auth/me')
+        .set('Authorization', `Bearer ${adminToken}`)
+        .expect(200);
+      assert.strictEqual(res.body.login, 'testadmin');
+    });
+
+    it('returns 401 without token', async () => {
+      await request.get('/api/auth/me').expect(401);
+    });
+  });
+
+  // Store token for use in later tests
+  after(() => {
+    global.adminToken = adminToken;
+  });
+});
+
+// ─── Admin ────────────────────────────────────────────────────────────────────
+describe('Admin', () => {
+  it('activates second user', async () => {
+    // Find user2's id
+    const users = await request.get('/api/admin/users')
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(200);
+    const user2 = users.body.find(u => u.login === 'testuser2');
+    assert.ok(user2);
+
+    await request.put(`/api/admin/users/${user2.id}/activate`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .send({ isActive: true })
+      .expect(200);
+
+    // user2 can now login
+    const res = await request.post('/api/auth/login')
+      .send({ login: 'testuser2', password: 'pass123' })
+      .expect(200);
+    global.user2Token = res.body.token;
+  });
+
+  it('lists all users', async () => {
+    const res = await request.get('/api/admin/users')
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(200);
+    assert.ok(Array.isArray(res.body));
+    assert.ok(res.body.length >= 2);
+    assert.ok(res.body.every(u => 'trackCount' in u));
+  });
+
+  it('rejects non-admin access', async () => {
+    await request.get('/api/admin/users')
+      .set('Authorization', `Bearer ${global.user2Token}`)
+      .expect(403);
+  });
+});
+
+// ─── Directories ──────────────────────────────────────────────────────────────
+describe('Directories', () => {
+  let dirId;
+
+  it('creates a root directory', async () => {
+    const res = await request.post('/api/directories')
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .send({ name: 'My Rides' })
+      .expect(201);
+    assert.strictEqual(res.body.name, 'My Rides');
+    assert.strictEqual(res.body.parentId, null);
+    dirId = res.body.id;
+    global.testDirId = dirId;
+  });
+
+  it('creates a subdirectory', async () => {
+    const res = await request.post('/api/directories')
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .send({ name: 'Summer 2024', parentId: dirId })
+      .expect(201);
+    assert.strictEqual(res.body.parentId, dirId);
+    global.subDirId = res.body.id;
+  });
+
+  it('lists root directories', async () => {
+    const res = await request.get('/api/directories')
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(200);
+    assert.ok(Array.isArray(res.body));
+    assert.ok(res.body.some(d => d.name === 'My Rides'));
+  });
+
+  it('gets directory with children', async () => {
+    const res = await request.get(`/api/directories/${dirId}`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(200);
+    assert.ok(Array.isArray(res.body.children));
+    assert.ok(res.body.children.some(c => c.name === 'Summer 2024'));
+  });
+
+  it('renames directory', async () => {
+    await request.put(`/api/directories/${dirId}`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .send({ name: 'All Rides' })
+      .expect(200);
+    const res = await request.get(`/api/directories/${dirId}`)
+      .set('Authorization', `Bearer ${global.adminToken}`);
+    assert.strictEqual(res.body.name, 'All Rides');
+  });
+
+  it('prevents accessing other users directories', async () => {
+    await request.get(`/api/directories/${dirId}`)
+      .set('Authorization', `Bearer ${global.user2Token}`)
+      .expect(404);
+  });
+});
+
+// ─── Tracks ───────────────────────────────────────────────────────────────────
+describe('Tracks', () => {
+  let trackId;
+
+  it('uploads a GPX file', async () => {
+    const res = await request.post('/api/tracks/upload')
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .attach('file', Buffer.from(TEST_GPX), { filename: 'test.gpx', contentType: 'application/gpx+xml' })
+      .field('directoryId', global.testDirId)
+      .expect(201);
+
+    assert.strictEqual(res.body.name, 'Test Track');
+    assert.ok(res.body.pointCount > 0);
+    assert.ok(res.body.totalDistance > 0);
+    assert.ok(res.body.trackDate);
+    trackId = res.body.id;
+    global.testTrackId = trackId;
+  });
+
+  it('rejects non-GPX upload', async () => {
+    await request.post('/api/tracks/upload')
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .attach('file', Buffer.from('not gpx'), { filename: 'file.txt', contentType: 'text/plain' })
+      .expect(400);
+  });
+
+  it('lists tracks in directory', async () => {
+    const res = await request.get(`/api/tracks?directoryId=${global.testDirId}`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(200);
+    assert.ok(Array.isArray(res.body));
+    assert.ok(res.body.some(t => t.id === trackId));
+  });
+
+  it('gets track points in compact format', async () => {
+    const res = await request.get(`/api/tracks/${trackId}/points`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(200);
+    assert.ok(res.body.meta);
+    assert.ok(Array.isArray(res.body.segments));
+    assert.ok(res.body.segments.length > 0);
+    const pts = res.body.segments[0];
+    assert.ok(Array.isArray(pts));
+    assert.ok(pts.length > 0);
+    // Each point is [lat, lon, ele, time]
+    const pt = pts[0];
+    assert.ok(Array.isArray(pt) && pt.length === 4);
+    assert.ok(typeof pt[0] === 'number'); // lat
+    assert.ok(typeof pt[1] === 'number'); // lon
+  });
+
+  it('updates track name and directory', async () => {
+    await request.put(`/api/tracks/${trackId}`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .send({ name: 'Renamed Track', directoryId: global.subDirId })
+      .expect(200);
+
+    const res = await request.get(`/api/tracks/${trackId}`)
+      .set('Authorization', `Bearer ${global.adminToken}`);
+    assert.strictEqual(res.body.name, 'Renamed Track');
+    assert.strictEqual(res.body.directoryId, global.subDirId);
+  });
+
+  it('prevents accessing other users tracks', async () => {
+    await request.get(`/api/tracks/${trackId}/points`)
+      .set('Authorization', `Bearer ${global.user2Token}`)
+      .expect(404);
+  });
+});
+
+// ─── Share Links ──────────────────────────────────────────────────────────────
+describe('Share Links', () => {
+  let shareCode;
+
+  it('creates a share link', async () => {
+    const res = await request.post(`/api/tracks/${global.testTrackId}/share`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(200);
+    assert.ok(res.body.code);
+    assert.match(res.body.code, /^[bcdfghjklmnprstvwxyz][aeiou]{1}[bcdfghjklmnprstvwxyz][aeiou]{1}[bcdfghjklmnprstvwxyz][aeiou]{1}[bcdfghjklmnprstvwxyz][aeiou]{1}[bcdfghjklmnprstvwxyz][aeiou]{1}$/);
+    shareCode = res.body.code;
+    global.shareCode = shareCode;
+  });
+
+  it('returns same code on second call (idempotent)', async () => {
+    const res = await request.post(`/api/tracks/${global.testTrackId}/share`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(200);
+    assert.strictEqual(res.body.code, shareCode);
+  });
+
+  it('public share endpoint returns track data without auth', async () => {
+    const res = await request.get(`/api/share/${shareCode}`).expect(200);
+    assert.ok(res.body.meta);
+    assert.ok(Array.isArray(res.body.segments));
+    assert.strictEqual(res.body.meta.name, 'Renamed Track');
+  });
+
+  it('public share returns 404 for unknown code', async () => {
+    await request.get('/api/share/unknownxxx').expect(404);
+  });
+
+  it('revokes share link', async () => {
+    await request.delete(`/api/tracks/${global.testTrackId}/share`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(200);
+
+    await request.get(`/api/share/${shareCode}`).expect(404);
+  });
+});
+
+// ─── Stats ────────────────────────────────────────────────────────────────────
+describe('Stats', () => {
+  it('returns distance stats by year/month/week', async () => {
+    const res = await request.get('/api/stats')
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(200);
+    assert.ok(Array.isArray(res.body.byYear));
+    assert.ok(Array.isArray(res.body.byMonth));
+    assert.ok(Array.isArray(res.body.byWeek));
+    // Should have stats for 2024 (track date)
+    assert.ok(res.body.byYear.some(y => y.year === 2024));
+  });
+
+  it('returns directory stats', async () => {
+    const res = await request.get(`/api/stats/directory/${global.subDirId}`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(200);
+    assert.ok(Array.isArray(res.body.tracks));
+    assert.ok(typeof res.body.totalDistance === 'number');
+  });
+});
+
+// ─── Track Deletion ───────────────────────────────────────────────────────────
+describe('Track and Directory Deletion', () => {
+  it('deletes a track', async () => {
+    await request.delete(`/api/tracks/${global.testTrackId}`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(200);
+
+    await request.get(`/api/tracks/${global.testTrackId}`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(404);
+  });
+
+  it('deletes a directory recursively', async () => {
+    // Upload a track to subdir first
+    await request.post('/api/tracks/upload')
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .attach('file', Buffer.from(TEST_GPX), { filename: 'test2.gpx', contentType: 'application/gpx+xml' })
+      .field('directoryId', global.subDirId);
+
+    // Delete parent (should cascade to subdir and its track)
+    await request.delete(`/api/directories/${global.testDirId}`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(200);
+
+    await request.get(`/api/directories/${global.subDirId}`)
+      .set('Authorization', `Bearer ${global.adminToken}`)
+      .expect(404);
+  });
+});

+ 63 - 0
gpx-vis-backend/test/geo.test.js

@@ -0,0 +1,63 @@
+const assert = require('assert');
+const { haversineDistance, bearing, bearingDiff } = require('../src/utils/geo');
+
+describe('geo utilities', () => {
+  describe('haversineDistance', () => {
+    it('returns 0 for same point', () => {
+      assert.strictEqual(haversineDistance(51.5, -0.1, 51.5, -0.1), 0);
+    });
+
+    it('calculates distance between London and Paris (~340 km)', () => {
+      const d = haversineDistance(51.5074, -0.1278, 48.8566, 2.3522);
+      assert.ok(d > 340000 && d < 345000, `Expected ~342 km, got ${(d/1000).toFixed(1)} km`);
+    });
+
+    it('calculates short distance accurately (~1 m steps)', () => {
+      // 0.00001 degrees lat ≈ 1.11 m
+      const d = haversineDistance(51.5000, 0, 51.5001, 0);
+      assert.ok(d > 10 && d < 12, `Expected ~11 m, got ${d.toFixed(2)} m`);
+    });
+  });
+
+  describe('bearing', () => {
+    it('returns 0 for due north', () => {
+      const b = bearing(51.0, 0.0, 52.0, 0.0);
+      assert.ok(Math.abs(b) < 1, `Expected ~0°, got ${b.toFixed(1)}°`);
+    });
+
+    it('returns 90 for due east', () => {
+      const b = bearing(51.0, 0.0, 51.0, 1.0);
+      assert.ok(Math.abs(b - 90) < 2, `Expected ~90°, got ${b.toFixed(1)}°`);
+    });
+
+    it('returns 180 for due south', () => {
+      const b = bearing(52.0, 0.0, 51.0, 0.0);
+      assert.ok(Math.abs(b - 180) < 1, `Expected ~180°, got ${b.toFixed(1)}°`);
+    });
+
+    it('returns 270 for due west', () => {
+      const b = bearing(51.0, 1.0, 51.0, 0.0);
+      assert.ok(Math.abs(b - 270) < 2, `Expected ~270°, got ${b.toFixed(1)}°`);
+    });
+  });
+
+  describe('bearingDiff', () => {
+    it('returns 0 for same bearing', () => {
+      assert.strictEqual(bearingDiff(90, 90), 0);
+    });
+
+    it('returns 90 for perpendicular bearings', () => {
+      assert.strictEqual(bearingDiff(0, 90), 90);
+      assert.strictEqual(bearingDiff(90, 0), 90);
+    });
+
+    it('returns 180 for opposite bearings', () => {
+      assert.strictEqual(bearingDiff(0, 180), 180);
+    });
+
+    it('wraps around 360', () => {
+      assert.strictEqual(bearingDiff(350, 10), 20);
+      assert.strictEqual(bearingDiff(10, 350), 20);
+    });
+  });
+});

+ 139 - 0
gpx-vis-backend/test/gpx-processor.test.js

@@ -0,0 +1,139 @@
+const assert = require('assert');
+const { parseAndCompress } = require('../src/utils/gpx-processor');
+
+function makeGPX(points, name = 'Test Track') {
+  const trkpts = points.map(p => {
+    const timeTag = p.time ? `<time>${p.time}</time>` : '';
+    const eleTag = p.ele != null ? `<ele>${p.ele}</ele>` : '';
+    return `<trkpt lat="${p.lat}" lon="${p.lon}">${eleTag}${timeTag}</trkpt>`;
+  }).join('\n');
+  return `<?xml version="1.0"?>
+<gpx version="1.1">
+  <trk>
+    <name>${name}</name>
+    <trkseg>
+      ${trkpts}
+    </trkseg>
+  </trk>
+</gpx>`;
+}
+
+describe('GPX processor', () => {
+  describe('parseAndCompress', () => {
+    it('parses a minimal GPX and returns correct structure', async () => {
+      const gpx = makeGPX([
+        { lat: 51.5, lon: -0.1, ele: 10, time: '2024-01-01T10:00:00Z' },
+        { lat: 51.501, lon: -0.1, ele: 11, time: '2024-01-01T10:01:00Z' },
+      ], 'My Track');
+
+      const result = await parseAndCompress(gpx);
+      assert.strictEqual(result.trackName, 'My Track');
+      assert.ok(result.segments.length > 0);
+      assert.ok(result.pointCount >= 2);
+      assert.ok(result.totalDistance > 0);
+    });
+
+    it('sets trackDate from first point timestamp', async () => {
+      const gpx = makeGPX([
+        { lat: 51.5, lon: -0.1, time: '2024-06-15T08:30:00Z' },
+        { lat: 51.51, lon: -0.1, time: '2024-06-15T09:00:00Z' },
+      ]);
+      const result = await parseAndCompress(gpx);
+      assert.ok(result.trackDate instanceof Date);
+      assert.strictEqual(result.trackDate.getFullYear(), 2024);
+      assert.strictEqual(result.trackDate.getMonth(), 5); // June = 5 (0-indexed)
+    });
+
+    it('skips points closer than 2 m', async () => {
+      // Points almost on top of each other (< 2m apart)
+      const gpx = makeGPX([
+        { lat: 51.5000000, lon: -0.1, time: '2024-01-01T10:00:00Z' },
+        { lat: 51.5000001, lon: -0.1, time: '2024-01-01T10:00:30Z' }, // ~1 cm away
+        { lat: 51.5000002, lon: -0.1, time: '2024-01-01T10:01:00Z' }, // still < 2m from first
+        { lat: 51.5010000, lon: -0.1, time: '2024-01-01T10:01:30Z' }, // ~111 m away
+      ]);
+      const result = await parseAndCompress(gpx);
+      // Should keep first, skip tiny points, keep last far point
+      assert.ok(result.pointCount < 4, `Expected fewer than 4 points, got ${result.pointCount}`);
+      assert.ok(result.pointCount >= 2);
+    });
+
+    it('skips points with < 30s gap (no sharp turn)', async () => {
+      // Points 10s apart in a straight line - should be skipped
+      const gpx = makeGPX([
+        { lat: 51.500, lon: -0.100, time: '2024-01-01T10:00:00Z' },
+        { lat: 51.501, lon: -0.100, time: '2024-01-01T10:00:10Z' }, // 10s gap, straight line
+        { lat: 51.502, lon: -0.100, time: '2024-01-01T10:00:20Z' }, // 10s gap, straight line
+        { lat: 51.503, lon: -0.100, time: '2024-01-01T10:01:00Z' }, // 40s gap, should keep
+      ]);
+      const result = await parseAndCompress(gpx);
+      // Middle points should be mostly skipped due to < 30s and no sharp turn
+      assert.ok(result.pointCount <= 3, `Expected ≤3 points, got ${result.pointCount}`);
+    });
+
+    it('keeps points with < 30s gap if sharp turn', async () => {
+      // U-turn: going north then suddenly south
+      const gpx = makeGPX([
+        { lat: 51.500, lon: -0.100, time: '2024-01-01T10:00:00Z' },
+        { lat: 51.510, lon: -0.100, time: '2024-01-01T10:00:10Z' }, // 10s, going north
+        { lat: 51.500, lon: -0.100, time: '2024-01-01T10:00:20Z' }, // 10s, sharp turn back south
+        { lat: 51.490, lon: -0.100, time: '2024-01-01T10:01:00Z' }, // continuing south
+      ]);
+      const result = await parseAndCompress(gpx);
+      // The middle point (apex of U-turn) should be kept
+      assert.ok(result.pointCount >= 3, `Expected ≥3 points (turn apex kept), got ${result.pointCount}`);
+    });
+
+    it('keeps points at >= 30s intervals regardless of distance', async () => {
+      const gpx = makeGPX([
+        { lat: 51.500, lon: -0.100, time: '2024-01-01T10:00:00Z' },
+        { lat: 51.501, lon: -0.100, time: '2024-01-01T10:00:30Z' }, // exactly 30s
+        { lat: 51.502, lon: -0.100, time: '2024-01-01T10:01:00Z' }, // 30s later
+      ]);
+      const result = await parseAndCompress(gpx);
+      assert.strictEqual(result.pointCount, 3);
+    });
+
+    it('handles GPX with no timestamps', async () => {
+      const gpx = makeGPX([
+        { lat: 51.500, lon: -0.100 },
+        { lat: 51.510, lon: -0.100 },
+        { lat: 51.520, lon: -0.100 },
+      ]);
+      const result = await parseAndCompress(gpx);
+      assert.ok(result.pointCount >= 2);
+      assert.strictEqual(result.trackDate, null);
+    });
+
+    it('calculates total distance correctly', async () => {
+      // Two points ~111m apart (0.001 degree lat)
+      const gpx = makeGPX([
+        { lat: 51.5000, lon: 0, time: '2024-01-01T10:00:00Z' },
+        { lat: 51.5010, lon: 0, time: '2024-01-01T10:01:00Z' },
+      ]);
+      const result = await parseAndCompress(gpx);
+      assert.ok(result.totalDistance > 100 && result.totalDistance < 120,
+        `Expected ~111 m, got ${result.totalDistance.toFixed(1)} m`);
+    });
+
+    it('handles multiple segments', async () => {
+      const xml = `<?xml version="1.0"?>
+<gpx version="1.1">
+  <trk>
+    <name>Multi Segment</name>
+    <trkseg>
+      <trkpt lat="51.500" lon="0.000"><time>2024-01-01T10:00:00Z</time></trkpt>
+      <trkpt lat="51.510" lon="0.000"><time>2024-01-01T10:01:00Z</time></trkpt>
+    </trkseg>
+    <trkseg>
+      <trkpt lat="52.000" lon="0.000"><time>2024-01-01T11:00:00Z</time></trkpt>
+      <trkpt lat="52.010" lon="0.000"><time>2024-01-01T11:01:00Z</time></trkpt>
+    </trkseg>
+  </trk>
+</gpx>`;
+      const result = await parseAndCompress(xml);
+      assert.strictEqual(result.segments.length, 2);
+      assert.ok(result.pointCount >= 4);
+    });
+  });
+});

+ 26 - 0
gpx-vis-backend/test/setup.js

@@ -0,0 +1,26 @@
+/**
+ * Test setup: uses an in-memory SQLite database and a fixed JWT secret.
+ * All test files require this module first via mocha --require.
+ */
+
+// Override the config module before anything else loads it
+const Module = require('module');
+const originalLoad = Module._load;
+
+const testConfig = {
+  port: 3099,
+  database: { type: 'sqlite', storage: ':memory:' },
+  jwt: { secret: 'test-secret', expiresIn: '1h' },
+  upload: { maxFileSizeMB: 10, tempDir: './temp' },
+  cors: { origin: '*' },
+};
+
+Module._load = function (request, parent, isMain) {
+  if (request === '../config' || request === '../../config') {
+    return testConfig;
+  }
+  return originalLoad.apply(this, arguments);
+};
+
+// Also expose the config globally for test files
+global.testConfig = testConfig;

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov