Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
571517b
feat(tests+front): couverture e2e SKI-49/50/51/52 + pages mentions et…
jeremie0342 Aug 12, 2026
e651a99
feat(guilds): page /guilds/new + retablissement des CTA (SKI-290)
jeremie0342 Aug 12, 2026
604895e
test(auth): invitation entreprise + depart SSO (SKI-49)
jeremie0342 Aug 12, 2026
f698a1d
test(enterprise): membres, KYC et pipeline + correction salaire pipel…
jeremie0342 Aug 12, 2026
0075212
test(enterprise): facturation, messages et sourcing (SKI-53)
jeremie0342 Aug 12, 2026
6852241
test(enterprise): profil, listes, interets, SSO, onboarding (SKI-53)
jeremie0342 Aug 12, 2026
f1dfba8
feat(guilds+tests): consommer les endpoints backend livres (SKI-286/2…
jeremie0342 Aug 12, 2026
9869753
feat(verify): og:url et og:image absolus sur /verify/[hash] (SKI-103)
jeremie0342 Aug 12, 2026
1f8eaa3
feat(notifications): accepter/refuser une invitation validateur en li…
jeremie0342 Aug 12, 2026
a465ab9
fix(wallet): le retrait appelait deux endpoints qui n'existent plus
jeremie0342 Aug 14, 2026
d74087d
feat(settings): regler les notifications type par type, et les heures…
jeremie0342 Aug 14, 2026
f8859b1
feat(notifications): expose backend grouping, load-more and open kind…
jeremie0342 Aug 14, 2026
b9c08a3
feat(payments): pay by mobile money without leaving the page
jeremie0342 Aug 14, 2026
ac23974
feat(disputes): the escrow window gets a way to use it
jeremie0342 Aug 14, 2026
e0bcfd6
test: stop the unit suite failing on module transform time
jeremie0342 Aug 14, 2026
c67b2f9
ci: run checks on pull requests targeting prep-prod
jeremie0342 Aug 15, 2026
4f79bf1
test(e2e): the credit pack no longer leaves the page
jeremie0342 Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,27 @@ PUBLIC_P26_ENABLED=false
# PUBLIC_APP_VERSION=
# PUBLIC_POSTHOG_KEY=
# PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com

# Comptes e2e (tests/e2e/parcours) — JAMAIS en dur dans le code, ce repo est public.
# Le compte user doit exister cote backend ; sinon le helper le cree via register.
E2E_USER_EMAIL=
E2E_USER_USERNAME=
E2E_USER_PASSWORD=
E2E_USER_FIRST_NAME=Test
E2E_USER_LAST_NAME=User

# Compte enterprise (TOTP arme automatiquement au premier run).
E2E_ENTERPRISE_EMAIL=
E2E_ENTERPRISE_USERNAME=
E2E_ENTERPRISE_PASSWORD=
E2E_ENTERPRISE_FIRST_NAME=Test
E2E_ENTERPRISE_LAST_NAME=Enterprise
E2E_ENTERPRISE_COMPANY=Test Enterprise

# --- CI ---
# Le job `e2e-parcours` (.github/workflows/ci.yml) attend ces secrets GitHub :
# E2E_API_BASE_URL (ex. https://api.skill-uv.com)
# E2E_USER_EMAIL / E2E_USER_USERNAME / E2E_USER_PASSWORD
# E2E_ENTERPRISE_EMAIL / E2E_ENTERPRISE_USERNAME / E2E_ENTERPRISE_PASSWORD
# Sans E2E_API_BASE_URL le job echoue volontairement : des specs qui skippent
# toutes ressemblent trop a un run vert.
72 changes: 65 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ name: CI

on:
push:
branches: [master, main]
branches: [master, main, prep-prod]
pull_request:
branches: [master, main]
# prep-prod is an integration branch that work merges into before it
# reaches main. Without it here, every pull request targeting prep-prod
# ran no check at all and merged unverified.
branches: [master, main, prep-prod]

jobs:
check:
Expand Down Expand Up @@ -50,9 +53,11 @@ jobs:
name: Playwright end-to-end
runs-on: ubuntu-latest
needs: check
# Playwright currently requires a running backend (proxied /api/* calls).
# TODO: mock API calls or spin up backend services here. Non-blocking for now.
continue-on-error: true
# Blocking on purpose: the mocked suite no longer needs a real backend.
# Browser calls are intercepted by `page.route` and SSR auth is served by
# tests/e2e/utils/mock-backend.mjs, started as a webServer by
# playwright.config.ts. Do not reintroduce `continue-on-error`: that is what
# let 35 broken tests go unnoticed.
steps:
- uses: actions/checkout@v7

Expand All @@ -68,8 +73,10 @@ jobs:
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Playwright tests (unit + axe-core a11y assertions)
run: npx playwright test
# Mocked suite only: hermetic, no real backend. The parcours specs run in
# their own job below so a backend outage cannot redden this one.
- name: Playwright mocked suite
run: npx playwright test --project=legacy-chromium

- name: Upload Playwright report
if: failure()
Expand All @@ -79,6 +86,57 @@ jobs:
path: playwright-report/
retention-days: 7

e2e-parcours:
name: Playwright parcours (real backend)
runs-on: ubuntu-latest
needs: check
# Hits the shared test backend, so it only runs where the secrets exist.
# Skipped on forks rather than failing with empty credentials.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
env:
PUBLIC_API_BASE_URL: ${{ secrets.E2E_API_BASE_URL }}
API_URL: ${{ secrets.E2E_API_BASE_URL }}/api
E2E_USER_EMAIL: ${{ secrets.E2E_USER_EMAIL }}
E2E_USER_USERNAME: ${{ secrets.E2E_USER_USERNAME }}
E2E_USER_PASSWORD: ${{ secrets.E2E_USER_PASSWORD }}
E2E_ENTERPRISE_EMAIL: ${{ secrets.E2E_ENTERPRISE_EMAIL }}
E2E_ENTERPRISE_USERNAME: ${{ secrets.E2E_ENTERPRISE_USERNAME }}
E2E_ENTERPRISE_PASSWORD: ${{ secrets.E2E_ENTERPRISE_PASSWORD }}
steps:
- uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '24'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

# Without the backend URL the specs would all skip silently, which reads
# exactly like a green run. Fail loudly instead.
- name: Verify backend configuration
run: |
if [ -z "$PUBLIC_API_BASE_URL" ]; then
echo "E2E_API_BASE_URL secret is not set; parcours specs would all skip." >&2
exit 1
fi

- name: Playwright parcours suite
run: npx playwright test --project=parcours-chromium

- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v7
with:
name: playwright-report-parcours
path: playwright-report/
retention-days: 7

lighthouse:
name: Lighthouse mobile perf budget
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ vite.config.ts.timestamp-*
# Tests
test-results
playwright-report
# Playwright storage state + plaintext test-account credentials.
tests/e2e/.auth/

# OS
.DS_Store
Expand Down
86 changes: 78 additions & 8 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';

// Playwright does not read `.env` (Vite does). Without this the `parcours/`
// specs saw an empty `PUBLIC_API_BASE_URL` and skipped all 91 of themselves
// even though the file was filled in. Existing environment variables win, so CI
// can impose its own. `quiet` keeps dotenv's banner off stdout, which would
// otherwise corrupt `--reporter=json`.
dotenv.config({ quiet: true });

const BASE_URL = process.env.PUBLIC_BASE_URL ?? 'http://localhost:5173';
const IS_SMOKE_ONLY = process.env.SMOKE_ONLY === '1';
const IS_CROSS_BROWSER = process.env.CROSS_BROWSER === '1';

/** The `parcours/` specs only run when a backend is configured. */
const HAS_BACK = Boolean(process.env.PUBLIC_API_BASE_URL);

/**
* Whether this invocation can run `parcours/` specs at all. Starting the dev
* server for a legacy-only run just steals CPU from it and made the mocked
* suite flaky, so gate it on the requested project.
*/
const TARGETS_PARCOURS =
!process.argv.some((a) => a.startsWith('--project=')) ||
process.argv.some((a) => a.includes('parcours'));

/** Mock backend port, kept off 3001 so a real local backend never clashes. */
const MOCK_BACKEND_PORT = 3099;

/**
* Deux univers de tests coexistent :
*
Expand All @@ -26,6 +49,9 @@ const IS_CROSS_BROWSER = process.env.CROSS_BROWSER === '1';
*/
export default defineConfig({
testDir: 'tests/e2e',
// Only meaningful when the dev server is started (parcours runs); it returns
// immediately otherwise.
globalSetup: TARGETS_PARCOURS && HAS_BACK ? './tests/e2e/utils/global-setup.ts' : undefined,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? 'github' : 'list',
fullyParallel: true,
Expand All @@ -38,16 +64,57 @@ export default defineConfig({
trace: process.env.CI ? 'on' : 'on-first-retry',
video: process.env.CI ? 'on' : 'retain-on-failure'
},
// Le webServer legacy (build + preview :4173) reste utilise par les projets
// `legacy-*`. Il est desactive quand on ne lance que les smoke/parcours
// (SMOKE_ONLY=1) pour ne pas rebuild inutilement.
/**
* Servers started automatically.
*
* 1. Mock backend on :3099 — serves `GET /api/auth/me` so SSR auth
* (hooks.server.ts) can be simulated in the mocked suite. See
* tests/e2e/utils/mock-backend.mjs.
*
* 2. App on :4173 — adapter-node build, not `vite preview`: only the node
* server actually replays the hooks, cookies included. Its environment is
* pinned to the mock backend so the mocked suite stays hermetic even when
* `.env` points at a remote backend.
*
* 3. Dev server on :5173 — only when `PUBLIC_API_BASE_URL` is set AND the run
* targets the `parcours/` specs. It proxies `/api` to the target backend
* (vite.config.ts).
*/
webServer: IS_SMOKE_ONLY
? undefined
: {
command: 'npm run build && npm run preview',
port: 4173,
reuseExistingServer: !process.env.CI
},
: [
{
command: 'node tests/e2e/utils/mock-backend.mjs',
port: MOCK_BACKEND_PORT,
env: { MOCK_BACKEND_PORT: String(MOCK_BACKEND_PORT) },
reuseExistingServer: !process.env.CI
},
{
command: 'npm run build && node build/index.js',
port: 4173,
env: {
PORT: '4173',
// The mocked suite must never hit the real backend, even when
// `.env` configures one for the parcours specs.
API_URL: `http://localhost:${MOCK_BACKEND_PORT}/api`,
PUBLIC_API_BASE_URL: `http://localhost:${MOCK_BACKEND_PORT}`
},
reuseExistingServer: !process.env.CI,
// The build runs inside this command: the 60s default was not
// enough and failed the Playwright job before the first test.
timeout: 300_000
},
...(HAS_BACK && TARGETS_PARCOURS
? [
{
command: 'npm run dev -- --port 5173',
port: 5173,
reuseExistingServer: true,
timeout: 120_000
}
]
: [])
],
projects: [
{
name: 'legacy-chromium',
Expand Down Expand Up @@ -79,6 +146,9 @@ export default defineConfig({
// Sans back, chaque test skip proprement via test.skip(!HAS_BACK).
name: 'parcours-chromium',
testMatch: 'parcours/**/*.spec.ts',
// These specs cross the network to a real backend, so they need more
// headroom than the mocked suite.
timeout: 90_000,
use: {
...devices['Desktop Chrome'],
viewport: { width: 1440, height: 900 },
Expand Down
12 changes: 6 additions & 6 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@
--sk-border-strong: #7a6350;

--sk-text: #f4ede0; /* crème patinée */
--sk-text-muted: #b8a68a;
--sk-text-muted: #c2b195; /* AA: 4.99:1 on surface-overlay (was 4.42) */

/* Surfaces catégorielles — zones colorées assumées (Persona-style) */
--sk-surface-craft: #3a2510; /* ambre profond */
Expand All @@ -219,8 +219,8 @@
--sk-accent-fg: #18130f;

--sk-shadow-color: rgba(0, 0, 0, 0.5);
--sk-success: #2a9d8f;
--sk-success-soft: rgba(42, 157, 143, 0.2);
--sk-success: #32b8ab; /* AA: 4.72:1 on bg-success/15 (was 3.66) */
--sk-success-soft: rgba(50, 184, 171, 0.2);
--sk-warning: #e9c46a;
--sk-warning-soft: rgba(233, 196, 106, 0.2);
--sk-error: #c1272d; /* rouge (assombri pour contrast WCAG AA >= 4.5 avec text-white) */
Expand Down Expand Up @@ -283,7 +283,7 @@
--sk-border-strong: #5a6a95;

--sk-text: #f4e8c8;
--sk-text-muted: #9d8865;
--sk-text-muted: #beb098; /* AA: 4.70:1 on surface-overlay (was 2.93) */

--sk-surface-craft: #2f2618;
--sk-surface-create: #38141a;
Expand Down Expand Up @@ -380,8 +380,8 @@
--sk-accent-fg: #f4ede0;

--sk-shadow-color: rgba(0, 0, 0, 0.7);
--sk-success: #2a9d8f;
--sk-success-soft: rgba(42, 157, 143, 0.2);
--sk-success: #32b8ab; /* AA: 4.72:1 on bg-success/15 (was 3.66) */
--sk-success-soft: rgba(50, 184, 171, 0.2);
--sk-warning: #e9c46a;
--sk-warning-soft: rgba(233, 196, 106, 0.2);
--sk-error: #ea580c;
Expand Down
10 changes: 9 additions & 1 deletion src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Handle, HandleServerError } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit';
import { env } from '$env/dynamic/private';
import { env as publicEnv } from '$env/dynamic/public';
import type { UserPrivate } from '$lib/types';

/**
Expand Down Expand Up @@ -59,7 +60,14 @@ export const handle: Handle = async ({ event, resolve }) => {

if (accessToken) {
try {
const apiUrl = env.API_URL ?? 'http://localhost:3001/api';
// `PUBLIC_API_BASE_URL` drives both the dev server proxy
// (vite.config.ts) and this SSR call. `API_URL` still wins for
// deployments that need an internal URL distinct from the public one.
const apiUrl =
env.API_URL ??
(publicEnv.PUBLIC_API_BASE_URL
? `${publicEnv.PUBLIC_API_BASE_URL.replace(/\/+$/, '')}/api`
: 'http://localhost:3001/api');
const response = await fetch(`${apiUrl}/auth/me`, {
headers: {
Cookie: `access_token=${accessToken}`
Expand Down
42 changes: 32 additions & 10 deletions src/lib/api/attestation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { env } from '$env/dynamic/public';
import { createApiClient } from './client';

// --- Types (P26 v2 attestation publique) ---
Expand Down Expand Up @@ -29,27 +30,48 @@ export interface AttestationInvalid {

export type AttestationResponse = AttestationValid | AttestationInvalid;

// SKI-115 endpoint hors /api (public verify)
const publicApi = createApiClient(fetch, '');
const api = createApiClient();

/**
* Origin of the backend, without the `/api` prefix.
*
* The PDF and the badge SVGs are served from the backend ROOT, not under
* `/api`. Referencing them with a relative path resolved them against the
* frontend origin instead, where `/badge/*` does not exist and `/verify/*` is
* taken by the verification page itself — so every badge and every PDF link was
* broken. They are consumed as `href` / `src`, never fetched, so an absolute
* cross-origin URL needs no CORS.
*/
function backendOrigin(): string {
return (env.PUBLIC_API_BASE_URL ?? '').replace(/\/+$/, '');
}

export const attestationApi = {
// GET /verify/{hash} — retourne le JSON attestation
/**
* Verification payload.
*
* Goes through `/api` so it stays same-origin behind the existing proxy.
* See SKI-288: the backend also serves this at its root, but that path
* collides with this app's own `/verify/[hash]` page.
*/
verify(hash: string) {
return publicApi.get<AttestationResponse>(`/verify/${encodeURIComponent(hash)}`);
// Bare payload, not the `{ data, meta }` envelope: this route mirrors the
// public root endpoint, which returns the object directly.
return api.get<AttestationResponse>(`/verify/${encodeURIComponent(hash)}`);
},

// URL directe du PDF (deep link, pas de fetch cote front)
/** Direct PDF link (SKI-118). */
pdfUrl(hash: string): string {
return `/verify/${encodeURIComponent(hash)}.pdf`;
return `${backendOrigin()}/verify/${encodeURIComponent(hash)}.pdf`;
},

// SKI-116 badge user SVG (URL directe)
/** User badge SVG (SKI-116). */
badgeUserUrl(username: string): string {
return `/badge/user/${encodeURIComponent(username)}/validated.svg`;
return `${backendOrigin()}/badge/user/${encodeURIComponent(username)}/validated.svg`;
},

// SKI-117 badge repo SVG (URL directe)
/** Repo badge SVG (SKI-117). */
badgeRepoUrl(owner: string, name: string): string {
return `/badge/repo/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/validated.svg`;
return `${backendOrigin()}/badge/repo/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/validated.svg`;
}
};
2 changes: 2 additions & 0 deletions src/lib/api/certifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface Certification {
export interface PurchaseResponse {
attempt_id: string;
checkout_url?: string;
/** Our identifier for the charge, when a payment was opened. */
payment_id?: string;
session_id?: string;
status?: string;
message?: string;
Expand Down
Loading
Loading