Files
DumbDrop/test/auth.test.js
abite fc8bff9a14 feat: upgrade dependencies for security and add comprehensive test suite (#72)
* feat: upgrade dependencies for security and add comprehensive test suite

Major security and quality improvements to address GitHub issue #69

BREAKING CHANGES:
- ESLint upgraded from 8.x to 9.x with new flat config system
- Migrated from eslint-plugin-node to eslint-plugin-n

Security Fixes:
- Upgraded Multer from 1.4.5-lts.1 to 2.0.0
  * Fixes known security vulnerabilities in file upload handling
  * Addresses path traversal and exploit concerns
- Upgraded ESLint from 8.56.0 to 9.0.0
  * Ensures continued security patches and support
- Replaced deprecated eslint-plugin-node with eslint-plugin-n (v17.0.0)
- npm audit: Reduced vulnerabilities from 5 (4 high, 1 low) to 0

Configuration Changes:
- Created eslint.config.js using new flat config format
- Removed deprecated .eslintrc.json and .eslintignore files
- Added ignores configuration for test files and service workers
- Disabled cleanup intervals during tests to prevent hanging

Code Quality:
- Fixed all ESLint errors across codebase
- Removed unused variables and imports
- Added proper ESLint disable comments where needed
- Fixed no-control-regex warnings with proper comments

Test Suite (NEW):
- Added Node.js built-in test runner (no extra dependencies)
- Created 43 tests across 4 test files:
  * test/upload.test.js - Upload API tests
  * test/files.test.js - File management tests
  * test/auth.test.js - Authentication tests
  * test/security.test.js - Security and validation tests
- Test coverage: 81% pass rate (35/43 tests passing)
- Added npm test script to package.json

Docker Optimization:
- Updated .dockerignore to exclude test files from production images
- Excluded development configs (eslint.config.js, .prettierrc, nodemon.json)
- Reduces production image size and attack surface

Fixes #69

Test Results:
- 43 tests, 24 suites
- 35 passing, 8 failing (minor edge cases)
- Execution time: 469ms
- All tests complete without hanging

* Update multer dependency to v2.0.2

Bumped multer from version 2.0.0 to 2.0.2 in package.json and package-lock.json to include the latest bug fixes and improvements.

* Update ESLint ignore patterns and improve config validation

Added 'test/**' to ESLint ignore patterns. Enhanced BASE_URL validation error handling to log specific error messages and provide more informative feedback.
2025-11-04 21:50:00 -06:00

218 lines
5.4 KiB
JavaScript

/**
* Authentication tests
* Tests PIN protection and authentication middleware
*/
// Disable batch cleanup for tests
process.env.DISABLE_BATCH_CLEANUP = 'true';
const { describe, it, before, after, beforeEach } = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
// Import the app
const { app, initialize } = require('../src/app');
let server;
let baseUrl;
const originalPin = process.env.PIN;
before(async () => {
// Set PIN for testing
process.env.PIN = '1234';
// Initialize app
await initialize();
// Start server on random port
server = http.createServer(app);
await new Promise((resolve) => {
server.listen(0, () => {
const { port } = server.address();
baseUrl = `http://localhost:${port}`;
resolve();
});
});
});
after(async () => {
// Restore original PIN
if (originalPin) {
process.env.PIN = originalPin;
} else {
delete process.env.PIN;
}
// Close server
if (server) {
await new Promise((resolve) => server.close(resolve));
}
});
/**
* Helper function to make HTTP requests
*/
async function makeRequest(options, body = null) {
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const parsed = data ? JSON.parse(data) : {};
resolve({ status: res.statusCode, data: parsed, headers: res.headers, cookies: res.headers['set-cookie'] });
} catch {
resolve({ status: res.statusCode, data, headers: res.headers, cookies: res.headers['set-cookie'] });
}
});
});
req.on('error', reject);
if (body) {
req.write(JSON.stringify(body));
}
req.end();
});
}
describe('Authentication API Tests', () => {
describe('GET /api/auth/pin-required', () => {
it('should indicate if PIN is required', async () => {
const response = await makeRequest({
host: 'localhost',
port: server.address().port,
path: '/api/auth/pin-required',
method: 'GET',
});
assert.strictEqual(response.status, 200);
assert.strictEqual(typeof response.data.required, 'boolean');
});
});
describe('POST /api/auth/verify-pin', () => {
it('should accept correct PIN', async () => {
const response = await makeRequest({
host: 'localhost',
port: server.address().port,
path: '/api/auth/verify-pin',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}, {
pin: '1234',
});
assert.strictEqual(response.status, 200);
assert.ok(response.cookies);
});
it('should reject incorrect PIN', async () => {
const response = await makeRequest({
host: 'localhost',
port: server.address().port,
path: '/api/auth/verify-pin',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}, {
pin: 'wrong',
});
assert.strictEqual(response.status, 401);
});
it('should reject empty PIN', async () => {
const response = await makeRequest({
host: 'localhost',
port: server.address().port,
path: '/api/auth/verify-pin',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}, {
pin: '',
});
assert.strictEqual(response.status, 400);
});
});
describe('Protected Routes', () => {
it('should require PIN for upload init', async () => {
const response = await makeRequest({
host: 'localhost',
port: server.address().port,
path: '/api/upload/init',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}, {
filename: 'test.txt',
fileSize: 100,
});
// Should be redirected or unauthorized without PIN
assert.ok(response.status === 401 || response.status === 403);
});
it('should allow upload with valid PIN cookie', async () => {
// First, get PIN cookie
const authResponse = await makeRequest({
host: 'localhost',
port: server.address().port,
path: '/api/auth/verify-pin',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}, {
pin: '1234',
});
// Extract cookie
const cookies = authResponse.cookies;
const cookie = cookies ? cookies[0].split(';')[0] : '';
// Try upload with cookie
const uploadResponse = await makeRequest({
host: 'localhost',
port: server.address().port,
path: '/api/upload/init',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cookie': cookie,
},
}, {
filename: 'test.txt',
fileSize: 100,
});
assert.strictEqual(uploadResponse.status, 200);
});
});
describe('POST /api/auth/logout', () => {
it('should clear authentication cookie', async () => {
const response = await makeRequest({
host: 'localhost',
port: server.address().port,
path: '/api/auth/logout',
method: 'POST',
});
assert.strictEqual(response.status, 200);
});
});
});