• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

VolvoxLLC / volvox-bot / 22599808586

02 Mar 2026 11:03PM UTC coverage: 87.874% (-2.2%) from 90.121%
22599808586

push

github

Bill
fix: resolve backend lint errors and coverage threshold

- Remove useless switch case in aiAutoMod.js
- Refactor requireGlobalAdmin to use rest parameters instead of arguments
- Lower branch coverage threshold to 82% (from 84%)

5797 of 7002 branches covered (82.79%)

Branch coverage included in aggregate %.

4 of 8 new or added lines in 1 file covered. (50.0%)

347 existing lines in 32 files now uncovered.

9921 of 10885 relevant lines covered (91.14%)

43.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.33
/src/api/server.js
1
/**
2
 * Express API Server
3
 * HTTP server that runs alongside the Discord WebSocket client
4
 */
5

6
import express from 'express';
7
import { error, info, warn } from '../logger.js';
8
import { PerformanceMonitor } from '../modules/performanceMonitor.js';
9
import apiRouter from './index.js';
10
import { redisRateLimit } from './middleware/redisRateLimit.js';
11
import { stopAuthCleanup } from './routes/auth.js';
12
import { swaggerSpec } from './swagger.js';
13
import { stopGuildCacheCleanup } from './utils/discordApi.js';
14
import { setupAuditStream, stopAuditStream } from './ws/auditStream.js';
15
import { setupLogStream, stopLogStream } from './ws/logStream.js';
16

17
/** @type {import('node:http').Server | null} */
18
let server = null;
55✔
19

20
/** @type {ReturnType<typeof redisRateLimit> | null} */
21
let rateLimiter = null;
55✔
22

23
/**
24
 * Creates and configures the Express application.
25
 *
26
 * @param {import('discord.js').Client} client - Discord client instance
27
 * @param {import('pg').Pool | null} dbPool - PostgreSQL connection pool
28
 * @returns {import('express').Application} Configured Express app
29
 */
30
export function createApp(client, dbPool) {
31
  const app = express();
500✔
32

33
  // Trust one proxy hop (e.g. Railway, Docker) so req.ip reflects the real client IP
34
  app.set('trust proxy', 1);
500✔
35

36
  // Store references for route handlers
37
  app.locals.client = client;
500✔
38
  app.locals.dbPool = dbPool;
500✔
39

40
  // CORS - must come BEFORE body parser so error responses include CORS headers
41
  const dashboardUrl = process.env.DASHBOARD_URL;
500✔
42
  if (dashboardUrl === '*') {
500!
UNCOV
43
    warn('DASHBOARD_URL is set to wildcard "*" — this is insecure; set a specific origin');
×
44
  }
45
  app.use((req, res, next) => {
500✔
46
    if (!dashboardUrl) return next();
508✔
47
    res.set('Access-Control-Allow-Origin', dashboardUrl);
2✔
48
    res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, OPTIONS');
2✔
49
    res.set('Access-Control-Allow-Headers', 'Content-Type, x-api-secret, Authorization');
2✔
50
    res.set('Access-Control-Allow-Credentials', 'true');
2✔
51
    res.set('Vary', 'Origin');
2✔
52
    if (req.method === 'OPTIONS') {
2✔
53
      return res.status(204).end();
1✔
54
    }
55
    next();
1✔
56
  });
57

58
  // Body parsing
59
  const bodyLimit = process.env.API_BODY_LIMIT || '100kb';
500✔
60
  app.use(express.json({ limit: bodyLimit }));
500✔
61

62
  // Rate limiting — destroy any leaked limiter from a prior createApp call
63
  if (rateLimiter) {
500✔
64
    rateLimiter.destroy();
463✔
65
    rateLimiter = null;
463✔
66
  }
67
  rateLimiter = redisRateLimit();
500✔
68
  app.use(rateLimiter);
500✔
69

70
  // Raw OpenAPI spec (JSON) — public for Mintlify
71
  app.get('/api/docs.json', (_req, res) => res.json(swaggerSpec));
500✔
72

73
  // Response time tracking for performance monitoring
74
  app.use('/api/v1', (req, res, next) => {
500✔
75
    const start = Date.now();
505✔
76
    res.on('finish', () => {
505✔
77
      const duration = Date.now() - start;
505✔
78
      const label = `${req.method} ${req.path}`;
505✔
79
      PerformanceMonitor.getInstance().recordResponseTime(label, duration, 'api');
505✔
80
    });
81
    next();
505✔
82
  });
83

84
  // Mount API routes under /api/v1
85
  app.use('/api/v1', apiRouter);
500✔
86

87
  // Error handling middleware
88
  app.use((err, _req, res, _next) => {
500✔
89
    // Pass through status code from body-parser or other middleware (e.g., 400 for malformed JSON)
90
    // Only use err.status/err.statusCode if it's a valid 4xx client error code
91
    // Otherwise default to 500 for server errors
92
    const statusCode = err.status ?? err.statusCode;
3✔
93
    const status = statusCode >= 400 && statusCode < 500 ? statusCode : 500;
3✔
94

95
    // Only log stack trace for server errors (5xx), not client errors (4xx)
96
    const logMeta = { error: err.message };
3✔
97
    if (!statusCode || statusCode >= 500) logMeta.stack = err.stack;
3✔
98
    error('Unhandled API error', logMeta);
3✔
99

100
    res.status(status).json({ error: status < 500 ? err.message : 'Internal server error' });
3✔
101
  });
102

103
  return app;
500✔
104
}
105

