diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..120f81c --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,36 @@ +# CI/CD Workflow + +This directory contains GitHub Actions workflows for automated testing and validation. + +## CI / Validation Workflow (`.github/workflows/test.yml`) + +**Purpose**: Prevents regressions by running all test suites and linting on every pull request to main. + +**Triggers**: + +- Pull requests targeting the `main` branch + +**What it does**: + +1. **Setup**: Installs Rust (stable) and Node.js (v20) with dependency caching +2. **Dependency Installation**: Uses `npm install` for both backend and frontend +3. **Code Quality**: Runs linting on both backend and frontend (if configured) +4. **Backend Tests**: Runs `npm test` in `/backend` (Vitest + TypeScript) +5. **Frontend Tests**: Runs `npm test --if-present` in `/frontend` (no test framework configured yet) +6. **Smart Contract Tests**: Runs `cargo test` in `/contracts` (Rust) + +**Failure Behavior**: + +- Workflow fails if any test or linting step fails +- Blocks PR merge until all checks pass +- Uses `--if-present` to avoid failures when test scripts are missing + +**Requirements for Contributors**: + +- Ensure lint and test scripts are properly configured in `package.json` +- Tests must pass in all directories where they exist +- New dependencies should be added to respective `package.json` files +- Backend uses Vitest, frontend has no test framework configured yet +- react-hot-toast is available as a root dependency + +**Note**: The workflow uses `continue-on-error: false` to ensure strict validation - any failure will prevent the PR from being merged. diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml new file mode 100644 index 0000000..2a848c6 --- /dev/null +++ b/.github/workflows/backend-ci.yml @@ -0,0 +1,62 @@ +name: Backend CI + +on: + pull_request: + branches: [ main ] + +jobs: + backend-tests: + name: Backend Tests + runs-on: ubuntu-latest + services: + postgres: + image: postgres:15 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: flowfi_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: package-lock.json + + - name: Install Node dependencies + run: npm ci --include=optional + + - name: Setup Database + run: | + npx prisma generate --schema=prisma/schema.prisma + npx prisma db push --accept-data-loss --schema=prisma/schema.prisma + working-directory: backend + env: + DATABASE_URL: postgresql://postgres:password@127.0.0.1:5431/flowfi_test + + - name: Run Cargo Tests + run: cargo test + working-directory: contracts + + - name: Run NPM Tests + run: npm test + working-directory: backend + env: + DATABASE_URL: postgresql://postgres:password@127.0.0.1:5431/flowfi_test + NODE_ENV: test diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..01660c9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,68 @@ +name: CI / Validation + +on: + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + + - name: Cache Rust dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + contracts/target + key: ${{ runner.os }}-cargo-${{ hashFiles('contracts/**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Install backend dependencies + run: npm install + working-directory: ./backend + + - name: Install frontend dependencies + run: npm install + working-directory: ./frontend + + - name: Run linting (backend) + run: npm run lint --if-present + working-directory: ./backend + continue-on-error: false + + - name: Run linting (frontend) + run: npm run lint --if-present + working-directory: ./frontend + continue-on-error: false + + - name: Run Rust tests + run: cargo test + working-directory: ./contracts + continue-on-error: false + + - name: Run backend tests + run: npm test + working-directory: ./backend + continue-on-error: false + + - name: Run frontend tests + run: npm test + working-directory: ./frontend + continue-on-error: false diff --git a/backend/package.json b/backend/package.json index e61b139..de75230 100644 --- a/backend/package.json +++ b/backend/package.json @@ -40,6 +40,9 @@ "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/node": "^25.2.3", + + "@types/supertest": "^6.0.3", + "@types/pg": "^8.16.0", "@types/supertest": "^7.2.0", "@types/swagger-jsdoc": "^6.0.4", diff --git a/backend/tsconfig.json b/backend/tsconfig.json index cfba994..2f81df9 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -43,4 +43,4 @@ "src/**/*", "tests/**/*" ] -} \ No newline at end of file +} diff --git a/frontend/__tests__/setup.ts b/frontend/__tests__/setup.ts new file mode 100644 index 0000000..d0de870 --- /dev/null +++ b/frontend/__tests__/setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom"; diff --git a/frontend/package.json b/frontend/package.json index 094fcd2..5c1fe44 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "test": "vitest run" }, "dependencies": { "@stellar/freighter-api": "^6.0.1", @@ -17,16 +18,23 @@ "react": "19.2.4", "react-dom": "19.2.4", "react-hot-toast": "^2.6.0" + }, "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitest/ui": "^4.0.18", "eslint": "^9", "eslint-config-next": "16.1.6", + "jsdom": "^28.1.0", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.0.18" } } diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..7d902f5 --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,16 @@ +import { resolve } from "path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./__tests__/setup.ts"], + passWithNoTests: true, + }, + resolve: { + alias: { + "@": resolve(__dirname, "./"), + }, + }, +}); diff --git a/package-lock.json b/package-lock.json index c7b1966..ae96a3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,9 @@ "@types/cors": "^2.8.19", "@types/express": "^5.0.6", "@types/node": "^25.2.3", + + "@types/supertest": "^6.0.3", + "@types/pg": "^8.16.0", "@types/supertest": "^7.2.0", "@types/swagger-jsdoc": "^6.0.4", @@ -419,13 +422,19 @@ "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@tailwindcss/postcss": "^4", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitest/ui": "^4.0.18", "eslint": "^9", "eslint-config-next": "16.1.6", + "jsdom": "^28.1.0", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.0.18" } }, "node_modules/@alloc/quick-lru": { @@ -1471,8 +1480,8 @@ "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, "node_modules/@jsdevtools/ono": { @@ -2020,7 +2029,7 @@ "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/node/node_modules/undici-types": { @@ -2325,14 +2334,16 @@ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.7.4", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/utils": { "version": "8.56.1", "dev": true, @@ -2479,11 +2490,20 @@ "version": "4.3.0", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, + "peer": true, "engines": { "node": ">=8" + } + }, + "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" @@ -3570,12 +3590,28 @@ "node_modules/doctrine": { "version": "2.1.0", "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=6.0.0" } }, "node_modules/dotenv": { @@ -5291,7 +5327,7 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/has-property-descriptors": { @@ -6108,6 +6144,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -6418,10 +6455,14 @@ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "mime-db": "^1.54.0" }, "engines": { - "node": "*" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/minimist": { @@ -7436,7 +7477,8 @@ "node_modules/react-is": { "version": "16.13.1", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/readable-stream": { "version": "3.6.2", @@ -7548,16 +7590,48 @@ "version": "4.0.0", "dev": true, "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "dev": true, "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/restore-cursor": { @@ -7762,8 +7836,26 @@ "version": "6.3.1", "dev": true, "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "devOptional": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/send": { @@ -8109,7 +8201,7 @@ "node_modules/stable-hash": { "version": "0.0.5", "dev": true, - "license": "MIT" + "license": "ISC" }, "node_modules/stack-trace": { "version": "0.0.10", @@ -8173,11 +8265,7 @@ "node_modules/stop-iteration-iterator": { "version": "1.1.0", "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, + "license": "ISC", "engines": { "node": ">= 0.4" } @@ -8230,25 +8318,10 @@ "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" + "semver": "^7.5.3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10" } }, "node_modules/string.prototype.repeat": { @@ -8274,10 +8347,7 @@ "has-property-descriptors": "^1.0.2" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, "node_modules/string.prototype.trimend": { @@ -8285,32 +8355,25 @@ "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, "node_modules/string.prototype.trimstart": { "version": "1.0.8", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/strip-ansi": { @@ -8330,9 +8393,11 @@ "node_modules/strip-bom": { "version": "3.0.0", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", + "optional": true, + "peer": true, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, "node_modules/strip-json-comments": { @@ -8340,10 +8405,7 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/styled-jsx": { @@ -8530,32 +8592,14 @@ "node_modules/tinyglobby": { "version": "0.2.15", "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } + "license": "MIT" }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.5.0", "dev": true, "license": "MIT", "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">= 0.8" } }, "node_modules/tinyglobby/node_modules/picomatch": { @@ -8606,7 +8650,8 @@ "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" }, "engines": { "node": ">=8.0" @@ -8696,10 +8741,7 @@ "dev": true, "license": "MIT", "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "safe-buffer": "~5.2.0" } }, "node_modules/tsconfig-paths/node_modules/json5": { @@ -8747,7 +8789,7 @@ "prelude-ls": "^1.2.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">=0.6.19" } }, "node_modules/type-is": { @@ -8766,12 +8808,14 @@ "version": "1.0.3", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">= 0.4" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/typed-array-byte-length": { @@ -8779,37 +8823,24 @@ "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/typed-array-byte-offset": { "version": "1.0.4", "dev": true, "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/typed-array-length": { @@ -8818,17 +8849,11 @@ "license": "MIT", "dependencies": { "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/typescript": { @@ -8871,9 +8896,17 @@ "license": "MIT", "dependencies": { "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8935,20 +8968,6 @@ "node_modules/update-browserslist-db": { "version": "1.2.3", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { "escalade": "^3.2.0", @@ -9192,19 +9211,10 @@ "dev": true, "license": "MIT", "dependencies": { + "call-bind": "^1.0.8", "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -9218,10 +9228,9 @@ "dev": true, "license": "MIT", "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -9234,13 +9243,7 @@ "version": "1.1.20", "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" + "min-indent": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -9416,7 +9419,7 @@ "node": ">=8.0.0" }, "optionalDependencies": { - "commander": "^9.4.1" + "commander": "^10.0.0" } }, "node_modules/z-schema/node_modules/commander": { @@ -9424,7 +9427,7 @@ "license": "MIT", "optional": true, "engines": { - "node": "^12.20.0 || >=14" + "node": ">=14" } }, "node_modules/zeptomatch": {