From 038fc5defc8a605eb5c5c27019193328893e6906 Mon Sep 17 00:00:00 2001 From: CrHackHead Date: Fri, 8 Aug 2025 18:07:38 +0200 Subject: [PATCH] Add frontend testing and CI setup --- .github/workflows/ci.yml | 100 ++++++++++ .pre-commit-config.yaml | 6 + frontend/package-lock.json | 174 ++++++++++++++++++ frontend/package.json | 1 + frontend/src/lib/api.ts | 8 +- .../src/pages/__tests__/Dashboard.test.tsx | 19 ++ frontend/vitest.config.ts | 7 + 7 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .pre-commit-config.yaml create mode 100644 frontend/src/pages/__tests__/Dashboard.test.tsx create mode 100644 frontend/vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f117f17 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,100 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + +jobs: + backend: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: catchattack + ports: [ "5432:5432" ] + options: >- + --health-cmd="pg_isready -U postgres" + --health-interval=5s --health-timeout=5s --health-retries=10 + elastic: + image: docker.elastic.co/elasticsearch/elasticsearch:8.14.1 + env: + discovery.type: single-node + xpack.security.enabled: "false" + ports: [ "9200:9200" ] + options: >- + --health-cmd="curl -s http://localhost:9200 >/dev/null || exit 1" + --health-interval=10s --health-timeout=5s --health-retries=12 + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: { python-version: "3.11" } + - name: Install backend + run: | + pip install -U pip + pip install -e backend + pip install ruff mypy + - name: Lint & type check + run: | + ruff backend + mypy backend || true + - name: Alembic migrate + env: + DB_DSN: postgresql+psycopg://postgres:postgres@localhost:5432/catchattack + run: | + cd backend + alembic upgrade head + - name: Run API (background) + env: + DB_DSN: postgresql+psycopg://postgres:postgres@localhost:5432/catchattack + ELASTIC_URL: http://localhost:9200 + run: | + nohup uvicorn app.main:app --host 0.0.0.0 --port 8000 & sleep 3 + - name: Backend tests + run: | + pytest -q + - name: Upload coverage + uses: actions/upload-artifact@v4 + with: + name: backend-coverage + path: backend/.coverage + if-no-files-found: ignore + + frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: { node-version: "20" } + - name: Install & test + working-directory: frontend + run: | + npm ci || npm i + npm run test -- --coverage + - name: ESLint + working-directory: frontend + run: npx eslint src --max-warnings=0 + + docker-build: + runs-on: ubuntu-latest + needs: [backend, frontend] + steps: + - uses: actions/checkout@v4 + - name: Build images + run: | + docker build -f docker/Dockerfile.api -t ghcr.io/${{ github.repository }}/api:${{ github.sha }} . + docker build -f docker/Dockerfile.web -t ghcr.io/${{ github.repository }}/web:${{ github.sha }} . + - name: Save images + run: | + docker save ghcr.io/${{ github.repository }}/api:${{ github.sha }} | gzip > api-image.tar.gz + docker save ghcr.io/${{ github.repository }}/web:${{ github.sha }} | gzip > web-image.tar.gz + - uses: actions/upload-artifact@v4 + with: + name: images + path: | + api-image.tar.gz + web-image.tar.gz diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ed67a62 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.5.4 + hooks: + - id: ruff + - id: ruff-format diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 618a4e1..22f2a3f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@testing-library/react": "^16.3.0", "@types/node": "^20.11.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", @@ -52,6 +53,22 @@ "lru-cache": "^10.4.3" } }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -88,6 +105,16 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.28.2", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", @@ -1263,6 +1290,63 @@ "win32" ] }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1754,6 +1838,17 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2021,6 +2116,25 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3140,6 +3254,17 @@ "dev": true, "license": "ISC" }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -3497,6 +3622,47 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -3573,6 +3739,14 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 678fbaa..6aabe9a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@testing-library/react": "^16.3.0", "@types/node": "^20.11.0", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 2d79256..084594d 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -14,7 +14,13 @@ export async function login(username:string,password:string){ export function withAuth(token:string){ const h = { "Authorization":`Bearer ${token}", "Content-Type":"application/json" }; return { - listRules: (technique?:string)=> fetch(`${API_BASE}/api/v1/rules${technique?`?technique=${technique}`:''}`, {headers:h}).then(j), + listRules: (technique?: string) => + fetch( + API_BASE + + '/api/v1/rules' + + (technique ? '?technique=' + technique : ''), + { headers: h } + ).then(j), createRule: (body:any)=> fetch(`${API_BASE}/api/v1/rules`, {method:"POST",headers:h,body:JSON.stringify(body)}).then(j), lintRule: (id:string)=> fetch(`${API_BASE}/api/v1/rules/${id}/lint`, {method:"POST",headers:h}).then(j), compileRule: (id:string,target:string)=> fetch(`${API_BASE}/api/v1/rules/${id}/compile?target=${target}`, {method:"POST",headers:h}).then(j), diff --git a/frontend/src/pages/__tests__/Dashboard.test.tsx b/frontend/src/pages/__tests__/Dashboard.test.tsx new file mode 100644 index 0000000..e675d44 --- /dev/null +++ b/frontend/src/pages/__tests__/Dashboard.test.tsx @@ -0,0 +1,19 @@ +import { describe, it, expect, vi } from "vitest"; +import React from "react"; +import { render } from "@testing-library/react"; + +vi.mock("../../lib/api", () => ({ + withAuth: () => ({ + coverage: () => Promise.resolve([]), + priorities: () => Promise.resolve([]) + }) +})); + +import Dashboard from "../Dashboard"; + +describe("Dashboard", () => { + it("renders header", () => { + const { getByText } = render(); + expect(getByText(/catchattack-beta/i)).toBeTruthy(); + }); +}); diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..1bdb5a7 --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; +export default defineConfig({ + test: { + environment: "jsdom", + coverage: { reporter: ["text", "json-summary"] } + }, +});