diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..b2e384fd6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,73 @@ +name: CI Pipeline + +on: + push: + branches: + - develop + +jobs: + backend-tests: + name: Backend Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install backend dependencies + run: | + cd backend + python -m venv venv + source venv/bin/activate + pip install --upgrade pip + pip install -r requirements.txt + + - name: Run backend tests + run: | + cd backend + source venv/bin/activate + pytest + + frontend-tests: + name: Frontend E2E Tests + runs-on: ubuntu-latest + needs: backend-tests + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install frontend dependencies + run: | + cd frontend + npm ci + + - name: Install Playwright browsers + run: | + cd frontend + npx playwright install --with-deps + + - name: Start backend + run: | + cd backend + python -m venv venv + source venv/bin/activate + pip install -r requirements.txt + nohup uvicorn app.main:app --host 0.0.0.0 --port 8000 & + + - name: Run frontend E2E tests + run: | + cd frontend + npm run dev & + sleep 10 + npx playwright test diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 000000000..8da1dc730 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,27 @@ +# -------- Builder stage -------- +FROM python:3.12-slim AS builder + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt + +# -------- Runtime stage -------- +FROM python:3.12-slim + +WORKDIR /app + +# Create non-root user +RUN useradd -m appuser + +COPY --from=builder /usr/local/lib/python3.12 /usr/local/lib/python3.12 +COPY --from=builder /usr/local/bin /usr/local/bin + +COPY app ./app + +USER appuser + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..53bd8c5ba --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +version: "3.9" + +services: + backend: + build: ./backend + ports: + - "8000:8000" + + frontend: + build: ./frontend + ports: + - "3000:3000" + environment: + - NEXT_PUBLIC_API_URL=http://backend:8000 + depends_on: + - backend diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 000000000..cc84f6b4f --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,23 @@ +# -------- Builder stage -------- +FROM node:18-alpine AS builder + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +# -------- Runtime stage -------- +FROM node:18-alpine + +WORKDIR /app + +ENV NODE_ENV=production + +COPY --from=builder /app ./ + +EXPOSE 3000 + +CMD ["npm", "run", "start"] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 52c6f47a0..de22fb0b4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@playwright/test": "^1.57.0", "eslint": "^8.54.0", "eslint-config-next": "14.0.0" } @@ -374,6 +375,21 @@ "node": ">=12.4.0" } }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "devOptional": true, + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2320,6 +2336,20 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3750,6 +3780,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "devOptional": true, + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "devOptional": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0b8f13abf..f81d9ee7b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,12 +9,13 @@ "lint": "next lint" }, "dependencies": { + "axios": "^1.6.0", "next": "^14.0.0", "react": "^18.2.0", - "react-dom": "^18.2.0", - "axios": "^1.6.0" + "react-dom": "^18.2.0" }, "devDependencies": { + "@playwright/test": "^1.57.0", "eslint": "^8.54.0", "eslint-config-next": "14.0.0" } diff --git a/frontend/tests/home.spec.js b/frontend/tests/home.spec.js new file mode 100644 index 000000000..a7a57ee7a --- /dev/null +++ b/frontend/tests/home.spec.js @@ -0,0 +1,14 @@ +const { test, expect } = require('@playwright/test'); + +test('homepage loads', async ({ page }) => { + await page.goto('http://localhost:3000'); + await expect(page).toHaveTitle(/DevOps Assignment/); +}); + +test('backend message is visible', async ({ page }) => { + await page.goto('http://localhost:3000'); + await expect( + page.locator('text=Backend Message') + ).toBeVisible(); +}); +