Skip to content

Commit 2351fa2

Browse files
authored
Merge pull request #1382 from dcoric/denis-coric/mongo-ci
feat: Add MongoDB integration tests with CI support
2 parents 6fb63d0 + b691168 commit 2351fa2

10 files changed

Lines changed: 815 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ jobs:
6161
npm run test-coverage-ci
6262
npm run test-coverage-ci --workspaces --if-present
6363
64+
- name: MongoDB Integration Tests
65+
env:
66+
RUN_MONGO_TESTS: 'true'
67+
GIT_PROXY_MONGO_CONNECTION_STRING: mongodb://localhost:27017/git-proxy-test
68+
run: npm run test:integration
69+
6470
- name: Upload test coverage report
6571
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
6672
with:

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"test:e2e:watch": "vitest --config vitest.config.e2e.ts",
6060
"test-coverage": "cross-env NODE_ENV=test vitest --run --dir ./test --coverage",
6161
"test-coverage-ci": "cross-env NODE_ENV=test vitest --run --dir ./test --coverage.enabled=true --coverage.reporter=lcovonly --coverage.reporter=text",
62+
"test:integration": "NODE_ENV=test vitest --run --config vitest.config.integration.ts",
6263
"test-watch": "cross-env NODE_ENV=test vitest --dir ./test --watch",
6364
"prepare": "node ./scripts/prepare.js",
6465
"lint": "eslint",

src/db/mongo/helper.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ import MongoDBStore from 'connect-mongo';
44
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
55

66
let _db: Db | null = null;
7+
let _client: MongoClient | null = null;
8+
9+
export const resetConnection = async (): Promise<void> => {
10+
if (_client) {
11+
await _client.close();
12+
_client = null;
13+
_db = null;
14+
}
15+
};
16+
17+
export const getDb = (): Db | null => _db;
718

819
export const connect = async (collectionName: string): Promise<Collection> => {
920
//retrieve config at point of use (rather than import)
@@ -21,9 +32,9 @@ export const connect = async (collectionName: string): Promise<Collection> => {
2132
(options.authMechanismProperties.AWS_CREDENTIAL_PROVIDER as any) = fromNodeProviderChain();
2233
}
2334

24-
const client = new MongoClient(connectionString, options);
25-
await client.connect();
26-
_db = client.db();
35+
_client = new MongoClient(connectionString, options);
36+
await _client.connect();
37+
_db = _client.db();
2738
}
2839

2940
return _db.collection(collectionName);

