Skip to main content

max / makenotwork

22.8 KB · 714 lines History Blame Raw
1 //! Centralized application constants
2 //!
3 //! Magic numbers that were scattered across modules. Storage-specific limits
4 //! (file sizes, presign expiry, allowed types) remain in storage.rs since
5 //! they're only used there.
6
7 // -- Database --
8 pub const DB_POOL_MAX_CONNECTIONS: u32 = 25;
9 pub const DB_POOL_MIN_CONNECTIONS: u32 = 2;
10 pub const DB_ACQUIRE_TIMEOUT_SECS: u64 = 3;
11 /// Rotate connections after 30 minutes to prevent stale sessions.
12 pub const DB_MAX_LIFETIME_SECS: u64 = 1800;
13 /// Prune idle connections after 10 minutes.
14 pub const DB_IDLE_TIMEOUT_SECS: u64 = 600;
15
16 // -- Sessions --
17 pub const SESSION_EXPIRY_DAYS: i64 = 7;
18 /// Skip DB touch if validated within this window. Doubles as the upper bound on
19 /// session-revocation lag (admin suspend, logout-everywhere, password change) —
20 /// shorter = tighter revocation, slightly more DB load on the auth hot path.
21 pub const SESSION_TOUCH_CACHE_SECS: u64 = 5;
22
23 // -- Login security --
24 pub const MAX_LOGIN_ATTEMPTS: i32 = 5;
25 pub const LOCKOUT_MINUTES: i64 = 15;
26 /// How long a half-completed login (password verified, awaiting 2FA) stays
27 /// valid before the user must re-enter their password. Defends against the
28 /// "unattended browser one TOTP from logged in" failure mode.
29 pub const PENDING_2FA_TTL_SECS: i64 = 600;
30
31 // -- Email link expiry (seconds) --
32 pub const PASSWORD_RESET_EXPIRY_SECS: i64 = 900; // 15 minutes
33 pub const EMAIL_VERIFICATION_EXPIRY_SECS: i64 = 86400; // 24 hours
34 pub const ACCOUNT_DELETION_EXPIRY_SECS: i64 = 3600; // 1 hour
35
36 // -- Stripe fees (for display only — actual fees set by Stripe) --
37 pub const STRIPE_FEE_PERCENTAGE: f64 = 0.029; // 2.9%
38 pub const STRIPE_FEE_FIXED_CENTS: f64 = 30.0; // $0.30
39 /// Stripe minimum charge amount in cents (USD). Charges below this are rejected.
40 pub const STRIPE_MINIMUM_CHARGE_CENTS: i64 = 50; // $0.50
41
42 // -- Page / query limits --
43 pub const DASHBOARD_TRANSACTION_LIMIT: i64 = 100;
44
45 // -- SyncKit --
46 pub const SYNCKIT_JWT_EXPIRY_SECS: i64 = 7 * 24 * 3600; // 7 days
47 pub const SYNCKIT_PUSH_MAX_CHANGES: usize = 500;
48 pub const SYNCKIT_PULL_PAGE_SIZE: i64 = 500;
49 pub const SYNCKIT_API_KEY_LENGTH: usize = 32; // 32 bytes = 64 hex chars
50 pub const SYNC_LOG_RETAIN_DAYS: i64 = 90;
51 pub const SYNC_LOG_COMPACT_MIN_AGE_DAYS: i64 = 7; // Safety margin for cursor-based compaction
52 pub const SYNCKIT_MAX_BLOB_SIZE_BYTES: i64 = 500 * 1024 * 1024; // 500 MB
53 pub const SYNCKIT_MAX_BLOB_STORAGE_BYTES: i64 = 10 * 1024 * 1024 * 1024; // 10 GB per user per app
54 pub const SYNCKIT_MAX_DEVICES_PER_APP: i64 = 50; // Max devices per user per app
55 pub const SYNCKIT_BLOB_PRESIGN_EXPIRY_SECS: u64 = 3600; // 1 hour
56 pub const SYNCKIT_MAX_SSE_CONNECTIONS_PER_USER: usize = 10;
57 pub const SYNCKIT_ROTATION_STALE_HOURS: i64 = 24;
58 pub const SYNCKIT_ROTATION_BATCH_MAX: usize = 500;
59
60 // -- Subscriptions --
61 pub const MIN_SUBSCRIPTION_PRICE_CENTS: i32 = 100; // $1.00 minimum
62
63 // -- OAuth --
64 pub const OAUTH_CODE_EXPIRY_SECS: i64 = 600; // 10 minutes
65 pub const OAUTH_CODE_LENGTH: usize = 32; // 32 bytes = 64 hex chars
66
67 // -- Health monitoring --
68 pub const HEALTH_CHECK_INTERVAL_SECS: u64 = 60;
69 pub const ALERT_COOLDOWN_SECS: u64 = 300; // 5 minutes
70 pub const HEALTH_HISTORY_RETAIN_DAYS: i64 = 90;
71
72 // -- Scheduled publish --
73 pub const SCHEDULER_INTERVAL_SECS: u64 = 60;
74
75 // -- TOTP / 2FA --
76 pub const TOTP_SKEW: u8 = 1; // Allow +/-1 time step (+/-30s)
77 pub const TOTP_STEP: u64 = 30; // 30-second windows
78 pub const TOTP_DIGITS: usize = 6; // 6-digit codes
79 pub const BACKUP_CODE_COUNT: usize = 10; // Generate 10 codes
80 pub const BACKUP_CODE_LENGTH: usize = 8; // 8 alphanumeric chars
81
82 // -- Anti-enumeration --
83 pub const USERNAME_CHECK_DELAY_MS: u64 = 400;
84
85 // -- Rate limiting --
86 // Auth endpoints (login, join): burst 5, then 2/sec.
87 // fast-tests: relaxed to burst 20 so lockout tests can fire 5+ attempts without hitting rate limiter.
88 #[cfg(not(feature = "fast-tests"))]
89 pub const AUTH_RATE_LIMIT_MS: u64 = 500;
90 #[cfg(not(feature = "fast-tests"))]
91 pub const AUTH_RATE_LIMIT_BURST: u32 = 5;
92 #[cfg(feature = "fast-tests")]
93 pub const AUTH_RATE_LIMIT_MS: u64 = 10;
94 #[cfg(feature = "fast-tests")]
95 pub const AUTH_RATE_LIMIT_BURST: u32 = 20;
96 // Username validation: burst 10, then 1/sec
97 pub const VALIDATE_RATE_LIMIT_PER_SEC: u64 = 1;
98 pub const VALIDATE_RATE_LIMIT_BURST: u32 = 10;
99 // API write endpoints (CRUD): burst 30, then 2/sec
100 pub const API_WRITE_RATE_LIMIT_MS: u64 = 500;
101 pub const API_WRITE_RATE_LIMIT_BURST: u32 = 30;
102 // API read endpoints (GET): burst 60, then 10/sec (prevents enumeration)
103 pub const API_READ_RATE_LIMIT_MS: u64 = 100;
104 pub const API_READ_RATE_LIMIT_BURST: u32 = 60;
105 // API export endpoints: burst 3, then 1/sec
106 pub const API_EXPORT_RATE_LIMIT_PER_SEC: u64 = 1;
107 pub const API_EXPORT_RATE_LIMIT_BURST: u32 = 3;
108 // Guest checkout (public, no auth): burst 10, then 1/sec
109 pub const GUEST_CHECKOUT_RATE_LIMIT_PER_SEC: u64 = 1;
110 pub const GUEST_CHECKOUT_RATE_LIMIT_BURST: u32 = 10;
111 // License key validation (public): burst 20, then 5/sec
112 pub const LICENSE_KEY_RATE_LIMIT_MS: u64 = 200;
113 pub const LICENSE_KEY_RATE_LIMIT_BURST: u32 = 20;
114 // File upload: burst 10, then 2/sec
115 pub const UPLOAD_RATE_LIMIT_MS: u64 = 500;
116 pub const UPLOAD_RATE_LIMIT_BURST: u32 = 10;
117 // OAuth authorize/token: burst 5/10, then 2/sec
118 pub const OAUTH_RATE_LIMIT_MS: u64 = 500;
119 pub const OAUTH_RATE_LIMIT_BURST: u32 = 5;
120 pub const OAUTH_TOKEN_RATE_LIMIT_MS: u64 = 500;
121 pub const OAUTH_TOKEN_RATE_LIMIT_BURST: u32 = 10;
122 // SyncKit auth: burst 5, then 1/sec
123 pub const SYNCKIT_AUTH_RATE_LIMIT_PER_SEC: u64 = 1;
124 pub const SYNCKIT_AUTH_RATE_LIMIT_BURST: u32 = 5;
125 // SyncKit sync (push/pull) — per-IP: burst 30, then 10/sec
126 pub const SYNCKIT_SYNC_RATE_LIMIT_MS: u64 = 100;
127 pub const SYNCKIT_SYNC_RATE_LIMIT_BURST: u32 = 30;
128 // SyncKit sync — per-app: burst 60, then 20/sec (higher than per-IP because
129 // a single app may have many users behind different IPs)
130 pub const SYNCKIT_APP_RATE_LIMIT_MS: u64 = 50;
131 pub const SYNCKIT_APP_RATE_LIMIT_BURST: u32 = 60;
132 // 2FA verification: burst 5, then 2/sec (same as auth)
133 pub const TWO_FACTOR_RATE_LIMIT_MS: u64 = 500;
134 pub const TWO_FACTOR_RATE_LIMIT_BURST: u32 = 5;
135
136 // Dashboard tab reads: generous but bounded (5/sec, burst 20)
137 pub const DASHBOARD_READ_RATE_LIMIT_MS: u64 = 200;
138 pub const DASHBOARD_READ_RATE_LIMIT_BURST: u32 = 20;
139
140 // -- Pagination --
141 pub const DISCOVER_PAGE_SIZE: u32 = 25;
142 pub const FEED_PAGE_SIZE: u32 = 25;
143 pub const PAGINATION_WINDOW_SIZE: u32 = 5;
144
145 // -- Creator broadcast fan-out --
146 /// Max concurrent in-flight email sends per broadcast. The outer worker
147 /// task spawns up to this many child tasks, then waits on one to drain
148 /// before spawning the next.
149 pub const BROADCAST_PARALLELISM: usize = 16;
150 /// Delay between successive broadcast send-task spawns. Spreads Postmark
151 /// API load when a creator with thousands of followers fires a broadcast
152 /// — at parallelism 16 + 100 ms cadence, steady-state is ~10 sends/sec.
153 pub const BROADCAST_CHUNK_DELAY_MS: u64 = 100;
154 /// Recipient cap per broadcast send. Above this, the request is refused
155 /// with an instruction to contact support. Bounds Postmark spend exposure
156 /// from any single approved creator. Founder-window cohort is well under
157 /// this; the cap is the floor we'd lift on request, not the ceiling.
158 pub const BROADCAST_MAX_RECIPIENTS: usize = 10_000;
159
160 /// Cap on buyer-departure notification fan-out per creator-deletion event.
161 /// Account deletion notifies historical buyers about content removal. A
162 /// creator with millions of completed sales should not turn one deletion
163 /// into a Postmark bomb. The cap bounds both the in-memory buyer list and
164 /// total outbound email volume; if hit, we log a warning and notify the
165 /// oldest-buyers slice the SQL chose.
166 pub const BUYER_DEPARTURE_MAX_NOTIFICATIONS: i64 = 50_000;
167
168 // -- File scanning --
169 pub const SCAN_MAX_MEMORY_BYTES: usize = 100 * 1024 * 1024; // 100 MB in-memory threshold
170 pub const SCAN_MAX_CONCURRENT: usize = 4; // Max concurrent file scans (each can use up to 100 MB RAM)
171 pub const SCAN_WORKER_COUNT: usize = 2; // Background worker tasks draining scan_jobs queue
172 /// Retention window for terminal-state (`done`, `failed`) `scan_jobs` rows.
173 /// Queued/running rows are operational queue state and not affected.
174 pub const SCAN_JOB_RETENTION_DAYS: u32 = 30;
175 /// Directory under which the scanner spools large objects to tempfiles
176 /// before invoking path/stream-based layer entries. On production, systemd
177 /// provisions this via `StateDirectory=mnw/scan-spool` so the path resolves
178 /// to `/var/lib/mnw/scan-spool`. Override with `MNW_SCAN_SPOOL_DIR` for dev.
179 pub const SCAN_SPOOL_DIR: &str = "/var/lib/makenotwork/scan-spool";
180 /// Files in `SCAN_SPOOL_DIR` older than this are considered orphaned
181 /// (a panic, OOM, or hard kill left them behind) and reaped on the
182 /// next sweep. RAII drop in `SpoolHandle` covers the live path; this
183 /// covers process-death.
184 pub const SCAN_SPOOL_ORPHAN_AGE_SECS: u64 = 3600;
185 /// Hard cap on a single spooled object. Above this, the scanner refuses
186 /// the job rather than risk filling the volume. 8 GiB matches the largest
187 /// payload the upload tier currently allows.
188 pub const SCAN_SPOOL_MAX_BYTES: u64 = 8 * 1024 * 1024 * 1024;
189 /// Minimum free space the spool volume must retain after writing the
190 /// pending object. The scanner refuses if `statvfs(free) - expected_size`
191 /// would drop below this threshold.
192 pub const SCAN_SPOOL_FREE_RESERVE_BYTES: u64 = 2 * 1024 * 1024 * 1024;
193
194 // -- Caddy on-demand TLS --
195 // Caps concurrent cache-miss DB lookups in `/api/domains/caddy-ask`. Cache hits
196 // are unbounded (DashMap). At capacity, the handler returns 503 so Caddy retries
197 // later instead of stampeding the DB pool or driving ACME issuance for garbage
198 // domains. Sized small because the slow path is one indexed lookup.
199 pub const CADDY_ASK_MAX_CONCURRENT: usize = 8;
200 pub const SCAN_ZIP_MAX_RATIO: f64 = 100.0; // Max compression ratio before ZIP bomb
201 pub const SCAN_ZIP_MAX_DEPTH: u32 = 2; // Max nested archives (detection is 1 level deep; decompressed size limit is the primary defense)
202 pub const SCAN_ZIP_MAX_UNCOMPRESSED: u64 = 2 * 1024 * 1024 * 1024; // 2 GB uncompressed limit
203 pub const SCAN_MALWAREBAZAAR_TIMEOUT_SECS: u64 = 5;
204 pub const SCAN_CLAMAV_TIMEOUT_SECS: u64 = 30;
205
206 // -- Invite system --
207 pub const INVITES_ENABLED: bool = true;
208 pub const INVITE_LIMIT_PER_CREATOR: i64 = 5; // max unredeemed codes per creator
209
210 // -- Git source browser --
211 pub const GIT_MAX_FILE_SIZE_BYTES: usize = 1_024_000; // 1MB display limit
212 pub const GIT_COMMITS_PER_PAGE: usize = 30;
213 pub const GIT_DIFF_MAX_FILES: usize = 20; // Inline diff hunks for first N files
214 pub const GIT_DIFF_MAX_LINES: usize = 500; // Per-file line cap for diff display
215 pub const GIT_REPOS_PER_PAGE: usize = 30;
216 pub const GIT_FILE_LOG_MAX_WALK: usize = 1000; // Max commits to walk for per-file history
217 pub const GIT_RAW_MAX_BYTES: usize = 100 * 1024 * 1024; // 100 MB raw download limit
218 pub const GIT_UPLOAD_PACK_MAX_BYTES: usize = 10 * 1024 * 1024; // 10 MB upload-pack body limit
219
220 // -- Webhook security --
221 pub const WEBHOOK_TIMESTAMP_TOLERANCE_SECS: u64 = 300; // 5 minutes
222
223 // -- Collections --
224 pub const MAX_COLLECTIONS_PER_USER: i64 = 50;
225 pub const MAX_ITEMS_PER_COLLECTION: i64 = 200;
226
227 // -- OTA updates --
228 pub const OTA_PRESIGN_EXPIRY_SECS: u64 = 3600; // 1 hour
229 // OTA management: burst 10, then 2/sec (same as API write)
230 pub const OTA_WRITE_RATE_LIMIT_MS: u64 = 500;
231 pub const OTA_WRITE_RATE_LIMIT_BURST: u32 = 10;
232 // OTA public (updater check, download): burst 30, then 10/sec
233 pub const OTA_READ_RATE_LIMIT_MS: u64 = 100;
234 pub const OTA_READ_RATE_LIMIT_BURST: u32 = 30;
235
236 // -- Build pipeline --
237 pub const BUILD_TIMEOUT_SECS: u64 = 1800; // 30 min
238 pub const BUILD_MAX_LOG_BYTES: usize = 5_242_880; // 5 MB
239 pub const BUILD_HISTORY_LIMIT: i64 = 50;
240 pub const BUILD_TRIGGER_RATE_LIMIT_PER_SEC: u64 = 1;
241 pub const BUILD_TRIGGER_RATE_LIMIT_BURST: u32 = 3;
242 pub const BUILD_WRITE_RATE_LIMIT_MS: u64 = 500;
243 pub const BUILD_WRITE_RATE_LIMIT_BURST: u32 = 10;
244 // Git browsing: burst 30, then 5/sec (blame/log can be expensive)
245 pub const GIT_BROWSE_RATE_LIMIT_MS: u64 = 200;
246 pub const GIT_BROWSE_RATE_LIMIT_BURST: u32 = 30;
247 pub const BUILD_ALLOWED_TARGETS: &[&str] = &[
248 "linux/x86_64",
249 "linux/aarch64",
250 "darwin/x86_64",
251 "darwin/aarch64",
252 ];
253
254 // -- Streaming --
255 pub const STREAMING_CACHE_MAX_SECS: u64 = 86400; // 24 hours max presigned URL lifetime
256 /// Rate limit for stream/download URL requests: 1 per 3 seconds, burst of 10.
257 pub const STREAM_RATE_LIMIT_MS: u64 = 3000;
258 pub const STREAM_RATE_LIMIT_BURST: u32 = 10;
259
260 // -- Date display formats --
261 pub const DATE_FMT_SHORT: &str = "%b %d"; // "Mar 25"
262 pub const DATE_FMT_FULL: &str = "%b %d, %Y"; // "Mar 25, 2026"
263 pub const DATE_FMT_ISO: &str = "%Y-%m-%d"; // "2026-03-25"
264 pub const DATE_FMT_DATETIME: &str = "%b %d, %Y %H:%M"; // "Mar 25, 2026 14:30"
265 pub const DATE_FMT_DATETIME_UTC: &str = "%b %d, %Y %H:%M UTC"; // "Mar 25, 2026 14:30 UTC"
266
267 // -- Platform content --
268 pub const CHANGELOG_PROJECT_SLUG: &str = "changelog";
269
270 // -- String / buffer limits --
271 pub const USER_AGENT_MAX_LENGTH: usize = 512;
272 pub const SYNCKIT_MAX_KEY_ENVELOPE_BYTES: usize = 4096;
273 pub const MAX_PRICE_CENTS: i32 = 1_000_000; // $10,000
274
275 // -- Sandbox accounts --
276 /// How long a sandbox session lasts before auto-cleanup.
277 pub const SANDBOX_EXPIRY_SECS: i64 = 3600; // 1 hour
278 /// How often the cleanup job runs.
279 pub const SANDBOX_CLEANUP_INTERVAL_SECS: u64 = 300; // 5 minutes
280 /// Rate limit: sandbox creation.
281 /// Production: 1 per 30 seconds, burst 2. fast-tests: 1 per 10ms, burst 10.
282 /// Run integration tests with `cargo test --features fast-tests` to avoid rate-limit failures.
283 #[cfg(not(feature = "fast-tests"))]
284 pub const SANDBOX_RATE_LIMIT_MS: u64 = 30_000;
285 #[cfg(not(feature = "fast-tests"))]
286 pub const SANDBOX_RATE_LIMIT_BURST: u32 = 2;
287 #[cfg(feature = "fast-tests")]
288 pub const SANDBOX_RATE_LIMIT_MS: u64 = 10;
289 #[cfg(feature = "fast-tests")]
290 pub const SANDBOX_RATE_LIMIT_BURST: u32 = 10;
291 /// Max concurrent active sandboxes per IP.
292 pub const SANDBOX_MAX_PER_IP: i64 = 3;
293
294 #[cfg(test)]
295 mod tests {
296 use super::*;
297
298 // -- Price constants --
299
300 #[test]
301 fn max_price_cents_is_positive() {
302 assert!(MAX_PRICE_CENTS > 0);
303 }
304
305 #[test]
306 fn max_price_cents_sane_upper_bound() {
307 // Should not exceed $100,000
308 assert!(MAX_PRICE_CENTS <= 10_000_000);
309 }
310
311 #[test]
312 fn min_subscription_price_positive() {
313 assert!(MIN_SUBSCRIPTION_PRICE_CENTS > 0);
314 }
315
316 #[test]
317 fn min_subscription_price_below_max() {
318 assert!(MIN_SUBSCRIPTION_PRICE_CENTS < MAX_PRICE_CENTS);
319 }
320
321 // -- Stripe fee constants --
322
323 #[test]
324 fn stripe_fee_percentage_reasonable() {
325 assert!(STRIPE_FEE_PERCENTAGE > 0.0);
326 assert!(STRIPE_FEE_PERCENTAGE < 0.5); // less than 50%
327 }
328
329 #[test]
330 fn stripe_fee_fixed_positive() {
331 assert!(STRIPE_FEE_FIXED_CENTS > 0.0);
332 }
333
334 // -- Database pool --
335
336 #[test]
337 fn db_pool_max_exceeds_min() {
338 assert!(DB_POOL_MAX_CONNECTIONS > DB_POOL_MIN_CONNECTIONS);
339 }
340
341 #[test]
342 fn db_pool_min_positive() {
343 assert!(DB_POOL_MIN_CONNECTIONS > 0);
344 }
345
346 #[test]
347 fn db_acquire_timeout_positive() {
348 assert!(DB_ACQUIRE_TIMEOUT_SECS > 0);
349 }
350
351 #[test]
352 fn db_max_lifetime_exceeds_idle_timeout() {
353 assert!(DB_MAX_LIFETIME_SECS > DB_IDLE_TIMEOUT_SECS);
354 }
355
356 // -- Session constants --
357
358 #[test]
359 fn session_expiry_positive() {
360 assert!(SESSION_EXPIRY_DAYS > 0);
361 }
362
363 #[test]
364 fn session_expiry_not_absurd() {
365 assert!(SESSION_EXPIRY_DAYS <= 365);
366 }
367
368 #[test]
369 fn session_touch_cache_positive() {
370 assert!(SESSION_TOUCH_CACHE_SECS > 0);
371 }
372
373 #[test]
374 fn session_touch_cache_less_than_one_day() {
375 assert!(SESSION_TOUCH_CACHE_SECS < 86400);
376 }
377
378 // -- Login security --
379
380 #[test]
381 fn max_login_attempts_positive() {
382 assert!(MAX_LOGIN_ATTEMPTS > 0);
383 }
384
385 #[test]
386 fn lockout_minutes_positive() {
387 assert!(LOCKOUT_MINUTES > 0);
388 }
389
390 // -- Email link expiry ordering --
391
392 #[test]
393 fn password_reset_expiry_positive() {
394 assert!(PASSWORD_RESET_EXPIRY_SECS > 0);
395 }
396
397 #[test]
398 fn email_verification_longer_than_password_reset() {
399 assert!(EMAIL_VERIFICATION_EXPIRY_SECS > PASSWORD_RESET_EXPIRY_SECS);
400 }
401
402 #[test]
403 fn account_deletion_expiry_positive() {
404 assert!(ACCOUNT_DELETION_EXPIRY_SECS > 0);
405 }
406
407 // -- Scheduler --
408
409 #[test]
410 fn scheduler_interval_positive() {
411 assert!(SCHEDULER_INTERVAL_SECS > 0);
412 }
413
414 // -- Rate limit bursts all positive --
415
416 #[test]
417 fn auth_rate_limit_burst_positive() {
418 assert!(AUTH_RATE_LIMIT_BURST > 0);
419 }
420
421 #[test]
422 fn validate_rate_limit_burst_positive() {
423 assert!(VALIDATE_RATE_LIMIT_BURST > 0);
424 }
425
426 #[test]
427 fn api_write_rate_limit_burst_positive() {
428 assert!(API_WRITE_RATE_LIMIT_BURST > 0);
429 }
430
431 #[test]
432 fn api_read_rate_limit_burst_positive() {
433 assert!(API_READ_RATE_LIMIT_BURST > 0);
434 }
435
436 #[test]
437 fn api_export_rate_limit_burst_positive() {
438 assert!(API_EXPORT_RATE_LIMIT_BURST > 0);
439 }
440
441 #[test]
442 fn license_key_rate_limit_burst_positive() {
443 assert!(LICENSE_KEY_RATE_LIMIT_BURST > 0);
444 }
445
446 #[test]
447 fn upload_rate_limit_burst_positive() {
448 assert!(UPLOAD_RATE_LIMIT_BURST > 0);
449 }
450
451 #[test]
452 fn oauth_rate_limit_burst_positive() {
453 assert!(OAUTH_RATE_LIMIT_BURST > 0);
454 }
455
456 #[test]
457 fn oauth_token_rate_limit_burst_positive() {
458 assert!(OAUTH_TOKEN_RATE_LIMIT_BURST > 0);
459 }
460
461 // -- Rate limit burst ordering: read > write > auth --
462
463 #[test]
464 fn api_read_burst_exceeds_write_burst() {
465 assert!(API_READ_RATE_LIMIT_BURST > API_WRITE_RATE_LIMIT_BURST);
466 }
467
468 #[test]
469 fn api_write_burst_exceeds_auth_burst() {
470 assert!(API_WRITE_RATE_LIMIT_BURST > AUTH_RATE_LIMIT_BURST);
471 }
472
473 // -- Rate limit intervals positive --
474
475 #[test]
476 fn auth_rate_limit_ms_positive() {
477 assert!(AUTH_RATE_LIMIT_MS > 0);
478 }
479
480 #[test]
481 fn api_write_rate_limit_ms_positive() {
482 assert!(API_WRITE_RATE_LIMIT_MS > 0);
483 }
484
485 #[test]
486 fn api_read_rate_limit_ms_positive() {
487 assert!(API_READ_RATE_LIMIT_MS > 0);
488 }
489
490 // -- File size limits --
491
492 #[test]
493 fn scan_max_memory_positive() {
494 assert!(SCAN_MAX_MEMORY_BYTES > 0);
495 }
496
497 #[test]
498 fn scan_spool_reserve_below_max() {
499 assert!(SCAN_SPOOL_FREE_RESERVE_BYTES < SCAN_SPOOL_MAX_BYTES);
500 }
501
502 #[test]
503 fn scan_spool_max_exceeds_memory_threshold() {
504 assert!(SCAN_SPOOL_MAX_BYTES > SCAN_MAX_MEMORY_BYTES as u64);
505 }
506
507 #[test]
508 fn scan_job_retention_days_safe_floor() {
509 // Guards against an accidental same-day purge that would race the
510 // worker stamping completed_at.
511 assert!(SCAN_JOB_RETENTION_DAYS >= 7);
512 }
513
514 #[test]
515 fn broadcast_parallelism_sane() {
516 assert!(BROADCAST_PARALLELISM > 0 && BROADCAST_PARALLELISM <= 64);
517 }
518
519 #[test]
520 fn scan_zip_max_uncompressed_exceeds_memory_threshold() {
521 assert!(SCAN_ZIP_MAX_UNCOMPRESSED > SCAN_MAX_MEMORY_BYTES as u64);
522 }
523
524 #[test]
525 fn git_raw_max_exceeds_file_display_limit() {
526 assert!(GIT_RAW_MAX_BYTES > GIT_MAX_FILE_SIZE_BYTES);
527 }
528
529 #[test]
530 fn scan_zip_max_ratio_positive() {
531 assert!(SCAN_ZIP_MAX_RATIO > 0.0);
532 }
533
534 #[test]
535 fn scan_zip_max_depth_positive() {
536 assert!(SCAN_ZIP_MAX_DEPTH > 0);
537 }
538
539 // -- SyncKit --
540
541 #[test]
542 fn synckit_push_max_changes_positive() {
543 assert!(SYNCKIT_PUSH_MAX_CHANGES > 0);
544 }
545
546 #[test]
547 fn synckit_pull_page_size_positive() {
548 assert!(SYNCKIT_PULL_PAGE_SIZE > 0);
549 }
550
551 #[test]
552 fn synckit_max_blob_size_positive() {
553 assert!(SYNCKIT_MAX_BLOB_SIZE_BYTES > 0);
554 }
555
556 #[test]
557 fn synckit_jwt_expiry_positive() {
558 assert!(SYNCKIT_JWT_EXPIRY_SECS > 0);
559 }
560
561 // -- TOTP --
562
563 #[test]
564 fn totp_digits_is_six() {
565 assert_eq!(TOTP_DIGITS, 6);
566 }
567
568 #[test]
569 fn totp_step_is_30() {
570 assert_eq!(TOTP_STEP, 30);
571 }
572
573 #[test]
574 fn backup_code_count_positive() {
575 assert!(BACKUP_CODE_COUNT > 0);
576 }
577
578 #[test]
579 fn backup_code_length_positive() {
580 assert!(BACKUP_CODE_LENGTH > 0);
581 }
582
583 // -- Pagination --
584
585 #[test]
586 fn discover_page_size_positive() {
587 assert!(DISCOVER_PAGE_SIZE > 0);
588 }
589
590 #[test]
591 fn feed_page_size_positive() {
592 assert!(FEED_PAGE_SIZE > 0);
593 }
594
595 #[test]
596 fn pagination_window_size_positive() {
597 assert!(PAGINATION_WINDOW_SIZE > 0);
598 }
599
600 // -- String constants non-empty --
601
602 #[test]
603 fn date_formats_non_empty() {
604 assert!(!DATE_FMT_SHORT.is_empty());
605 assert!(!DATE_FMT_FULL.is_empty());
606 assert!(!DATE_FMT_ISO.is_empty());
607 assert!(!DATE_FMT_DATETIME.is_empty());
608 assert!(!DATE_FMT_DATETIME_UTC.is_empty());
609 }
610
611 #[test]
612 fn changelog_project_slug_non_empty() {
613 assert!(!CHANGELOG_PROJECT_SLUG.is_empty());
614 }
615
616 #[test]
617 fn build_allowed_targets_non_empty() {
618 assert!(!BUILD_ALLOWED_TARGETS.is_empty());
619 for target in BUILD_ALLOWED_TARGETS {
620 assert!(!target.is_empty());
621 assert!(target.contains('/'), "target should be os/arch format: {}", target);
622 }
623 }
624
625 // -- Collections --
626
627 #[test]
628 fn max_collections_per_user_positive() {
629 assert!(MAX_COLLECTIONS_PER_USER > 0);
630 }
631
632 #[test]
633 fn max_items_per_collection_positive() {
634 assert!(MAX_ITEMS_PER_COLLECTION > 0);
635 }
636
637 // -- Build pipeline --
638
639 #[test]
640 fn build_timeout_positive() {
641 assert!(BUILD_TIMEOUT_SECS > 0);
642 }
643
644 #[test]
645 fn build_max_log_bytes_positive() {
646 assert!(BUILD_MAX_LOG_BYTES > 0);
647 }
648
649 // -- Health monitoring --
650
651 #[test]
652 fn health_check_interval_positive() {
653 assert!(HEALTH_CHECK_INTERVAL_SECS > 0);
654 }
655
656 #[test]
657 fn alert_cooldown_exceeds_health_check() {
658 assert!(ALERT_COOLDOWN_SECS > HEALTH_CHECK_INTERVAL_SECS);
659 }
660
661 // -- Sandbox --
662
663 #[test]
664 fn sandbox_expiry_positive() {
665 assert!(SANDBOX_EXPIRY_SECS > 0);
666 }
667
668 #[test]
669 fn sandbox_cleanup_interval_positive() {
670 assert!(SANDBOX_CLEANUP_INTERVAL_SECS > 0);
671 }
672
673 #[test]
674 fn sandbox_cleanup_less_than_expiry() {
675 assert!(SANDBOX_CLEANUP_INTERVAL_SECS < SANDBOX_EXPIRY_SECS as u64);
676 }
677
678 #[test]
679 fn sandbox_max_per_ip_positive() {
680 assert!(SANDBOX_MAX_PER_IP > 0);
681 }
682
683 // -- Webhook --
684
685 #[test]
686 fn webhook_timestamp_tolerance_positive() {
687 assert!(WEBHOOK_TIMESTAMP_TOLERANCE_SECS > 0);
688 }
689
690 // -- OAuth --
691
692 #[test]
693 fn oauth_code_expiry_positive() {
694 assert!(OAUTH_CODE_EXPIRY_SECS > 0);
695 }
696
697 #[test]
698 fn oauth_code_length_positive() {
699 assert!(OAUTH_CODE_LENGTH > 0);
700 }
701
702 // -- Buffer limits --
703
704 #[test]
705 fn user_agent_max_length_positive() {
706 assert!(USER_AGENT_MAX_LENGTH > 0);
707 }
708
709 #[test]
710 fn synckit_max_key_envelope_bytes_positive() {
711 assert!(SYNCKIT_MAX_KEY_ENVELOPE_BYTES > 0);
712 }
713 }
714