From 2d86cceadc1f48e2da626889e46721bdc907d479 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Thu, 12 Feb 2026 15:15:20 +0100 Subject: [PATCH 01/21] fix: forward disabled prop to MUI Button in CustomButton component --- src/ui/components/CustomButtons/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/components/CustomButtons/Button.tsx b/src/ui/components/CustomButtons/Button.tsx index e6eea281a..91d40c5c3 100644 --- a/src/ui/components/CustomButtons/Button.tsx +++ b/src/ui/components/CustomButtons/Button.tsx @@ -62,7 +62,7 @@ export default function RegularButton(props: RegularButtonProps) { }); return ( - ); From 3b91f8c1966c0c373f3d664fc7fc41c38427cbb4 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Thu, 12 Feb 2026 15:15:51 +0100 Subject: [PATCH 02/21] feat: add data-testid attributes to push detail actions --- src/ui/views/PushDetails/PushDetails.tsx | 6 +++--- src/ui/views/PushDetails/components/Attestation.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ui/views/PushDetails/PushDetails.tsx b/src/ui/views/PushDetails/PushDetails.tsx index aec01fa20..ce3f4a1f4 100644 --- a/src/ui/views/PushDetails/PushDetails.tsx +++ b/src/ui/views/PushDetails/PushDetails.tsx @@ -146,14 +146,14 @@ const Dashboard: React.FC = () => { {generateIcon(headerData.title)} -

{headerData.title}

+

{headerData.title}

{!(push.canceled || push.rejected || push.authorised) && (
- - diff --git a/src/ui/views/PushDetails/components/Attestation.tsx b/src/ui/views/PushDetails/components/Attestation.tsx index c405eb2cf..ebf7cb50c 100644 --- a/src/ui/views/PushDetails/components/Attestation.tsx +++ b/src/ui/views/PushDetails/components/Attestation.tsx @@ -55,7 +55,7 @@ const Attestation: React.FC = ({ approveFn }) => { return (
- = ({ approveFn }) => { onClose={handleClose} aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description' + data-testid='attestation-dialog' style={{ margin: '0px 15px 0px 15px', padding: '20px' }} > = ({ approveFn }) => { - From 01a7881c07865b8e08ca3548831fadfe4e04501a Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Thu, 12 Feb 2026 15:17:06 +0100 Subject: [PATCH 03/21] feat: add e2e tests for dashboard push approve, reject and cancel --- cypress.config.js | 4 + cypress/e2e/pushActions.cy.js | 274 ++++++++++++++++++++++++++++++++++ cypress/e2e/repo.cy.js | 7 +- cypress/support/commands.js | 88 ++++++++++- 4 files changed, 369 insertions(+), 4 deletions(-) create mode 100644 cypress/e2e/pushActions.cy.js diff --git a/cypress.config.js b/cypress.config.js index 52b6317b6..783b54eef 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -4,6 +4,10 @@ module.exports = defineConfig({ e2e: { baseUrl: process.env.CYPRESS_BASE_URL || 'http://localhost:3000', chromeWebSecurity: false, // Required for OIDC testing + env: { + GIT_PROXY_URL: process.env.CYPRESS_GIT_PROXY_URL || 'http://localhost:8000', + GIT_SERVER_TARGET: process.env.CYPRESS_GIT_SERVER_TARGET || 'git-server:8443', + }, setupNodeEvents(on, config) { on('task', { log(message) { diff --git a/cypress/e2e/pushActions.cy.js b/cypress/e2e/pushActions.cy.js new file mode 100644 index 000000000..9d7bc413d --- /dev/null +++ b/cypress/e2e/pushActions.cy.js @@ -0,0 +1,274 @@ +describe('Push Actions (Approve, Reject, Cancel)', () => { + const testUser = { + username: 'testuser', + password: 'user123', + email: 'testuser@example.com', + gitAccount: 'testuser', + }; + + const approverUser = { + username: 'approver', + password: 'approver123', + email: 'approver@example.com', + gitAccount: 'approver', + }; + + before(() => { + // Setup: login as admin, create test users, assign permissions + cy.login('admin', 'admin'); + + cy.createUser(testUser.username, testUser.password, testUser.email, testUser.gitAccount); + cy.createUser( + approverUser.username, + approverUser.password, + approverUser.email, + approverUser.gitAccount, + ); + + cy.getTestRepoId().then((repoId) => { + cy.addUserPushPermission(repoId, testUser.username); + cy.addUserAuthorisePermission(repoId, approverUser.username); + }); + + cy.logout(); + }); + + describe('Approve flow', () => { + let pushId; + + before(() => { + const suffix = `approve-${Date.now()}`; + cy.createPush(testUser.username, testUser.password, testUser.email, suffix).then((id) => { + pushId = id; + }); + }); + + it('should approve a pending push via attestation dialog', () => { + cy.login(approverUser.username, approverUser.password); + cy.visit(`/dashboard/push/${pushId}`); + + // Verify push is Pending + cy.get('[data-testid="push-status"]').should('contain', 'Pending'); + + // Action buttons should be visible + cy.get('[data-testid="push-cancel-btn"]').should('be.visible'); + cy.get('[data-testid="push-reject-btn"]').should('be.visible'); + cy.get('[data-testid="attestation-open-btn"]').should('be.visible'); + + // Open attestation dialog + cy.get('[data-testid="attestation-open-btn"]').click(); + cy.get('[data-testid="attestation-dialog"]').should('be.visible'); + + // Confirm button should be disabled until all checkboxes are checked + cy.get('[data-testid="attestation-confirm-btn"]').should('be.disabled'); + + // Check all attestation checkboxes + cy.get('[data-testid="attestation-dialog"]') + .find('input[type="checkbox"]') + .each(($checkbox) => { + cy.wrap($checkbox).check({ force: true }); + }); + + // Confirm button should now be enabled + cy.get('[data-testid="attestation-confirm-btn"]').should('not.be.disabled'); + + // Click confirm to approve + cy.get('[data-testid="attestation-confirm-btn"]').click(); + + // Should navigate back to push list + cy.url().should('include', '/dashboard/push'); + cy.url().should('not.include', pushId); + + // Verify push is now Approved by revisiting its detail page + cy.visit(`/dashboard/push/${pushId}`); + cy.get('[data-testid="push-status"]').should('contain', 'Approved'); + + // Action buttons should no longer be visible for an approved push + cy.get('[data-testid="push-cancel-btn"]').should('not.exist'); + cy.get('[data-testid="push-reject-btn"]').should('not.exist'); + cy.get('[data-testid="attestation-open-btn"]').should('not.exist'); + + cy.logout(); + }); + }); + + describe('Reject flow', () => { + let pushId; + + before(() => { + const suffix = `reject-${Date.now()}`; + cy.createPush(testUser.username, testUser.password, testUser.email, suffix).then((id) => { + pushId = id; + }); + }); + + it('should reject a pending push', () => { + cy.login(approverUser.username, approverUser.password); + cy.visit(`/dashboard/push/${pushId}`); + + // Verify push is Pending + cy.get('[data-testid="push-status"]').should('contain', 'Pending'); + + // Click Reject + cy.get('[data-testid="push-reject-btn"]').click(); + + // Should navigate back to push list + cy.url().should('include', '/dashboard/push'); + cy.url().should('not.include', pushId); + + // Verify push is now Rejected + cy.visit(`/dashboard/push/${pushId}`); + cy.get('[data-testid="push-status"]').should('contain', 'Rejected'); + + // Action buttons should no longer be visible + cy.get('[data-testid="push-cancel-btn"]').should('not.exist'); + cy.get('[data-testid="push-reject-btn"]').should('not.exist'); + cy.get('[data-testid="attestation-open-btn"]').should('not.exist'); + + cy.logout(); + }); + }); + + describe('Cancel flow', () => { + let pushId; + + before(() => { + const suffix = `cancel-${Date.now()}`; + cy.createPush(testUser.username, testUser.password, testUser.email, suffix).then((id) => { + pushId = id; + }); + }); + + it('should cancel a pending push', () => { + // Cancel can be done by the push author + cy.login(testUser.username, testUser.password); + cy.visit(`/dashboard/push/${pushId}`); + + // Verify push is Pending + cy.get('[data-testid="push-status"]').should('contain', 'Pending'); + + // Click Cancel + cy.get('[data-testid="push-cancel-btn"]').click(); + + // Should navigate back to push list + cy.url().should('include', '/dashboard/push'); + + // Verify push is now Canceled + cy.visit(`/dashboard/push/${pushId}`); + cy.get('[data-testid="push-status"]').should('contain', 'Canceled'); + + // Action buttons should no longer be visible + cy.get('[data-testid="push-cancel-btn"]').should('not.exist'); + cy.get('[data-testid="push-reject-btn"]').should('not.exist'); + cy.get('[data-testid="attestation-open-btn"]').should('not.exist'); + + cy.logout(); + }); + }); + + describe('Negative: unauthorized approve', () => { + let pushId; + + before(() => { + const suffix = `neg-approve-${Date.now()}`; + cy.createPush(testUser.username, testUser.password, testUser.email, suffix).then((id) => { + pushId = id; + }); + }); + + it('should not change push state when user lacks canAuthorise permission', () => { + // Login as testuser (has canPush but NOT canAuthorise) + cy.login(testUser.username, testUser.password); + cy.visit(`/dashboard/push/${pushId}`); + + cy.get('[data-testid="push-status"]').should('contain', 'Pending'); + + // Open attestation dialog and attempt to approve + cy.get('[data-testid="attestation-open-btn"]').click(); + cy.get('[data-testid="attestation-dialog"]').should('be.visible'); + + // Check all checkboxes + cy.get('[data-testid="attestation-dialog"]') + .find('input[type="checkbox"]') + .each(($checkbox) => { + cy.wrap($checkbox).check({ force: true }); + }); + + cy.get('[data-testid="attestation-confirm-btn"]').click(); + + // TODO: The server correctly returns 403 but the UI (src/ui/services/git-push.ts) + // only handles 401 errors in authorisePush/rejectPush. The 403 is silently + // ignored and the user is navigated away without feedback. Once the UI properly + // handles 403, this test should assert a snackbar error message is shown. + cy.visit(`/dashboard/push/${pushId}`); + cy.get('[data-testid="push-status"]').should('contain', 'Pending'); + + cy.logout(); + }); + }); + + describe('Negative: unauthorized reject', () => { + let pushId; + + before(() => { + const suffix = `neg-reject-${Date.now()}`; + cy.createPush(testUser.username, testUser.password, testUser.email, suffix).then((id) => { + pushId = id; + }); + }); + + it('should not change push state when user lacks canAuthorise permission', () => { + // Login as testuser + cy.login(testUser.username, testUser.password); + cy.visit(`/dashboard/push/${pushId}`); + + cy.get('[data-testid="push-status"]').should('contain', 'Pending'); + + // Click Reject + cy.get('[data-testid="push-reject-btn"]').click(); + + // TODO: Same issue as unauthorized approve — UI ignores 403 from server. + // Once fixed, assert snackbar error message is shown. + cy.visit(`/dashboard/push/${pushId}`); + cy.get('[data-testid="push-status"]').should('contain', 'Pending'); + + cy.logout(); + }); + }); + + describe('Attestation dialog cancel does not cancel the push', () => { + let pushId; + + before(() => { + const suffix = `dialog-cancel-${Date.now()}`; + cy.createPush(testUser.username, testUser.password, testUser.email, suffix).then((id) => { + pushId = id; + }); + }); + + it('should close attestation dialog without affecting push status', () => { + cy.login(approverUser.username, approverUser.password); + cy.visit(`/dashboard/push/${pushId}`); + + cy.get('[data-testid="push-status"]').should('contain', 'Pending'); + + // Open attestation dialog + cy.get('[data-testid="attestation-open-btn"]').click(); + cy.get('[data-testid="attestation-dialog"]').should('be.visible'); + + // Click the dialog's Cancel button (NOT the push cancel button) + cy.get('[data-testid="attestation-cancel-btn"]').click(); + + // Dialog should close, push should still be pending + cy.get('[data-testid="attestation-dialog"]').should('not.exist'); + cy.get('[data-testid="push-status"]').should('contain', 'Pending'); + + // Action buttons should still be visible (push is still pending) + cy.get('[data-testid="push-cancel-btn"]').should('be.visible'); + cy.get('[data-testid="push-reject-btn"]').should('be.visible'); + cy.get('[data-testid="attestation-open-btn"]').should('be.visible'); + + cy.logout(); + }); + }); +}); diff --git a/cypress/e2e/repo.cy.js b/cypress/e2e/repo.cy.js index 5eca98737..9a53aee6c 100644 --- a/cypress/e2e/repo.cy.js +++ b/cypress/e2e/repo.cy.js @@ -84,11 +84,12 @@ describe('Repo', () => { // Create a new repo cy.getCSRFToken().then((csrfToken) => { repoName = `${Date.now()}`; - cloneURL = `http://localhost:8000/github.com/cypress-test/${repoName}.git`; + const gitProxyUrl = Cypress.env('GIT_PROXY_URL') || 'http://localhost:8000'; + cloneURL = `${gitProxyUrl}/github.com/cypress-test/${repoName}.git`; cy.request({ method: 'POST', - url: 'http://localhost:8080/api/v1/repo', + url: `${Cypress.env('API_BASE_URL') || Cypress.config('baseUrl')}/api/v1/repo`, body: { project: 'cypress-test', name: repoName, @@ -145,7 +146,7 @@ describe('Repo', () => { cy.getCSRFToken().then((csrfToken) => { cy.request({ method: 'DELETE', - url: `http://localhost:8080/api/v1/repo/${repoName}/delete`, + url: `${Cypress.env('API_BASE_URL') || Cypress.config('baseUrl')}/api/v1/repo/${repoName}/delete`, headers: { cookie: cookies?.join('; ') || '', 'X-CSRF-TOKEN': csrfToken, diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 5117d6cfc..ddbdfb198 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -45,7 +45,8 @@ Cypress.Commands.add('logout', () => { }); Cypress.Commands.add('getCSRFToken', () => { - return cy.request('GET', 'http://localhost:8080/api/v1/repo').then((res) => { + const apiBaseUrl = Cypress.env('API_BASE_URL') || Cypress.config('baseUrl'); + return cy.request('GET', `${apiBaseUrl}/api/v1/repo`).then((res) => { let cookies = res.headers['set-cookie']; if (typeof cookies === 'string') { @@ -65,3 +66,88 @@ Cypress.Commands.add('getCSRFToken', () => { return cy.wrap(decodeURIComponent(token)); }); }); + +Cypress.Commands.add('createUser', (username, password, email, gitAccount) => { + const apiBaseUrl = Cypress.env('API_BASE_URL') || Cypress.config('baseUrl'); + cy.request({ + method: 'POST', + url: `${apiBaseUrl}/api/auth/create-user`, + body: { username, password, email, gitAccount, admin: false }, + failOnStatusCode: false, + }); +}); + +Cypress.Commands.add('addUserPushPermission', (repoId, username) => { + const apiBaseUrl = Cypress.env('API_BASE_URL') || Cypress.config('baseUrl'); + cy.request({ + method: 'PATCH', + url: `${apiBaseUrl}/api/v1/repo/${repoId}/user/push`, + body: { username }, + failOnStatusCode: false, + }); +}); + +Cypress.Commands.add('addUserAuthorisePermission', (repoId, username) => { + const apiBaseUrl = Cypress.env('API_BASE_URL') || Cypress.config('baseUrl'); + cy.request({ + method: 'PATCH', + url: `${apiBaseUrl}/api/v1/repo/${repoId}/user/authorise`, + body: { username }, + failOnStatusCode: false, + }); +}); + +Cypress.Commands.add('getTestRepoId', () => { + const apiBaseUrl = Cypress.env('API_BASE_URL') || Cypress.config('baseUrl'); + cy.request('GET', `${apiBaseUrl}/api/v1/repo`).then((res) => { + const repo = res.body.find( + (r) => r.url === 'https://git-server:8443/coopernetes/test-repo.git', + ); + if (!repo) { + throw new Error('coopernetes/test-repo not found in database'); + } + return cy.wrap(repo._id); + }); +}); + +Cypress.Commands.add('createPush', (gitUser, gitPassword, gitEmail, uniqueSuffix) => { + const proxyUrl = Cypress.env('GIT_PROXY_URL') || 'http://localhost:8000'; + const gitServerTarget = Cypress.env('GIT_SERVER_TARGET') || 'git-server:8443'; + const repoUrl = `${proxyUrl.replace('://', `://${gitUser}:${gitPassword}@`)}/${gitServerTarget}/coopernetes/test-repo.git`; + const cloneDir = `/tmp/cypress-push-${uniqueSuffix}`; + + cy.exec(`rm -rf ${cloneDir}`, { failOnNonZeroExit: false }); + cy.exec(`git clone ${repoUrl} ${cloneDir}`, { + timeout: 30000, + env: { GIT_TERMINAL_PROMPT: '0', NODE_TLS_REJECT_UNAUTHORIZED: '0' }, + }); + cy.exec(`git -C ${cloneDir} config user.name "${gitUser}"`); + cy.exec(`git -C ${cloneDir} config user.email "${gitEmail}"`); + + // Pull any upstream changes to avoid conflicts from previous test runs + cy.exec(`git -C ${cloneDir} pull --rebase origin main`, { + failOnNonZeroExit: false, + timeout: 30000, + env: { GIT_TERMINAL_PROMPT: '0', NODE_TLS_REJECT_UNAUTHORIZED: '0' }, + }); + + const timestamp = Date.now(); + cy.exec( + `echo "test-${uniqueSuffix}-${timestamp}" > ${cloneDir}/cypress-test-${uniqueSuffix}.txt`, + ); + cy.exec(`git -C ${cloneDir} add .`); + cy.exec(`git -C ${cloneDir} commit -m "cypress e2e test: ${uniqueSuffix}"`); + cy.exec(`git -C ${cloneDir} push origin main 2>&1`, { + failOnNonZeroExit: false, + timeout: 30000, + env: { GIT_TERMINAL_PROMPT: '0', NODE_TLS_REJECT_UNAUTHORIZED: '0' }, + }).then((result) => { + const output = result.stdout + result.stderr; + const match = output.match(/dashboard\/push\/([a-f0-9_]+)/); + if (!match) { + throw new Error(`Could not extract push ID from git output:\n${output}`); + } + cy.exec(`rm -rf ${cloneDir}`, { failOnNonZeroExit: false }); + return cy.wrap(match[1]); + }); +}); From 1a4bf35395548af1aea8f8f91107dc6a8f4077ef Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Thu, 12 Feb 2026 15:17:26 +0100 Subject: [PATCH 04/21] ci: add Cypress e2e tests to CI workflow --- .github/workflows/e2e.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f23bc42f4..f9fa63e13 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -54,6 +54,19 @@ jobs: - name: Run E2E tests run: npm run test:e2e + - name: Run Cypress E2E tests + run: npx cypress run --env API_BASE_URL=http://localhost:8081,GIT_PROXY_URL=http://localhost:8000,GIT_SERVER_TARGET=git-server:8443 + env: + CYPRESS_BASE_URL: http://localhost:8081 + + - name: Upload Cypress screenshots on failure + uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: cypress/screenshots + retention-days: 7 + - name: Stop services if: always() run: docker compose down -v From 84a00463b418c5b4eda881306a9a0897bcb06488 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Fri, 13 Feb 2026 10:47:33 +0100 Subject: [PATCH 05/21] fix: improve Cypress commands reliability for CI/Docker environment --- cypress/e2e/pushActions.cy.js | 1 + cypress/e2e/repo.cy.js | 4 ++-- cypress/support/commands.js | 39 +++++++++++++++++++++++------------ 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/cypress/e2e/pushActions.cy.js b/cypress/e2e/pushActions.cy.js index 9d7bc413d..3b37691c3 100644 --- a/cypress/e2e/pushActions.cy.js +++ b/cypress/e2e/pushActions.cy.js @@ -16,6 +16,7 @@ describe('Push Actions (Approve, Reject, Cancel)', () => { before(() => { // Setup: login as admin, create test users, assign permissions cy.login('admin', 'admin'); + cy.visit('/'); // Ensure session cookies are active for cy.request calls cy.createUser(testUser.username, testUser.password, testUser.email, testUser.gitAccount); cy.createUser( diff --git a/cypress/e2e/repo.cy.js b/cypress/e2e/repo.cy.js index 9a53aee6c..963d14d8a 100644 --- a/cypress/e2e/repo.cy.js +++ b/cypress/e2e/repo.cy.js @@ -89,7 +89,7 @@ describe('Repo', () => { cy.request({ method: 'POST', - url: `${Cypress.env('API_BASE_URL') || Cypress.config('baseUrl')}/api/v1/repo`, + url: '/api/v1/repo', body: { project: 'cypress-test', name: repoName, @@ -146,7 +146,7 @@ describe('Repo', () => { cy.getCSRFToken().then((csrfToken) => { cy.request({ method: 'DELETE', - url: `${Cypress.env('API_BASE_URL') || Cypress.config('baseUrl')}/api/v1/repo/${repoName}/delete`, + url: `/api/v1/repo/${repoName}/delete`, headers: { cookie: cookies?.join('; ') || '', 'X-CSRF-TOKEN': csrfToken, diff --git a/cypress/support/commands.js b/cypress/support/commands.js index ddbdfb198..e9bc709f6 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -45,21 +45,21 @@ Cypress.Commands.add('logout', () => { }); Cypress.Commands.add('getCSRFToken', () => { - const apiBaseUrl = Cypress.env('API_BASE_URL') || Cypress.config('baseUrl'); - return cy.request('GET', `${apiBaseUrl}/api/v1/repo`).then((res) => { + return cy.request('GET', '/api/v1/repo').then((res) => { let cookies = res.headers['set-cookie']; if (typeof cookies === 'string') { cookies = [cookies]; } + // CSRF protection is disabled when NODE_ENV=test (Docker/CI) if (!cookies) { - throw new Error('No cookies found in response'); + return cy.wrap(''); } const csrfCookie = cookies.find((c) => c.startsWith('csrf=')); if (!csrfCookie) { - throw new Error('No CSRF cookie found in response headers'); + return cy.wrap(''); } const token = csrfCookie.split('=')[1].split(';')[0]; @@ -68,43 +68,56 @@ Cypress.Commands.add('getCSRFToken', () => { }); Cypress.Commands.add('createUser', (username, password, email, gitAccount) => { - const apiBaseUrl = Cypress.env('API_BASE_URL') || Cypress.config('baseUrl'); cy.request({ method: 'POST', - url: `${apiBaseUrl}/api/auth/create-user`, + url: '/api/auth/create-user', body: { username, password, email, gitAccount, admin: false }, failOnStatusCode: false, }); }); Cypress.Commands.add('addUserPushPermission', (repoId, username) => { - const apiBaseUrl = Cypress.env('API_BASE_URL') || Cypress.config('baseUrl'); cy.request({ method: 'PATCH', - url: `${apiBaseUrl}/api/v1/repo/${repoId}/user/push`, + url: `/api/v1/repo/${repoId}/user/push`, body: { username }, failOnStatusCode: false, }); }); Cypress.Commands.add('addUserAuthorisePermission', (repoId, username) => { - const apiBaseUrl = Cypress.env('API_BASE_URL') || Cypress.config('baseUrl'); cy.request({ method: 'PATCH', - url: `${apiBaseUrl}/api/v1/repo/${repoId}/user/authorise`, + url: `/api/v1/repo/${repoId}/user/authorise`, body: { username }, failOnStatusCode: false, }); }); Cypress.Commands.add('getTestRepoId', () => { - const apiBaseUrl = Cypress.env('API_BASE_URL') || Cypress.config('baseUrl'); - cy.request('GET', `${apiBaseUrl}/api/v1/repo`).then((res) => { + cy.request({ + method: 'GET', + url: '/api/v1/repo', + headers: { Accept: 'application/json' }, + failOnStatusCode: false, + }).then((res) => { + if (res.status !== 200) { + throw new Error( + `GET /api/v1/repo returned status ${res.status}: ${JSON.stringify(res.body).slice(0, 500)}`, + ); + } + if (!Array.isArray(res.body)) { + throw new Error( + `GET /api/v1/repo returned non-array (${typeof res.body}): ${JSON.stringify(res.body).slice(0, 500)}`, + ); + } const repo = res.body.find( (r) => r.url === 'https://git-server:8443/coopernetes/test-repo.git', ); if (!repo) { - throw new Error('coopernetes/test-repo not found in database'); + throw new Error( + `coopernetes/test-repo not found in database. Repos: ${res.body.map((r) => r.url).join(', ')}`, + ); } return cy.wrap(repo._id); }); From 39ae5b2f36ee6369bd25b7588e8985340f865954 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Fri, 13 Feb 2026 11:02:50 +0100 Subject: [PATCH 06/21] fix: use absolute URLs for Cypress API calls and handle CSRF gracefully --- cypress.config.js | 1 + cypress/e2e/pushActions.cy.js | 1 - cypress/e2e/repo.cy.js | 5 +++-- cypress/support/commands.js | 24 +++++++++++++++++------- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/cypress.config.js b/cypress.config.js index 783b54eef..7e5d6b758 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -5,6 +5,7 @@ module.exports = defineConfig({ baseUrl: process.env.CYPRESS_BASE_URL || 'http://localhost:3000', chromeWebSecurity: false, // Required for OIDC testing env: { + API_BASE_URL: process.env.CYPRESS_API_BASE_URL || 'http://localhost:8080', GIT_PROXY_URL: process.env.CYPRESS_GIT_PROXY_URL || 'http://localhost:8000', GIT_SERVER_TARGET: process.env.CYPRESS_GIT_SERVER_TARGET || 'git-server:8443', }, diff --git a/cypress/e2e/pushActions.cy.js b/cypress/e2e/pushActions.cy.js index 3b37691c3..9d7bc413d 100644 --- a/cypress/e2e/pushActions.cy.js +++ b/cypress/e2e/pushActions.cy.js @@ -16,7 +16,6 @@ describe('Push Actions (Approve, Reject, Cancel)', () => { before(() => { // Setup: login as admin, create test users, assign permissions cy.login('admin', 'admin'); - cy.visit('/'); // Ensure session cookies are active for cy.request calls cy.createUser(testUser.username, testUser.password, testUser.email, testUser.gitAccount); cy.createUser( diff --git a/cypress/e2e/repo.cy.js b/cypress/e2e/repo.cy.js index 963d14d8a..53a9dc43f 100644 --- a/cypress/e2e/repo.cy.js +++ b/cypress/e2e/repo.cy.js @@ -87,9 +87,10 @@ describe('Repo', () => { const gitProxyUrl = Cypress.env('GIT_PROXY_URL') || 'http://localhost:8000'; cloneURL = `${gitProxyUrl}/github.com/cypress-test/${repoName}.git`; + const apiBaseUrl = Cypress.env('API_BASE_URL') || Cypress.config('baseUrl'); cy.request({ method: 'POST', - url: '/api/v1/repo', + url: `${apiBaseUrl}/api/v1/repo`, body: { project: 'cypress-test', name: repoName, @@ -146,7 +147,7 @@ describe('Repo', () => { cy.getCSRFToken().then((csrfToken) => { cy.request({ method: 'DELETE', - url: `/api/v1/repo/${repoName}/delete`, + url: `${Cypress.env('API_BASE_URL') || Cypress.config('baseUrl')}/api/v1/repo/${repoName}/delete`, headers: { cookie: cookies?.join('; ') || '', 'X-CSRF-TOKEN': csrfToken, diff --git a/cypress/support/commands.js b/cypress/support/commands.js index e9bc709f6..a52a56e57 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -24,6 +24,15 @@ // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +/** + * Helper to get the API base URL for cy.request calls. + * cy.request with relative URLs may not resolve correctly in all environments, + * so we use absolute URLs constructed from Cypress.config('baseUrl'). + */ +function getApiBaseUrl() { + return Cypress.env('API_BASE_URL') || Cypress.config('baseUrl'); +} + // start of a login command with sessions // TODO: resolve issues with the CSRF token Cypress.Commands.add('login', (username, password) => { @@ -45,7 +54,7 @@ Cypress.Commands.add('logout', () => { }); Cypress.Commands.add('getCSRFToken', () => { - return cy.request('GET', '/api/v1/repo').then((res) => { + return cy.request('GET', `${getApiBaseUrl()}/api/v1/repo`).then((res) => { let cookies = res.headers['set-cookie']; if (typeof cookies === 'string') { @@ -70,7 +79,7 @@ Cypress.Commands.add('getCSRFToken', () => { Cypress.Commands.add('createUser', (username, password, email, gitAccount) => { cy.request({ method: 'POST', - url: '/api/auth/create-user', + url: `${getApiBaseUrl()}/api/auth/create-user`, body: { username, password, email, gitAccount, admin: false }, failOnStatusCode: false, }); @@ -79,7 +88,7 @@ Cypress.Commands.add('createUser', (username, password, email, gitAccount) => { Cypress.Commands.add('addUserPushPermission', (repoId, username) => { cy.request({ method: 'PATCH', - url: `/api/v1/repo/${repoId}/user/push`, + url: `${getApiBaseUrl()}/api/v1/repo/${repoId}/user/push`, body: { username }, failOnStatusCode: false, }); @@ -88,27 +97,28 @@ Cypress.Commands.add('addUserPushPermission', (repoId, username) => { Cypress.Commands.add('addUserAuthorisePermission', (repoId, username) => { cy.request({ method: 'PATCH', - url: `/api/v1/repo/${repoId}/user/authorise`, + url: `${getApiBaseUrl()}/api/v1/repo/${repoId}/user/authorise`, body: { username }, failOnStatusCode: false, }); }); Cypress.Commands.add('getTestRepoId', () => { + const url = `${getApiBaseUrl()}/api/v1/repo`; cy.request({ method: 'GET', - url: '/api/v1/repo', + url, headers: { Accept: 'application/json' }, failOnStatusCode: false, }).then((res) => { if (res.status !== 200) { throw new Error( - `GET /api/v1/repo returned status ${res.status}: ${JSON.stringify(res.body).slice(0, 500)}`, + `GET ${url} returned status ${res.status}: ${JSON.stringify(res.body).slice(0, 500)}`, ); } if (!Array.isArray(res.body)) { throw new Error( - `GET /api/v1/repo returned non-array (${typeof res.body}): ${JSON.stringify(res.body).slice(0, 500)}`, + `GET ${url} returned non-array (${typeof res.body}): ${JSON.stringify(res.body).slice(0, 500)}`, ); } const repo = res.body.find( From 12340850fdccc6fa8a0128774a3fe17ec6a7c4ea Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Fri, 13 Feb 2026 11:15:47 +0100 Subject: [PATCH 07/21] fix: ensure test config is loaded in Docker and disable CSRF for e2e --- docker-compose.yml | 1 + test-e2e.proxy.config.json | 1 + 2 files changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 27157df0c..3221c33c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ services: - git-network environment: - NODE_ENV=test + - CONFIG_FILE=/app/test-e2e.proxy.config.json - GIT_PROXY_UI_PORT=8081 - GIT_PROXY_SERVER_PORT=8000 - NODE_OPTIONS=--trace-warnings diff --git a/test-e2e.proxy.config.json b/test-e2e.proxy.config.json index ccf0926f4..6c0b002cf 100644 --- a/test-e2e.proxy.config.json +++ b/test-e2e.proxy.config.json @@ -1,5 +1,6 @@ { "cookieSecret": "integration-test-cookie-secret", + "csrfProtection": false, "sessionMaxAgeHours": 12, "rateLimit": { "windowMs": 60000, From fc074b54304e0dd5735e2b28c95594624a03676c Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Tue, 17 Feb 2026 12:36:24 +0100 Subject: [PATCH 08/21] chore(ci): add debug logging for git-proxy config in e2e workflow --- .github/workflows/e2e.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f9fa63e13..45f005e89 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -51,6 +51,15 @@ jobs: done ' || { echo "Service readiness check failed:"; docker compose ps; exit 1; } + - name: Debug git-proxy config + run: | + echo "=== Container env ===" + docker compose exec -T git-proxy sh -c 'echo CONFIG_FILE=$CONFIG_FILE && echo NODE_ENV=$NODE_ENV' + echo "=== Config file check ===" + docker compose exec -T git-proxy sh -c 'ls -la /app/test-e2e.proxy.config.json 2>&1 || echo "FILE NOT FOUND"' + echo "=== Server logs (last 30 lines) ===" + docker compose logs git-proxy --tail=30 + - name: Run E2E tests run: npm run test:e2e @@ -59,6 +68,10 @@ jobs: env: CYPRESS_BASE_URL: http://localhost:8081 + - name: Dump git-proxy logs on failure + if: failure() + run: docker compose logs git-proxy --tail=100 + - name: Upload Cypress screenshots on failure uses: actions/upload-artifact@v4 if: failure() From 6250d6a2771ad53a8db9a34d4fc823fe0500c1fa Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Tue, 17 Feb 2026 18:07:41 +0100 Subject: [PATCH 09/21] fix: bake test config into Docker image and add config diagnostics --- .github/workflows/e2e.yml | 13 +++++++++++-- Dockerfile | 1 + index.ts | 3 ++- src/config/index.ts | 13 +++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 45f005e89..45cd707c8 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -57,8 +57,17 @@ jobs: docker compose exec -T git-proxy sh -c 'echo CONFIG_FILE=$CONFIG_FILE && echo NODE_ENV=$NODE_ENV' echo "=== Config file check ===" docker compose exec -T git-proxy sh -c 'ls -la /app/test-e2e.proxy.config.json 2>&1 || echo "FILE NOT FOUND"' - echo "=== Server logs (last 30 lines) ===" - docker compose logs git-proxy --tail=30 + echo "=== Config file content ===" + docker compose exec -T git-proxy sh -c 'cat /app/test-e2e.proxy.config.json 2>&1 || echo "CANNOT READ"' + echo "=== Server logs (last 50 lines) ===" + docker compose logs git-proxy --tail=50 + echo "=== Host: what is listening on port 8081 ===" + ss -tlnp | grep 8081 || echo "nothing found with ss" + echo "=== Host: curl API repos endpoint ===" + curl -s http://localhost:8081/api/v1/repo | head -c 500 || echo "curl failed" + echo "" + echo "=== Host: curl healthcheck ===" + curl -s http://localhost:8081/api/v1/healthcheck || echo "healthcheck failed" - name: Run E2E tests run: npm run test:e2e diff --git a/Dockerfile b/Dockerfile index c99a098ca..7d32fb9da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ COPY --from=builder /out/node_modules/ /app/node_modules/ COPY --from=builder /out/dist/ /app/dist/ COPY --from=builder /out/build /app/dist/build/ COPY proxy.config.json config.schema.json ./ +COPY test-e2e.proxy.config.json /app/test-e2e.proxy.config.json COPY docker-entrypoint.sh /docker-entrypoint.sh USER root diff --git a/index.ts b/index.ts index 553d7a2c4..ea83cc706 100755 --- a/index.ts +++ b/index.ts @@ -5,7 +5,7 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import * as fs from 'fs'; import { getConfigFile, setConfigFile, validate } from './src/config/file'; -import { initUserConfig } from './src/config'; +import { initUserConfig, logConfiguration } from './src/config'; import { Proxy } from './src/proxy'; import { Service } from './src/service'; @@ -33,6 +33,7 @@ const argv = yargs(hideBin(process.argv)) console.log('Setting config file to: ' + (argv.c as string) || ''); setConfigFile((argv.c as string) || ''); initUserConfig(); +logConfiguration(); const configFile = getConfigFile(); if (argv.v) { diff --git a/src/config/index.ts b/src/config/index.ts index ca35c8b06..503c318e8 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -54,6 +54,11 @@ function loadFullConfiguration(): GitProxyConfig { let userSettings: Partial = {}; const userConfigFile = process.env.CONFIG_FILE || getConfigFile(); + console.log( + `[CONFIG] Resolving user config: CONFIG_FILE=${process.env.CONFIG_FILE}, getConfigFile()=${getConfigFile()}, resolved=${userConfigFile}`, + ); + console.log(`[CONFIG] File exists: ${existsSync(userConfigFile)}`); + if (existsSync(userConfigFile)) { try { const userConfigContent = readFileSync(userConfigFile, 'utf-8'); @@ -61,10 +66,18 @@ function loadFullConfiguration(): GitProxyConfig { // Don't use QuickType validation for partial configurations const rawUserConfig = JSON.parse(userConfigContent); userSettings = cleanUndefinedValues(rawUserConfig); + console.log(`[CONFIG] Loaded user config with keys: ${Object.keys(userSettings).join(', ')}`); + if (userSettings.authorisedList) { + console.log( + `[CONFIG] authorisedList from user config: ${JSON.stringify(userSettings.authorisedList)}`, + ); + } } catch (error) { console.error(`Error loading user config from ${userConfigFile}:`, error); throw error; } + } else { + console.log(`[CONFIG] User config file not found at ${userConfigFile}, using defaults only`); } _currentConfig = mergeConfigurations(defaultConfig, userSettings); From e5d3e8204cbc006ba04f598e987623cf19532f1a Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Tue, 17 Feb 2026 18:22:27 +0100 Subject: [PATCH 10/21] fix: bake test config into Docker image and add pre-Cypress diagnostics --- .github/workflows/e2e.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 45cd707c8..b2570db04 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -72,6 +72,21 @@ jobs: - name: Run E2E tests run: npm run test:e2e + - name: Pre-Cypress diagnostics + run: | + echo "=== All node processes ===" + ps aux | grep node | grep -v grep || echo "no node processes" + echo "=== Ports 8000, 8080, 8081 ===" + ss -tlnp | grep -E ':(8000|8080|8081)\b' || echo "nothing found" + echo "=== Kill any host node servers (not Docker) ===" + pkill -f 'tsx index.ts' || true + pkill -f 'concurrently' || true + sleep 1 + echo "=== After cleanup: ports ===" + ss -tlnp | grep -E ':(8000|8080|8081)\b' || echo "nothing found" + echo "=== Verify Docker repos API ===" + curl -s http://localhost:8081/api/v1/repo | python3 -m json.tool || echo "curl failed" + - name: Run Cypress E2E tests run: npx cypress run --env API_BASE_URL=http://localhost:8081,GIT_PROXY_URL=http://localhost:8000,GIT_SERVER_TARGET=git-server:8443 env: From 80ca6490651c69e1af6afd44a783692753ce86a2 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Tue, 17 Feb 2026 18:54:17 +0100 Subject: [PATCH 11/21] fix: move all Cypress tests to e2e.yml --- .github/workflows/ci.yml | 21 --------------------- .github/workflows/e2e.yml | 33 --------------------------------- index.ts | 3 +-- src/config/index.ts | 13 ------------- 4 files changed, 1 insertion(+), 69 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9234ed8af..c1482c906 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,27 +76,6 @@ jobs: - name: Build frontend run: npm run build-ui - - name: Save build folder - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: build-ubuntu-node-${{ matrix.node-version }}-mongo-${{ matrix.mongodb-version }} - if-no-files-found: error - path: build - - - name: Download the build folders - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 - with: - name: build-ubuntu-node-${{ matrix.node-version }}-mongo-${{ matrix.mongodb-version }} - path: build - - - name: Run cypress test - uses: cypress-io/github-action@f790eee7a50d9505912f50c2095510be7de06aa7 # v6.10.9 - with: - start: npm start & - wait-on: 'http://localhost:3000' - wait-on-timeout: 120 - command: npm run cypress:run - # Windows build - single combination for development support build-windows: runs-on: windows-latest diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b2570db04..7bc847b97 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -51,42 +51,9 @@ jobs: done ' || { echo "Service readiness check failed:"; docker compose ps; exit 1; } - - name: Debug git-proxy config - run: | - echo "=== Container env ===" - docker compose exec -T git-proxy sh -c 'echo CONFIG_FILE=$CONFIG_FILE && echo NODE_ENV=$NODE_ENV' - echo "=== Config file check ===" - docker compose exec -T git-proxy sh -c 'ls -la /app/test-e2e.proxy.config.json 2>&1 || echo "FILE NOT FOUND"' - echo "=== Config file content ===" - docker compose exec -T git-proxy sh -c 'cat /app/test-e2e.proxy.config.json 2>&1 || echo "CANNOT READ"' - echo "=== Server logs (last 50 lines) ===" - docker compose logs git-proxy --tail=50 - echo "=== Host: what is listening on port 8081 ===" - ss -tlnp | grep 8081 || echo "nothing found with ss" - echo "=== Host: curl API repos endpoint ===" - curl -s http://localhost:8081/api/v1/repo | head -c 500 || echo "curl failed" - echo "" - echo "=== Host: curl healthcheck ===" - curl -s http://localhost:8081/api/v1/healthcheck || echo "healthcheck failed" - - name: Run E2E tests run: npm run test:e2e - - name: Pre-Cypress diagnostics - run: | - echo "=== All node processes ===" - ps aux | grep node | grep -v grep || echo "no node processes" - echo "=== Ports 8000, 8080, 8081 ===" - ss -tlnp | grep -E ':(8000|8080|8081)\b' || echo "nothing found" - echo "=== Kill any host node servers (not Docker) ===" - pkill -f 'tsx index.ts' || true - pkill -f 'concurrently' || true - sleep 1 - echo "=== After cleanup: ports ===" - ss -tlnp | grep -E ':(8000|8080|8081)\b' || echo "nothing found" - echo "=== Verify Docker repos API ===" - curl -s http://localhost:8081/api/v1/repo | python3 -m json.tool || echo "curl failed" - - name: Run Cypress E2E tests run: npx cypress run --env API_BASE_URL=http://localhost:8081,GIT_PROXY_URL=http://localhost:8000,GIT_SERVER_TARGET=git-server:8443 env: diff --git a/index.ts b/index.ts index 2522486cb..433e8cd0a 100755 --- a/index.ts +++ b/index.ts @@ -5,7 +5,7 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import * as fs from 'fs'; import { getConfigFile, setConfigFile, validate } from './src/config/file'; -import { initUserConfig, logConfiguration } from './src/config'; +import { initUserConfig } from './src/config'; import { Proxy } from './src/proxy'; import { Service } from './src/service'; @@ -33,7 +33,6 @@ const argv = yargs(hideBin(process.argv)) console.log('Setting config file to: ' + (argv.c as string) || ''); setConfigFile((argv.c as string) || ''); initUserConfig(); -logConfiguration(); const configFile = getConfigFile(); if (argv.v) { diff --git a/src/config/index.ts b/src/config/index.ts index 668866fb3..133750dbb 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -55,11 +55,6 @@ function loadFullConfiguration(): GitProxyConfig { let userSettings: Partial = {}; const userConfigFile = process.env.CONFIG_FILE || getConfigFile(); - console.log( - `[CONFIG] Resolving user config: CONFIG_FILE=${process.env.CONFIG_FILE}, getConfigFile()=${getConfigFile()}, resolved=${userConfigFile}`, - ); - console.log(`[CONFIG] File exists: ${existsSync(userConfigFile)}`); - if (existsSync(userConfigFile)) { try { const userConfigContent = readFileSync(userConfigFile, 'utf-8'); @@ -67,18 +62,10 @@ function loadFullConfiguration(): GitProxyConfig { // Don't use QuickType validation for partial configurations const rawUserConfig = JSON.parse(userConfigContent); userSettings = cleanUndefinedValues(rawUserConfig); - console.log(`[CONFIG] Loaded user config with keys: ${Object.keys(userSettings).join(', ')}`); - if (userSettings.authorisedList) { - console.log( - `[CONFIG] authorisedList from user config: ${JSON.stringify(userSettings.authorisedList)}`, - ); - } } catch (error) { console.error(`Error loading user config from ${userConfigFile}:`, error); throw error; } - } else { - console.log(`[CONFIG] User config file not found at ${userConfigFile}, using defaults only`); } _currentConfig = mergeConfigurations(defaultConfig, userSettings); From 382dec0ab98c309f3e5321c9279ab21db9407714 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Wed, 18 Feb 2026 10:55:40 +0100 Subject: [PATCH 12/21] fix: separate local and Docker Cypress tests, restore ci.yml Cypress step --- .github/workflows/ci.yml | 21 +++++++++++++++++++++ .github/workflows/e2e.yml | 7 +++++-- Dockerfile | 1 - cypress.config.js | 1 + package.json | 1 + 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1482c906..9234ed8af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,27 @@ jobs: - name: Build frontend run: npm run build-ui + - name: Save build folder + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: build-ubuntu-node-${{ matrix.node-version }}-mongo-${{ matrix.mongodb-version }} + if-no-files-found: error + path: build + + - name: Download the build folders + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 + with: + name: build-ubuntu-node-${{ matrix.node-version }}-mongo-${{ matrix.mongodb-version }} + path: build + + - name: Run cypress test + uses: cypress-io/github-action@f790eee7a50d9505912f50c2095510be7de06aa7 # v6.10.9 + with: + start: npm start & + wait-on: 'http://localhost:3000' + wait-on-timeout: 120 + command: npm run cypress:run + # Windows build - single combination for development support build-windows: runs-on: windows-latest diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 7bc847b97..b51724416 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -55,16 +55,19 @@ jobs: run: npm run test:e2e - name: Run Cypress E2E tests - run: npx cypress run --env API_BASE_URL=http://localhost:8081,GIT_PROXY_URL=http://localhost:8000,GIT_SERVER_TARGET=git-server:8443 + run: npm run cypress:run:docker env: CYPRESS_BASE_URL: http://localhost:8081 + CYPRESS_API_BASE_URL: http://localhost:8081 + CYPRESS_GIT_PROXY_URL: http://localhost:8000 + CYPRESS_GIT_SERVER_TARGET: git-server:8443 - name: Dump git-proxy logs on failure if: failure() run: docker compose logs git-proxy --tail=100 - name: Upload Cypress screenshots on failure - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: failure() with: name: cypress-screenshots diff --git a/Dockerfile b/Dockerfile index 7d32fb9da..c99a098ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,6 @@ COPY --from=builder /out/node_modules/ /app/node_modules/ COPY --from=builder /out/dist/ /app/dist/ COPY --from=builder /out/build /app/dist/build/ COPY proxy.config.json config.schema.json ./ -COPY test-e2e.proxy.config.json /app/test-e2e.proxy.config.json COPY docker-entrypoint.sh /docker-entrypoint.sh USER root diff --git a/cypress.config.js b/cypress.config.js index 7e5d6b758..88b9b9cef 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -3,6 +3,7 @@ const { defineConfig } = require('cypress'); module.exports = defineConfig({ e2e: { baseUrl: process.env.CYPRESS_BASE_URL || 'http://localhost:3000', + specPattern: 'cypress/e2e/*.cy.{js,ts}', chromeWebSecurity: false, // Required for OIDC testing env: { API_BASE_URL: process.env.CYPRESS_API_BASE_URL || 'http://localhost:8080', diff --git a/package.json b/package.json index 422869fe8..d6961bde4 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md,yml,yaml,css,scss}\" --ignore-path .gitignore --config ./.prettierrc", "gen-schema-doc": "node ./scripts/doc-schema.js", "cypress:run": "cypress run", + "cypress:run:docker": "cypress run --spec 'cypress/e2e/docker/**/*.cy.{js,ts}'", "cypress:open": "cypress open", "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" }, From e0f5c4339c12cb03397981daadcfd41f6dd37848 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Wed, 18 Feb 2026 10:57:57 +0100 Subject: [PATCH 13/21] fix: improve Cypress command reliability and test pattern --- cypress/e2e/{ => docker}/pushActions.cy.js | 102 +++++++-------------- cypress/support/commands.js | 4 +- 2 files changed, 38 insertions(+), 68 deletions(-) rename cypress/e2e/{ => docker}/pushActions.cy.js (83%) diff --git a/cypress/e2e/pushActions.cy.js b/cypress/e2e/docker/pushActions.cy.js similarity index 83% rename from cypress/e2e/pushActions.cy.js rename to cypress/e2e/docker/pushActions.cy.js index 9d7bc413d..3d8c6d089 100644 --- a/cypress/e2e/pushActions.cy.js +++ b/cypress/e2e/docker/pushActions.cy.js @@ -33,19 +33,19 @@ describe('Push Actions (Approve, Reject, Cancel)', () => { cy.logout(); }); - describe('Approve flow', () => { - let pushId; + afterEach(() => { + cy.logout(); + }); - before(() => { + describe('Approve flow', () => { + beforeEach(() => { const suffix = `approve-${Date.now()}`; - cy.createPush(testUser.username, testUser.password, testUser.email, suffix).then((id) => { - pushId = id; - }); + cy.createPush(testUser.username, testUser.password, testUser.email, suffix).as('pushId'); }); - it('should approve a pending push via attestation dialog', () => { + it('should approve a pending push via attestation dialog', function () { cy.login(approverUser.username, approverUser.password); - cy.visit(`/dashboard/push/${pushId}`); + cy.visit(`/dashboard/push/${this.pushId}`); // Verify push is Pending cy.get('[data-testid="push-status"]').should('contain', 'Pending'); @@ -77,34 +77,28 @@ describe('Push Actions (Approve, Reject, Cancel)', () => { // Should navigate back to push list cy.url().should('include', '/dashboard/push'); - cy.url().should('not.include', pushId); + cy.url().should('not.include', this.pushId); // Verify push is now Approved by revisiting its detail page - cy.visit(`/dashboard/push/${pushId}`); + cy.visit(`/dashboard/push/${this.pushId}`); cy.get('[data-testid="push-status"]').should('contain', 'Approved'); // Action buttons should no longer be visible for an approved push cy.get('[data-testid="push-cancel-btn"]').should('not.exist'); cy.get('[data-testid="push-reject-btn"]').should('not.exist'); cy.get('[data-testid="attestation-open-btn"]').should('not.exist'); - - cy.logout(); }); }); describe('Reject flow', () => { - let pushId; - - before(() => { + beforeEach(() => { const suffix = `reject-${Date.now()}`; - cy.createPush(testUser.username, testUser.password, testUser.email, suffix).then((id) => { - pushId = id; - }); + cy.createPush(testUser.username, testUser.password, testUser.email, suffix).as('pushId'); }); - it('should reject a pending push', () => { + it('should reject a pending push', function () { cy.login(approverUser.username, approverUser.password); - cy.visit(`/dashboard/push/${pushId}`); + cy.visit(`/dashboard/push/${this.pushId}`); // Verify push is Pending cy.get('[data-testid="push-status"]').should('contain', 'Pending'); @@ -114,35 +108,29 @@ describe('Push Actions (Approve, Reject, Cancel)', () => { // Should navigate back to push list cy.url().should('include', '/dashboard/push'); - cy.url().should('not.include', pushId); + cy.url().should('not.include', this.pushId); // Verify push is now Rejected - cy.visit(`/dashboard/push/${pushId}`); + cy.visit(`/dashboard/push/${this.pushId}`); cy.get('[data-testid="push-status"]').should('contain', 'Rejected'); // Action buttons should no longer be visible cy.get('[data-testid="push-cancel-btn"]').should('not.exist'); cy.get('[data-testid="push-reject-btn"]').should('not.exist'); cy.get('[data-testid="attestation-open-btn"]').should('not.exist'); - - cy.logout(); }); }); describe('Cancel flow', () => { - let pushId; - - before(() => { + beforeEach(() => { const suffix = `cancel-${Date.now()}`; - cy.createPush(testUser.username, testUser.password, testUser.email, suffix).then((id) => { - pushId = id; - }); + cy.createPush(testUser.username, testUser.password, testUser.email, suffix).as('pushId'); }); - it('should cancel a pending push', () => { + it('should cancel a pending push', function () { // Cancel can be done by the push author cy.login(testUser.username, testUser.password); - cy.visit(`/dashboard/push/${pushId}`); + cy.visit(`/dashboard/push/${this.pushId}`); // Verify push is Pending cy.get('[data-testid="push-status"]').should('contain', 'Pending'); @@ -154,32 +142,26 @@ describe('Push Actions (Approve, Reject, Cancel)', () => { cy.url().should('include', '/dashboard/push'); // Verify push is now Canceled - cy.visit(`/dashboard/push/${pushId}`); + cy.visit(`/dashboard/push/${this.pushId}`); cy.get('[data-testid="push-status"]').should('contain', 'Canceled'); // Action buttons should no longer be visible cy.get('[data-testid="push-cancel-btn"]').should('not.exist'); cy.get('[data-testid="push-reject-btn"]').should('not.exist'); cy.get('[data-testid="attestation-open-btn"]').should('not.exist'); - - cy.logout(); }); }); describe('Negative: unauthorized approve', () => { - let pushId; - - before(() => { + beforeEach(() => { const suffix = `neg-approve-${Date.now()}`; - cy.createPush(testUser.username, testUser.password, testUser.email, suffix).then((id) => { - pushId = id; - }); + cy.createPush(testUser.username, testUser.password, testUser.email, suffix).as('pushId'); }); - it('should not change push state when user lacks canAuthorise permission', () => { + it('should not change push state when user lacks canAuthorise permission', function () { // Login as testuser (has canPush but NOT canAuthorise) cy.login(testUser.username, testUser.password); - cy.visit(`/dashboard/push/${pushId}`); + cy.visit(`/dashboard/push/${this.pushId}`); cy.get('[data-testid="push-status"]').should('contain', 'Pending'); @@ -200,27 +182,21 @@ describe('Push Actions (Approve, Reject, Cancel)', () => { // only handles 401 errors in authorisePush/rejectPush. The 403 is silently // ignored and the user is navigated away without feedback. Once the UI properly // handles 403, this test should assert a snackbar error message is shown. - cy.visit(`/dashboard/push/${pushId}`); + cy.visit(`/dashboard/push/${this.pushId}`); cy.get('[data-testid="push-status"]').should('contain', 'Pending'); - - cy.logout(); }); }); describe('Negative: unauthorized reject', () => { - let pushId; - - before(() => { + beforeEach(() => { const suffix = `neg-reject-${Date.now()}`; - cy.createPush(testUser.username, testUser.password, testUser.email, suffix).then((id) => { - pushId = id; - }); + cy.createPush(testUser.username, testUser.password, testUser.email, suffix).as('pushId'); }); - it('should not change push state when user lacks canAuthorise permission', () => { + it('should not change push state when user lacks canAuthorise permission', function () { // Login as testuser cy.login(testUser.username, testUser.password); - cy.visit(`/dashboard/push/${pushId}`); + cy.visit(`/dashboard/push/${this.pushId}`); cy.get('[data-testid="push-status"]').should('contain', 'Pending'); @@ -229,26 +205,20 @@ describe('Push Actions (Approve, Reject, Cancel)', () => { // TODO: Same issue as unauthorized approve — UI ignores 403 from server. // Once fixed, assert snackbar error message is shown. - cy.visit(`/dashboard/push/${pushId}`); + cy.visit(`/dashboard/push/${this.pushId}`); cy.get('[data-testid="push-status"]').should('contain', 'Pending'); - - cy.logout(); }); }); describe('Attestation dialog cancel does not cancel the push', () => { - let pushId; - - before(() => { + beforeEach(() => { const suffix = `dialog-cancel-${Date.now()}`; - cy.createPush(testUser.username, testUser.password, testUser.email, suffix).then((id) => { - pushId = id; - }); + cy.createPush(testUser.username, testUser.password, testUser.email, suffix).as('pushId'); }); - it('should close attestation dialog without affecting push status', () => { + it('should close attestation dialog without affecting push status', function () { cy.login(approverUser.username, approverUser.password); - cy.visit(`/dashboard/push/${pushId}`); + cy.visit(`/dashboard/push/${this.pushId}`); cy.get('[data-testid="push-status"]').should('contain', 'Pending'); @@ -267,8 +237,6 @@ describe('Push Actions (Approve, Reject, Cancel)', () => { cy.get('[data-testid="push-cancel-btn"]').should('be.visible'); cy.get('[data-testid="push-reject-btn"]').should('be.visible'); cy.get('[data-testid="attestation-open-btn"]').should('be.visible'); - - cy.logout(); }); }); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index a52a56e57..6d0b72edc 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -61,13 +61,15 @@ Cypress.Commands.add('getCSRFToken', () => { cookies = [cookies]; } - // CSRF protection is disabled when NODE_ENV=test (Docker/CI) if (!cookies) { + // No Set-Cookie header: CSRF protection is disabled (expected in NODE_ENV=test). + cy.log('getCSRFToken: no cookies in response, assuming CSRF is disabled'); return cy.wrap(''); } const csrfCookie = cookies.find((c) => c.startsWith('csrf=')); if (!csrfCookie) { + cy.log('getCSRFToken: no csrf cookie found, assuming CSRF is disabled'); return cy.wrap(''); } From ec80ad8e6e4e85dd28ee2d689454472f54f30513 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Wed, 18 Feb 2026 11:24:00 +0100 Subject: [PATCH 14/21] fix: use --config to override specPattern for Docker Cypress tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d6961bde4..e5eb56f35 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md,yml,yaml,css,scss}\" --ignore-path .gitignore --config ./.prettierrc", "gen-schema-doc": "node ./scripts/doc-schema.js", "cypress:run": "cypress run", - "cypress:run:docker": "cypress run --spec 'cypress/e2e/docker/**/*.cy.{js,ts}'", + "cypress:run:docker": "cypress run --config specPattern='cypress/e2e/docker/**/*.cy.{js,ts}'", "cypress:open": "cypress open", "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" }, From 44bb506e3829a719b47dc6cf4255ea3b055e6166 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Wed, 18 Feb 2026 15:49:01 +0100 Subject: [PATCH 15/21] fix: replace clearAllSavedSessions with clearCookies in cy.logout --- cypress/support/commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 6d0b72edc..1900e6083 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -50,7 +50,7 @@ Cypress.Commands.add('login', (username, password) => { }); Cypress.Commands.add('logout', () => { - Cypress.session.clearAllSavedSessions(); + cy.clearCookies(); }); Cypress.Commands.add('getCSRFToken', () => { From 39372eac5999df064a96d0a1492389b0af55d3b0 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Wed, 18 Feb 2026 16:06:30 +0100 Subject: [PATCH 16/21] fix: raise rateLimit to 1000 in e2e test config --- test-e2e.proxy.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-e2e.proxy.config.json b/test-e2e.proxy.config.json index 6c0b002cf..4c64ddbf4 100644 --- a/test-e2e.proxy.config.json +++ b/test-e2e.proxy.config.json @@ -4,7 +4,7 @@ "sessionMaxAgeHours": 12, "rateLimit": { "windowMs": 60000, - "limit": 150 + "limit": 1000 }, "tempPassword": { "sendEmail": false, From 3fc1ab37551dacd8344dfed478fba77c65c76b66 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Fri, 20 Feb 2026 10:56:54 +0100 Subject: [PATCH 17/21] fix: hide git credentials from Cypress logs and cap e2e timeout --- .github/workflows/e2e.yml | 1 + cypress/support/commands.js | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b51724416..f85f5ce17 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -56,6 +56,7 @@ jobs: - name: Run Cypress E2E tests run: npm run cypress:run:docker + timeout-minutes: 10 env: CYPRESS_BASE_URL: http://localhost:8081 CYPRESS_API_BASE_URL: http://localhost:8081 diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 1900e6083..e3ec53c02 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -123,8 +123,9 @@ Cypress.Commands.add('getTestRepoId', () => { `GET ${url} returned non-array (${typeof res.body}): ${JSON.stringify(res.body).slice(0, 500)}`, ); } + const gitServerTarget = Cypress.env('GIT_SERVER_TARGET') || 'git-server:8443'; const repo = res.body.find( - (r) => r.url === 'https://git-server:8443/coopernetes/test-repo.git', + (r) => r.url === `https://${gitServerTarget}/coopernetes/test-repo.git`, ); if (!repo) { throw new Error( @@ -138,13 +139,22 @@ Cypress.Commands.add('getTestRepoId', () => { Cypress.Commands.add('createPush', (gitUser, gitPassword, gitEmail, uniqueSuffix) => { const proxyUrl = Cypress.env('GIT_PROXY_URL') || 'http://localhost:8000'; const gitServerTarget = Cypress.env('GIT_SERVER_TARGET') || 'git-server:8443'; - const repoUrl = `${proxyUrl.replace('://', `://${gitUser}:${gitPassword}@`)}/${gitServerTarget}/coopernetes/test-repo.git`; + const repoUrl = `${proxyUrl}/${gitServerTarget}/coopernetes/test-repo.git`; const cloneDir = `/tmp/cypress-push-${uniqueSuffix}`; + // Pass credentials via GIT_CONFIG_* env vars to avoid exposing them in command output + const gitCredentialEnv = { + GIT_TERMINAL_PROMPT: '0', + NODE_TLS_REJECT_UNAUTHORIZED: '0', + GIT_CONFIG_COUNT: '1', + GIT_CONFIG_KEY_0: `url.${proxyUrl.replace('://', `://${gitUser}:${gitPassword}@`)}.insteadOf`, + GIT_CONFIG_VALUE_0: proxyUrl, + }; + cy.exec(`rm -rf ${cloneDir}`, { failOnNonZeroExit: false }); cy.exec(`git clone ${repoUrl} ${cloneDir}`, { timeout: 30000, - env: { GIT_TERMINAL_PROMPT: '0', NODE_TLS_REJECT_UNAUTHORIZED: '0' }, + env: gitCredentialEnv, }); cy.exec(`git -C ${cloneDir} config user.name "${gitUser}"`); cy.exec(`git -C ${cloneDir} config user.email "${gitEmail}"`); @@ -153,7 +163,7 @@ Cypress.Commands.add('createPush', (gitUser, gitPassword, gitEmail, uniqueSuffix cy.exec(`git -C ${cloneDir} pull --rebase origin main`, { failOnNonZeroExit: false, timeout: 30000, - env: { GIT_TERMINAL_PROMPT: '0', NODE_TLS_REJECT_UNAUTHORIZED: '0' }, + env: gitCredentialEnv, }); const timestamp = Date.now(); @@ -165,7 +175,7 @@ Cypress.Commands.add('createPush', (gitUser, gitPassword, gitEmail, uniqueSuffix cy.exec(`git -C ${cloneDir} push origin main 2>&1`, { failOnNonZeroExit: false, timeout: 30000, - env: { GIT_TERMINAL_PROMPT: '0', NODE_TLS_REJECT_UNAUTHORIZED: '0' }, + env: gitCredentialEnv, }).then((result) => { const output = result.stdout + result.stderr; const match = output.match(/dashboard\/push\/([a-f0-9_]+)/); From 2949e9e018a04832f299776a6a85f4c0f4d366b9 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Wed, 4 Mar 2026 11:08:21 +0100 Subject: [PATCH 18/21] test(e2e): add repo cleanup commands and fix delete using _id --- cypress/e2e/repo.cy.js | 9 ++++++--- cypress/support/commands.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/cypress/e2e/repo.cy.js b/cypress/e2e/repo.cy.js index 53a9dc43f..befe109dd 100644 --- a/cypress/e2e/repo.cy.js +++ b/cypress/e2e/repo.cy.js @@ -2,6 +2,11 @@ describe('Repo', () => { let cookies; let repoName; + before(() => { + cy.login('admin', 'admin'); + cy.cleanupTestRepos(); + }); + describe('Anonymous users', () => { beforeEach(() => { cy.visit('/dashboard/repo'); @@ -54,8 +59,6 @@ describe('Repo', () => { }); cy.contains('a', `cypress-test/${repoName}`, { timeout: 10000 }).click(); - - // cy.get('[data-testid="delete-repo-button"]').click(); }); it('Displays an error when adding an existing repo', () => { @@ -147,7 +150,7 @@ describe('Repo', () => { cy.getCSRFToken().then((csrfToken) => { cy.request({ method: 'DELETE', - url: `${Cypress.env('API_BASE_URL') || Cypress.config('baseUrl')}/api/v1/repo/${repoName}/delete`, + url: `${Cypress.env('API_BASE_URL') || Cypress.config('baseUrl')}/api/v1/repo/${repoId}/delete`, headers: { cookie: cookies?.join('; ') || '', 'X-CSRF-TOKEN': csrfToken, diff --git a/cypress/support/commands.js b/cypress/support/commands.js index e3ec53c02..7ed44fa02 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -136,6 +136,40 @@ Cypress.Commands.add('getTestRepoId', () => { }); }); +Cypress.Commands.add('cleanupTestRepos', () => { + cy.getCSRFToken().then((csrfToken) => { + cy.request({ + method: 'GET', + url: `${getApiBaseUrl()}/api/v1/repo`, + failOnStatusCode: false, + }).then((res) => { + if (res.status !== 200 || !Array.isArray(res.body)) return; + const testRepos = res.body.filter((r) => r.project === 'cypress-test'); + testRepos.forEach((repo) => { + cy.request({ + method: 'DELETE', + url: `${getApiBaseUrl()}/api/v1/repo/${repo._id}/delete`, + headers: { 'X-CSRF-TOKEN': csrfToken }, + failOnStatusCode: false, + }); + }); + }); + }); +}); + +Cypress.Commands.add('deleteRepo', (repoId) => { + cy.getCSRFToken().then((csrfToken) => { + cy.request({ + method: 'DELETE', + url: `${getApiBaseUrl()}/api/v1/repo/${repoId}/delete`, + headers: { + 'X-CSRF-TOKEN': csrfToken, + }, + failOnStatusCode: false, + }); + }); +}); + Cypress.Commands.add('createPush', (gitUser, gitPassword, gitEmail, uniqueSuffix) => { const proxyUrl = Cypress.env('GIT_PROXY_URL') || 'http://localhost:8000'; const gitServerTarget = Cypress.env('GIT_SERVER_TARGET') || 'git-server:8443'; From 95d2a8d34fcbd54baf3c2c7748b0f4c7cd5c3c61 Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Wed, 4 Mar 2026 11:37:32 +0100 Subject: [PATCH 19/21] fix(e2e): use correct test-owner/test-repo in Cypress commands --- cypress/support/commands.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 7ed44fa02..a2228b932 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -125,11 +125,11 @@ Cypress.Commands.add('getTestRepoId', () => { } const gitServerTarget = Cypress.env('GIT_SERVER_TARGET') || 'git-server:8443'; const repo = res.body.find( - (r) => r.url === `https://${gitServerTarget}/coopernetes/test-repo.git`, + (r) => r.url === `https://${gitServerTarget}/test-owner/test-repo.git`, ); if (!repo) { throw new Error( - `coopernetes/test-repo not found in database. Repos: ${res.body.map((r) => r.url).join(', ')}`, + `test-owner/test-repo not found in database. Repos: ${res.body.map((r) => r.url).join(', ')}`, ); } return cy.wrap(repo._id); @@ -173,7 +173,7 @@ Cypress.Commands.add('deleteRepo', (repoId) => { Cypress.Commands.add('createPush', (gitUser, gitPassword, gitEmail, uniqueSuffix) => { const proxyUrl = Cypress.env('GIT_PROXY_URL') || 'http://localhost:8000'; const gitServerTarget = Cypress.env('GIT_SERVER_TARGET') || 'git-server:8443'; - const repoUrl = `${proxyUrl}/${gitServerTarget}/coopernetes/test-repo.git`; + const repoUrl = `${proxyUrl}/${gitServerTarget}/test-owner/test-repo.git`; const cloneDir = `/tmp/cypress-push-${uniqueSuffix}`; // Pass credentials via GIT_CONFIG_* env vars to avoid exposing them in command output From 0c76acdb7e607e3b71ae3e3301991ca01559c9db Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Wed, 4 Mar 2026 12:12:48 +0100 Subject: [PATCH 20/21] fix(e2e): logout after cleaning --- cypress/e2e/repo.cy.js | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress/e2e/repo.cy.js b/cypress/e2e/repo.cy.js index befe109dd..a1f087f60 100644 --- a/cypress/e2e/repo.cy.js +++ b/cypress/e2e/repo.cy.js @@ -5,6 +5,7 @@ describe('Repo', () => { before(() => { cy.login('admin', 'admin'); cy.cleanupTestRepos(); + cy.logout(); }); describe('Anonymous users', () => { From 1c4ba161b71409f59ec18543b559d05280f4a04c Mon Sep 17 00:00:00 2001 From: fabiovincenzi Date: Mon, 9 Mar 2026 15:49:46 +0100 Subject: [PATCH 21/21] chore: add missing header --- cypress/e2e/docker/pushActions.cy.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cypress/e2e/docker/pushActions.cy.js b/cypress/e2e/docker/pushActions.cy.js index afaee103e..690a2eb6e 100644 --- a/cypress/e2e/docker/pushActions.cy.js +++ b/cypress/e2e/docker/pushActions.cy.js @@ -1,3 +1,19 @@ +/** + * Copyright 2026 GitProxy Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + describe('Push Actions (Approve, Reject, Cancel)', () => { const testUser = { username: 'testuser',