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

VolvoxLLC / volvox-bot / 22536363373

01 Mar 2026 04:59AM UTC coverage: 90.104% (-0.2%) from 90.276%
22536363373

push

github

BillChirico
fix: resolve CI failures on main (lint, coverage, secret scanning)

- Migrate biome.json schema to 2.4.4 and fix formatting/lint violations
- Replace isNaN with Number.isNaN in db.js
- Remove unused getConfig import in ticket tests
- Fix import ordering in scheduler and reload tests
- Fix noArrayIndexKey lint in config-editor.tsx
- Add welcome command and scheduler tick tests to meet 85% branch threshold
- Allowlist historical false positive commit in .gitleaks.toml

4581 of 5384 branches covered (85.09%)

Branch coverage included in aggregate %.

4 of 4 new or added lines in 2 files covered. (100.0%)

106 existing lines in 14 files now uncovered.

7856 of 8419 relevant lines covered (93.31%)

49.06 hits per line

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

82.01
/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 apiRouter from './index.js';
9
import { rateLimit } from './middleware/rateLimit.js';
10
import { stopAuthCleanup } from './routes/auth.js';
11
import { swaggerSpec } from './swagger.js';
12
import { stopGuildCacheCleanup } from './utils/discordApi.js';
13
import { setupLogStream, stopLogStream } from './ws/logStream.js';
14

15
/** @type {import('node:http').Server | null} */
16
let server = null;
50✔
17

18
/** @type {ReturnType<typeof rateLimit> | null} */
19
let rateLimiter = null;
50✔
20

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

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

34
  // Store references for route handlers
35
  app.locals.client = client;
392✔
36
  app.locals.dbPool = dbPool;
392✔
37

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

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

60
  // Rate limiting — destroy any leaked limiter from a prior createApp call
61
  if (rateLimiter) {
392✔
62
    rateLimiter.destroy();
360✔
63
    rateLimiter = null;
360✔
64
  }
65
  rateLimiter = rateLimit();
392✔
66
  app.use(rateLimiter);
392✔
67

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

71
  // Mount API routes under /api/v1
72
  app.use('/api/v1', apiRouter);
392✔
73

74
  // Error handling middleware
75
  app.use((err, _req, res, _next) => {
392✔
76
    // Pass through status code from body-parser or other middleware (e.g., 400 for malformed JSON)
77
    // Only use err.status/err.statusCode if it's a valid 4xx client error code
78
    // Otherwise default to 500 for server errors
79
    const statusCode = err.status ?? err.statusCode;
1!
80
    const status = statusCode >= 400 && statusCode < 500 ? statusCode : 500;
1!
81

82
    // Only log stack trace for server errors (5xx), not client errors (4xx)
83
    const logMeta = { error: err.message };
1✔
84
    if (!statusCode || statusCode >= 500) logMeta.stack = err.stack;
1!
85
    error('Unhandled API error', logMeta);
1✔
86

87
    res.status(status).json({ error: status < 500 ? err.message : 'Internal server error' });
1!
88
  });
89

90
  return app;
392✔
91
}
92

93
/**
94
 * Starts the Express HTTP server.
95
 *
96
 * @param {import('discord.js').Client} client - Discord client instance
97
 * @param {import('pg').Pool | null} dbPool - PostgreSQL connection pool
98
 * @param {Object} [options] - Additional options
99
 * @param {import('../transports/websocket.js').WebSocketTransport} [options.wsTransport] - WebSocket transport for log streaming
100
 * @returns {Promise<import('node:http').Server>} The HTTP server instance
101
 */
102
export async function startServer(client, dbPool, options = {}) {
8✔
103
  if (server) {
8✔
104
    warn('startServer called while a server is already running — closing orphaned server');
1✔
105
    await stopServer();
1✔
106
  }
107

108
  const app = createApp(client, dbPool);
8✔
109
  const portEnv = process.env.BOT_API_PORT;
8✔
110
  const parsed = portEnv != null ? Number.parseInt(portEnv, 10) : NaN;
8!
111
  const isValidPort = !Number.isNaN(parsed) && parsed >= 0 && parsed <= 65535;
8✔
112
  if (portEnv != null && !isValidPort) {
8✔
113
    warn('Invalid BOT_API_PORT value, falling back to default', {
1✔
114
      provided: portEnv,
115
      parsed,
116
      fallback: 3001,
117
    });
118
  }
119
  const port = isValidPort ? parsed : 3001;
8✔
120

121
  return new Promise((resolve, reject) => {
8✔
122
    server = app.listen(port, () => {
8✔
123
      info('API server started', { port });
8✔
124

125
      // Attach WebSocket log stream if transport provided
126
      if (options.wsTransport) {
8✔
127
        try {
2✔
128
          setupLogStream(server, options.wsTransport);
2✔
129
        } catch (err) {
130
          error('Failed to setup WebSocket log stream', { error: err.message });
1✔
131
          // Non-fatal — HTTP server still works without WS streaming
132
        }
133
      }
134

135
      resolve(server);
8✔
136
    });
137
    server.once('error', (err) => {
8✔
UNCOV
138
      error('API server failed to start', { error: err.message });
×
UNCOV
139
      server = null;
×
UNCOV
140
      reject(err);
×
141
    });
142
  });
143
}
144

145
/**
146
 * Stops the Express HTTP server gracefully.
147
 *
148
 * @returns {Promise<void>}
149
 */
150
export async function stopServer() {
151
  // Stop WebSocket log stream before closing HTTP server
152
  await stopLogStream();
29✔
153

154
  stopAuthCleanup();
29✔
155
  stopGuildCacheCleanup();
29✔
156

157
  if (rateLimiter) {
29✔
158
    rateLimiter.destroy();
16✔
159
    rateLimiter = null;
16✔
160
  }
161

162
  if (!server) {
29✔
163
    warn('API server stop called but no server running');
21✔
164
    return;
21✔
165
  }
166

167
  const SHUTDOWN_TIMEOUT_MS = 5_000;
8✔
168
  const closing = server;
8✔
169

170
  return new Promise((resolve, reject) => {
8✔
171
    let settled = false;
8✔
172

173
    const timeout = setTimeout(() => {
8✔
174
      if (settled) return;
×
UNCOV
175
      settled = true;
×
176
      warn('API server close timed out, forcing connections closed');
×
177
      if (typeof closing.closeAllConnections === 'function') {
×
UNCOV
178
        closing.closeAllConnections();
×
179
      }
UNCOV
180
      server = null;
×
UNCOV
181
      resolve();
×
182
    }, SHUTDOWN_TIMEOUT_MS);
183

184
    closing.close((err) => {
8✔
185
      clearTimeout(timeout);
8✔
186
      if (settled) return;
8!
187
      settled = true;
8✔
188
      server = null;
8✔
189
      if (err) {
8!
UNCOV
190
        error('Error closing API server', { error: err.message });
×
UNCOV
191
        reject(err);
×
192
      } else {
193
        info('API server stopped');
8✔
194
        resolve();
8✔
195
      }
196
    });
197
  });
198
}
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