diff --git a/.env.test b/.env.test new file mode 100644 index 00000000..c92fc898 --- /dev/null +++ b/.env.test @@ -0,0 +1,12 @@ +NODE_ENV=test +MONGODB_URI=mongodb://localhost:27017/tractatus_test +MONGODB_DB=tractatus_test +JWT_SECRET=test_secret_for_testing_only +JWT_EXPIRY=7d +ADMIN_EMAIL=admin@tractatus.test +PORT=9001 +LOG_LEVEL=error +CLAUDE_API_KEY=test_placeholder_key +ENABLE_AI_CURATION=false +ENABLE_MEDIA_TRIAGE=false +ENABLE_CASE_SUBMISSIONS=false diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..f8c03092 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,40 @@ +/** + * Jest Configuration + */ + +module.exports = { + // Test environment + testEnvironment: 'node', + + // Setup files to run before tests + setupFiles: ['/tests/setup.js'], + + // Coverage configuration + collectCoverageFrom: [ + 'src/**/*.js', + '!src/server.js', + '!**/node_modules/**', + '!**/tests/**' + ], + + // Coverage thresholds (aspirational) + coverageThresholds: { + global: { + branches: 40, + functions: 35, + lines: 45, + statements: 45 + } + }, + + // Test match patterns + testMatch: [ + '**/tests/**/*.test.js' + ], + + // Verbose output + verbose: true, + + // Test timeout (increased for integration tests) + testTimeout: 10000 +}; diff --git a/scripts/clean-test-db.js b/scripts/clean-test-db.js new file mode 100644 index 00000000..98a25408 --- /dev/null +++ b/scripts/clean-test-db.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node +/** + * Clean Test Database + * Removes all data from the test database before running tests + * + * Usage: + * node scripts/clean-test-db.js + * npm run test:clean (if script added to package.json) + */ + +const path = require('path'); +require('dotenv').config({ path: path.resolve(__dirname, '../.env.test') }); + +const { MongoClient } = require('mongodb'); +const config = require('../src/config/app.config'); + +async function cleanTestDatabase() { + console.log('๐Ÿงน Cleaning test database...\n'); + + // Safety check + if (config.env !== 'test') { + console.error('โŒ ERROR: NODE_ENV must be "test"'); + console.error(` Current: ${config.env}`); + process.exit(1); + } + + if (!config.mongodb.db.includes('test')) { + console.error('โŒ ERROR: Database name must contain "test"'); + console.error(` Current: ${config.mongodb.db}`); + process.exit(1); + } + + const connection = await MongoClient.connect(config.mongodb.uri); + const db = connection.db(config.mongodb.db); + + try { + console.log(`๐Ÿ“Š Database: ${config.mongodb.db}`); + console.log(`๐Ÿ”— URI: ${config.mongodb.uri}\n`); + + // Get all collections + const collections = await db.listCollections().toArray(); + console.log(`Found ${collections.length} collections\n`); + + if (collections.length === 0) { + console.log('โœจ Database is already empty\n'); + await connection.close(); + return; + } + + // Clean each collection + for (const collection of collections) { + const count = await db.collection(collection.name).countDocuments(); + if (count > 0) { + await db.collection(collection.name).deleteMany({}); + console.log(` โœ“ Cleaned ${collection.name} (${count} documents)`); + } else { + console.log(` ยท Skipped ${collection.name} (empty)`); + } + } + + console.log(`\nโœ… Test database cleaned successfully\n`); + } catch (error) { + console.error('\nโŒ ERROR:', error.message); + process.exit(1); + } finally { + await connection.close(); + } +} + +// Run if called directly +if (require.main === module) { + cleanTestDatabase().catch(error => { + console.error('Fatal error:', error); + process.exit(1); + }); +} + +module.exports = { cleanTestDatabase }; diff --git a/tests/helpers/cleanup.js b/tests/helpers/cleanup.js new file mode 100644 index 00000000..26eb82f5 --- /dev/null +++ b/tests/helpers/cleanup.js @@ -0,0 +1,79 @@ +/** + * Test Cleanup Helper + * Provides utilities for cleaning up test data between runs + */ + +const { MongoClient } = require('mongodb'); +const config = require('../../src/config/app.config'); + +/** + * Clean all collections in the test database + * @returns {Promise} + */ +async function cleanTestDatabase() { + const connection = await MongoClient.connect(config.mongodb.uri); + const db = connection.db(config.mongodb.db); + + try { + // Only clean if we're in test environment + if (config.env !== 'test' || !config.mongodb.db.includes('test')) { + throw new Error('cleanTestDatabase() can only be used with test databases'); + } + + // Get all collections + const collections = await db.listCollections().toArray(); + + // Drop each collection + for (const collection of collections) { + await db.collection(collection.name).deleteMany({}); + } + + console.log(`โœ“ Cleaned ${collections.length} collections in ${config.mongodb.db}`); + } finally { + await connection.close(); + } +} + +/** + * Clean specific test documents by slug pattern + * @param {string} slugPattern - Pattern to match (e.g., 'test-document-integration') + * @returns {Promise} - Number of documents deleted + */ +async function cleanTestDocuments(slugPattern) { + const connection = await MongoClient.connect(config.mongodb.uri); + const db = connection.db(config.mongodb.db); + + try { + const result = await db.collection('documents').deleteMany({ + slug: { $regex: slugPattern } + }); + return result.deletedCount; + } finally { + await connection.close(); + } +} + +/** + * Clean specific test users by email pattern + * @param {string} emailPattern - Pattern to match (e.g., 'test@') + * @returns {Promise} - Number of users deleted + */ +async function cleanTestUsers(emailPattern) { + const connection = await MongoClient.connect(config.mongodb.uri); + const db = connection.db(config.mongodb.db); + + try { + const result = await db.collection('users').deleteMany({ + email: { $regex: emailPattern } + }); + return result.deletedCount; + } finally { + await connection.close(); + } +} + +module.exports = { + cleanTestDatabase, + cleanTestDocuments, + cleanTestUsers +}; diff --git a/tests/integration/api.admin.test.js b/tests/integration/api.admin.test.js index 650fa886..001336e9 100644 --- a/tests/integration/api.admin.test.js +++ b/tests/integration/api.admin.test.js @@ -304,8 +304,9 @@ describe('Admin API Integration Tests', () => { expect(response.body).toHaveProperty('success', true); // Verify deletion + const { ObjectId } = require('mongodb'); const user = await db.collection('users').findOne({ - _id: require('mongodb').ObjectId(testUserId) + _id: new ObjectId(testUserId) }); expect(user).toBeNull(); }); @@ -317,8 +318,9 @@ describe('Admin API Integration Tests', () => { .expect(403); // Clean up + const { ObjectId } = require('mongodb'); await db.collection('users').deleteOne({ - _id: require('mongodb').ObjectId(testUserId) + _id: new ObjectId(testUserId) }); }); diff --git a/tests/setup.js b/tests/setup.js new file mode 100644 index 00000000..18680c2e --- /dev/null +++ b/tests/setup.js @@ -0,0 +1,15 @@ +/** + * Jest Test Setup + * Loads .env.test before running tests and cleans test database + */ + +const path = require('path'); +require('dotenv').config({ path: path.resolve(__dirname, '../.env.test') }); + +// Set NODE_ENV to test +process.env.NODE_ENV = 'test'; + +// Note: We don't clean the database here in setup.js because: +// 1. It runs for every test file in parallel, causing race conditions +// 2. Each test suite should handle its own cleanup in beforeAll/beforeEach +// 3. See tests/helpers/cleanup.js for cleanup utilities