mirror of
https://github.com/DumbWareio/DumbDrop.git
synced 2025-11-15 19:31:25 +00:00
* 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.
292 lines
7.5 KiB
JavaScript
292 lines
7.5 KiB
JavaScript
/**
|
|
* Upload functionality tests
|
|
* Tests file upload initialization, chunked uploads, and batch operations
|
|
*/
|
|
|
|
// Disable batch cleanup for tests
|
|
process.env.DISABLE_BATCH_CLEANUP = 'true';
|
|
|
|
const { describe, it, before, after } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const http = require('node:http');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
// Import the app
|
|
const { app, initialize, config } = require('../src/app');
|
|
|
|
let server;
|
|
let baseUrl;
|
|
|
|
before(async () => {
|
|
// 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 () => {
|
|
// Close server
|
|
if (server) {
|
|
await new Promise((resolve) => server.close(resolve));
|
|
}
|
|
|
|
// Clean up test uploads
|
|
try {
|
|
const testFiles = await fs.readdir(config.uploadDir);
|
|
for (const file of testFiles) {
|
|
if (file !== '.metadata') {
|
|
const filePath = path.join(config.uploadDir, file);
|
|
const stat = await fs.stat(filePath);
|
|
if (stat.isFile()) {
|
|
await fs.unlink(filePath);
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// Ignore cleanup errors
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 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 });
|
|
} catch {
|
|
resolve({ status: res.statusCode, data, headers: res.headers });
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', reject);
|
|
|
|
if (body) {
|
|
if (Buffer.isBuffer(body)) {
|
|
req.write(body);
|
|
} else {
|
|
req.write(JSON.stringify(body));
|
|
}
|
|
}
|
|
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
describe('Upload API Tests', () => {
|
|
describe('POST /api/upload/init', () => {
|
|
it('should initialize a new upload', 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,
|
|
});
|
|
|
|
assert.strictEqual(response.status, 200);
|
|
assert.ok(response.data.uploadId);
|
|
});
|
|
|
|
it('should reject uploads without filename', async () => {
|
|
const response = await makeRequest({
|
|
host: 'localhost',
|
|
port: server.address().port,
|
|
path: '/api/upload/init',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}, {
|
|
fileSize: 100,
|
|
});
|
|
|
|
assert.strictEqual(response.status, 400);
|
|
assert.ok(response.data.error);
|
|
});
|
|
|
|
it('should reject uploads without fileSize', 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',
|
|
});
|
|
|
|
assert.strictEqual(response.status, 400);
|
|
assert.ok(response.data.error);
|
|
});
|
|
|
|
it('should handle zero-byte files', async () => {
|
|
const response = await makeRequest({
|
|
host: 'localhost',
|
|
port: server.address().port,
|
|
path: '/api/upload/init',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}, {
|
|
filename: 'empty.txt',
|
|
fileSize: 0,
|
|
});
|
|
|
|
assert.strictEqual(response.status, 200);
|
|
assert.ok(response.data.uploadId);
|
|
});
|
|
});
|
|
|
|
describe('POST /api/upload/chunk/:uploadId', () => {
|
|
it('should accept chunks for a valid upload', async () => {
|
|
// Initialize upload first
|
|
const initResponse = await makeRequest({
|
|
host: 'localhost',
|
|
port: server.address().port,
|
|
path: '/api/upload/init',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}, {
|
|
filename: 'chunk-test.txt',
|
|
fileSize: 50,
|
|
});
|
|
|
|
const { uploadId } = initResponse.data;
|
|
|
|
// Send chunk
|
|
const chunk = Buffer.from('Hello, World!');
|
|
const chunkResponse = await makeRequest({
|
|
host: 'localhost',
|
|
port: server.address().port,
|
|
path: `/api/upload/chunk/${uploadId}`,
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/octet-stream',
|
|
},
|
|
}, chunk);
|
|
|
|
assert.strictEqual(chunkResponse.status, 200);
|
|
assert.ok(chunkResponse.data.bytesReceived > 0);
|
|
});
|
|
|
|
it('should reject chunks for invalid uploadId', async () => {
|
|
const chunk = Buffer.from('Test data');
|
|
const response = await makeRequest({
|
|
host: 'localhost',
|
|
port: server.address().port,
|
|
path: '/api/upload/chunk/invalid-id',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/octet-stream',
|
|
},
|
|
}, chunk);
|
|
|
|
assert.strictEqual(response.status, 404);
|
|
});
|
|
});
|
|
|
|
describe('POST /api/upload/cancel/:uploadId', () => {
|
|
it('should cancel an active upload', async () => {
|
|
// Initialize upload
|
|
const initResponse = await makeRequest({
|
|
host: 'localhost',
|
|
port: server.address().port,
|
|
path: '/api/upload/init',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
}, {
|
|
filename: 'cancel-test.txt',
|
|
fileSize: 100,
|
|
});
|
|
|
|
const { uploadId } = initResponse.data;
|
|
|
|
// Cancel upload
|
|
const cancelResponse = await makeRequest({
|
|
host: 'localhost',
|
|
port: server.address().port,
|
|
path: `/api/upload/cancel/${uploadId}`,
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
assert.strictEqual(cancelResponse.status, 200);
|
|
});
|
|
});
|
|
|
|
describe('Batch uploads', () => {
|
|
it('should handle multiple files with same batch ID', async () => {
|
|
const batchId = `batch-${crypto.randomBytes(4).toString('hex')}`;
|
|
|
|
// Initialize first file
|
|
const file1Response = await makeRequest({
|
|
host: 'localhost',
|
|
port: server.address().port,
|
|
path: '/api/upload/init',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Batch-Id': batchId,
|
|
},
|
|
}, {
|
|
filename: 'batch-file1.txt',
|
|
fileSize: 50,
|
|
});
|
|
|
|
assert.strictEqual(file1Response.status, 200);
|
|
|
|
// Initialize second file
|
|
const file2Response = await makeRequest({
|
|
host: 'localhost',
|
|
port: server.address().port,
|
|
path: '/api/upload/init',
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Batch-Id': batchId,
|
|
},
|
|
}, {
|
|
filename: 'batch-file2.txt',
|
|
fileSize: 50,
|
|
});
|
|
|
|
assert.strictEqual(file2Response.status, 200);
|
|
assert.notStrictEqual(file1Response.data.uploadId, file2Response.data.uploadId);
|
|
});
|
|
});
|
|
});
|
|
|