From 89c7af0f4c9342247752bf33e19fa11cd2005c6c Mon Sep 17 00:00:00 2001 From: Jerry Gu Date: Sun, 30 Nov 2025 23:06:48 -0500 Subject: [PATCH 1/4] Add Postman API testing + CI workflow --- .github/workflows/maven.yml | 369 +++++ postman/postman_collection.json | 2519 ++++++++++++++++++++++++++++++ postman/postman_environment.json | 37 + 3 files changed, 2925 insertions(+) create mode 100644 .github/workflows/maven.yml create mode 100644 postman/postman_collection.json create mode 100644 postman/postman_environment.json diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..682bee7 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,369 @@ +# GitHub Actions CI Workflow for ASE Team Project +# This workflow runs unit tests, integration tests, coverage, static analysis, +# and API tests using Newman + +name: CI Pipeline - Unit, Integration & API Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + JAVA_VERSION: '17' + NODE_VERSION: '18' + +jobs: + # =========================================== + # Job 1: Unit Tests + # =========================================== + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'temurin' + cache: maven + + - name: Run Unit Tests + run: mvn test -Dtest="*UnitTests,*Tests" -DfailIfNoTests=false --batch-mode + + - name: Generate Unit Test Report + if: always() + run: mvn surefire-report:report-only --batch-mode + + - name: Upload Unit Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: unit-test-results + path: | + target/surefire-reports/ + target/site/surefire-report.html + + # =========================================== + # Job 2: Integration Tests + # =========================================== + integration-tests: + name: Integration Tests + runs-on: ubuntu-latest + needs: unit-tests # Run after unit tests pass + + services: + postgres: + image: postgres:15 + env: + POSTGRES_DB: budget_app_test + POSTGRES_USER: testuser + POSTGRES_PASSWORD: Test12345678! + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'temurin' + cache: maven + + - name: Wait for PostgreSQL + run: | + until pg_isready -h localhost -p 5432; do + echo "Waiting for PostgreSQL..." + sleep 2 + done + + - name: Initialize Test Database Schema + env: + PGPASSWORD: Test12345678! + run: | + psql -h localhost -U testuser -d budget_app_test -f src/main/resources/schema.sql || true + + - name: Run Integration Tests + env: + SPRING_PROFILES_ACTIVE: test + SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/budget_app_test + SPRING_DATASOURCE_USERNAME: testuser + SPRING_DATASOURCE_PASSWORD: Test12345678! + run: mvn test -Dtest="*IntegrationTests" -DfailIfNoTests=false --batch-mode + + - name: Upload Integration Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: integration-test-results + path: target/surefire-reports/ + + # =========================================== + # Job 3: API Tests with Newman + # =========================================== + api-tests: + name: API Tests (Newman/Postman) + runs-on: ubuntu-latest + needs: unit-tests # Run after unit tests pass + + services: + postgres: + image: postgres:15 + env: + POSTGRES_DB: budget_app_test + POSTGRES_USER: testuser + POSTGRES_PASSWORD: Test12345678! + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'temurin' + cache: maven + + - name: Set up Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Install Newman + run: npm install -g newman newman-reporter-htmlextra + + - name: Wait for PostgreSQL + run: | + until pg_isready -h localhost -p 5432; do + echo "Waiting for PostgreSQL..." + sleep 2 + done + + - name: Initialize Test Database Schema + env: + PGPASSWORD: Test12345678! + run: | + psql -h localhost -U testuser -d budget_app_test -f src/main/resources/schema.sql || true + + - name: Build Application (skip tests) + run: mvn clean package -DskipTests --batch-mode + + - name: Start Application + env: + SPRING_PROFILES_ACTIVE: test + SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/budget_app_test + SPRING_DATASOURCE_USERNAME: testuser + SPRING_DATASOURCE_PASSWORD: Test12345678! + run: | + java -jar target/*.jar & + echo $! > app.pid + echo "Waiting for application to start..." + + - name: Wait for Application to be Ready + run: | + timeout=120 + while [ $timeout -gt 0 ]; do + if curl -s http://localhost:8080/users > /dev/null 2>&1; then + echo "Application is ready!" + break + fi + echo "Waiting for application... ($timeout seconds remaining)" + sleep 5 + timeout=$((timeout - 5)) + done + if [ $timeout -le 0 ]; then + echo "Application failed to start within timeout" + exit 1 + fi + + - name: Run Newman API Tests + run: | + newman run postman/postman_collection.json \ + --reporters cli,htmlextra,junit \ + --reporter-htmlextra-export newman-report.html \ + --reporter-junit-export newman-results.xml \ + --env-var "baseUrl=http://localhost:8080" \ + --delay-request 100 \ + --timeout-request 30000 + + - name: Stop Application + if: always() + run: | + if [ -f app.pid ]; then + kill $(cat app.pid) || true + fi + + - name: Upload Newman HTML Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: newman-html-report + path: newman-report.html + + - name: Upload Newman JUnit Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: newman-junit-results + path: newman-results.xml + + - name: Publish Test Results + if: always() + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + files: newman-results.xml + check_name: API Test Results + + # =========================================== + # Job 4: Code Coverage Report + # =========================================== + coverage: + name: Code Coverage + runs-on: ubuntu-latest + needs: [unit-tests, integration-tests] + + services: + postgres: + image: postgres:15 + env: + POSTGRES_DB: budget_app_test + POSTGRES_USER: testuser + POSTGRES_PASSWORD: Test12345678! + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'temurin' + cache: maven + + - name: Wait for PostgreSQL + run: | + until pg_isready -h localhost -p 5432; do + echo "Waiting for PostgreSQL..." + sleep 2 + done + + - name: Initialize Test Database Schema + env: + PGPASSWORD: Test12345678! + run: | + psql -h localhost -U testuser -d budget_app_test -f src/main/resources/schema.sql || true + + - name: Run All Tests with Coverage + env: + SPRING_PROFILES_ACTIVE: test + SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/budget_app_test + SPRING_DATASOURCE_USERNAME: testuser + SPRING_DATASOURCE_PASSWORD: Test12345678! + run: mvn clean verify --batch-mode + + - name: Generate JaCoCo Report + run: mvn jacoco:report --batch-mode + + - name: Upload Coverage Report + uses: actions/upload-artifact@v4 + with: + name: jacoco-coverage-report + path: target/site/jacoco/ + + # =========================================== + # Job 5: Static Analysis + # =========================================== + static-analysis: + name: Static Analysis (Checkstyle & PMD) + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'temurin' + cache: maven + + - name: Run Checkstyle + run: mvn checkstyle:check --batch-mode + + - name: Generate Checkstyle Report + run: mvn checkstyle:checkstyle --batch-mode + + - name: Run PMD + run: mvn pmd:check --batch-mode + + - name: Generate PMD Report + run: mvn site -DgenerateReports=false --batch-mode + + - name: Upload Static Analysis Reports + uses: actions/upload-artifact@v4 + with: + name: static-analysis-reports + path: | + target/site/checkstyle.html + target/site/pmd.html + target/site/cpd.html + + # =========================================== + # Job 6: Build Summary + # =========================================== + build-summary: + name: Build Summary + runs-on: ubuntu-latest + needs: [unit-tests, integration-tests, api-tests, coverage, static-analysis] + if: always() + + steps: + - name: Download All Artifacts + uses: actions/download-artifact@v4 + with: + path: all-artifacts + + - name: Create Build Summary + run: | + echo "# Build Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Test Results" >> $GITHUB_STEP_SUMMARY + echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Unit Tests | ${{ needs.unit-tests.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Integration Tests | ${{ needs.integration-tests.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| API Tests | ${{ needs.api-tests.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Coverage | ${{ needs.coverage.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Static Analysis | ${{ needs.static-analysis.result }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Artifacts are available for download in the Actions tab." >> $GITHUB_STEP_SUMMARY diff --git a/postman/postman_collection.json b/postman/postman_collection.json new file mode 100644 index 0000000..f7ec7ac --- /dev/null +++ b/postman/postman_collection.json @@ -0,0 +1,2519 @@ +{ + "info": { + "_postman_id": "ase-team-project-api-tests", + "name": "ASE Team Project - Personal Finance Tracker API Tests", + "description": "Comprehensive API tests covering all endpoints with equivalence partitions.\n\n## Equivalence Partitions Documentation\n\nSee individual requests for partition coverage details.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:8080", + "type": "string" + }, + { + "key": "testUserId", + "value": "", + "type": "string" + }, + { + "key": "testUserId2", + "value": "", + "type": "string" + }, + { + "key": "testTransactionId", + "value": "", + "type": "string" + }, + { + "key": "testTransactionId2", + "value": "", + "type": "string" + } + ], + "item": [ + { + "name": "0. Setup - Clean State", + "item": [ + { + "name": "Get All Users (for cleanup)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Store user IDs for potential cleanup", + "pm.test('Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const users = pm.response.json();", + "pm.environment.set('existingUsers', JSON.stringify(users));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/users", + "host": ["{{baseUrl}}"], + "path": ["users"] + } + } + } + ] + }, + { + "name": "1. Index/Home Endpoint Tests", + "item": [ + { + "name": "GET / - Home Page (Valid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P1 - Valid request to home page", + "// Expected: 200 OK with HTML content", + "", + "pm.test('P1: Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('P1: Response is HTML', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.include('text/html');", + "});", + "", + "pm.test('P1: Contains welcome message', function () {", + " pm.expect(pm.response.text()).to.include('Welcome to the Personal Finance Tracker');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/", + "host": ["{{baseUrl}}"], + "path": [""] + } + } + }, + { + "name": "GET /index - Index Page (Valid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P1 - Valid request to index", + "pm.test('P1: Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('P1: Response is HTML', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.include('text/html');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/index", + "host": ["{{baseUrl}}"], + "path": ["index"] + } + } + }, + { + "name": "GET /invalid-path - Invalid Path", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P2 - Invalid path (boundary test)", + "pm.test('P2: Status code is 404 for invalid path', function () {", + " pm.response.to.have.status(404);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/invalid-path", + "host": ["{{baseUrl}}"], + "path": ["invalid-path"] + } + } + } + ] + }, + { + "name": "2. User Management - GET /users", + "item": [ + { + "name": "GET /users - List All Users (Valid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P1 - Valid request to list users", + "// Input: None required", + "// Expected: 200 OK with JSON array", + "", + "pm.test('P1: Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('P1: Response is JSON', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json');", + "});", + "", + "pm.test('P1: Response is an array', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData).to.be.an('array');", + "});", + "", + "// Log count for debugging", + "console.log('User count: ' + pm.response.json().length);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/users", + "host": ["{{baseUrl}}"], + "path": ["users"] + } + } + } + ] + }, + { + "name": "3. User Management - POST /users (JSON)", + "item": [ + { + "name": "POST /users - Create Valid User (P1: All fields valid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P1 - Valid user with all fields", + "// Input: username (non-null), email (non-null), budget (positive)", + "// Expected: 201 Created", + "", + "pm.test('P1: Status code is 201 Created', function () {", + " pm.response.to.have.status(201);", + "});", + "", + "pm.test('P1: Response contains userId', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('userId');", + " pm.expect(jsonData.userId).to.not.be.null;", + " // Store for later tests", + " pm.collectionVariables.set('testUserId', jsonData.userId);", + "});", + "", + "pm.test('P1: Response contains correct username', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData.username).to.eql('TestUser_' + pm.iterationData.get('timestamp') || jsonData.username);", + "});", + "", + "pm.test('P1: Response is JSON', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json');", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "// Generate unique values to avoid conflicts", + "const timestamp = Date.now();", + "pm.variables.set('timestamp', timestamp);", + "pm.variables.set('uniqueUsername', 'TestUser_' + timestamp);", + "pm.variables.set('uniqueEmail', 'testuser_' + timestamp + '@test.com');" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"{{uniqueUsername}}\",\n \"email\": \"{{uniqueEmail}}\",\n \"budget\": 1000.00\n}" + }, + "url": { + "raw": "{{baseUrl}}/users", + "host": ["{{baseUrl}}"], + "path": ["users"] + } + } + }, + { + "name": "POST /users - Create Second User for Testing", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Setup: Create second user for multi-user testing", + "pm.test('Status code is 201 Created', function () {", + " pm.response.to.have.status(201);", + "});", + "", + "const jsonData = pm.response.json();", + "pm.collectionVariables.set('testUserId2', jsonData.userId);" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const timestamp = Date.now();", + "pm.variables.set('uniqueUsername2', 'TestUser2_' + timestamp);", + "pm.variables.set('uniqueEmail2', 'testuser2_' + timestamp + '@test.com');" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"{{uniqueUsername2}}\",\n \"email\": \"{{uniqueEmail2}}\",\n \"budget\": 500.00\n}" + }, + "url": { + "raw": "{{baseUrl}}/users", + "host": ["{{baseUrl}}"], + "path": ["users"] + } + } + }, + { + "name": "POST /users - Zero Budget (P2: Boundary - minimum valid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P2 - Boundary test: zero budget", + "// Input: budget = 0 (minimum valid value)", + "// Expected: 201 Created (0 is valid budget)", + "", + "pm.test('P2: Status code is 201 for zero budget', function () {", + " pm.response.to.have.status(201);", + "});", + "", + "pm.test('P2: Budget is 0', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData.budget).to.eql(0);", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const timestamp = Date.now();", + "pm.variables.set('zeroBudgetUsername', 'ZeroBudget_' + timestamp);", + "pm.variables.set('zeroBudgetEmail', 'zero_' + timestamp + '@test.com');" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"{{zeroBudgetUsername}}\",\n \"email\": \"{{zeroBudgetEmail}}\",\n \"budget\": 0\n}" + }, + "url": { + "raw": "{{baseUrl}}/users", + "host": ["{{baseUrl}}"], + "path": ["users"] + } + } + }, + { + "name": "POST /users - Large Budget (P3: Boundary - high value)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P3 - Boundary test: large budget", + "// Input: budget = 99999999.99 (max reasonable value)", + "// Expected: 201 Created", + "", + "pm.test('P3: Status code is 201 for large budget', function () {", + " pm.response.to.have.status(201);", + "});", + "", + "pm.test('P3: Large budget stored correctly', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData.budget).to.be.above(99999999);", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const timestamp = Date.now();", + "pm.variables.set('largeBudgetUsername', 'LargeBudget_' + timestamp);", + "pm.variables.set('largeBudgetEmail', 'large_' + timestamp + '@test.com');" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"{{largeBudgetUsername}}\",\n \"email\": \"{{largeBudgetEmail}}\",\n \"budget\": 99999999.99\n}" + }, + "url": { + "raw": "{{baseUrl}}/users", + "host": ["{{baseUrl}}"], + "path": ["users"] + } + } + }, + { + "name": "POST /users - Missing Username (P4: Invalid - null required field)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P4 - Invalid: missing username", + "// Input: username = null", + "// Expected: 400 Bad Request", + "", + "pm.test('P4: Status code is 400 for missing username', function () {", + " pm.response.to.have.status(400);", + "});", + "", + "pm.test('P4: Error message indicates username required', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData.error || JSON.stringify(jsonData)).to.include('sername');", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const timestamp = Date.now();", + "pm.variables.set('noUsernameEmail', 'nousername_' + timestamp + '@test.com');" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"email\": \"{{noUsernameEmail}}\",\n \"budget\": 500\n}" + }, + "url": { + "raw": "{{baseUrl}}/users", + "host": ["{{baseUrl}}"], + "path": ["users"] + } + } + }, + { + "name": "POST /users - Missing Email (P5: Invalid - null required field)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P5 - Invalid: missing email", + "// Input: email = null", + "// Expected: 400 Bad Request", + "", + "pm.test('P5: Status code is 400 for missing email', function () {", + " pm.response.to.have.status(400);", + "});", + "", + "pm.test('P5: Error message indicates email required', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData.error || JSON.stringify(jsonData)).to.include('mail');", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const timestamp = Date.now();", + "pm.variables.set('noEmailUsername', 'NoEmail_' + timestamp);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"{{noEmailUsername}}\",\n \"budget\": 500\n}" + }, + "url": { + "raw": "{{baseUrl}}/users", + "host": ["{{baseUrl}}"], + "path": ["users"] + } + } + }, + { + "name": "POST /users - Malformed JSON (P6: Invalid input format)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P6 - Invalid: malformed JSON", + "// Expected: 400 Bad Request", + "", + "pm.test('P6: Status code is 400 for malformed JSON', function () {", + " pm.response.to.have.status(400);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{invalid json}" + }, + "url": { + "raw": "{{baseUrl}}/users", + "host": ["{{baseUrl}}"], + "path": ["users"] + } + } + }, + { + "name": "POST /users - Duplicate Username (P7: Constraint violation)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P7 - Invalid: duplicate username", + "// Expected: 400 Bad Request with error message", + "", + "pm.test('P7: Status code is 400 for duplicate username', function () {", + " pm.response.to.have.status(400);", + "});", + "", + "pm.test('P7: Error message mentions username exists', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData.error).to.include('already exists');", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "// First, we need to get an existing username", + "// This test assumes testUserId user was created with a known username pattern" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"DuplicateTestUser\",\n \"email\": \"duplicate_first@test.com\",\n \"budget\": 100\n}" + }, + "url": { + "raw": "{{baseUrl}}/users", + "host": ["{{baseUrl}}"], + "path": ["users"] + } + } + } + ] + }, + { + "name": "4. User Management - GET /users/{userId}", + "item": [ + { + "name": "GET /users/{userId} - Existing User (P1: Valid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P1 - Valid: existing user ID", + "// Expected: 200 OK with user data", + "", + "pm.test('P1: Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('P1: Response contains user data', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('userId');", + " pm.expect(jsonData).to.have.property('username');", + " pm.expect(jsonData).to.have.property('email');", + " pm.expect(jsonData).to.have.property('budget');", + "});", + "", + "pm.test('P1: Response is JSON', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.include('application/json');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/users/{{testUserId}}", + "host": ["{{baseUrl}}"], + "path": ["users", "{{testUserId}}"] + } + } + }, + { + "name": "GET /users/{userId} - Non-existent UUID (P2: Invalid - not found)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P2 - Invalid: non-existent but valid UUID format", + "// Expected: 404 Not Found", + "", + "pm.test('P2: Status code is 404 for non-existent user', function () {", + " pm.response.to.have.status(404);", + "});", + "", + "pm.test('P2: Error message indicates user not found', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData.error).to.include('not found');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/users/00000000-0000-0000-0000-000000000000", + "host": ["{{baseUrl}}"], + "path": ["users", "00000000-0000-0000-0000-000000000000"] + } + } + }, + { + "name": "GET /users/{userId} - Invalid UUID Format (P3: Invalid format)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P3 - Invalid: malformed UUID", + "// Expected: 400 Bad Request", + "", + "pm.test('P3: Status code is 400 for invalid UUID format', function () {", + " pm.response.to.have.status(400);", + "});", + "", + "pm.test('P3: Error message indicates invalid UUID', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData.error).to.include('Invalid UUID');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/users/not-a-valid-uuid", + "host": ["{{baseUrl}}"], + "path": ["users", "not-a-valid-uuid"] + } + } + } + ] + }, + { + "name": "5. User Management - PUT /users/{userId}", + "item": [ + { + "name": "PUT /users/{userId} - Update All Fields (P1: Valid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P1 - Valid: update existing user", + "// Expected: 200 OK with updated user", + "", + "pm.test('P1: Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('P1: User data is updated', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData.budget).to.eql(1500);", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const timestamp = Date.now();", + "pm.variables.set('updatedUsername', 'Updated_' + timestamp);", + "pm.variables.set('updatedEmail', 'updated_' + timestamp + '@test.com');" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"{{updatedUsername}}\",\n \"email\": \"{{updatedEmail}}\",\n \"budget\": 1500\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/{{testUserId}}", + "host": ["{{baseUrl}}"], + "path": ["users", "{{testUserId}}"] + } + } + }, + { + "name": "PUT /users/{userId} - Non-existent User (P2: Invalid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P2 - Invalid: non-existent user", + "// Expected: 404 Not Found", + "", + "pm.test('P2: Status code is 404 for non-existent user', function () {", + " pm.response.to.have.status(404);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"username\": \"Ghost\",\n \"email\": \"ghost@test.com\",\n \"budget\": 100\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/00000000-0000-0000-0000-000000000000", + "host": ["{{baseUrl}}"], + "path": ["users", "00000000-0000-0000-0000-000000000000"] + } + } + } + ] + }, + { + "name": "6. User Management - POST /users/form (HTML)", + "item": [ + { + "name": "POST /users/form - Valid Form Data (P1: Valid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P1 - Valid form submission", + "// Expected: 201 Created with HTML response", + "", + "pm.test('P1: Status code is 201', function () {", + " pm.response.to.have.status(201);", + "});", + "", + "pm.test('P1: Response is HTML', function () {", + " pm.expect(pm.response.headers.get('Content-Type')).to.include('text/html');", + "});", + "", + "pm.test('P1: Success message present', function () {", + " pm.expect(pm.response.text()).to.include('User Created Successfully');", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "const timestamp = Date.now();", + "pm.variables.set('formUsername', 'FormUser_' + timestamp);", + "pm.variables.set('formEmail', 'formuser_' + timestamp + '@test.com');" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/x-www-form-urlencoded" + } + ], + "body": { + "mode": "urlencoded", + "urlencoded": [ + { + "key": "username", + "value": "{{formUsername}}", + "type": "text" + }, + { + "key": "email", + "value": "{{formEmail}}", + "type": "text" + }, + { + "key": "budget", + "value": "750", + "type": "text" + } + ] + }, + "url": { + "raw": "{{baseUrl}}/users/form", + "host": ["{{baseUrl}}"], + "path": ["users", "form"] + } + } + } + ] + }, + { + "name": "7. User Management - GET Forms", + "item": [ + { + "name": "GET /users/create-form (P1: Valid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P1 - Valid: get create form", + "pm.test('P1: Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('P1: Response contains form', function () {", + " pm.expect(pm.response.text()).to.include(' 0)", + "pm.test('P5: Status code indicates error for zero amount', function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"description\": \"Free item\",\n \"amount\": 0,\n \"category\": \"OTHER\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/{{testUserId}}/transactions", + "host": ["{{baseUrl}}"], + "path": ["users", "{{testUserId}}", "transactions"] + } + } + }, + { + "name": "POST Transaction - Empty Description (P6: Invalid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P6 - Invalid: empty description", + "pm.test('P6: Status indicates error for empty description', function () {", + " pm.expect(pm.response.code).to.be.oneOf([400, 500]);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"description\": \"\",\n \"amount\": 50,\n \"category\": \"FOOD\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/{{testUserId}}/transactions", + "host": ["{{baseUrl}}"], + "path": ["users", "{{testUserId}}", "transactions"] + } + } + } + ] + }, + { + "name": "9. Transaction Management - GET /users/{userId}/transactions", + "item": [ + { + "name": "GET Transactions - Valid User (P1: Valid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P1 - Valid: get transactions for existing user", + "pm.test('P1: Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('P1: Response is array', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData).to.be.an('array');", + "});", + "", + "pm.test('P1: Transactions have required fields', function () {", + " const jsonData = pm.response.json();", + " if (jsonData.length > 0) {", + " pm.expect(jsonData[0]).to.have.property('transactionId');", + " pm.expect(jsonData[0]).to.have.property('amount');", + " pm.expect(jsonData[0]).to.have.property('category');", + " }", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/users/{{testUserId}}/transactions", + "host": ["{{baseUrl}}"], + "path": ["users", "{{testUserId}}", "transactions"] + } + } + }, + { + "name": "GET Transactions - Non-existent User (P2: Invalid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P2 - Invalid: non-existent user", + "pm.test('P2: Status code is 404', function () {", + " pm.response.to.have.status(404);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/users/00000000-0000-0000-0000-000000000000/transactions", + "host": ["{{baseUrl}}"], + "path": ["users", "00000000-0000-0000-0000-000000000000", "transactions"] + } + } + } + ] + }, + { + "name": "10. Transaction Management - GET /users/{userId}/transactions/{txId}", + "item": [ + { + "name": "GET Single Transaction - Valid (P1: Valid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P1 - Valid: existing user and transaction", + "pm.test('P1: Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('P1: Transaction data returned', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData).to.have.property('transactionId');", + " pm.expect(jsonData).to.have.property('userId');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/users/{{testUserId}}/transactions/{{testTransactionId}}", + "host": ["{{baseUrl}}"], + "path": ["users", "{{testUserId}}", "transactions", "{{testTransactionId}}"] + } + } + }, + { + "name": "GET Single Transaction - Wrong User (P2: Invalid - user mismatch)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P2 - Invalid: transaction belongs to different user", + "pm.test('P2: Status code is 404', function () {", + " pm.response.to.have.status(404);", + "});", + "", + "pm.test('P2: Error indicates not found for user', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData.error).to.include('not found');", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/users/{{testUserId2}}/transactions/{{testTransactionId}}", + "host": ["{{baseUrl}}"], + "path": ["users", "{{testUserId2}}", "transactions", "{{testTransactionId}}"] + } + } + }, + { + "name": "GET Single Transaction - Non-existent Transaction (P3: Invalid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P3 - Invalid: non-existent transaction", + "pm.test('P3: Status code is 404', function () {", + " pm.response.to.have.status(404);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/users/{{testUserId}}/transactions/00000000-0000-0000-0000-000000000000", + "host": ["{{baseUrl}}"], + "path": ["users", "{{testUserId}}", "transactions", "00000000-0000-0000-0000-000000000000"] + } + } + } + ] + }, + { + "name": "11. Transaction Management - PUT /users/{userId}/transactions/{txId}", + "item": [ + { + "name": "PUT Transaction - Update Amount (P1: Valid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Equivalence Partition: P1 - Valid: update transaction", + "pm.test('P1: Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('P1: Amount is updated', function () {", + " const jsonData = pm.response.json();", + " pm.expect(jsonData.amount).to.eql(75);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"amount\": 75,\n \"description\": \"Updated lunch expense\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/{{testUserId}}/transactions/{{testTransactionId}}", + "host": ["{{baseUrl}}"], + "path": ["users", "{{testUserId}}", "transactions", "{{testTransactionId}}"] + } + } + }, + { + "name": "PUT Transaction - Non-existent User (P2: Invalid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('P2: Status code is 404', function () {", + " pm.response.to.have.status(404);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"amount\": 100\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/00000000-0000-0000-0000-000000000000/transactions/{{testTransactionId}}", + "host": ["{{baseUrl}}"], + "path": ["users", "00000000-0000-0000-0000-000000000000", "transactions", "{{testTransactionId}}"] + } + } + }, + { + "name": "PUT Transaction - Wrong User (P3: Invalid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('P3: Status code is 404 for wrong user', function () {", + " pm.response.to.have.status(404);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"amount\": 100\n}" + }, + "url": { + "raw": "{{baseUrl}}/users/{{testUserId2}}/transactions/{{testTransactionId}}", + "host": ["{{baseUrl}}"], + "path": ["users", "{{testUserId2}}", "transactions", "{{testTransactionId}}"] + } + } + } + ] + }, + { + "name": "12. Transaction Form Endpoints", + "item": [ + { + "name": "GET /users/{userId}/transactions/create-form (P1: Valid)", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test('P1: Status code is 200', function () {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test('P1: Contains form HTML', function () {", + " pm.expect(pm.response.text()).to.include(' Date: Sun, 30 Nov 2025 23:19:40 -0500 Subject: [PATCH 2/4] Fixed HTML content type assertion in RouteController integration test --- .../dev/ase/teamproject/RouteControllerIntegrationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/dev/ase/teamproject/RouteControllerIntegrationTests.java b/src/test/java/dev/ase/teamproject/RouteControllerIntegrationTests.java index f7c0271..12299a0 100644 --- a/src/test/java/dev/ase/teamproject/RouteControllerIntegrationTests.java +++ b/src/test/java/dev/ase/teamproject/RouteControllerIntegrationTests.java @@ -197,7 +197,7 @@ public void createUserFromFormHtml_validForm_returns201CreatedHtml() throws Exce .param("email", "form@example.com") .param("budget", "300")) .andExpect(status().isCreated()) - .andExpect(content().contentType("text/html")) + .andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML)) .andExpect(content().string(containsString("User Created Successfully!"))) .andExpect(content().string(containsString("FormUser"))); } From afaeaa12ba0f633556dd8b55f16102ce005ac6f9 Mon Sep 17 00:00:00 2001 From: Jerry Gu Date: Sun, 30 Nov 2025 23:39:35 -0500 Subject: [PATCH 3/4] Fix duplicate username validation and update Postman P7 duplicate test --- postman/postman_collection.json | 5 +++-- .../dev/ase/teamproject/controller/RouteController.java | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/postman/postman_collection.json b/postman/postman_collection.json index f7ec7ac..b08baee 100644 --- a/postman/postman_collection.json +++ b/postman/postman_collection.json @@ -238,7 +238,8 @@ "", "pm.test('P1: Response contains correct username', function () {", " const jsonData = pm.response.json();", - " pm.expect(jsonData.username).to.eql('TestUser_' + pm.iterationData.get('timestamp') || jsonData.username);", + " const expectedUsername = pm.variables.get('uniqueUsername');", + " pm.expect(jsonData.username).to.eql(expectedUsername);", "});", "", "pm.test('P1: Response is JSON', function () {", @@ -624,7 +625,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"username\": \"DuplicateTestUser\",\n \"email\": \"duplicate_first@test.com\",\n \"budget\": 100\n}" + "raw": "{\n \"username\": \"{{uniqueUsername}}\",\n \"email\": \"duplicate_{{timestamp}}@test.com\",\n \"budget\": 100\n}" }, "url": { "raw": "{{baseUrl}}/users", diff --git a/src/main/java/dev/ase/teamproject/controller/RouteController.java b/src/main/java/dev/ase/teamproject/controller/RouteController.java index fbbde0b..be55c38 100644 --- a/src/main/java/dev/ase/teamproject/controller/RouteController.java +++ b/src/main/java/dev/ase/teamproject/controller/RouteController.java @@ -200,6 +200,14 @@ public ResponseEntity createUserJson(@RequestBody final User user) { if (user.getEmail() == null) { throw new IllegalArgumentException("Email field is required"); } + if (mockApiService.isUsernameExists(user.getUsername(), null)) { + LOGGER.warning("Duplicate username violation (JSON): " + user.getUsername()); + throw new IllegalArgumentException("Username already exists: " + user.getUsername()); + } + if (mockApiService.isEmailExists(user.getEmail(), null)) { + LOGGER.warning("Duplicate email violation (JSON): " + user.getEmail()); + throw new IllegalArgumentException("Email already exists: " + user.getEmail()); + } final User saved = mockApiService.addUser(user); return ResponseEntity.status(HttpStatus.CREATED).body(saved); } catch (DataIntegrityViolationException e) { From d616736549431337e2c962011ee038a4a8d261b8 Mon Sep 17 00:00:00 2001 From: Jerry Gu Date: Sun, 30 Nov 2025 23:54:03 -0500 Subject: [PATCH 4/4] Fix CI pipeline: remove publish step and upload Newman reports --- .github/workflows/maven.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 682bee7..fc5e418 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -228,13 +228,6 @@ jobs: name: newman-junit-results path: newman-results.xml - - name: Publish Test Results - if: always() - uses: EnricoMi/publish-unit-test-result-action@v2 - with: - files: newman-results.xml - check_name: API Test Results - # =========================================== # Job 4: Code Coverage Report # ===========================================