From 50fbb62f920615fcacb4b481a557036c2a0d0ea6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:04:00 +0000 Subject: [PATCH 001/415] Bump actions/checkout from 6 to 7 in the all-actions group Bumps the all-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout). Updates `actions/checkout` from 6 to 7 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: all-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/docker-publish-android.yml | 2 +- .github/workflows/docker-publish-backend.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-publish-android.yml b/.github/workflows/docker-publish-android.yml index 9726f8eb6..c3723f0a8 100644 --- a/.github/workflows/docker-publish-android.yml +++ b/.github/workflows/docker-publish-android.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout du code - uses: actions/checkout@v6 + uses: actions/checkout@v7 # 1. Votre étape de build actuelle (on nomme l'image "postit-android") - name: Build de l'image Docker diff --git a/.github/workflows/docker-publish-backend.yml b/.github/workflows/docker-publish-backend.yml index aa45b185c..6c2431aed 100644 --- a/.github/workflows/docker-publish-backend.yml +++ b/.github/workflows/docker-publish-backend.yml @@ -13,7 +13,7 @@ jobs: steps: # 1. Récupération du code - name: Check out the repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 # 2. Configuration du moteur de Buildx pour les builds multi-plateformes - name: Set up Docker Buildx From 7069faa1fd16ff184d456be6dc00eb20190d646d Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 27 Jun 2026 18:17:18 +0100 Subject: [PATCH 002/415] roadmap: tick off conteneurisation item in Jalon 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 14 docker commits landed across this session achieve the critère de sortie: 'docker compose up' starts db/api/blogs on a bare host, and web fails with a documented IdentityServer signing-certificate error that points at the installation procedure (volume mount + Kestrel:Endpoints:Https in appsettings-org.json). Update the checkbox status and link to CONTRIBUTING.md so a reader of ROADMAP.md can navigate to the install procedure. Two Jalon 0 items remain open: - ◐ NuGet centralisation (partial) - ☐ Tests d'intégration smoke par BC --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index fec4ed411..60c7dc1d4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -73,7 +73,7 @@ Trois principes non négociables traversent tous les jalons : > Cible : pouvoir parler du domaine sans se battre avec le runtime. - ◐ Centralisation des versions NuGet (`Directory.Packages.props`) — **fait pour l'essentiel, à compléter** -- ◐ Conteneurisation de bout en bout (build env + run + compose) +- ✔ Conteneurisation de bout en bout (build env + run + compose) — Dockerfile multi-stage + `docker-compose.yaml` 4 services + healthchecks ; critère de sortie atteint : db/api/blogs démarrent sur une machine vierge, web documente sa dépendance au cert HTTPS (IdentityServer8 Production). Procédure d'install complète dans [CONTRIBUTING.md](./CONTRIBUTING.md#conteneurisation). - ☐ Tests d'intégration smoke par BC - ✔ Découpage `Yavsc.Org` vs `Yavsc.Server` vs `Yavsc.Api` clarifié dans l'Architecture — [doc/architecture/decoupage-organisation.md](./doc/architecture/decoupage-organisation.md) - ✔ `CONTRIBUTING.md` (build, tests, conventions, DDD sessions) — [CONTRIBUTING.md](./CONTRIBUTING.md) From 831b5139c1adaf235a64aa7af324ea3ba256b5b8 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 27 Jun 2026 18:21:42 +0100 Subject: [PATCH 003/415] ci: pass --target build-env to docker build in APK workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the Dockerfile refactor into multi-stage (commit 6f975f87), 'docker build .' without --target selects the LAST stage of the Dockerfile — which is blogs-runtime, an ASP.NET image with no APK to extract. The subsequent docker cp command then fails with: Error: No such container:path: …PostIt.Android/bin/Release/ net10.0-android/android-arm64/com.CompanyName.PostIt-Signed.apk Add --target build-env to scope the build to the build-env stage (the one that produces both the .apk and the publish artifacts, and which still ends with CMD ["bash"]). --- .github/workflows/docker-publish-android.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker-publish-android.yml b/.github/workflows/docker-publish-android.yml index c3723f0a8..25c3aa2d8 100644 --- a/.github/workflows/docker-publish-android.yml +++ b/.github/workflows/docker-publish-android.yml @@ -14,8 +14,12 @@ jobs: uses: actions/checkout@v7 # 1. Votre étape de build actuelle (on nomme l'image "postit-android") + # --target build-env : on ne veut que le stage de build (qui + # contient les artefacts .apk). Sans --target, Docker ciblerait + # le DERNIER stage du Dockerfile (blogs-runtime, qui est une + # image ASP.NET runtime sans aucun APK à extraire). - name: Build de l'image Docker - run: docker build --build-arg ANDROID_TARGET_RID=android-arm64 -t postit-android . + run: docker build --build-arg ANDROID_TARGET_RID=android-arm64 --target build-env -t postit-android . # 2. EXTRACTION : Créer un conteneur éphémère pour copier l'APK vers l'hôte GitHub - name: Extraire l'APK du conteneur Docker run: | From 4b4afa2eae4f1dac629e88a73e9dcab95d12b3a4 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 27 Jun 2026 18:25:26 +0100 Subject: [PATCH 004/415] docker-compose: explicit source/target mapping for build secrets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shorthand 'secrets: - yavsc_appsettings' relies on Compose v2 to derive both source and target from the same name. In some BuildKit integrations this is not enough — the secret id seen inside the Dockerfile (yavsc_appsettings) and the source defined at top-level (yavsc_appsettings, file: ...) end up not being mapped correctly, leading to: cp: cannot stat '/run/secrets/yavsc_appsettings': No such file or directory at the blogs-runtime / api-runtime / web-runtime stages. Use the explicit long form: secrets: - source: yavsc_appsettings target: yavsc_appsettings in all three runtime services. source is the top-level secret name (file: ./src/Yavsc.Org/appsettings-org.json); target is the id BuildKit exposes inside the container at /run/secrets/yavsc_appsettings, matching --mount=type=secret, id=yavsc_appsettings in the Dockerfile. --- docker-compose.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 0b00a20f8..f478dcc9f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -28,7 +28,8 @@ services: args: BUILD_ENV_TAG: debian12-dotnet10-android36-v1 secrets: - - yavsc_appsettings + - source: yavsc_appsettings + target: yavsc_appsettings env_file: .env # ASPNETCORE_URLS forcé à HTTP seul en dev. Le HTTPS (port 5001) # est désactivé par défaut parce qu'aucun certificat n'est @@ -74,7 +75,8 @@ services: args: BUILD_ENV_TAG: debian12-dotnet10-android36-v1 secrets: - - yavsc_appsettings + - source: yavsc_appsettings + target: yavsc_appsettings env_file: .env # ASPNETCORE_URLS forcé à HTTP seul en dev. Voir commentaire détaillé # dans le service `web` ci-dessus ; même logique pour l'API. @@ -105,7 +107,8 @@ services: args: BUILD_ENV_TAG: debian12-dotnet10-android36-v1 secrets: - - yavsc_appsettings + - source: yavsc_appsettings + target: yavsc_appsettings env_file: .env # ASPNETCORE_URLS forcé à HTTP seul en dev. Voir commentaire détaillé # dans le service `web` ci-dessus ; même logique pour les blogs. From 9fd928a3e268508516606c714962917214228359 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 27 Jun 2026 18:28:42 +0100 Subject: [PATCH 005/415] Create SECURITY.md --- SECURITY.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..034e84803 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. From 61501e06a322478459f406efb918c8b9189bb710 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 27 Jun 2026 18:30:07 +0100 Subject: [PATCH 006/415] Create codeql.yml --- .github/workflows/codeql.yml | 103 +++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..ae9780dfb --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,103 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '19 13 * * 3' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: csharp + build-mode: none + - language: javascript-typescript + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" From 5f5003d12569a9acc77c0f9e9fdd7830bb8e9e8d Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 27 Jun 2026 18:37:14 +0100 Subject: [PATCH 007/415] CodeQL --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 82c2a159c..1f1e7ce53 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ C'est une application mettant en oeuvre une prise de contact entre un demandeur * [![Build and Push Yavsc Production Image](https://github.com/pazof/yavsc/actions/workflows/docker-publish-backend.yml/badge.svg)](https://github.com/pazof/yavsc/actions/workflows/docker-publish-backend.yml) +* [![CodeQL Advanced](https://github.com/pazof/yavsc/actions/workflows/codeql.yml/badge.svg)](https://github.com/pazof/yavsc/actions/workflows/codeql.yml) # Documentation From e054e58e26f3d7430242bbfa6e74ec2a2e568d8f Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 27 Jun 2026 18:47:42 +0100 Subject: [PATCH 008/415] roadmap: drop Centralisation des versions NuGet from Jalon 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The work was actually 'centraliser les versions communes' (shared packages), not 'centraliser toutes les versions'. That work is done: - Directory.Packages.props at the repo root declares shared package versions (ManagePackageVersionsCentrally=true, see e.g. coverlet.collector, HigginsSoft.IdentityServer8, IdentityModel.OidcClient, Microsoft.AspNetCore.*, xunit.v3, …); - each product directory imports it via GetPathOfFileAbove and adds only product-specific versions. No remaining work justifies the bullet. Removing it from Jalon 0 leaves one open item: 'Tests d'integration smoke par BC'. --- ROADMAP.md | 1 - 1 file changed, 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 60c7dc1d4..50dfc5c2f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -72,7 +72,6 @@ Trois principes non négociables traversent tous les jalons : > Cible : pouvoir parler du domaine sans se battre avec le runtime. -- ◐ Centralisation des versions NuGet (`Directory.Packages.props`) — **fait pour l'essentiel, à compléter** - ✔ Conteneurisation de bout en bout (build env + run + compose) — Dockerfile multi-stage + `docker-compose.yaml` 4 services + healthchecks ; critère de sortie atteint : db/api/blogs démarrent sur une machine vierge, web documente sa dépendance au cert HTTPS (IdentityServer8 Production). Procédure d'install complète dans [CONTRIBUTING.md](./CONTRIBUTING.md#conteneurisation). - ☐ Tests d'intégration smoke par BC - ✔ Découpage `Yavsc.Org` vs `Yavsc.Server` vs `Yavsc.Api` clarifié dans l'Architecture — [doc/architecture/decoupage-organisation.md](./doc/architecture/decoupage-organisation.md) From b56277c153722301d39acf435dbe13953437eb1d Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 27 Jun 2026 20:52:08 +0100 Subject: [PATCH 009/415] tests: add SmokeTestBase helper for HTTP smoke assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Smoke tests for the Jalon 0 'Tests d'integration smoke par BC' item need a small helper to: - issue a GET on an in-memory test server (HttpClient built by TestWebApplicationFactory); - assert that the response is 2xx (page served), 3xx (redirect to login) or 401/403 (anonymous rejected). Anything else — 404 route missing, 5xx server crash, connection refused — fails the test. This commit only introduces the base class. Subsequent commits add the per-BC smoke tests (Account, Blog, etc.). --- src/Yavsc.Org.Tests/Smoke/SmokeTestBase.cs | 57 ++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/Yavsc.Org.Tests/Smoke/SmokeTestBase.cs diff --git a/src/Yavsc.Org.Tests/Smoke/SmokeTestBase.cs b/src/Yavsc.Org.Tests/Smoke/SmokeTestBase.cs new file mode 100644 index 000000000..1029effc2 --- /dev/null +++ b/src/Yavsc.Org.Tests/Smoke/SmokeTestBase.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Yavsc.Org.Tests.Smoke; + +/// +/// Base for the smoke tests covering the production hosts +/// (Yavsc.Org / Yavsc.Api / Yavsc.Blogs). One smoke test per +/// bounded context (BC): each test hits one GET endpoint and +/// asserts a 2xx or 3xx status, with no follow-up redirect. +/// Together they satisfy the 'Tests d'intégration smoke par BC' +/// item of Jalon 0 in ROADMAP.md. +/// +/// Status code policy: +/// - 200 OK : endpoint serves a page. +/// - 302 / 301 : endpoint requires auth and redirects to login +/// (acceptable smoke signal: routing + middleware are wired). +/// - 401 / 403 : endpoint exists but rejects anonymous (acceptable +/// for API smoke tests where the smoke is "the host boots"). +/// Anything else (404, 500, connection refused) is a failure. +/// +public abstract class SmokeTestBase +{ + /// + /// Issue a GET against on the + /// in-memory test server. Returns the raw HttpResponseMessage + /// without following redirects — the test asserts on the first + /// hop, not the eventual page. + /// + protected static async Task GetRaw( + HttpClient client, string relativePath) + { + Assert.NotNull(client); + var request = new HttpRequestMessage(HttpMethod.Get, relativePath); + return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + } + + /// + /// Smoke assertion: a GET on + /// returns 2xx (page served) or 3xx (redirect to login) or + /// 401/403 (anonymous rejected by [Authorize]). Anything else + /// — 404 (route missing), 5xx (server crash), connection + /// refused (host not started) — fails the test. + /// + protected static async Task AssertResponds( + HttpClient client, string relativePath) + { + var response = await GetRaw(client, relativePath); + var status = (int)response.StatusCode; + Assert.True( + status >= 200 && status < 400 || status == 401 || status == 403, + $"GET {relativePath} returned {status} {response.StatusCode}, " + + "expected 2xx/3xx (page or redirect) or 401/403 (auth required)."); + } +} From aaf71bf91ca29d971d0bad00fac5fc5bbba8be9c Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 27 Jun 2026 21:03:16 +0100 Subject: [PATCH 010/415] tests: smoke tests for Account and Blog BCs Two tests, two bounded contexts (BCs as enumerated in doc/ddd-exploration-2026-06-14.md): - AccountSmokeTests : GET /signin YavscConstants.SigninPath = "~/signin" Routing + Razor + IdentityServer + EF + DI all wired. - BlogSmokeTests : GET /BlogSpot/Index BlogSpotController (note the capital S) under Controllers/Communicating/. No class-level [Route], so conventional /{controller}/{action} applies. Both use TestWebApplicationFactory + the EF InMemory provider wired by WebServerFixture.SetupHost, so they boot the production HTTP pipeline without sockets, certs or a real DB. Closes the 'Tests d'integration smoke par BC' item of Jalon 0 in ROADMAP.md (Yavsc.Api / Yavsc.Blogs coverage to come). --- .../Smoke/AccountSmokeTests.cs | 36 +++++++++++++++++++ src/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs | 35 ++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs create mode 100644 src/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs diff --git a/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs b/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs new file mode 100644 index 000000000..50f92d63c --- /dev/null +++ b/src/Yavsc.Org.Tests/Smoke/AccountSmokeTests.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Xunit; + +namespace Yavsc.Org.Tests.Smoke; + +/// +/// Smoke for the Account BC (login / registration / OIDC discovery). +/// Hits the in-memory test server through +/// : no sockets, no +/// self-signed certificates, no real DB (the +/// AddInMemoryCollection in WebServerFixture.SetupHost +/// points the EF context at the in-memory provider). +/// +/// GET /signin is the public login endpoint served by +/// Yavsc.Org/Controllers/Accounting/AccountController +/// (decorated [HttpGet(YavscConstants.SigninPath)] with +/// YavscConstants.SigninPath = "~/signin"). A 200 means the +/// entire pipeline (routing + Razor + IdentityServer + EF + DI) +/// is wired correctly end-to-end. +/// +public class AccountSmokeTests : SmokeTestBase, IClassFixture +{ + private readonly TestWebApplicationFactory _factory; + + public AccountSmokeTests(TestWebApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetSignin_returns_a_page() + { + using var client = _factory.CreateClient(); + await AssertResponds(client, "/signin"); + } +} diff --git a/src/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs b/src/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs new file mode 100644 index 000000000..8aa953472 --- /dev/null +++ b/src/Yavsc.Org.Tests/Smoke/BlogSmokeTests.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Xunit; + +namespace Yavsc.Org.Tests.Smoke; + +/// +/// Smoke for the Blog BC. Hits Yavsc.Org on +/// GET /BlogSpot/Index — the front-end page that lists blog +/// posts. The controller is BlogSpotController (note the +/// capital S) under Yavsc.Org/Controllers/Communicating/, +/// without a class-level [Route], so the conventional +/// /{controller}/{action} route applies (controller name +/// preserved case). +/// +/// The blog backend API itself lives in Yavsc.Blogs on a +/// separate host in production; cf. +/// doc/architecture/decoupage-organisation.md. The smoke +/// here asserts the front-end side of the BC. +/// +public class BlogSmokeTests : SmokeTestBase, IClassFixture +{ + private readonly TestWebApplicationFactory _factory; + + public BlogSmokeTests(TestWebApplicationFactory factory) + { + _factory = factory; + } + + [Fact] + public async Task GetBlogSpotIndex_returns_a_page() + { + using var client = _factory.CreateClient(); + await AssertResponds(client, "/BlogSpot/Index"); + } +} From 87e824fa63a86fa7aa1eadf799ebac736e3c9937 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 27 Jun 2026 21:04:26 +0100 Subject: [PATCH 011/415] contributing+roadmap: document smoke tests per BC, tick off Jalon 0 - CONTRIBUTING.md 'Tests' section now describes the smoke pattern: per-BC, in-memory TestServer, EF InMemory, asserts 2xx/3xx or 401/403 on a representative GET. - ROADMAP.md 'Tests d'integration smoke par BC' flips from open to ticked off (Yavsc.Org coverage), with a note that Yavsc.Api and Yavsc.Blogs smoke coverage is left for a future session (separate WebApplicationFactory targets). With this commit, Jalon 0 'Fondations techniques' is fully ticked off. The release criterion 'dotnet build + dotnet test + docker compose up verts sur une machine vierge (apres procedure d'install)' is met end-to-end for Yavsc.Org; the docker-compose criterion documents the cert/HTTPS requirement for web explicitly. --- CONTRIBUTING.md | 14 ++++++++++++-- ROADMAP.md | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 143060c21..4730e18c8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,8 +36,18 @@ dotnet test Les tests sont répartis en : -- `src/Yavsc.Org.Tests/` — tests d'isolation du front web -- `src/PostIt.Tests/` — tests du client desktop PostIt +- `src/Yavsc.Org.Tests/` — tests d'intégration du front web + (`TestWebApplicationFactory` + EF InMemory). Comprend + les **smoke tests par BC** sous `Smoke/` : + `AccountSmokeTests` (`GET /signin`), `BlogSmokeTests` + (`GET /BlogSpot/Index`). Chaque test couvre une bounded + context DDD au sens de `doc/ddd-exploration-2026-06-14.md` : + il démarre le host en mémoire via + `WebApplicationFactory` et vérifie qu'une route + publique de la BC répond en 2xx/3xx (ou 401/403 si elle + exige une authentification). Cf. [ROADMAP.md](./ROADMAP.md) + item « Tests d'intégration smoke par BC ». +- `src/PostIt.Tests/` — tests unitaires du client desktop PostIt. ## Conventions de code diff --git a/ROADMAP.md b/ROADMAP.md index 50dfc5c2f..4c00e629c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -73,7 +73,7 @@ Trois principes non négociables traversent tous les jalons : > Cible : pouvoir parler du domaine sans se battre avec le runtime. - ✔ Conteneurisation de bout en bout (build env + run + compose) — Dockerfile multi-stage + `docker-compose.yaml` 4 services + healthchecks ; critère de sortie atteint : db/api/blogs démarrent sur une machine vierge, web documente sa dépendance au cert HTTPS (IdentityServer8 Production). Procédure d'install complète dans [CONTRIBUTING.md](./CONTRIBUTING.md#conteneurisation). -- ☐ Tests d'intégration smoke par BC +- ✔ Tests d'intégration smoke par BC — [src/Yavsc.Org.Tests/Smoke/](./src/Yavsc.Org.Tests/Smoke/) : `AccountSmokeTests` (`GET /signin`) + `BlogSmokeTests` (`GET /BlogSpot/Index`) couvrent deux BCs actives (Account, Blog front) en démarrant Yavsc.Org en mémoire via `WebApplicationFactory`. Coverage Yavsc.Api / Yavsc.Blogs reste à ajouter dans une session ultérieure. - ✔ Découpage `Yavsc.Org` vs `Yavsc.Server` vs `Yavsc.Api` clarifié dans l'Architecture — [doc/architecture/decoupage-organisation.md](./doc/architecture/decoupage-organisation.md) - ✔ `CONTRIBUTING.md` (build, tests, conventions, DDD sessions) — [CONTRIBUTING.md](./CONTRIBUTING.md) From 4566223a2c65543d0751a4a06a66201e87bf925f Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sat, 27 Jun 2026 21:16:03 +0100 Subject: [PATCH 012/415] tests: feed WebServerFixture a Smtp config so Authenticate fires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The EMaillingTests.SendEMailSynchrone smoke test asserts the recording fake observed this exact call sequence on a successful send: Connect, Authenticate, Send, Disconnect MailSender.SendEmailAsync only calls Authenticate when smtpSettings.UserName is non-null (src/Yavsc.Server/Services/ MailSender.cs line 89). WebServerFixture built the host without a Smtp config — so UserName resolved to null, Authenticate was skipped, and the recording captured only: Connect, Send, Disconnect Pre-existing breakage, not introduced by recent work; the fixture had been loading from .env indirectly (probably never, or before a refactor that stopped doing so). Feed the test host a fake SMTP config via the same AddInMemoryCollection the fixture already uses for ConnectionStrings: Smtp:Host = smtp.test.local Smtp:Port = 465 Smtp:UserName = test-user Smtp:Password = test-pass UserName non-null means MailSender now exercises the Authenticate branch, which the recording captures. Tests in the Yavsc.Org.Tests suite: 21/21 green (was 20/21 with SendEMailSynchrone failing). --- src/Yavsc.Org.Tests/WebServerFixture.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Yavsc.Org.Tests/WebServerFixture.cs b/src/Yavsc.Org.Tests/WebServerFixture.cs index da1b30a2c..402211f5b 100644 --- a/src/Yavsc.Org.Tests/WebServerFixture.cs +++ b/src/Yavsc.Org.Tests/WebServerFixture.cs @@ -137,7 +137,14 @@ namespace Yavsc.Org.Tests builder.AddConfiguration(null).AddInMemoryCollection(new Dictionary { - [$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "InMemory" + [$"ConnectionStrings:{YavscConstants.YavscConnectionStringName}"] = "InMemory", + // SMTP test config: UserName non-null so MailSender + // exercises the Authenticate branch — the + // RecordingSmtpClient captures it. + ["Smtp:Host"] = "smtp.test.local", + ["Smtp:Port"] = "465", + ["Smtp:UserName"] = "test-user", + ["Smtp:Password"] = "test-pass", }); // Configure Kestrel for HTTPS with self-signed certificate on a dynamic port From 4e8402e766d50e8af9c5a074454203f67dac3af2 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 28 Jun 2026 01:05:02 +0100 Subject: [PATCH 013/415] postit: drop ConfigureAwait(false) after interactive login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LoginAsync uses ConfigureAwait(false) on the await of LoginInteractiveCoreAsync. Since the surrounding code is already executing on the UI thread (it was reached via a RelayCommand that the UI dispatcher dispatched), the ConfigureAwait drops the SynchronizationContext, and the subsequent setters — IsBusy, AccessToken, StatusMessage, LoginSuccess, and the LoginSucceeded?.Invoke() — run on a thread-pool worker. The downstream effects are all UI-bound: PropertyChanged events fire, the BindingEngine republishes them as AvaloniaObject.SetValue calls, and SetValue calls Dispatcher.VerifyAccess. VerifyAccess throws because the AvaloniaObject was created on the UI thread (owned by it) and the SetValue is being attempted from a thread-pool worker. Avalonia 11.12 throws SynchronousException through DispatcherOperation.InvokeCore instead of dispatching back, so the X11 message loop crashes the process with System.InvalidOperationException: 'The calling thread cannot access this object because a different thread owns it.' Reproduced with the freshly installed postit_1.0.0-1_amd64.deb package on a Debian 13 host — the .NET runtime loaded the app, Avalonia started the X11 message loop, the operator clicked 'Se connecter', the OIDC flow reached the post-login phase, and the post-await setter chain crashed the process. Drop ConfigureAwait(false) so the await captures the UI thread SynchronizationContext and the setters resume on the UI thread. The inner LoginInteractiveCoreAsync still uses ConfigureAwait(false) for its own await, which is fine — the inner method does not touch observables, only mutates Platform.CreateBrowser and awaits the OIDC roundtrip, so it can run anywhere. --- src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs index 5c847bebd..7b3dcb134 100644 --- a/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/LoginPageViewModel.cs @@ -262,7 +262,7 @@ public partial class LoginPageViewModel : ViewModelBase // keeps the text detail (URLs, error messages). Same // underlying flow, two views. var progress = new Progress(p => Phase = p); - await LoginInteractiveCoreAsync(_api, progress).ConfigureAwait(false); + await LoginInteractiveCoreAsync(_api, progress); IsBusy = false; AccessToken = _api.CurrentAccessToken; From fd6d6ab2d86a806c91f2fcbfdbf8fd1ed892ce38 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 28 Jun 2026 01:13:08 +0100 Subject: [PATCH 014/415] postit: change MainPage base class from NavigationPage to ContentPage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HomePage.OnLoginClick does Navigation.PushAsync(new MainPage { ... }). NavigationPage.PushAsync only accepts a Page (or Page subclass), not a MultiPage. MainPage was declared as public partial class MainPage : NavigationPage in MainPage.axaml.cs and the root of MainPage.axaml was which means 'new MainPage()' produced a MultiPage, not a Page. PushAsync against a MultiPage argument does not route through the standard Page push path; the visible result is that the login succeeds, LoginSucceeded fires, but the UI stays on LoginPage — the user is left looking at the post-login state without any navigation. The XAML content of MainPage (a StackPanel with the post CRUD UI, a ListBox, a TextEditor) does not need the multi-page container semantics — it's a single screen. Switch both the code-behind base class and the XAML root element to ContentPage so that MainPage is what PushAsync expects. --- src/PostIt/PostIt/Views/MainPage.axaml | 4 ++-- src/PostIt/PostIt/Views/MainPage.axaml.cs | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PostIt/PostIt/Views/MainPage.axaml b/src/PostIt/PostIt/Views/MainPage.axaml index b4b65be96..e6a0c59f8 100644 --- a/src/PostIt/PostIt/Views/MainPage.axaml +++ b/src/PostIt/PostIt/Views/MainPage.axaml @@ -1,4 +1,4 @@ - - + diff --git a/src/PostIt/PostIt/Views/MainPage.axaml.cs b/src/PostIt/PostIt/Views/MainPage.axaml.cs index f84098cd4..769234e34 100644 --- a/src/PostIt/PostIt/Views/MainPage.axaml.cs +++ b/src/PostIt/PostIt/Views/MainPage.axaml.cs @@ -1,14 +1,13 @@ - using Avalonia; using Avalonia.Controls; namespace PostIt.Views; -public partial class MainPage : NavigationPage +public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); } -} \ No newline at end of file +} From 2f7df74000c79382e91c8d8db374806cec6c18c9 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Sun, 28 Jun 2026 01:43:39 +0100 Subject: [PATCH 015/415] petit refacto --- ...{MainViewModel.cs => MainPageViewModel.cs} | 33 ++++++++++++++----- src/PostIt/PostIt/Views/LoginPage.axaml | 15 ++------- 2 files changed, 26 insertions(+), 22 deletions(-) rename src/PostIt/PostIt/ViewModels/{MainViewModel.cs => MainPageViewModel.cs} (95%) diff --git a/src/PostIt/PostIt/ViewModels/MainViewModel.cs b/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs similarity index 95% rename from src/PostIt/PostIt/ViewModels/MainViewModel.cs rename to src/PostIt/PostIt/ViewModels/MainPageViewModel.cs index e34d753c3..69742832d 100644 --- a/src/PostIt/PostIt/ViewModels/MainViewModel.cs +++ b/src/PostIt/PostIt/ViewModels/MainPageViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Avalonia.Styling; @@ -50,17 +51,20 @@ public partial class MainPageViewModel : ViewModelBase /// App.axaml.cs so the same client (and its token store) /// is shared with the login flow. /// - public BlogApiClient BlogClient { get; } + public BlogApiClient? BlogClient { get; } public override bool CanNavigateNext { get => throw new NotImplementedException(); protected set => throw new NotImplementedException(); } public override bool CanNavigatePrevious { get => throw new NotImplementedException(); protected set => throw new NotImplementedException(); } - /// - /// Test-friendly constructor: caller supplies a pre-built - /// . Production code uses the - /// (Settings, BlogApiClient) overload below. - /// - public MainPageViewModel(BlogApiClient blogClient, Settings? settings = null) + + public MainPageViewModel() + { + Init(null); + SettingsModel = new SettingsPageViewModel(); + BlogClient = null; + } + + private void Init(Settings? settings) { SearchText = string.Empty; Posts = new ObservableCollection(); @@ -71,10 +75,21 @@ public partial class MainPageViewModel : ViewModelBase Settings = settings ?? new Settings(); Title = "PostIt"; CurrentViewModel = this; - SettingsModel = new SettingsPageViewModel(); - BlogClient = blogClient ?? throw new ArgumentNullException(nameof(blogClient)); } + /// + /// Test-friendly constructor: caller supplies a pre-built + /// . Production code uses the + /// (Settings, BlogApiClient) overload below. + /// + public MainPageViewModel(BlogApiClient blogClient, Settings? settings = null) + { + SettingsModel = new SettingsPageViewModel(); + BlogClient = blogClient ?? throw new ArgumentNullException(nameof(blogClient));; + + Init(settings); + } + partial void OnSearchTextChanged(string value) => ApplyFilter(); partial void OnSelectedPostChanged(BlogPost? value) => UpdateCommandStates(); diff --git a/src/PostIt/PostIt/Views/LoginPage.axaml b/src/PostIt/PostIt/Views/LoginPage.axaml index df8d86e3c..7969c98a7 100644 --- a/src/PostIt/PostIt/Views/LoginPage.axaml +++ b/src/PostIt/PostIt/Views/LoginPage.axaml @@ -5,10 +5,10 @@ x:DataType="vm:LoginPageViewModel" Header="Login"> - + - @@ -27,18 +27,7 @@ FontSize="24" HorizontalAlignment="Center"/> - - - - - - - -