Skip to content

Commit c6c703e

Browse files
committed
Merge branch 'main' into 1281-improve-processors-logging
2 parents cd42eeb + d65c9b8 commit c6c703e

14 files changed

Lines changed: 491 additions & 22 deletions

File tree

.github/workflows/e2e.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,27 @@ jobs:
5252
- name: Run E2E tests
5353
run: npm run test:e2e
5454

55+
- name: Run Cypress E2E tests
56+
run: npm run cypress:run:docker
57+
timeout-minutes: 10
58+
env:
59+
CYPRESS_BASE_URL: http://localhost:8081
60+
CYPRESS_API_BASE_URL: http://localhost:8081
61+
CYPRESS_GIT_PROXY_URL: http://localhost:8000
62+
CYPRESS_GIT_SERVER_TARGET: git-server:8443
63+
64+
- name: Dump git-proxy logs on failure
65+
if: failure()
66+
run: docker compose logs git-proxy --tail=100
67+
68+
- name: Upload Cypress screenshots on failure
69+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
70+
if: failure()
71+
with:
72+
name: cypress-screenshots
73+
path: cypress/screenshots
74+
retention-days: 7
75+
5576
- name: Stop services
5677
if: always()
5778
run: docker compose down -v

cypress.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ const { defineConfig } = require('cypress');
1919
module.exports = defineConfig({
2020
e2e: {
2121
baseUrl: process.env.CYPRESS_BASE_URL || 'http://localhost:3000',
22+
specPattern: 'cypress/e2e/*.cy.{js,ts}',
2223
chromeWebSecurity: false, // Required for OIDC testing
24+
env: {
25+
API_BASE_URL: process.env.CYPRESS_API_BASE_URL || 'http://localhost:8080',
26+
GIT_PROXY_URL: process.env.CYPRESS_GIT_PROXY_URL || 'http://localhost:8000',
27+
GIT_SERVER_TARGET: process.env.CYPRESS_GIT_SERVER_TARGET || 'git-server:8443',
28+
},
2329
setupNodeEvents(on, config) {
2430
on('task', {
2531
log(message) {
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/**
2+
* Copyright 2026 GitProxy Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
describe('Push Actions (Approve, Reject, Cancel)', () => {
18+
const testUser = {
19+
username: 'testuser',
20+
password: 'user123',
21+
email: 'testuser@example.com',
22+
gitAccount: 'testuser',
23+
};
24+
25+
const approverUser = {
26+
username: 'approver',
27+
password: 'approver123',
28+
email: 'approver@example.com',
29+
gitAccount: 'approver',
30+
};
31+
32+
before(() => {
33+
// Setup: login as admin, create test users, assign permissions
34+
cy.login('admin', 'admin');
35+
36+
cy.createUser(testUser.username, testUser.password, testUser.email, testUser.gitAccount);
37+
cy.createUser(
38+
approverUser.username,
39+
approverUser.password,
40+
approverUser.email,
41+
approverUser.gitAccount,
42+
);
43+
44+
cy.getTestRepoId().then((repoId) => {
45+
cy.addUserPushPermission(repoId, testUser.username);
46+
cy.addUserAuthorisePermission(repoId, approverUser.username);
47+
});
48+
49+
cy.logout();
50+
});
51+
52+
afterEach(() => {
53+
cy.logout();
54+
});
55+
56+
describe('Approve flow', () => {
57+
beforeEach(() => {
58+
const suffix = `approve-${Date.now()}`;
59+
cy.createPush(testUser.username, testUser.password, testUser.email, suffix).as('pushId');
60+
});
61+
62+
it('should approve a pending push via attestation dialog', function () {
63+
cy.login(approverUser.username, approverUser.password);
64+
cy.visit(`/dashboard/push/${this.pushId}`);
65+
66+
// Verify push is Pending
67+
cy.get('[data-testid="push-status"]').should('contain', 'Pending');
68+
69+
// Action buttons should be visible
70+
cy.get('[data-testid="push-cancel-btn"]').should('be.visible');
71+
cy.get('[data-testid="push-reject-btn"]').should('be.visible');
72+
cy.get('[data-testid="attestation-open-btn"]').should('be.visible');
73+
74+
// Open attestation dialog
75+
cy.get('[data-testid="attestation-open-btn"]').click();
76+
cy.get('[data-testid="attestation-dialog"]').should('be.visible');
77+
78+
// Confirm button should be disabled until all checkboxes are checked
79+
cy.get('[data-testid="attestation-confirm-btn"]').should('be.disabled');
80+
81+
// Check all attestation checkboxes
82+
cy.get('[data-testid="attestation-dialog"]')
83+
.find('input[type="checkbox"]')
84+
.each(($checkbox) => {
85+
cy.wrap($checkbox).check({ force: true });
86+
});
87+
88+
// Confirm button should now be enabled
89+
cy.get('[data-testid="attestation-confirm-btn"]').should('not.be.disabled');
90+
91+
// Click confirm to approve
92+
cy.get('[data-testid="attestation-confirm-btn"]').click();
93+
94+
// Should navigate back to push list
95+
cy.url().should('include', '/dashboard/push');
96+
cy.url().should('not.include', this.pushId);
97+
98+
// Verify push is now Approved by revisiting its detail page
99+
cy.visit(`/dashboard/push/${this.pushId}`);
100+
cy.get('[data-testid="push-status"]').should('contain', 'Approved');
101+
102+
// Action buttons should no longer be visible for an approved push
103+
cy.get('[data-testid="push-cancel-btn"]').should('not.exist');
104+
cy.get('[data-testid="push-reject-btn"]').should('not.exist');
105+
cy.get('[data-testid="attestation-open-btn"]').should('not.exist');
106+
});
107+
});
108+
109+
describe('Reject flow', () => {
110+
beforeEach(() => {
111+
const suffix = `reject-${Date.now()}`;
112+
cy.createPush(testUser.username, testUser.password, testUser.email, suffix).as('pushId');
113+
});
114+
115+
it('should reject a pending push', function () {
116+
cy.login(approverUser.username, approverUser.password);
117+
cy.visit(`/dashboard/push/${this.pushId}`);
118+
119+
// Verify push is Pending
120+
cy.get('[data-testid="push-status"]').should('contain', 'Pending');
121+
122+
// Open reject dialog
123+
cy.get('[data-testid="push-reject-btn"]').click();
124+
125+
// Confirm button should be disabled until reason is provided
126+
cy.get('[data-testid="push-reject-confirm-btn"]').should('be.disabled');
127+
128+
// Fill in rejection reason
129+
cy.get('#reason').type('Rejecting for test purposes');
130+
131+
// Confirm button should now be enabled
132+
cy.get('[data-testid="push-reject-confirm-btn"]').should('not.be.disabled');
133+
134+
// Confirm rejection
135+
cy.get('[data-testid="push-reject-confirm-btn"]').click();
136+
137+
// Should navigate back to push list
138+
cy.url().should('include', '/dashboard/push');
139+
cy.url().should('not.include', this.pushId);
140+
141+
// Verify push is now Rejected
142+
cy.visit(`/dashboard/push/${this.pushId}`);
143+
cy.get('[data-testid="push-status"]').should('contain', 'Rejected');
144+
145+
// Action buttons should no longer be visible
146+
cy.get('[data-testid="push-cancel-btn"]').should('not.exist');
147+
cy.get('[data-testid="push-reject-btn"]').should('not.exist');
148+
cy.get('[data-testid="attestation-open-btn"]').should('not.exist');
149+
});
150+
});
151+
152+
describe('Cancel flow', () => {
153+
beforeEach(() => {
154+
const suffix = `cancel-${Date.now()}`;
155+
cy.createPush(testUser.username, testUser.password, testUser.email, suffix).as('pushId');
156+
});
157+
158+
it('should cancel a pending push', function () {
159+
// Cancel can be done by the push author
160+
cy.login(testUser.username, testUser.password);
161+
cy.visit(`/dashboard/push/${this.pushId}`);
162+
163+
// Verify push is Pending
164+
cy.get('[data-testid="push-status"]').should('contain', 'Pending');
165+
166+
// Click Cancel
167+
cy.get('[data-testid="push-cancel-btn"]').click();
168+
169+
// Should navigate back to push list
170+
cy.url().should('include', '/dashboard/push');
171+
172+
// Verify push is now Canceled
173+
cy.visit(`/dashboard/push/${this.pushId}`);
174+
cy.get('[data-testid="push-status"]').should('contain', 'Canceled');
175+
176+
// Action buttons should no longer be visible
177+
cy.get('[data-testid="push-cancel-btn"]').should('not.exist');
178+
cy.get('[data-testid="push-reject-btn"]').should('not.exist');
179+
cy.get('[data-testid="attestation-open-btn"]').should('not.exist');
180+
});
181+
});
182+
183+
describe('Negative: unauthorized approve', () => {
184+
beforeEach(() => {
185+
const suffix = `neg-approve-${Date.now()}`;
186+
cy.createPush(testUser.username, testUser.password, testUser.email, suffix).as('pushId');
187+
});
188+
189+
it('should not change push state when user lacks canAuthorise permission', function () {
190+
// Login as testuser (has canPush but NOT canAuthorise)
191+
cy.login(testUser.username, testUser.password);
192+
cy.visit(`/dashboard/push/${this.pushId}`);
193+
194+
cy.get('[data-testid="push-status"]').should('contain', 'Pending');
195+
196+
// Open attestation dialog and attempt to approve
197+
cy.get('[data-testid="attestation-open-btn"]').click();
198+
cy.get('[data-testid="attestation-dialog"]').should('be.visible');
199+
200+
// Check all checkboxes
201+
cy.get('[data-testid="attestation-dialog"]')
202+
.find('input[type="checkbox"]')
203+
.each(($checkbox) => {
204+
cy.wrap($checkbox).check({ force: true });
205+
});
206+
207+
cy.get('[data-testid="attestation-confirm-btn"]').click();
208+
209+
// TODO: The server correctly returns 403 but the UI (src/ui/services/git-push.ts)
210+
// only handles 401 errors in authorisePush/rejectPush. The 403 is silently
211+
// ignored and the user is navigated away without feedback. Once the UI properly
212+
// handles 403, this test should assert a snackbar error message is shown.
213+
cy.visit(`/dashboard/push/${this.pushId}`);
214+
cy.get('[data-testid="push-status"]').should('contain', 'Pending');
215+
});
216+
});
217+
218+
describe('Negative: unauthorized reject', () => {
219+
beforeEach(() => {
220+
const suffix = `neg-reject-${Date.now()}`;
221+
cy.createPush(testUser.username, testUser.password, testUser.email, suffix).as('pushId');
222+
});
223+
224+
it('should not change push state when user lacks canAuthorise permission', function () {
225+
// Login as testuser
226+
cy.login(testUser.username, testUser.password);
227+
cy.visit(`/dashboard/push/${this.pushId}`);
228+
229+
cy.get('[data-testid="push-status"]').should('contain', 'Pending');
230+
231+
// Click Reject
232+
cy.get('[data-testid="push-reject-btn"]').click();
233+
234+
// TODO: Same issue as unauthorized approve — UI ignores 403 from server.
235+
// Once fixed, assert snackbar error message is shown.
236+
cy.visit(`/dashboard/push/${this.pushId}`);
237+
cy.get('[data-testid="push-status"]').should('contain', 'Pending');
238+
});
239+
});
240+
241+
describe('Attestation dialog cancel does not cancel the push', () => {
242+
beforeEach(() => {
243+
const suffix = `dialog-cancel-${Date.now()}`;
244+
cy.createPush(testUser.username, testUser.password, testUser.email, suffix).as('pushId');
245+
});
246+
247+
it('should close attestation dialog without affecting push status', function () {
248+
cy.login(approverUser.username, approverUser.password);
249+
cy.visit(`/dashboard/push/${this.pushId}`);
250+
251+
cy.get('[data-testid="push-status"]').should('contain', 'Pending');
252+
253+
// Open attestation dialog
254+
cy.get('[data-testid="attestation-open-btn"]').click();
255+
cy.get('[data-testid="attestation-dialog"]').should('be.visible');
256+
257+
// Click the dialog's Cancel button (NOT the push cancel button)
258+
cy.get('[data-testid="attestation-cancel-btn"]').click();
259+
260+
// Dialog should close, push should still be pending
261+
cy.get('[data-testid="attestation-dialog"]').should('not.exist');
262+
cy.get('[data-testid="push-status"]').should('contain', 'Pending');
263+
264+
// Action buttons should still be visible (push is still pending)
265+
cy.get('[data-testid="push-cancel-btn"]').should('be.visible');
266+
cy.get('[data-testid="push-reject-btn"]').should('be.visible');
267+
cy.get('[data-testid="attestation-open-btn"]').should('be.visible');
268+
});
269+
});
270+
});

cypress/e2e/repo.cy.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ describe('Repo', () => {
1818
let cookies;
1919
let repoName;
2020

21+
before(() => {
22+
cy.login('admin', 'admin');
23+
cy.cleanupTestRepos();
24+
cy.logout();
25+
});
26+
2127
describe('Anonymous users', () => {
2228
beforeEach(() => {
2329
cy.visit('/dashboard/repo');
@@ -70,8 +76,6 @@ describe('Repo', () => {
7076
});
7177

7278
cy.contains('a', `cypress-test/${repoName}`, { timeout: 10000 }).click();
73-
74-
// cy.get('[data-testid="delete-repo-button"]').click();
7579
});
7680

7781
it('Displays an error when adding an existing repo', () => {
@@ -100,11 +104,13 @@ describe('Repo', () => {
100104
// Create a new repo
101105
cy.getCSRFToken().then((csrfToken) => {
102106
repoName = `${Date.now()}`;
103-
cloneURL = `http://localhost:8000/github.com/cypress-test/${repoName}.git`;
107+
const gitProxyUrl = Cypress.env('GIT_PROXY_URL') || 'http://localhost:8000';
108+
cloneURL = `${gitProxyUrl}/github.com/cypress-test/${repoName}.git`;
104109

110+
const apiBaseUrl = Cypress.env('API_BASE_URL') || Cypress.config('baseUrl');
105111
cy.request({
106112
method: 'POST',
107-
url: 'http://localhost:8080/api/v1/repo',
113+
url: `${apiBaseUrl}/api/v1/repo`,
108114
body: {
109115
project: 'cypress-test',
110116
name: repoName,
@@ -161,7 +167,7 @@ describe('Repo', () => {
161167
cy.getCSRFToken().then((csrfToken) => {
162168
cy.request({
163169
method: 'DELETE',
164-
url: `http://localhost:8080/api/v1/repo/${repoName}/delete`,
170+
url: `${Cypress.env('API_BASE_URL') || Cypress.config('baseUrl')}/api/v1/repo/${repoId}/delete`,
165171
headers: {
166172
cookie: cookies?.join('; ') || '',
167173
'X-CSRF-TOKEN': csrfToken,

0 commit comments

Comments
 (0)