diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 000000000..e3a0a658b --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,45 @@ +name: 'WrongSecrets CTF Party: E2E Tests' + +on: + pull_request: + branches: [ main ] + +jobs: + e2e-test: + name: Run Cypress E2E Tests + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Start Minikube + uses: medyagh/setup-minikube@latest + with: + driver: docker + cpus: '2' + memory: '8000' + kubernetes-version: 'v1.32.0' + + - name: Install Helm + uses: azure/setup-helm@v4 + + - name: Install yq + run: sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq && sudo chmod +x /usr/bin/yq + + # This new step runs the deploy script in the background and then waits + - name: Run Deployment Script in Background and Wait + run: | + ./build-and-deploy-minikube.sh & + echo "Deployment script running in background. Waiting for pods..." + sleep 30 + kubectl wait --for=condition=ready pod -l app=wrongsecrets-balancer --timeout=5m + + - name: Install Cypress and Run Tests + uses: cypress-io/github-action@v6 + with: + working-directory: wrongsecrets-balancer + # The kubectl wait command is now the primary check + wait-on: 'http://localhost:3000' + wait-on-timeout: 120 # 2 minutes + browser: chrome + headless: true \ No newline at end of file diff --git a/build-and-deploy-minikube.sh b/build-and-deploy-minikube.sh index 08065b15a..8a26e92ce 100755 --- a/build-and-deploy-minikube.sh +++ b/build-and-deploy-minikube.sh @@ -4,7 +4,7 @@ source ./scripts/check-available-commands.sh checkCommandsAvailable helm docker kubectl yq minikube minikube delete -minikube start --cpus=8 --memory=12000MB --network-plugin=cni --cni=calico --driver=docker --kubernetes-version=1.32.0 +minikube start --cpus=2 --memory=8000MB --network-plugin=cni --cni=calico --driver=docker --kubernetes-version=1.32.0 eval $(minikube docker-env) ./build-and-deploy.sh diff --git a/scripts/run-e2e-tests.sh b/scripts/run-e2e-tests.sh new file mode 100644 index 000000000..883540cff --- /dev/null +++ b/scripts/run-e2e-tests.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# This script will exit immediately if any command fails +set -e + +echo "--- Starting Minikube ---" +minikube start --cpus=2 --memory=8000MB --driver=docker --network-plugin=cni --cni=calico --kubernetes-version=1.32.0 + +echo "--- Updating Helm Repositories ---" +helm repo update + +echo "--- Deploying Application ---" +helm upgrade --install wrongsecrets ./helm/wrongsecrets-ctf-party + +echo "--- Waiting for Balancer Deployment to be Ready ---" +kubectl wait --for=condition=available deployment/wrongsecrets-balancer --timeout=5m + +echo "--- Starting Port Forward in Background ---" +kubectl port-forward service/wrongsecrets-balancer 3000:3000 & +# Store the ID of the background process +PORT_FORWARD_PID=$! + +echo "--- Waiting for Port Forward to establish... ---" +sleep 5 + +echo "--- Getting Admin Password ---" +# Note: We get the password here for the test to use it +ADMIN_PASSWORD=$(kubectl get secrets wrongsecrets-balancer-secret -o=jsonpath='{.data.adminPassword}' | base64 --decode) +export CYPRESS_ADMIN_PASSWORD=$ADMIN_PASSWORD + +echo "--- Running Cypress Tests ---" +# Navigate to the correct directory to run the tests +cd wrongsecrets-balancer +# Run Cypress tests headlessly +npx cypress run + +echo "--- Cleaning up port-forward process ---" +# Stop the background port-forward process +kill $PORT_FORWARD_PID diff --git a/wrongsecrets-balancer/cypress.config.js b/wrongsecrets-balancer/cypress.config.js new file mode 100644 index 000000000..0969aae3f --- /dev/null +++ b/wrongsecrets-balancer/cypress.config.js @@ -0,0 +1,7 @@ +module.exports = { + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, +}; diff --git a/wrongsecrets-balancer/cypress/e2e/admin_login.cy.js b/wrongsecrets-balancer/cypress/e2e/admin_login.cy.js new file mode 100644 index 000000000..6a7635e66 --- /dev/null +++ b/wrongsecrets-balancer/cypress/e2e/admin_login.cy.js @@ -0,0 +1,22 @@ +describe('Admin Login', () => { + it('should allow the admin to log in through the main page', () => { + // NOTE: The admin password changes every time you deploy. + // You must get the new password from the terminal before running this test. + const adminPassword = 'RSX9I94K'; + + // Visit the homepage to log in. + cy.visit('http://localhost:3000'); + + // Type "admin" as the team name and click the button. + cy.get('[data-test-id="teamname-input"]').type('admin'); + cy.get('[data-test-id="create-join-team-button"]').click(); + + // On the next page, type the admin password. + cy.get('[data-test-id="passcode-input"]').type(adminPassword); + cy.contains('button', 'Join Team').click(); + + // Verify that the admin page has loaded. We give it a longer timeout (10 seconds) + // because the list of teams might take a moment to load from the server. + cy.contains('Active Teams', { timeout: 10000 }).should('be.visible'); + }); +}); diff --git a/wrongsecrets-balancer/cypress/e2e/team_workflow.cy.js b/wrongsecrets-balancer/cypress/e2e/team_workflow.cy.js new file mode 100644 index 000000000..1d6d7b0fd --- /dev/null +++ b/wrongsecrets-balancer/cypress/e2e/team_workflow.cy.js @@ -0,0 +1,37 @@ +describe('Team Creation and Joining Workflow', () => { + it('should create a team, then allow a user to join it with the passcode', () => { + // Generates a short, unique team name that is under the 16-character limit. + const teamName = `ctf-${Date.now().toString().slice(-9)}`; + + // Set up a "spy" to intercept the network request that creates the team. + cy.intercept('POST', `/balancer/teams/${teamName}/join`).as('createTeamRequest'); + + // === PART 1: CREATE THE TEAM === + cy.visit('http://localhost:3000'); + cy.get('[data-test-id="teamname-input"]').type(teamName); + cy.get('[data-test-id="create-join-team-button"]').click(); + + // === PART 2: CAPTURE PASSCODE & JOIN === + // Wait for the network request to finish and get the passcode from its response data. + cy.wait('@createTeamRequest').then((interception) => { + const passcode = interception.response.body.passcode; + + // Now that we have the real passcode, go back to the homepage. + cy.visit('http://localhost:3000'); + + // Enter the same unique team name again. + cy.get('[data-test-id="teamname-input"]').type(teamName); + cy.get('[data-test-id="create-join-team-button"]').click(); + + // On the "Joining team" page, type the real passcode we captured. + cy.get('[data-test-id="passcode-input"]').type(passcode); + cy.contains('button', 'Join Team').click(); + + // === PART 3: FINAL VERIFICATION (with a long timeout) === + // Instead of waiting for a network call, we wait directly for the final button to appear. + // We give it up to 2 minutes (120000ms) for the backend instance to get ready. + cy.contains('Start Hacking', { timeout: 120000 }).should('be.visible'); + cy.contains('Start your Webtop').should('be.visible'); + }); + }); +}); diff --git a/wrongsecrets-balancer/cypress/fixtures/example.json b/wrongsecrets-balancer/cypress/fixtures/example.json new file mode 100644 index 000000000..02e425437 --- /dev/null +++ b/wrongsecrets-balancer/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/wrongsecrets-balancer/cypress/support/commands.js b/wrongsecrets-balancer/cypress/support/commands.js new file mode 100644 index 000000000..119ab03f7 --- /dev/null +++ b/wrongsecrets-balancer/cypress/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) diff --git a/wrongsecrets-balancer/cypress/support/e2e.js b/wrongsecrets-balancer/cypress/support/e2e.js new file mode 100644 index 000000000..92e5881f4 --- /dev/null +++ b/wrongsecrets-balancer/cypress/support/e2e.js @@ -0,0 +1,17 @@ +// *********************************************************** +// This example support/e2e.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'