diff --git a/.gitea/workflows/validate.yml b/.gitea/workflows/validate.yml new file mode 100644 index 0000000..b151b63 --- /dev/null +++ b/.gitea/workflows/validate.yml @@ -0,0 +1,43 @@ +name: Validation applicative + +on: + push: + branches: [main] + paths-ignore: + - "**.md" + - "docs/**" + pull_request: + branches: [main] + paths-ignore: + - "**.md" + - "docs/**" + workflow_dispatch: + +jobs: + verify: + name: TypeScript, tests et build + runs-on: ci-node22 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - name: Calculer la clé de cache pnpm + id: pnpm-cache-key + shell: bash + run: | + echo "store=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + echo "lock=$(sha256sum pnpm-lock.yaml | cut -d ' ' -f 1)" >> "$GITHUB_OUTPUT" + + - name: Restaurer le store pnpm + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache-key.outputs.store }} + key: pnpm-${{ runner.os }}-${{ steps.pnpm-cache-key.outputs.lock }} + restore-keys: | + pnpm-${{ runner.os }}- + + - name: Installer les dépendances verrouillées + run: pnpm install --frozen-lockfile --prefer-offline + + - name: Vérifier TypeScript, tests et build + run: pnpm verify diff --git a/app.json b/app.json index bc0cff0..6a50ca3 100644 --- a/app.json +++ b/app.json @@ -6,8 +6,11 @@ "recette": "https://veille.recette.santinova-soft.org", "prod": "https://veille.santinova-soft.org" }, - "containerName": "veille-reglementaire", + "containerName": "veille-reglementaire-recette", "image": "images/veille-reglementaire.png", "giteaRepo": "veille-reglementaire", - "giteaOwner": "manus-admin" + "giteaOwner": "manus-admin", + "ci": { + "required": true + } } diff --git a/package.json b/package.json index 9bf9525..aa140fb 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "check": "tsc --noEmit", "format": "prettier --write .", "test": "vitest run", + "verify": "pnpm check && pnpm test && pnpm build", "db:push": "drizzle-kit generate && drizzle-kit migrate" }, "dependencies": { @@ -122,4 +123,4 @@ "tailwindcss>nanoid": "3.3.7" } } -} \ No newline at end of file +} diff --git a/server/_core/index.ts b/server/_core/index.ts index b3b66d1..850d1d1 100644 --- a/server/_core/index.ts +++ b/server/_core/index.ts @@ -246,4 +246,8 @@ async function startServer() { }); } -startServer().catch(console.error); +// Les routeurs importent certaines fonctions de ce module. Vitest ne doit jamais +// démarrer un serveur HTTP à cet effet, sinon les suites parallèles se disputent un port. +if (!process.env.VITEST) { + startServer().catch(console.error); +} diff --git a/server/azureAuth.test.ts b/server/azureAuth.test.ts index 6f799d0..e95932d 100644 --- a/server/azureAuth.test.ts +++ b/server/azureAuth.test.ts @@ -1,21 +1,47 @@ -import { describe, it, expect } from "vitest"; -import { isAzureAdConfigured, getAzureAuthUrl } from "./azureAuth"; +import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const azureTestConfig = { + tenantId: "00000000-0000-0000-0000-000000000001", + clientId: "00000000-0000-0000-0000-000000000002", + clientSecret: "test-secret-not-used-for-url", +}; + +const originalAzureEnv = { + tenantId: process.env.AZURE_AD_TENANT_ID, + clientId: process.env.AZURE_AD_CLIENT_ID, + clientSecret: process.env.AZURE_AD_CLIENT_SECRET, + redirectUri: process.env.AZURE_AD_REDIRECT_URI, +}; describe("Azure AD configuration", () => { - it("should detect Azure AD as configured when env vars are set", () => { - // Les variables sont injectées via webdev_request_secrets + beforeEach(() => { + // Chaque test utilise une instance MSAL neuve et ne dépend jamais des secrets CI. + vi.resetModules(); + process.env.AZURE_AD_TENANT_ID = azureTestConfig.tenantId; + process.env.AZURE_AD_CLIENT_ID = azureTestConfig.clientId; + process.env.AZURE_AD_CLIENT_SECRET = azureTestConfig.clientSecret; + process.env.AZURE_AD_REDIRECT_URI = "https://example.test/api/auth/azure/callback"; + }); + + afterAll(() => { + process.env.AZURE_AD_TENANT_ID = originalAzureEnv.tenantId; + process.env.AZURE_AD_CLIENT_ID = originalAzureEnv.clientId; + process.env.AZURE_AD_CLIENT_SECRET = originalAzureEnv.clientSecret; + process.env.AZURE_AD_REDIRECT_URI = originalAzureEnv.redirectUri; + }); + + it("détecte Azure AD lorsqu’une configuration complète est fournie", async () => { + const { isAzureAdConfigured } = await import("./azureAuth"); const configured = isAzureAdConfigured(); expect(configured).toBe(true); }); - it("should generate a valid Azure AD auth URL", async () => { - if (!isAzureAdConfigured()) { - console.warn("Azure AD not configured, skipping URL test"); - return; - } + it("génère une URL d’autorisation Azure AD à partir de la configuration de test", async () => { + const { getAzureAuthUrl } = await import("./azureAuth"); const url = await getAzureAuthUrl(); expect(url).toContain("login.microsoftonline.com"); expect(url).toContain("oauth2/v2.0/authorize"); - expect(url).toContain("f496da82-e18f-4567-bf05-8551ae6669b2"); // client_id + expect(url).toContain(azureTestConfig.tenantId); + expect(url).toContain(azureTestConfig.clientId); }); }); diff --git a/server/veille.test.ts b/server/veille.test.ts index 448a6f7..1c66535 100644 --- a/server/veille.test.ts +++ b/server/veille.test.ts @@ -1,4 +1,18 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; + +// Les tests de contrat tRPC ne doivent jamais dépendre d'une base ou d'une migration +// disponible dans l'environnement CI. Les quatre lectures publiques sont donc isolées. +vi.mock("./db", async () => { + const actual = await vi.importActual("./db"); + return { + ...actual, + getVeilleItems: vi.fn(async () => ({ items: [], total: 0 })), + getVeilleDistinctValues: vi.fn(async () => ({ categories: [], niveaux: [], territoires: [] })), + getAapItems: vi.fn(async () => ({ items: [], total: 0 })), + getAapDistinctValues: vi.fn(async () => ({ regions: [], departements: [] })), + }; +}); + import { appRouter } from "./routers"; import type { TrpcContext } from "./_core/context";