test-integration.proxy.config.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"cookieSecret": "integration-test-cookie-secret",
3+
"sessionMaxAgeHours": 12,
4+
"sink": [
5+
{
6+
"type": "fs",
7+
"enabled": false
8+
},
9+
{
10+
"type": "mongo",
11+
"connectionString": "mongodb://localhost:27017/git-proxy-test",
12+
"options": {
13+
"useNewUrlParser": true,
14+
"useUnifiedTopology": true
15+
},
16+
"enabled": true
17+
}
18+
],
19+
"authentication": [
20+
{
21+
"type": "local",
22+
"enabled": true
23+
}
24+
]
25+
}
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
import { describe, it, expect, beforeEach } from 'vitest';
2+
import {
3+
writeAudit,
4+
getPush,
5+
getPushes,
6+
deletePush,
7+
authorise,
8+
reject,
9+
cancel,
10+
} from '../../../src/db/mongo/pushes';
11+
import { Action } from '../../../src/proxy/actions';
12+
13+
const shouldRunMongoTests = process.env.RUN_MONGO_TESTS === 'true';
14+
15+
describe.runIf(shouldRunMongoTests)('MongoDB Pushes Integration Tests', () => {
16+
const createTestAction = (overrides: Partial<Action> = {}): Action => {
17+
const timestamp = Date.now();
18+
const action = new Action(
19+
overrides.id || `test-push-${timestamp}`,
20+
overrides.type || 'push',
21+
overrides.method || 'POST',
22+
overrides.timestamp || timestamp,
23+
overrides.url || 'https://github.com/test/repo.git',
24+
);
25+
26+
action.error = overrides.error ?? false;
27+
action.blocked = overrides.blocked ?? true;
28+
action.allowPush = overrides.allowPush ?? false;
29+
action.authorised = overrides.authorised ?? false;
30+
action.canceled = overrides.canceled ?? false;
31+
action.rejected = overrides.rejected ?? false;
32+
33+
return action;
34+
};
35+
36+
describe('writeAudit', () => {
37+
it('should write an action to the database', async () => {
38+
const action = createTestAction({ id: 'write-audit-test' });
39+
40+
await writeAudit(action);
41+
42+
const retrieved = await getPush('write-audit-test');
43+
expect(retrieved).not.toBeNull();
44+
expect(retrieved?.id).toBe('write-audit-test');
45+
});
46+
47+
it('should upsert an existing action', async () => {
48+
const action = createTestAction({ id: 'upsert-test' });
49+
await writeAudit(action);
50+
51+
action.blocked = false;
52+
action.allowPush = true;
53+
await writeAudit(action);
54+
55+
const retrieved = await getPush('upsert-test');
56+
expect(retrieved?.blocked).toBe(false);
57+
expect(retrieved?.allowPush).toBe(true);
58+
});
59+
60+
it('should throw error for invalid id', async () => {
61+
const action = createTestAction();
62+
(action as any).id = 123;
63+
64+
await expect(writeAudit(action)).rejects.toThrow('Invalid id');
65+
});
66+
67+
it('should strip _id from action before saving', async () => {
68+
const action = createTestAction({ id: 'strip-id-test' });
69+
(action as any)._id = 'should-be-removed';
70+
71+
await writeAudit(action);
72+
73+
const retrieved = await getPush('strip-id-test');
74+
expect(retrieved).not.toBeNull();
75+
expect(retrieved?.id).toBe('strip-id-test');
76+
});
77+
});
78+
79+
describe('getPush', () => {
80+
it('should retrieve a push by id', async () => {
81+
const action = createTestAction({ id: 'get-push-test' });
82+
await writeAudit(action);
83+
84+
const result = await getPush('get-push-test');
85+
86+
expect(result).not.toBeNull();
87+
expect(result?.id).toBe('get-push-test');
88+
expect(result?.type).toBe('push');
89+
});
90+
91+
it('should return null for non-existent push', async () => {
92+
const result = await getPush('non-existent-push');
93+
94+
expect(result).toBeNull();
95+
});
96+
97+
it('should return an Action instance', async () => {
98+
const action = createTestAction({ id: 'action-instance-test' });
99+
await writeAudit(action);
100+
101+
const result = await getPush('action-instance-test');
102+
103+
expect(Object.getPrototypeOf(result)).toBe(Action.prototype);
104+
});
105+
});
106+
107+
describe('getPushes', () => {
108+
beforeEach(async () => {
109+
await writeAudit(
110+
createTestAction({
111+
id: 'push-list-1',
112+
blocked: true,
113+
allowPush: false,
114+
authorised: false,
115+
error: false,
116+
}),
117+
);
118+
await writeAudit(
119+
createTestAction({
120+
id: 'push-list-2',
121+
blocked: true,
122+
allowPush: false,
123+
authorised: false,
124+
error: false,
125+
}),
126+
);
127+
await writeAudit(
128+
createTestAction({
129+
id: 'push-authorised',
130+
blocked: true,
131+
allowPush: false,
132+
authorised: true,
133+
error: false,
134+
}),
135+
);
136+
});
137+
138+
it('should retrieve pushes matching default query', async () => {
139+
const result = await getPushes();
140+
141+
const matchingPushes = result.filter((p) => ['push-list-1', 'push-list-2'].includes(p.id));
142+
expect(matchingPushes.length).toBe(2);
143+
});
144+
145+
it('should filter pushes by custom query', async () => {
146+
const result = await getPushes({ authorised: true });
147+
148+
const authorisedPush = result.find((p) => p.id === 'push-authorised');
149+
expect(authorisedPush).toBeDefined();
150+
});
151+
152+
it('should return projected fields only', async () => {
153+
const result = await getPushes();
154+
155+
result.forEach((push) => {
156+
expect((push as any)._id).toBeUndefined();
157+
expect(push.id).toBeDefined();
158+
});
159+
});
160+
});
161+
162+
describe('deletePush', () => {
163+
it('should delete a push by id', async () => {
164+
const action = createTestAction({ id: 'delete-test' });
165+
await writeAudit(action);
166+
167+
await deletePush('delete-test');
168+
169+
const result = await getPush('delete-test');
170+
expect(result).toBeNull();
171+
});
172+
173+
it('should not throw when deleting non-existent push', async () => {
174+
await expect(deletePush('non-existent')).resolves.not.toThrow();
175+
});
176+
});
177+
178+
describe('authorise', () => {
179+
it('should authorise a push and update flags', async () => {
180+
const action = createTestAction({
181+
id: 'authorise-test',
182+
authorised: false,
183+
canceled: true,
184+
rejected: true,
185+
});
186+
await writeAudit(action);
187+
188+
const result = await authorise('authorise-test', { note: 'approved' });
189+
190+
expect(result.message).toBe('authorised authorise-test');
191+
192+
const updated = await getPush('authorise-test');
193+
expect(updated?.authorised).toBe(true);
194+
expect(updated?.canceled).toBe(false);
195+
expect(updated?.rejected).toBe(false);
196+
expect(updated?.attestation).toEqual({ note: 'approved' });
197+
});
198+
199+
it('should throw error for non-existent push', async () => {
200+
await expect(authorise('non-existent', {})).rejects.toThrow('push non-existent not found');
201+
});
202+
});
203+
204+
describe('reject', () => {
205+
it('should reject a push and update flags', async () => {
206+
const action = createTestAction({
207+
id: 'reject-test',
208+
authorised: true,
209+
canceled: true,
210+
rejected: false,
211+
});
212+
await writeAudit(action);
213+
214+
const result = await reject('reject-test', { reason: 'policy violation' });
215+
216+
expect(result.message).toBe('reject reject-test');
217+
218+
const updated = await getPush('reject-test');
219+
expect(updated?.authorised).toBe(false);
220+
expect(updated?.canceled).toBe(false);
221+
expect(updated?.rejected).toBe(true);
222+
expect(updated?.attestation).toEqual({ reason: 'policy violation' });
223+
});
224+
225+
it('should throw error for non-existent push', async () => {
226+
await expect(reject('non-existent', {})).rejects.toThrow('push non-existent not found');
227+
});
228+
});
229+
230+
describe('cancel', () => {
231+
it('should cancel a push and update flags', async () => {
232+
const action = createTestAction({
233+
id: 'cancel-test',
234+
authorised: true,
235+
canceled: false,
236+
rejected: true,
237+
});
238+
await writeAudit(action);
239+
240+
const result = await cancel('cancel-test');
241+
242+
expect(result.message).toBe('canceled cancel-test');
243+
244+
const updated = await getPush('cancel-test');
245+
expect(updated?.authorised).toBe(false);
246+
expect(updated?.canceled).toBe(true);
247+
expect(updated?.rejected).toBe(false);
248+
});
249+
250+
it('should throw error for non-existent push', async () => {
251+
await expect(cancel('non-existent')).rejects.toThrow('push non-existent not found');
252+
});
253+
});
254+
});

0 commit comments

Comments
 (0)