CI Test Suite #349
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI Test Suite | |
| on: | |
| pull_request: | |
| branches: [main] | |
| push: | |
| branches: [main] | |
| schedule: | |
| # Run tests daily at 2 AM UTC | |
| - cron: '0 2 * * *' | |
| # Run key functionality tests every 6 hours | |
| - cron: '0 */6 * * *' | |
| permissions: | |
| contents: read | |
| packages: write | |
| security-events: write | |
| issues: write | |
| pull-requests: write | |
| env: | |
| COMMIT_SHA: ${{ github.sha }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITHUB_ACTOR: ${{ github.actor }} | |
| NODE_VERSION: '18' | |
| PYTHON_VERSION: '3.9' | |
| JAVA_VERSION: '21' | |
| jobs: | |
| # Job 1: Client Tests (React/TypeScript) | |
| client-tests: | |
| name: Client Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| cache-dependency-path: client/package-lock.json | |
| - name: Install dependencies | |
| run: | | |
| cd client | |
| npm ci | |
| - name: Run type checking | |
| run: | | |
| cd client | |
| npm run typecheck | |
| - name: Run tests with coverage | |
| run: | | |
| cd client | |
| npm run test:coverage | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| file: ./client/coverage/lcov.info | |
| flags: client | |
| name: client-coverage | |
| fail_ci_if_error: false | |
| - name: Archive test results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: client-test-results | |
| path: | | |
| client/coverage/ | |
| client/test-results.xml | |
| # Job 3: Auth Service Tests (Kotlin/Spring Boot) | |
| auth-service-tests: | |
| name: Auth Service Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| services: | |
| postgres: | |
| image: postgres:15 | |
| env: | |
| POSTGRES_DB: auth_test | |
| POSTGRES_USER: test | |
| POSTGRES_PASSWORD: test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U test -d auth_test" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: ${{ env.JAVA_VERSION }} | |
| distribution: 'temurin' | |
| cache: 'gradle' | |
| - name: Make gradlew executable | |
| run: chmod +x server/auth-service/gradlew | |
| - name: Run tests | |
| env: | |
| SPRING_PROFILES_ACTIVE: test | |
| SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/auth_test | |
| SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.postgresql.Driver | |
| SPRING_DATASOURCE_USERNAME: test | |
| SPRING_DATASOURCE_PASSWORD: test | |
| SPRING_JPA_DATABASE_PLATFORM: org.hibernate.dialect.PostgreSQLDialect | |
| run: | | |
| cd server/auth-service | |
| ./gradlew test --info --stacktrace | |
| - name: Archive test results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: auth-service-test-results | |
| path: | | |
| server/auth-service/build/reports/tests/test/ | |
| server/auth-service/build/test-results/test/ | |
| # Job 4: Document Service Tests (Kotlin/Spring Boot) | |
| document-service-tests: | |
| name: Document Service Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| services: | |
| postgres: | |
| image: postgres:15 | |
| env: | |
| POSTGRES_DB: document_test | |
| POSTGRES_USER: test | |
| POSTGRES_PASSWORD: test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U test -d document_test" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: ${{ env.JAVA_VERSION }} | |
| distribution: 'temurin' | |
| cache: 'gradle' | |
| - name: Make gradlew executable | |
| run: chmod +x server/document-service/gradlew | |
| - name: Run tests | |
| env: | |
| SPRING_PROFILES_ACTIVE: test | |
| SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/document_test | |
| SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.postgresql.Driver | |
| SPRING_DATASOURCE_USERNAME: test | |
| SPRING_DATASOURCE_PASSWORD: test | |
| SPRING_JPA_DATABASE_PLATFORM: org.hibernate.dialect.PostgreSQLDialect | |
| run: | | |
| cd server/document-service | |
| ./gradlew test --info --stacktrace | |
| - name: Archive test results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: document-service-test-results | |
| path: | | |
| server/document-service/build/reports/tests/test/ | |
| server/document-service/build/test-results/test/ | |
| # Job 5: Integration Tests | |
| integration-tests: | |
| name: Integration Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| needs: [client-tests, auth-service-tests, document-service-tests] | |
| services: | |
| postgres: | |
| image: postgres:15 | |
| env: | |
| POSTGRES_DB: studymate_integration | |
| POSTGRES_USER: test | |
| POSTGRES_PASSWORD: test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U test -d studymate_integration" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| weaviate: | |
| image: cr.weaviate.io/semitechnologies/weaviate:1.30.3 | |
| ports: | |
| - 8080:8080 | |
| env: | |
| AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true' | |
| QUERY_DEFAULTS_LIMIT: 25 | |
| PERSISTENCE_DATA_PATH: '/var/lib/weaviate' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| cache-dependency-path: client/package-lock.json | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| cache: 'pip' | |
| - name: Setup Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: ${{ env.JAVA_VERSION }} | |
| distribution: 'temurin' | |
| cache: 'gradle' | |
| - name: Wait for Weaviate to be ready | |
| run: | | |
| echo "Waiting for Weaviate to start..." | |
| timeout 120 bash -c 'until curl -f http://localhost:8080/v1/.well-known/ready > /dev/null 2>&1; do sleep 5; done' || echo "Weaviate may not be ready, continuing anyway" | |
| - name: Start services | |
| run: | | |
| # Start GenAI service | |
| if [ ! -d "genAi" ]; then | |
| echo "Error: genAi directory not found for integration tests" | |
| ls -la | |
| exit 1 | |
| fi | |
| cd genAi | |
| pip install -r requirements.txt | |
| nohup python -m uvicorn main:app --host 0.0.0.0 --port 8081 & | |
| # Start microservices | |
| cd ../server/auth-service | |
| chmod +x gradlew | |
| nohup ./gradlew bootRun & | |
| cd ../document-service | |
| chmod +x gradlew | |
| nohup ./gradlew bootRun & | |
| # Wait for services to start | |
| sleep 30 | |
| - name: Run integration tests | |
| env: | |
| VITE_API_BASE_URL: http://localhost:8082 | |
| OPEN_WEBUI_API_KEY_CHAT: test-chat-key | |
| OPEN_WEBUI_API_KEY_GEN: test-gen-key | |
| WEAVIATE_HOST: localhost | |
| WEAVIATE_PORT: 8080 | |
| run: | | |
| cd client | |
| npm ci | |
| npm run test:integration || true # Allow integration tests to fail for now | |
| - name: Health check services | |
| run: | | |
| curl -f http://localhost:8081/health || echo "GenAI service not ready" | |
| curl -f http://localhost:8083/actuator/health || echo "Auth service not ready" | |
| curl -f http://localhost:8084/actuator/health || echo "Document service not ready" | |
| # Job 6: AI Features Functionality Tests | |
| ai-functionality-tests: | |
| name: AI Features Functionality | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| services: | |
| weaviate: | |
| image: cr.weaviate.io/semitechnologies/weaviate:1.30.3 | |
| ports: | |
| - 8080:8080 | |
| env: | |
| AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true' | |
| QUERY_DEFAULTS_LIMIT: 25 | |
| PERSISTENCE_DATA_PATH: '/var/lib/weaviate' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| cache: 'pip' | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| cache-dependency-path: client/package-lock.json | |
| - name: Wait for Weaviate to be ready | |
| run: | | |
| echo "Waiting for Weaviate to start..." | |
| timeout 120 bash -c 'until curl -f http://localhost:8080/v1/.well-known/ready > /dev/null 2>&1; do sleep 5; done' || echo "Weaviate may not be ready, continuing anyway" | |
| - name: Install Python dependencies | |
| run: | | |
| if [ ! -d "genAi" ]; then | |
| echo "Error: genAi directory not found" | |
| ls -la | |
| exit 1 | |
| fi | |
| cd genAi | |
| pip install -r requirements.txt | |
| pip install -r requirements-test.txt | |
| - name: Start GenAI Service | |
| env: | |
| OPEN_WEBUI_API_KEY_CHAT: test-chat-key | |
| OPEN_WEBUI_API_KEY_GEN: test-gen-key | |
| WEAVIATE_HOST: localhost | |
| WEAVIATE_PORT: 8080 | |
| run: | | |
| if [ ! -d "genAi" ]; then | |
| echo "Error: genAi directory not found" | |
| ls -la | |
| exit 1 | |
| fi | |
| cd genAi | |
| nohup python -m uvicorn main:app --host 0.0.0.0 --port 8081 & | |
| echo $! > genai-service.pid | |
| # Wait for service to start | |
| timeout 60 bash -c 'until curl -f http://localhost:8081/health; do sleep 5; done' || echo 'GenAI service health check failed' | |
| - name: Test AI functionality | |
| run: | | |
| if [ ! -d "genAi" ]; then | |
| echo "Error: genAi directory not found" | |
| ls -la | |
| exit 1 | |
| fi | |
| cd genAi | |
| # Test core AI features | |
| python -m pytest test_main.py::TestHealthEndpoint::test_health_check_success -v | |
| python -m pytest test_main.py::TestDocumentManagement::test_load_document_success -v | |
| python -m pytest test_main.py::TestChatEndpoint::test_chat_success -v | |
| python -m pytest test_main.py::TestSummaryEndpoint::test_summary_success -v | |
| python -m pytest test_main.py::TestQuizEndpoint::test_quiz_success -v | |
| python -m pytest test_main.py::TestFlashcardEndpoint::test_flashcard_success -v | |
| - name: Cleanup | |
| if: always() | |
| run: | | |
| if [ -f genAi/genai-service.pid ]; then | |
| kill $(cat genAi/genai-service.pid) || true | |
| fi | |
| # Job 7: Error Handling & Recovery Tests | |
| error-handling-tests: | |
| name: Error Handling & Recovery | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| cache-dependency-path: client/package-lock.json | |
| - name: Test client-side error handling | |
| run: | | |
| cd client | |
| npm ci | |
| # Create error handling test | |
| cat > src/test/error-handling.test.ts << 'EOF' | |
| import { describe, it, expect } from 'vitest' | |
| import { server } from './mocks/server' | |
| import { http, HttpResponse } from 'msw' | |
| describe('Error Handling', () => { | |
| it('should handle network errors', async () => { | |
| server.use( | |
| http.post('/api/auth/login', () => { | |
| return HttpResponse.error() | |
| }) | |
| ) | |
| try { | |
| await fetch('/api/auth/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ username: 'test', password: 'test' }) | |
| }) | |
| } catch (error) { | |
| expect(error).toBeDefined() | |
| } | |
| }) | |
| it('should handle 401 unauthorized', async () => { | |
| server.use( | |
| http.get('/api/auth/me', () => { | |
| return HttpResponse.json( | |
| { error: 'Unauthorized' }, | |
| { status: 401 } | |
| ) | |
| }) | |
| ) | |
| const response = await fetch('/api/auth/me') | |
| expect(response.status).toBe(401) | |
| }) | |
| it('should handle 500 server errors', async () => { | |
| server.use( | |
| http.post('/api/documents/upload', () => { | |
| return HttpResponse.json( | |
| { error: 'Internal server error' }, | |
| { status: 500 } | |
| ) | |
| }) | |
| ) | |
| const formData = new FormData() | |
| formData.append('file', new File(['test'], 'test.txt')) | |
| const response = await fetch('/api/documents/upload', { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| expect(response.status).toBe(500) | |
| }) | |
| }) | |
| EOF | |
| npm test -- src/test/error-handling.test.ts | |
| # Job 8: Security Tests | |
| security-tests: | |
| name: Security Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Run Trivy vulnerability scanner | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| scan-type: 'fs' | |
| scan-ref: '.' | |
| format: 'sarif' | |
| output: 'trivy-results.sarif' | |
| - name: Upload Trivy scan results to GitHub Security tab | |
| uses: github/codeql-action/upload-sarif@v3 | |
| if: always() | |
| with: | |
| sarif_file: 'trivy-results.sarif' | |
| - name: Scan for secrets | |
| uses: trufflesecurity/trufflehog@main | |
| if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.event.before != github.sha) | |
| with: | |
| path: ./ | |
| base: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before || 'HEAD~1' }} | |
| head: ${{ github.sha }} | |
| extra_args: --debug --only-verified | |
| # Job 9: Performance Tests | |
| performance-tests: | |
| name: Performance Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Install k6 | |
| run: | | |
| sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 | |
| echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list | |
| sudo apt-get update | |
| sudo apt-get install k6 | |
| - name: Run load tests | |
| run: | | |
| # Create a basic load test | |
| cat > load-test.js << 'EOF' | |
| import http from 'k6/http'; | |
| import { check, sleep } from 'k6'; | |
| export const options = { | |
| stages: [ | |
| { duration: '30s', target: 20 }, | |
| { duration: '1m', target: 20 }, | |
| { duration: '20s', target: 0 }, | |
| ], | |
| thresholds: { | |
| http_req_duration: ['p(99)<1500'], | |
| }, | |
| }; | |
| export default function () { | |
| const response = http.get('http://localhost:8081/health'); | |
| check(response, { 'status was 200': (r) => r.status == 200 }); | |
| sleep(1); | |
| } | |
| EOF | |
| # Run load test (will fail without running services, but validates config) | |
| k6 run --vus 1 --duration 10s load-test.js || true | |
| # Job 10: Test Summary | |
| test-summary: | |
| name: Test Summary | |
| runs-on: ubuntu-latest | |
| needs: [client-tests, auth-service-tests, document-service-tests, integration-tests, ai-functionality-tests, error-handling-tests, security-tests] | |
| if: always() | |
| steps: | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| - name: Generate test summary | |
| run: | | |
| echo "# Test Summary" > test-summary.md | |
| echo "" >> test-summary.md | |
| echo "## Test Results" >> test-summary.md | |
| echo "" >> test-summary.md | |
| # Check job results | |
| if [ "${{ needs.client-tests.result }}" = "success" ]; then | |
| echo "✅ Client Tests: PASSED" >> test-summary.md | |
| else | |
| echo "❌ Client Tests: FAILED" >> test-summary.md | |
| fi | |
| if [ "${{ needs.auth-service-tests.result }}" = "success" ]; then | |
| echo "✅ Auth Service Tests: PASSED" >> test-summary.md | |
| else | |
| echo "❌ Auth Service Tests: FAILED" >> test-summary.md | |
| fi | |
| if [ "${{ needs.document-service-tests.result }}" = "success" ]; then | |
| echo "✅ Document Service Tests: PASSED" >> test-summary.md | |
| else | |
| echo "❌ Document Service Tests: FAILED" >> test-summary.md | |
| fi | |
| if [ "${{ needs.integration-tests.result }}" = "success" ]; then | |
| echo "✅ Integration Tests: PASSED" >> test-summary.md | |
| else | |
| echo "❌ Integration Tests: FAILED" >> test-summary.md | |
| fi | |
| if [ "${{ needs.security-tests.result }}" = "success" ]; then | |
| echo "✅ Security Tests: PASSED" >> test-summary.md | |
| else | |
| echo "❌ Security Tests: FAILED" >> test-summary.md | |
| fi | |
| if [ "${{ needs.ai-functionality-tests.result }}" = "success" ]; then | |
| echo "✅ AI Features Functionality Tests: PASSED" >> test-summary.md | |
| else | |
| echo "❌ AI Features Functionality Tests: FAILED" >> test-summary.md | |
| fi | |
| if [ "${{ needs.error-handling-tests.result }}" = "success" ]; then | |
| echo "✅ Error Handling Tests: PASSED" >> test-summary.md | |
| else | |
| echo "❌ Error Handling Tests: FAILED" >> test-summary.md | |
| fi | |
| echo "" >> test-summary.md | |
| echo "## Coverage Reports" >> test-summary.md | |
| echo "" >> test-summary.md | |
| echo "Coverage reports are available in the artifacts section." >> test-summary.md | |
| cat test-summary.md | |
| - name: Upload test summary | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-summary | |
| path: test-summary.md | |
| - name: Comment PR | |
| uses: actions/github-script@v7 | |
| if: github.event_name == 'pull_request' | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const summary = fs.readFileSync('test-summary.md', 'utf8'); | |
| github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: summary | |
| }); | |
| # Job 11: Cleanup | |
| cleanup: | |
| name: Cleanup | |
| runs-on: ubuntu-latest | |
| needs: [test-summary] | |
| if: always() | |
| steps: | |
| - name: Clean up artifacts older than 30 days | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const cutoffDate = new Date(); | |
| cutoffDate.setDate(cutoffDate.getDate() - 30); | |
| const artifacts = await github.rest.actions.listArtifactsForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| per_page: 100 | |
| }); | |
| for (const artifact of artifacts.data.artifacts) { | |
| const createdAt = new Date(artifact.created_at); | |
| if (createdAt < cutoffDate) { | |
| await github.rest.actions.deleteArtifact({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| artifact_id: artifact.id | |
| }); | |
| console.log(`Deleted artifact: ${artifact.name}`); | |
| } | |
| } |