Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
start: npm start &
wait-on: 'http://localhost:3000'
wait-on-timeout: 120
command: npm run cypress:run
command: npm run cypress:run:ci

# Windows build - single combination for development support
build-windows:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,4 @@ website/.docusaurus

# Generated from testing
/test/fixtures/test-package/package-lock.json
cypress/screenshots/
58 changes: 11 additions & 47 deletions cypress/e2e/docker/pushActions.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ describe('Push Actions (Approve, Reject, Cancel)', () => {
};

before(() => {
// Setup: login as admin, create test users, assign permissions
cy.login('admin', 'admin');

cy.createUser(testUser.username, testUser.password, testUser.email, testUser.gitAccount);
Expand All @@ -49,10 +48,18 @@ describe('Push Actions (Approve, Reject, Cancel)', () => {
cy.logout();
});

afterEach(() => {
afterEach(function () {
if (this.pushId) {
cy.deleteTestPush(this.pushId);
}
cy.logout();
});

after(() => {
cy.deleteTestUser(testUser.username);
cy.deleteTestUser(approverUser.username);
});

describe('Approve flow', () => {
beforeEach(() => {
const suffix = `approve-${Date.now()}`;
Expand All @@ -63,43 +70,32 @@ describe('Push Actions (Approve, Reject, Cancel)', () => {
cy.login(approverUser.username, approverUser.password);
cy.visit(`/dashboard/push/${this.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', this.pushId);

// Verify push is now Approved by revisiting its detail page
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');
Expand All @@ -116,33 +112,22 @@ describe('Push Actions (Approve, Reject, Cancel)', () => {
cy.login(approverUser.username, approverUser.password);
cy.visit(`/dashboard/push/${this.pushId}`);

// Verify push is Pending
cy.get('[data-testid="push-status"]').should('contain', 'Pending');

// Open reject dialog
cy.get('[data-testid="push-reject-btn"]').click();

// Confirm button should be disabled until reason is provided
cy.get('[data-testid="push-reject-confirm-btn"]').should('be.disabled');

// Fill in rejection reason
cy.get('#reason').type('Rejecting for test purposes');

// Confirm button should now be enabled
cy.get('[data-testid="push-reject-confirm-btn"]').should('not.be.disabled');

// Confirm rejection
cy.get('[data-testid="push-reject-confirm-btn"]').click();

// Should navigate back to push list
cy.url().should('include', '/dashboard/push');
cy.url().should('not.include', this.pushId);

// Verify push is now Rejected
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');
Expand All @@ -156,24 +141,17 @@ describe('Push Actions (Approve, Reject, Cancel)', () => {
});

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/${this.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/${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');
Expand All @@ -187,17 +165,14 @@ describe('Push Actions (Approve, Reject, Cancel)', () => {
});

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/${this.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) => {
Expand All @@ -206,10 +181,7 @@ describe('Push Actions (Approve, Reject, Cancel)', () => {

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.
// TODO: Assert snackbar feedback when the UI handles 403 push action responses.
cy.visit(`/dashboard/push/${this.pushId}`);
cy.get('[data-testid="push-status"]').should('contain', 'Pending');
});
Expand All @@ -222,17 +194,13 @@ describe('Push Actions (Approve, Reject, Cancel)', () => {
});

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/${this.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.
// TODO: Assert snackbar feedback when the UI handles 403 push action responses.
cy.visit(`/dashboard/push/${this.pushId}`);
cy.get('[data-testid="push-status"]').should('contain', 'Pending');
});
Expand All @@ -250,18 +218,14 @@ describe('Push Actions (Approve, Reject, Cancel)', () => {

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');
Expand Down
60 changes: 60 additions & 0 deletions cypress/e2e/error-pages.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* 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.
*/

/**
* Error Pages
* Strategy: Direct navigation. No API needed.
*/
describe('Error Pages', () => {
beforeEach(() => {
cy.login('admin', 'admin');
});

afterEach(() => {
cy.logout();
});
it('Unknown route shows 404 page', () => {
cy.visit('/dashboard/nonexistent-page-xyz');

cy.get('[data-testid="not-found-page"]').should('be.visible');
cy.contains('404').should('be.visible');
});
it('Unauthorized route shows NotAuthorized page', () => {
const regularUser = {
username: `errorpage_user_${Date.now()}`,
password: 'pass123',
email: `errorpage_${Date.now()}@example.com`,
gitAccount: `errorpage_git_${Date.now()}`,
};

cy.request({
method: 'POST',
url: `${Cypress.env('API_BASE_URL') || Cypress.config('baseUrl')}/api/auth/create-user`,
body: regularUser,
failOnStatusCode: false,
});

cy.clearCookies();
cy.clearLocalStorage();
cy.login(regularUser.username, regularUser.password);
cy.visit('/not-authorized');

cy.get('[data-testid="not-authorized-page"]').should('be.visible');
cy.contains('403').should('be.visible');
cy.clearCookies();
cy.deleteTestUser(regularUser.username);
});
});
75 changes: 75 additions & 0 deletions cypress/e2e/navigation.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* 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.
*/

/**
* Navigation & Shell
* Strategy: Mix of real navigation and intercepts.
*/
describe('Navigation & Shell', () => {
beforeEach(() => {
cy.login('admin', 'admin');
});

afterEach(() => {
cy.logout();
});
it('Sidebar renders all visible links', () => {
cy.visit('/dashboard/repo');
cy.contains('Repositories').should('be.visible');
cy.contains('Dashboard').should('be.visible');
});
it('Sidebar links navigate correctly', () => {
cy.visit('/dashboard/repo');
cy.contains('Dashboard').click();
cy.url().should('include', '/dashboard/push');
cy.contains('Repositories').click();
cy.url().should('include', '/dashboard/repo');
});
it('Active sidebar item highlights', () => {
cy.visit('/dashboard/repo');
cy.get('[aria-current="page"]').should('exist');
});
it('Navbar renders correctly', () => {
cy.visit('/dashboard/repo');

cy.get('[data-testid="navbar"]').should('be.visible');
});
it('Footer renders', () => {
cy.visit('/dashboard/repo');

cy.get('[data-testid="footer"]').should('exist');
cy.get('[data-testid="footer"]').scrollIntoView();
cy.get('[data-testid="footer"]').should('be.visible');
});
// NOTE: Keep unauthenticated checks outside the logged-in hooks.
it('/ redirects to /dashboard/repo', () => {
cy.logout();
cy.visit('/');
cy.url().should('match', /\/(login|dashboard)/);
});
});

describe('Unauthenticated access', () => {
it('Unauthenticated user is redirected to /login', () => {
Cypress.session.clearAllSavedSessions();
cy.clearCookies();
cy.clearLocalStorage();

cy.visit('/dashboard/profile');

cy.url().should('include', '/login');
});
});
Loading
Loading