diff --git a/.eslintrc.json b/.eslintrc.json index 1d29c5a0c..07711f5e6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -23,9 +23,9 @@ }, "parser": "espree", "env": { - "cypress/globals": true + "node": true, + "mocha": true }, - "plugins": ["cypress"], "rules": { "@typescript-eslint/no-unused-expressions": "off" } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5903908f..ebb6a2ff0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,11 @@ jobs: npm run test-coverage-ci npm run test-coverage-ci --workspaces --if-present + - name: Test MongoDB Integration + run: npm run test:mongo + env: + GIT_PROXY_MONGO_CONNECTION_STRING: mongodb://localhost:27017/git-proxy-test + - name: Upload test coverage report uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: diff --git a/docker-compose.mongo-test.yml b/docker-compose.mongo-test.yml new file mode 100644 index 000000000..fc20d17c9 --- /dev/null +++ b/docker-compose.mongo-test.yml @@ -0,0 +1,21 @@ +version: '3.8' + +services: + mongodb-test: + image: mongo:4.4 + container_name: git-proxy-mongo-test + ports: + - '27017:27017' + environment: + MONGO_INITDB_DATABASE: git-proxy-test + volumes: + - mongo-test-data:/data/db + command: mongod --bind_ip_all + healthcheck: + test: ['CMD', 'mongosh', '--eval', "db.runCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + mongo-test-data: diff --git a/package.json b/package.json index bcf3dd650..f89b9dc78 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "gen-schema-doc": "node ./scripts/doc-schema.js", "cypress:run": "cypress run", "cypress:open": "cypress open", + "test:mongo": "node test/db/mongo/run-mongo-tests.js", + "test:mongo:docker": "node test/db/mongo/run-mongo-tests-docker.js", "generate-config-types": "quicktype --src-lang schema --lang typescript --out src/config/generated/config.ts --top-level GitProxyConfig config.schema.json && ts-node scripts/add-banner.ts src/config/generated/config.ts && prettier --write src/config/generated/config.ts" }, "bin": { diff --git a/src/db/mongo/repo.ts b/src/db/mongo/repo.ts index 655ef40b1..cb3893cdd 100644 --- a/src/db/mongo/repo.ts +++ b/src/db/mongo/repo.ts @@ -27,8 +27,13 @@ export const getRepoByUrl = async (repoUrl: string): Promise => { export const getRepoById = async (_id: string): Promise => { const collection = await connect(collectionName); - const doc = await collection.findOne({ _id: new ObjectId(_id) }); - return doc ? toClass(doc, Repo.prototype) : null; + try { + const doc = await collection.findOne({ _id: new ObjectId(_id) }); + return doc ? toClass(doc, Repo.prototype) : null; + } catch (error) { + // Handle invalid ObjectId strings gracefully + return null; + } }; export const createRepo = async (repo: Repo): Promise => { diff --git a/test/db/database-comparison.test.js b/test/db/database-comparison.test.js new file mode 100644 index 000000000..d1c1a03d5 --- /dev/null +++ b/test/db/database-comparison.test.js @@ -0,0 +1,281 @@ +const { expect } = require('chai'); +const { MongoClient } = require('mongodb'); +const fs = require('fs'); +const path = require('path'); + +// Import both database implementations +const fileRepo = require('../../src/db/file/repo'); +const fileUsers = require('../../src/db/file/users'); +const mongoRepo = require('../../src/db/mongo/repo'); +const mongoUsers = require('../../src/db/mongo/users'); + +describe('Database Comparison Tests', () => { + let mongoClient; + let mongoDb; + let fileDbPath; + + before(async () => { + // Setup MongoDB connection + const connectionString = + process.env.GIT_PROXY_MONGO_CONNECTION_STRING || 'mongodb://localhost:27017/git-proxy-test'; + mongoClient = new MongoClient(connectionString); + await mongoClient.connect(); + mongoDb = mongoClient.db('git-proxy-test'); + + // Setup file database path + fileDbPath = path.join(__dirname, '../../test-data'); + if (!fs.existsSync(fileDbPath)) { + fs.mkdirSync(fileDbPath, { recursive: true }); + } + }); + + after(async () => { + if (mongoClient) { + await mongoClient.close(); + } + + // Clean up file database + if (fs.existsSync(fileDbPath)) { + fs.rmSync(fileDbPath, { recursive: true, force: true }); + } + }); + + beforeEach(async () => { + // Clean MongoDB collections + await mongoDb.collection('repos').deleteMany({}); + await mongoDb.collection('users').deleteMany({}); + + // Clean file database + const reposPath = path.join(fileDbPath, 'repos'); + const usersPath = path.join(fileDbPath, 'users'); + + if (fs.existsSync(reposPath)) { + fs.rmSync(reposPath, { recursive: true, force: true }); + } + if (fs.existsSync(usersPath)) { + fs.rmSync(usersPath, { recursive: true, force: true }); + } + + // Ensure directories exist + fs.mkdirSync(reposPath, { recursive: true }); + fs.mkdirSync(usersPath, { recursive: true }); + + // Clear module cache to ensure fresh instances + delete require.cache[require.resolve('../../src/db/file/repo')]; + delete require.cache[require.resolve('../../src/db/file/users')]; + delete require.cache[require.resolve('../../src/db/mongo/repo')]; + delete require.cache[require.resolve('../../src/db/mongo/users')]; + }); + + describe('Repository Operations Comparison', () => { + it('should create repositories with same behavior', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + + // Create in both databases + const fileResult = await fileRepo.createRepo(repoData); + const mongoResult = await mongoRepo.createRepo(repoData); + + // Both should return success + expect(fileResult).to.have.property('insertedId'); + expect(mongoResult).to.have.property('insertedId'); + + // Both should be retrievable + const fileRepoData = await fileRepo.getRepo('test-repo'); + const mongoRepoData = await mongoRepo.getRepo('test-repo'); + + expect(fileRepoData).to.have.property('name', 'test-repo'); + expect(mongoRepoData).to.have.property('name', 'test-repo'); + expect(fileRepoData).to.have.property('url', 'https://github.com/test/test-repo'); + expect(mongoRepoData).to.have.property('url', 'https://github.com/test/test-repo'); + }); + + it('should get repositories by URL with same behavior', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + + await fileRepo.createRepo(repoData); + await mongoRepo.createRepo(repoData); + + const fileRepoData = await fileRepo.getRepoByUrl('https://github.com/test/test-repo'); + const mongoRepoData = await mongoRepo.getRepoByUrl('https://github.com/test/test-repo'); + + expect(fileRepoData).to.have.property('name', 'test-repo'); + expect(mongoRepoData).to.have.property('name', 'test-repo'); + }); + + it('should add users to canPush with same behavior', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + + await fileRepo.createRepo(repoData); + await mongoRepo.createRepo(repoData); + + await fileRepo.addUserCanPush('test-repo', 'newuser'); + await mongoRepo.addUserCanPush('test-repo', 'newuser'); + + const fileRepoData = await fileRepo.getRepo('test-repo'); + const mongoRepoData = await mongoRepo.getRepo('test-repo'); + + expect(fileRepoData.canPush).to.include('newuser'); + expect(mongoRepoData.canPush).to.include('newuser'); + }); + + it('should delete repositories with same behavior', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + + await fileRepo.createRepo(repoData); + await mongoRepo.createRepo(repoData); + + await fileRepo.deleteRepo('test-repo'); + await mongoRepo.deleteRepo('test-repo'); + + const fileRepoData = await fileRepo.getRepo('test-repo'); + const mongoRepoData = await mongoRepo.getRepo('test-repo'); + + expect(fileRepoData).to.be.null; + expect(mongoRepoData).to.be.null; + }); + }); + + describe('User Operations Comparison', () => { + it('should create users with same behavior', async () => { + const userData = { + username: 'testuser', + email: 'test@example.com', + gitAccount: 'testaccount', + admin: false, + }; + + const fileResult = await fileUsers.createUser(userData); + const mongoResult = await mongoUsers.createUser(userData); + + expect(fileResult).to.have.property('insertedId'); + expect(mongoResult).to.have.property('insertedId'); + + const fileUser = await fileUsers.findUser('testuser'); + const mongoUser = await mongoUsers.findUser('testuser'); + + expect(fileUser).to.have.property('username', 'testuser'); + expect(mongoUser).to.have.property('username', 'testuser'); + expect(fileUser).to.have.property('email', 'test@example.com'); + expect(mongoUser).to.have.property('email', 'test@example.com'); + }); + + it('should find users by email with same behavior', async () => { + const userData = { + username: 'testuser', + email: 'test@example.com', + gitAccount: 'testaccount', + admin: false, + }; + + await fileUsers.createUser(userData); + await mongoUsers.createUser(userData); + + const fileUser = await fileUsers.findUserByEmail('test@example.com'); + const mongoUser = await mongoUsers.findUserByEmail('test@example.com'); + + expect(fileUser).to.have.property('username', 'testuser'); + expect(mongoUser).to.have.property('username', 'testuser'); + }); + + it('should update users with same behavior', async () => { + const userData = { + username: 'testuser', + email: 'test@example.com', + gitAccount: 'testaccount', + admin: false, + }; + + const fileResult = await fileUsers.createUser(userData); + const mongoResult = await mongoUsers.createUser(userData); + + const fileUserId = fileResult.insertedId.toString(); + const mongoUserId = mongoResult.insertedId.toString(); + + await fileUsers.updateUser(fileUserId, { email: 'newemail@example.com', admin: true }); + await mongoUsers.updateUser(mongoUserId, { email: 'newemail@example.com', admin: true }); + + const fileUser = await fileUsers.findUser('testuser'); + const mongoUser = await mongoUsers.findUser('testuser'); + + expect(fileUser).to.have.property('email', 'newemail@example.com'); + expect(mongoUser).to.have.property('email', 'newemail@example.com'); + expect(fileUser).to.have.property('admin', true); + expect(mongoUser).to.have.property('admin', true); + }); + + it('should delete users with same behavior', async () => { + const userData = { + username: 'testuser', + email: 'test@example.com', + gitAccount: 'testaccount', + admin: false, + }; + + const fileResult = await fileUsers.createUser(userData); + const mongoResult = await mongoUsers.createUser(userData); + + const fileUserId = fileResult.insertedId.toString(); + const mongoUserId = mongoResult.insertedId.toString(); + + await fileUsers.deleteUser(fileUserId); + await mongoUsers.deleteUser(mongoUserId); + + const fileUser = await fileUsers.findUser('testuser'); + const mongoUser = await mongoUsers.findUser('testuser'); + + expect(fileUser).to.be.null; + expect(mongoUser).to.be.null; + }); + }); + + describe('Error Handling Comparison', () => { + it('should handle non-existent repositories consistently', async () => { + const fileRepo = await fileRepo.getRepo('non-existent'); + const mongoRepo = await mongoRepo.getRepo('non-existent'); + + expect(fileRepo).to.be.null; + expect(mongoRepo).to.be.null; + }); + + it('should handle non-existent users consistently', async () => { + const fileUser = await fileUsers.findUser('non-existent'); + const mongoUser = await mongoUsers.findUser('non-existent'); + + expect(fileUser).to.be.null; + expect(mongoUser).to.be.null; + }); + + it('should handle invalid operations consistently', async () => { + // Try to add user to non-existent repo + await fileRepo.addUserCanPush('non-existent', 'user1'); + await mongoRepo.addUserCanPush('non-existent', 'user1'); + + // Both should not throw errors (graceful handling) + const fileRepo = await fileRepo.getRepo('non-existent'); + const mongoRepo = await mongoRepo.getRepo('non-existent'); + + expect(fileRepo).to.be.null; + expect(mongoRepo).to.be.null; + }); + }); +}); diff --git a/test/db/mongo/README.md b/test/db/mongo/README.md new file mode 100644 index 000000000..24dd5ec5d --- /dev/null +++ b/test/db/mongo/README.md @@ -0,0 +1,144 @@ +# MongoDB Integration Testing + +This directory contains comprehensive integration tests for the MongoDB client implementation in git-proxy. + +## Overview + +The MongoDB integration tests ensure that: + +- All MongoDB client functions work correctly with a real database +- Database operations behave identically between file and MongoDB implementations +- Error handling works properly +- Test coverage is improved from 31.46% to match file DB coverage (91.41%) + +## Test Files + +- `integration.test.js` - Comprehensive MongoDB integration tests +- `database-comparison.test.js` - Tests comparing file DB vs MongoDB behavior +- `test-config.json` - Configuration for MongoDB testing +- `run-mongo-tests.js` - Test runner for local MongoDB +- `run-mongo-tests-docker.js` - Test runner using Docker + +## Running Tests + +### Prerequisites + +- Node.js and npm installed +- MongoDB instance running (local or Docker) + +### Local MongoDB Testing + +1. **Start MongoDB locally:** + + ```bash + # Using MongoDB service + brew services start mongodb-community + + # Or using Docker + docker run -d -p 27017:27017 --name mongodb mongo:4.4 + ``` + +2. **Set environment variable:** + + ```bash + export GIT_PROXY_MONGO_CONNECTION_STRING="mongodb://localhost:27017/git-proxy-test" + ``` + +3. **Run tests:** + ```bash + npm run test:mongo + ``` + +### Docker-based Testing + +1. **Run tests with Docker:** + + ```bash + npm run test:mongo:docker + ``` + + This will: + - Start a MongoDB container using `docker-compose.mongo-test.yml` + - Run the integration tests + - Stop the container when done + +## Test Coverage + +The integration tests cover: + +### Repository Operations + +- Create repository +- Get repository by name, URL, and ID +- Get all repositories +- Add/remove users from canPush and canAuthorise +- Delete repository +- Case-insensitive operations +- Error handling + +### User Operations + +- Create user +- Find user by username, email, and OIDC +- Get all users +- Update user +- Delete user +- Error handling + +### Database Comparison + +- Ensures file and MongoDB implementations behave identically +- Tests all major operations in both databases +- Validates error handling consistency + +## CI Integration + +The tests are integrated into the CI pipeline: + +1. MongoDB service starts using `supercharge/mongodb-github-action` +2. Environment variable `GIT_PROXY_MONGO_CONNECTION_STRING` is set +3. Tests run automatically after MongoDB is ready + +## Troubleshooting + +### Common Issues + +1. **MongoDB Connection Failed:** + - Ensure MongoDB is running + - Check connection string format + - Verify network connectivity + +2. **Test Timeouts:** + - Increase timeout in test configuration + - Check MongoDB performance + - Ensure proper cleanup between tests + +3. **Docker Issues:** + - Ensure Docker is running + - Check port 27017 is available + - Verify Docker Compose file syntax + +### Debug Mode + +Run tests with debug output: + +```bash +DEBUG=* npm run test:mongo +``` + +## Configuration + +The test configuration is managed through: + +- `test-config.json` - MongoDB-specific settings +- Environment variables for connection strings +- Docker Compose for containerized testing + +## Contributing + +When adding new MongoDB client functions: + +1. Add corresponding integration tests +2. Add database comparison tests +3. Update this README if needed +4. Ensure tests pass in both local and CI environments diff --git a/test/db/mongo/integration.test.js b/test/db/mongo/integration.test.js new file mode 100644 index 000000000..c39a6b760 --- /dev/null +++ b/test/db/mongo/integration.test.js @@ -0,0 +1,394 @@ +const { expect } = require('chai'); +const { MongoClient } = require('mongodb'); + +// Import the MongoDB client modules +const mongoRepo = require('../../../src/db/mongo/repo'); +const mongoUsers = require('../../../src/db/mongo/users'); + +describe('MongoDB Integration Tests', () => { + let client; + let db; + let reposCollection; + let usersCollection; + + before(async () => { + // Connect to MongoDB + const connectionString = + process.env.GIT_PROXY_MONGO_CONNECTION_STRING || 'mongodb://localhost:27017/git-proxy-test'; + client = new MongoClient(connectionString); + await client.connect(); + db = client.db('git-proxy-test'); + reposCollection = db.collection('repos'); + usersCollection = db.collection('users'); + }); + + after(async () => { + if (client) { + await client.close(); + } + }); + + beforeEach(async () => { + // Clean collections before each test + await reposCollection.deleteMany({}); + await usersCollection.deleteMany({}); + + // Clear module cache to ensure fresh instances + delete require.cache[require.resolve('../../../src/db/mongo/repo')]; + delete require.cache[require.resolve('../../../src/db/mongo/users')]; + }); + + describe('Repository Operations', () => { + it('should create a repository', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + + const result = await mongoRepo.createRepo(repoData); + expect(result).to.have.property('insertedId'); + + const createdRepo = await reposCollection.findOne({ _id: result.insertedId }); + expect(createdRepo).to.have.property('name', 'test-repo'); + expect(createdRepo).to.have.property('url', 'https://github.com/test/test-repo'); + }); + + it('should get a repository by name', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + + await mongoRepo.createRepo(repoData); + const repo = await mongoRepo.getRepo('test-repo'); + + expect(repo).to.have.property('name', 'test-repo'); + expect(repo).to.have.property('url', 'https://github.com/test/test-repo'); + }); + + it('should get a repository by URL', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + + await mongoRepo.createRepo(repoData); + const repo = await mongoRepo.getRepoByUrl('https://github.com/test/test-repo'); + + expect(repo).to.have.property('name', 'test-repo'); + expect(repo).to.have.property('url', 'https://github.com/test/test-repo'); + }); + + it('should get a repository by ID', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + + const result = await mongoRepo.createRepo(repoData); + const repo = await mongoRepo.getRepoById(result.insertedId.toString()); + + expect(repo).to.have.property('name', 'test-repo'); + expect(repo).to.have.property('url', 'https://github.com/test/test-repo'); + }); + + it('should get all repositories', async () => { + const repo1 = { + name: 'test-repo-1', + url: 'https://github.com/test/test-repo-1', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + const repo2 = { + name: 'test-repo-2', + url: 'https://github.com/test/test-repo-2', + canPush: ['user3'], + canAuthorise: ['user4'], + }; + + await mongoRepo.createRepo(repo1); + await mongoRepo.createRepo(repo2); + + const repos = await mongoRepo.getRepos(); + expect(repos).to.have.length(2); + }); + + it('should add user to canPush', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + + await mongoRepo.createRepo(repoData); + await mongoRepo.addUserCanPush('test-repo', 'newuser'); + + const repo = await mongoRepo.getRepo('test-repo'); + expect(repo.canPush).to.include('newuser'); + }); + + it('should add user to canAuthorise', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + + await mongoRepo.createRepo(repoData); + await mongoRepo.addUserCanAuthorise('test-repo', 'newuser'); + + const repo = await mongoRepo.getRepo('test-repo'); + expect(repo.canAuthorise).to.include('newuser'); + }); + + it('should remove user from canPush', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1', 'user2'], + canAuthorise: ['user3'], + }; + + await mongoRepo.createRepo(repoData); + await mongoRepo.removeUserCanPush('test-repo', 'user1'); + + const repo = await mongoRepo.getRepo('test-repo'); + expect(repo.canPush).to.not.include('user1'); + expect(repo.canPush).to.include('user2'); + }); + + it('should remove user from canAuthorise', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2', 'user3'], + }; + + await mongoRepo.createRepo(repoData); + await mongoRepo.removeUserCanAuthorise('test-repo', 'user2'); + + const repo = await mongoRepo.getRepo('test-repo'); + expect(repo.canAuthorise).to.not.include('user2'); + expect(repo.canAuthorise).to.include('user3'); + }); + + it('should delete a repository', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + + await mongoRepo.createRepo(repoData); + await mongoRepo.deleteRepo('test-repo'); + + const repo = await mongoRepo.getRepo('test-repo'); + expect(repo).to.be.null; + }); + + it('should handle case-insensitive repository names', async () => { + const repoData = { + name: 'Test-Repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + + await mongoRepo.createRepo(repoData); + const repo = await mongoRepo.getRepo('test-repo'); + + expect(repo).to.have.property('name', 'Test-Repo'); + }); + + it('should handle invalid ObjectId gracefully', async () => { + const repo = await mongoRepo.getRepoById('invalid-id'); + expect(repo).to.be.null; + }); + }); + + describe('User Operations', () => { + it('should create a user', async () => { + const userData = { + username: 'testuser', + email: 'test@example.com', + gitAccount: 'testaccount', + admin: false, + }; + + const result = await mongoUsers.createUser(userData); + expect(result).to.have.property('insertedId'); + + const createdUser = await usersCollection.findOne({ _id: result.insertedId }); + expect(createdUser).to.have.property('username', 'testuser'); + expect(createdUser).to.have.property('email', 'test@example.com'); + }); + + it('should find a user by username', async () => { + const userData = { + username: 'testuser', + email: 'test@example.com', + gitAccount: 'testaccount', + admin: false, + }; + + await mongoUsers.createUser(userData); + const user = await mongoUsers.findUser('testuser'); + + expect(user).to.have.property('username', 'testuser'); + expect(user).to.have.property('email', 'test@example.com'); + }); + + it('should find a user by email', async () => { + const userData = { + username: 'testuser', + email: 'test@example.com', + gitAccount: 'testaccount', + admin: false, + }; + + await mongoUsers.createUser(userData); + const user = await mongoUsers.findUserByEmail('test@example.com'); + + expect(user).to.have.property('username', 'testuser'); + expect(user).to.have.property('email', 'test@example.com'); + }); + + it('should find a user by OIDC', async () => { + const userData = { + username: 'testuser', + email: 'test@example.com', + gitAccount: 'testaccount', + admin: false, + oidc: 'oidc-123', + }; + + await mongoUsers.createUser(userData); + const user = await mongoUsers.findUserByOIDC('oidc-123'); + + expect(user).to.have.property('username', 'testuser'); + expect(user).to.have.property('oidc', 'oidc-123'); + }); + + it('should get all users', async () => { + const user1 = { + username: 'user1', + email: 'user1@example.com', + gitAccount: 'account1', + admin: false, + }; + const user2 = { + username: 'user2', + email: 'user2@example.com', + gitAccount: 'account2', + admin: true, + }; + + await mongoUsers.createUser(user1); + await mongoUsers.createUser(user2); + + const users = await mongoUsers.getUsers(); + expect(users).to.have.length(2); + }); + + it('should update a user', async () => { + const userData = { + username: 'testuser', + email: 'test@example.com', + gitAccount: 'testaccount', + admin: false, + }; + + const result = await mongoUsers.createUser(userData); + const userId = result.insertedId.toString(); + + await mongoUsers.updateUser(userId, { email: 'newemail@example.com', admin: true }); + + const updatedUser = await mongoUsers.findUser('testuser'); + expect(updatedUser).to.have.property('email', 'newemail@example.com'); + expect(updatedUser).to.have.property('admin', true); + }); + + it('should delete a user', async () => { + const userData = { + username: 'testuser', + email: 'test@example.com', + gitAccount: 'testaccount', + admin: false, + }; + + const result = await mongoUsers.createUser(userData); + const userId = result.insertedId.toString(); + + await mongoUsers.deleteUser(userId); + + const user = await mongoUsers.findUser('testuser'); + expect(user).to.be.null; + }); + }); + + describe('Error Handling', () => { + it('should handle duplicate repository names', async () => { + const repoData = { + name: 'test-repo', + url: 'https://github.com/test/test-repo', + canPush: ['user1'], + canAuthorise: ['user2'], + }; + + await mongoRepo.createRepo(repoData); + + try { + await mongoRepo.createRepo(repoData); + expect.fail('Should have thrown an error for duplicate repository'); + } catch (error) { + expect(error.code).to.equal(11000); // Duplicate key error + } + }); + + it('should handle duplicate usernames', async () => { + const userData = { + username: 'testuser', + email: 'test@example.com', + gitAccount: 'testaccount', + admin: false, + }; + + await mongoUsers.createUser(userData); + + try { + await mongoUsers.createUser(userData); + expect.fail('Should have thrown an error for duplicate username'); + } catch (error) { + expect(error.code).to.equal(11000); // Duplicate key error + } + }); + + it('should handle non-existent repository operations', async () => { + const repo = await mongoRepo.getRepo('non-existent-repo'); + expect(repo).to.be.null; + + const repoByUrl = await mongoRepo.getRepoByUrl('https://github.com/non/existent'); + expect(repoByUrl).to.be.null; + }); + + it('should handle non-existent user operations', async () => { + const user = await mongoUsers.findUser('non-existent-user'); + expect(user).to.be.null; + + const userByEmail = await mongoUsers.findUserByEmail('nonexistent@example.com'); + expect(userByEmail).to.be.null; + }); + }); +}); diff --git a/test/db/mongo/run-mongo-tests-docker.js b/test/db/mongo/run-mongo-tests-docker.js new file mode 100644 index 000000000..8512cc80c --- /dev/null +++ b/test/db/mongo/run-mongo-tests-docker.js @@ -0,0 +1,85 @@ +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const originalConfigPath = path.join(__dirname, '../../../proxy.config.json'); +const testConfigPath = path.join(__dirname, './test-config.json'); + +// Backup original config +if (fs.existsSync(originalConfigPath)) { + fs.copyFileSync(originalConfigPath, originalConfigPath + '.backup'); +} + +// Copy test config +fs.copyFileSync(testConfigPath, originalConfigPath); + +console.log('Starting MongoDB with Docker...'); + +// Start MongoDB container +const dockerComposeProcess = spawn( + 'docker', + ['compose', '-f', 'docker-compose.mongo-test.yml', 'up', '-d'], + { + cwd: path.join(__dirname, '../../..'), + stdio: 'inherit', + }, +); + +dockerComposeProcess.on('close', (code) => { + if (code !== 0) { + console.error('Failed to start MongoDB container.'); + process.exit(1); + } + + console.log('MongoDB container started, waiting for it to be ready...'); + + // Wait for MongoDB to be ready + setTimeout(() => { + process.env.GIT_PROXY_MONGO_CONNECTION_STRING = 'mongodb://localhost:27017/git-proxy-test'; + console.log('Running MongoDB integration tests...'); + console.log('MongoDB Connection String:', process.env.GIT_PROXY_MONGO_CONNECTION_STRING); + + // Run the MongoDB integration tests and database comparison tests + const testProcess = spawn( + 'npx', + [ + 'ts-mocha', + 'test/db/mongo/integration.test.js', + 'test/db/database-comparison.test.js', + '--timeout', + '30000', + '--exit', + '--project', + '../../tsconfig.json', + ], + { + cwd: path.join(__dirname, '../../..'), + stdio: 'inherit', + env: process.env, + }, + ); + + testProcess.on('close', (code) => { + // Restore original config + if (fs.existsSync(originalConfigPath + '.backup')) { + fs.copyFileSync(originalConfigPath + '.backup', originalConfigPath); + fs.unlinkSync(originalConfigPath + '.backup'); + } + + // Stop MongoDB container + console.log('Stopping MongoDB container...'); + const stopProcess = spawn( + 'docker', + ['compose', '-f', 'docker-compose.mongo-test.yml', 'down'], + { + cwd: path.join(__dirname, '../../..'), + stdio: 'inherit', + }, + ); + + stopProcess.on('close', () => { + process.exit(code); + }); + }); + }, 10000); // Wait 10 seconds for MongoDB to fully start +}); diff --git a/test/db/mongo/run-mongo-tests.js b/test/db/mongo/run-mongo-tests.js new file mode 100644 index 000000000..21fd5ed9a --- /dev/null +++ b/test/db/mongo/run-mongo-tests.js @@ -0,0 +1,47 @@ +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const originalConfigPath = path.join(__dirname, '../../../proxy.config.json'); +const testConfigPath = path.join(__dirname, './test-config.json'); + +// Backup original config +if (fs.existsSync(originalConfigPath)) { + fs.copyFileSync(originalConfigPath, originalConfigPath + '.backup'); +} + +// Copy test config +fs.copyFileSync(testConfigPath, originalConfigPath); + +console.log('Starting MongoDB integration tests...'); +console.log('MongoDB Connection String:', process.env.GIT_PROXY_MONGO_CONNECTION_STRING); + +// Run the MongoDB integration tests and database comparison tests +const testProcess = spawn( + 'npx', + [ + 'ts-mocha', + 'test/db/mongo/integration.test.js', + 'test/db/database-comparison.test.js', + '--timeout', + '15000', + '--exit', + '--project', + '../../tsconfig.json', + ], + { + cwd: path.join(__dirname, '../../..'), + stdio: 'inherit', + env: process.env, + }, +); + +testProcess.on('close', (code) => { + // Restore original config + if (fs.existsSync(originalConfigPath + '.backup')) { + fs.copyFileSync(originalConfigPath + '.backup', originalConfigPath); + fs.unlinkSync(originalConfigPath + '.backup'); + } + + process.exit(code); +}); diff --git a/test/db/mongo/test-config.json b/test/db/mongo/test-config.json new file mode 100644 index 000000000..8eca018a4 --- /dev/null +++ b/test/db/mongo/test-config.json @@ -0,0 +1,6 @@ +{ + "sink": { + "type": "mongo", + "enabled": true + } +}