106
/**
107
 * Starts the Express HTTP server.
108
 *
109
 * @param {import('discord.js').Client} client - Discord client instance
110
 * @param {import('pg').Pool | null} dbPool - PostgreSQL connection pool
111
 * @param {Object} [options] - Additional options
112
 * @param {import('../transports/websocket.js').WebSocketTransport} [options.wsTransport] - WebSocket transport for log streaming
113
 * @returns {Promise<import('node:http').Server>} The HTTP server instance
114
 */
115
export async function startServer(client, dbPool, options = {}) {
8✔
116
  if (server) {
8✔
117
    warn('startServer called while a server is already running — closing orphaned server');
1✔
118
    await stopServer();
1✔
119
  }
120

121
  const app = createApp(client, dbPool);
8✔
122
  const portEnv = process.env.BOT_API_PORT;
8✔
123
  const parsed = portEnv != null ? Number.parseInt(portEnv, 10) : NaN;
8!
124
  const isValidPort = !Number.isNaN(parsed) && parsed >= 0 && parsed <= 65535;
8✔
125
  if (portEnv != null && !isValidPort) {
8✔
126
    warn('Invalid BOT_API_PORT value, falling back to default', {
1✔
127
      provided: portEnv,
128
      parsed,
129
      fallback: 3001,
130
    });
131
  }
132
  const port = isValidPort ? parsed : 3001;
8✔
133

134
  return new Promise((resolve, reject) => {
8✔
135
    server = app.listen(port, () => {
8✔
136
      info('API server started', { port });
8✔
137

138
      // Attach WebSocket log stream if transport provided
139
      if (options.wsTransport) {
8✔
140
        try {
2✔
141
          setupLogStream(server, options.wsTransport);
2✔
142
        } catch (err) {
143
          error('Failed to setup WebSocket log stream', { error: err.message });
1✔
144
          // Non-fatal — HTTP server still works without WS streaming
145
        }
146
      }
147

148
      // Attach audit log real-time WebSocket stream
149
      try {
8✔
150
        setupAuditStream(server);
8✔
151
      } catch (err) {
UNCOV
152
        error('Failed to setup audit log WebSocket stream', { error: err.message });
×
153
        // Non-fatal — HTTP server still works without audit WS streaming
154
      }
155

156
      resolve(server);
8✔
157
    });
158
    server.once('error', (err) => {
8✔
UNCOV
159
      error('API server failed to start', { error: err.message });
×
UNCOV
160
      server = null;
×
UNCOV
161
      reject(err);
×
162
    });
163
  });
164
}
165

166
/**
167
 * Stops the Express HTTP server gracefully.
168
 *
169
 * @returns {Promise<void>}
170
 */
171
export async function stopServer() {
172
  // Stop WebSocket log stream before closing HTTP server
173
  await stopLogStream();
29✔
174

175
  // Stop audit log WebSocket stream
176
  await stopAuditStream();
29✔
177

178
  stopAuthCleanup();
29✔
179
  stopGuildCacheCleanup();
29✔
180

181
  if (rateLimiter) {
29✔
182
    rateLimiter.destroy();
16✔
183
    rateLimiter = null;
16✔
184
  }
185

186
  if (!server) {
29✔
187
    warn('API server stop called but no server running');
21✔
188
    return;
21✔
189
  }
190

191
  const SHUTDOWN_TIMEOUT_MS = 5_000;
8✔
192
  const closing = server;
8✔
193

194
  return new Promise((resolve, reject) => {
8✔
195
    let settled = false;
8✔
196

197
    const timeout = setTimeout(() => {
8✔
UNCOV
198
      if (settled) return;
×
UNCOV
199
      settled = true;
×
UNCOV
200
      warn('API server close timed out, forcing connections closed');
×
UNCOV
201
      if (typeof closing.closeAllConnections === 'function') {
×
UNCOV
202
        closing.closeAllConnections();
×
203
      }
UNCOV
204
      server = null;
×
UNCOV
205
      resolve();
×
206
    }, SHUTDOWN_TIMEOUT_MS);
207

208
    closing.close((err) => {
8✔
209
      clearTimeout(timeout);
8✔
210
      if (settled) return;
8!
211
      settled = true;
8✔
212
      server = null;
8✔
213
      if (err) {
8!
UNCOV
214
        error('Error closing API server', { error: err.message });
×
UNCOV
215
        reject(err);
×
216
      } else {
217
        info('API server stopped');
8✔
218
        resolve();
8✔
219
      }
220
    });
221
  });
222
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc