diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index 2f14e449e..86694c899 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -44,8 +44,4 @@ jobs: - name: Test run: | echo "🚀 Lancement des tests..." - cd /src/_src && dotnet test \ - --verbosity normal \ - --filter="Category!=Platform-Android" \ - --logger "xunit;LogFileName=test-results.xml" \ - && echo "✅ Success !" || { echo "❌ Fail ($?)!"; exit 1; } + cd /src/_src && dotnet test --verbosity normal && echo "✅ Success !" diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 911fc831d..a72f92bd6 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -175,12 +175,6 @@ jobs: run: | cd /src/_src dotnet restore - - name: Test - run: | - cd /src/_src && dotnet test \ - --verbosity normal \ - --filter="Category!=Platform-Android" \ - --logger "xunit;LogFileName=test-results.xml" - name: Build de PostIt.Android ARM64 run: | @@ -206,7 +200,6 @@ jobs: RELEASE_BODY: ${{ env.RELEASE_BODY }} IS_PRERELEASE: ${{ env.IS_PRERELEASE }} run: | - set -e if [[ -z "$TAG" ]]; then echo "::error::No tag resolved for the API call." exit 1 diff --git a/.github/workflows/docker-publish-backend.yml b/.github/workflows/docker-publish-backend.yml index d8466bdb8..6c2431aed 100644 --- a/.github/workflows/docker-publish-backend.yml +++ b/.github/workflows/docker-publish-backend.yml @@ -26,7 +26,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Test - run: dotnet test --no-build --verbosity normal --filter="Category!=Platform-Android" + run: dotnet test --no-build --verbosity normal # 4. Build et Push de l'image de production finale - name: Build and push production image uses: docker/build-push-action@v7 diff --git a/.gitignore b/.gitignore index 056cf69d2..b7813f604 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,6 @@ appsettings-*.*.json generated/ *.tmp -tmp/ DataDir/ *.tests.trx diff --git a/.vscode/settings.json b/.vscode/settings.json index 83a17ae38..1cb334e00 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,31 +2,29 @@ "dotnet-test-explorer.testProjectPath": "test/**/*Tests.csproj", "cSpell.words": [ - "appsettings", - "asciidoctor", - "ASPNETCORE", - "Avalonia", - "blogspot", - "Configurabilité", - "Cratie", - "DESTDIR", - "dotnet", - "DOTNET", - "ecdsa", - "envsubst", - "Forgejo", - "Hsts", - "Newtonsoft", - "Npgsql", - "Oidc", - "PKCE", - "postit", - "pschneider", - "SLNDIR", - "validable", - "www-data", - "yavsc", - "Yavsc" + "appsettings", + "asciidoctor", + "ASPNETCORE", + "Avalonia", + "Configurabilité", + "Cratie", + "DESTDIR", + "dotnet", + "DOTNET", + "ecdsa", + "envsubst", + "Hsts", + "Newtonsoft", + "Npgsql", + "Oidc", + "PKCE", + "postit", + "pschneider", + "SLNDIR", + "validable", + "www-data", + "yavsc", + "Yavsc" ], "cSpell.reportUnknownWords": true, "cSpell.language": "fr,en", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a30c9c065..3faedae05 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,13 +7,12 @@ "fileLocation": ["relative", "${workspaceFolder}"], "source": "dotnet", "pattern": { - "regexp": "^\\s*(.*)\\((\\d+),(\\d+)\\):\\s+(error|warning) (.*)$", + "regexp": "^\\s+(.*)\\((\\d+):(\\d+)\\):\\s+(error|warning)\\s+(.*)$", "file": 1, "line": 2, "column": 3, "severity": 4, - "code": 5, - "message": 6 + "message": 5 } } ], diff --git a/CHANGELOG.md b/CHANGELOG.md index 111c1491a..310989582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,162 +1,32 @@ # Changelog -## [1.0.8-rc14] - unstable +Toutes les modifications notables de PostIt et de la plateforme Yavsc +sont documentées dans ce fichier. + +Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), +et ce projet adhère au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +À noter : la **parité du numéro de patch** porte une signification de canal : + +- **patch pair** (ex. `1.0.0`, `1.0.2`) → **stable** +- **patch impair** (ex. `1.0.1`, `1.0.3`) → **preview** +- **suffixe** (ex. `1.0.0-rc1`, `1.0.0-alpha`) → **instable** + +Cette convention est partagée avec le dépôt +[`postit-debian`](https://forgejo.pschneider.fr/notazof/postit-debian) +pour la production des paquets `.deb`. + +## [1.0.8-rc4] - unstable ### Added -* [PostIt] Ajout d'un `BillingQueryDetailsPageViewModel` et de sa page associee pour afficher le detail d'une commande billing depuis l'historique. -* [PostIt] Ajout d'un mode detail avec section metier (statut, date, description, motif, infos) et section technique repliable (code, client, provision, lieu, prestations). -* [PostIt] Ajout d'un badge de statut enrichi (couleur + pictogramme) sur le detail d'une commande pour visualiser l'etat en un coup d'oeil. -* [PostIt] Ajout d'un bloc d'actions rapide en tete du detail (`Retour`, `Ouvrir en edition`) pour eviter le scroll jusqu'au bas de page. -* [PostIt] Ajout d'un style monospace sur les metadonnees techniques (code billing, client, provision, lieu, prestations) pour faciliter la lecture des identifiants et valeurs brutes. - ### Changed -* [PostIt] Le bouton d'ouverture depuis la liste billing ouvre maintenant une page de detail dediee avant l'eventuelle edition. -* [PostIt] Amelioration UX des pages billing: badges de statut colores, actions remontees en haut de page, et typographie monospace sur les metadonnees techniques. - ### Fixed -* [Yavsc.Api] Correction d'un 500 sur le refresh du catalogue d'activites lorsque `Activity.Description` est `NULL` en base (nullabilite explicite + projection null-safe + gardes sur codes vides). -* [Yavsc.Api] Correction des erreurs 400/500 sur les routes billing (`Rdv`, `Brush`, `MBrush`) en imposant `ClientId` depuis l'utilisateur authentifie et en ignorant les champs server-owned lors de la validation modele. -* [Yavsc.Api] Correction du `PUT /api/v1/billing/Rdv/{id}`: mise a jour controlee de l'entite existante (et non remplacement brut du graphe JSON), ce qui supprime les `BadRequest` parasites. -* [Yavsc.Api] Correction PostgreSQL `timestamptz` sur RDV: normalisation UTC de `EventDate` sur `POST/PUT /api/v1/billing/Rdv` pour eviter l'erreur `Cannot write DateTime with Kind=Local`. -* [Yavsc.Api] Correction du flux FrontOffice accept/reject de query: sauvegarde avec contexte utilisateur et fallback d'injection pour `IBillingService` afin d'eviter les erreurs serveur en environnement de test. -* [Yavsc.Blogs] Correction des `BadRequest` sur `POST/PUT /api/v1/blogspot` avec payload JSON (PostIt): les proprietes de navigation/serveur (`Author`, `Tags`, `Comments`, audit) ne bloquent plus la validation. -* [Yavsc.Org] Correction du flux MVC de creation de commentaire: `SaveChangesAsync(userId)` est utilise pour renseigner les champs d'audit requis (`UserCreated`/`UserModified`). -* [Yavsc.Api.Test] Stabilisation des fixtures de seed billing: remplissage des metadonnees d'audit (`UserCreated`, `UserModified`, dates) pour eviter les echecs SQLite `NOT NULL`. +* [TODO] bug loading a blog post from PostIt, ACL come along with and don't need any "Refresh" button. -## [1.0.8-rc13] - unstable - -### Added - -* [PostIt] Integration d'un selecteur de lieu RDV base sur Mapsui (carte interactive dans le formulaire `Rdv`). -* [PostIt] Ajout d'un marqueur de position et d'une action de recentrage sur la carte RDV. -* [PostIt] Ajout d'un service de reverse geocoding pour suggerer une adresse a partir des coordonnees carte. -* [PostIt] Cache et debounce des resolutions d'adresse RDV pour limiter les appels reseau et lisser l'UX. -* [PostIt.Tests] Nouvelles non-regressions sur le panneau d'adresse suggeree RDV et le comportement de la carte. -* [Yavsc.Abstract] Activation de `#nullable enable annotations` sur les fichiers legacy avec annotations nullable. -* [Yavsc.Server] Activation de `#nullable enable annotations` sur les fichiers legacy avec annotations nullable. - -### Changed - -* [PostIt] Generalisation de la barre de statut d'action (severite explicite) sur pages principales, dialogues et formulaires billing. -* [PostIt] Harmonisation des messages de statut utilisateur en francais. -* [PostIt] Renforcement des gardes de navigation dans les flux de gestion des membres de cercle. -* [PostIt] Le flux RDV conserve l'adresse saisie manuellement et propose l'adresse resolue comme suggestion explicite. -* [PostIt] Le flux de geolocalisation RDV tolere les positions proches dans le cache de suggestion d'adresse. - -### Fixed - -* [PostIt.Desktop] Correction d'un crash au demarrage OIDC (`No authority specified`) via durcissement des valeurs par defaut de configuration d'authentification. -* [PostIt] Correction de la persistance des settings: l'etat runtime de statut n'est plus serialize dans le JSON utilisateur. -* [PostIt.Tests] Ajout d'un verrou de non-regression sur le premier chargement des settings. -* [PostIt] Correction du binding de la date RDV: `DatePicker.SelectedDate` est aligne sur un proxy `DateTimeOffset?` (`EventDateSelection`). - -## [1.0.8-rc12] - unstable - -### Added - -* [PostIt] Nouveau helper d'image `ImageHelper` pour charger des bitmaps depuis les ressources et depuis le web. -* [PostIt] Affichage de l'avatar XS dans la liste des performers d'activites, avec fallback visuel (initiale utilisateur). -* [PostIt.Tests] Nouveaux tests autour des URLs avatar et de la source d'autorite. -* [contrib] Ajout d'un `README.md` utilitaire pour les symboles/icones. - -### Changed - -* [PostIt] Les avatars ne sont plus relies en string sur `Image.Source`: ils sont telecharges et lies en `Bitmap`. -* [Yavsc.Api.Client] `ActivityApiClient` accepte une base d'avatar dediee et construit les URLs avatar depuis l'autorite d'identification. -* [PostIt] Les clients Activites/Billing utilisent maintenant `ApiUrl` en lecture dynamique: un changement via Parametres prend effet sans redemarrer l'application (apres sauvegarde et rafraichissement de la page). -* [PostIt] Le header de `MainPage` n'utilise plus `ScrollViewer`; remplacement par une barre de commandes basee sur `WrapPanel`. -* [PostIt] Alignement de la navigation blogs: renommage `PushMainPageAsync` -> `PushBlogsPageAsync` et ajustement de `HomePageViewModel`. - -### Fixed - -* [PostIt.Android] Correction d'un 404 sur la page Activites au premier lancement: la configuration embarquee pointait `ApiUrl` vers le host Blogs au lieu de l'API metier. -* [PostIt] Correction du bouton Sauver de la page Parametres: binding vers `SaveCommand` pour persister correctement `ApiUrl`/`BlogsApiUrl`. - -## [1.0.8-rc11] - unstable - -### Added - -nothing - -### Changed - -* [Yavsc.Api.Test] Mise a jour de `Microsoft.EntityFrameworkCore.Sqlite` vers `10.0.11` afin de supprimer l'alerte NU1903 liee a `SQLitePCLRaw.lib.e_sqlite3` 2.1.11. -* [Yavsc.Org] Nettoyage de la configuration NuGet pour le restore: suppression du fichier local `Directory.Packages.props` au profit du fichier racine centralise. -* [Yavsc.Org] Suppression de references de packages redondantes dans le projet, sans impact fonctionnel attendu. - -### Fixed - -* [Yavsc.Api.Test] Le restore n'emet plus le warning de vulnerabilite `NU1903` sur `SQLitePCLRaw.lib.e_sqlite3`. -* [Yavsc.Org] Suppression d'une vulnerabilite de severite elevee sur AutoMapper apres publication et consommation de la nouvelle version candidate de `HigginsSoft.IdentityServer8`. - -## [1.0.8-rc10] - unstable - -### Added - -* [PostIt] Une page d'historique des commandes billing permet maintenant d'ouvrir une commande existante. -* [PostIt] Une vue "Demandes en cours" en lecture seule est disponible pour le performer, filtrée sur les statuts actifs (Inserted, Accepted, InProgress). -* [Yavsc.Org] Nouvelles entités `Country` et `PerformerCodeInputValidation` pour piloter la validation du code entreprise performer par pays. - -### Changed - -* [PostIt] La page détail billing se préremplit depuis une commande existante (Rdv, Brush, MBrush) et passe en mode mise à jour. -* [Yavsc.Org] Le formulaire `Manage/SetActivity` inclut désormais le pays d'exercice (`fr`, `en`, `pt`) et applique la regex associée au champ `SIREN`. -* [Yavsc.Org] La vérification externe du numéro d'entreprise est conservée uniquement pour le pays `fr`. - -### Fixed - -* [PostIt] Le flux historique n'est plus limité à une simple liste: l'action d'ouverture charge la commande cible puis navigue vers la page détail. -* [Yavsc.Org] Le champ `SIREN` n'est plus validé avec une règle unique indépendante du pays d'exercice. - -## [1.0.8-rc9] - unstable - -### Added - -nothing - -### Changed - -masquage non-owner côté backend de l'ACL du billet - -### Fixed - -On a maintenant le comportement attendu bout en bout: - -ACL chargée depuis le BlogPostDto -noms de cercles affichés dans le dialogue ACL côté PostIt - -## [1.0.8-rc8] - unstable - -### Added - -nothing - -### Changed - -nothing - -### Fixed - -The PostIt publish toggle button - -## [1.0.8-rc7] - unstable - -### Added - -* [PostIt] The search pattern now persists - -### Changed - -* The blog spot path is now `/api/v1/blogspot` (yet in last release) - -### Fixed - -* [Yavsc.Org] (Ticket #45) La forme de l'email de l'utilisateur est maintenant validée avant l'envoi du formulaire d'enregistrement - -## [1.0.8-rc6] - unstable +## [1.0.8-rc3] - unstable ### Added @@ -171,9 +41,8 @@ L'identifiant de l'application client Android a changé, il passe en minuscules ### Fixed -a bug posting and retrieving ACL from the backend, -the ACL now comes along with the article, -[TODO][PostIt] keep ACL along with the article +nothing, [TODO] a bug persists laoding a blog post from PostIt, ACL sould come along with and should not need any "Refresh" button. +Today, assigning ACL succeeds the first time, the following times result in a 409 ## [1.0.8-rc1] - unstable diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8aa0567d3..b9969ea50 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,39 +49,6 @@ Les tests sont répartis en : item « Tests d'intégration smoke par BC ». - `src/PostIt.Tests/` — tests unitaires du client desktop PostIt. -## Onboarding assiste par agents IA - -Pour accelerer la prise en main du depot avec Copilot/Plan/Explore : - -- Parcours pas-a-pas : [doc/onboarding-agents.md](./doc/onboarding-agents.md) -- Playbook d'usage des agents : [doc/agent-playbook.md](./doc/agent-playbook.md) -- Matrice intentions -> agent -> preuves : [doc/agent-intent-matrix.md](./doc/agent-intent-matrix.md) - -Regle minimale en contribution assistee par agent : -- expliciter l'impact architecture, -- justifier le niveau de tests execute, -- documenter les risques residuels. - -## Le CHANGELOG.md - -Le `CHANGELOG.md` est un document de changement de version - -Toutes les modifications notables de PostIt et de la plateforme Yavsc -sont documentées dans ce fichier. - -Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), -et ce projet adhère au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -À noter : la **parité du numéro de patch** porte une signification de canal : - -- **patch pair** (ex. `1.0.0`, `1.0.2`) → **preview** -- **patch impair** (ex. `1.0.1`, `1.0.3`) → **stable** -- **suffixe** (ex. `1.0.0-rc1`, `1.0.0-alpha`) → **instable** - -Cette convention est partagée avec le dépôt -[`postit-debian`](https://forgejo.pschneider.fr/notazof/postit-debian) -pour la production des paquets `.deb`. - ## Navigation (PostIt) La navigation est centralisée dans diff --git a/Directory.Packages.props b/Directory.Packages.props index f1be93f91..7505c8512 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,31 +1,15 @@ true - 8.1.0-pazofrc007 - - - - - - - - - - - - - - - - - - - - - - + + + + + + + @@ -33,24 +17,14 @@ - + - - - - - - - - - - - + \ No newline at end of file diff --git a/Makefile b/Makefile index 08f0461d2..fa9d4ecf1 100644 --- a/Makefile +++ b/Makefile @@ -77,6 +77,13 @@ release: echo " V : version semver (ex. 1.0.7-rc1) — sert à nommer la branche."; \ exit 1; \ fi + @CURRENT=$$(git branch --show-current); \ + if [ "$$CURRENT" != "main" ]; then \ + echo "Refus : la cible doit être lancée depuis main."; \ + echo " Branche courante : $$CURRENT"; \ + echo " Fais : git checkout main && git pull --ff-only origin main"; \ + exit 1; \ + fi @if [ -n "$$(git status --porcelain)" ]; then \ echo "Working tree sale, refus de créer une branche release."; \ git status --short; \ diff --git a/README.md b/README.md index 549348f4a..3f067ed32 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,11 @@ https://forgejo.pschneider.fr/notazof/yavsc/actions?workflow=release.yml # Statut actuel des actions GitHub -* [![CodeQL Advanced](https://github.com/pazof/yavsc/actions/workflows/codeql.yml/badge.svg)](https://github.com/pazof/yavsc/actions/workflows/codeql.yml) +-* [![CodeQL Advanced](https://github.com/pazof/yavsc/actions/workflows/codeql.yml/badge.svg)](https://github.com/pazof/yavsc/actions/workflows/codeql.yml) -* [![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) +* [![Build and Push Yavsc Apk](https://forgejo.pschneider.fr/notazof/yavsc/actions/workflows/docker-publish-android.yml/badge.svg)](https://forgejo.pschneider.fr/notazof/yavsc/actions/workflows/docker-publish-android.yml) +* [![Build and Push Yavsc Production Image](https://forgejo.pschneider.fr/notazof/yavsc/actions/workflows/docker-publish-backend.yml/badge.svg)](https://forgejo.pschneider.fr/notazof/yavsc/actions/workflows/docker-publish-backend.yml) # Documentation @@ -28,10 +29,6 @@ sous [`doc/`](./doc/). Voir l'[index de la documentation](./doc/README.md) pour le sommaire complet. La racine de l'architecture est [Architecture.md](./doc/Architecture.md). -Pour une prise en main guidee avec agents IA: -- parcours onboarding: [doc/onboarding-agents.md](./doc/onboarding-agents.md) -- playbook d'usage: [doc/agent-playbook.md](./doc/agent-playbook.md) - # Construction et déploiement diff --git a/ROADMAP.md b/ROADMAP.md index 364b98bda..4c00e629c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -68,7 +68,7 @@ Trois principes non négociables traversent tous les jalons : > > Chaque jalon a un **critère de sortie** vérifiable. -### Jalon 0 — Fondations techniques +### Jalon 0 — Fondations techniques *(en cours)* > Cible : pouvoir parler du domaine sans se battre avec le runtime. @@ -81,7 +81,7 @@ Trois principes non négociables traversent tous les jalons : --- -### Jalon 1 — Prestation signée de bout en bout *(en cours)* +### Jalon 1 — Prestation signée de bout en bout > Cible : un projet client/fournisseur aboutit à un **devis signé par les deux parties**, traçable, avec notifications. diff --git a/contrib/.env-sample b/contrib/.env-sample deleted file mode 100644 index fb01ede43..000000000 --- a/contrib/.env-sample +++ /dev/null @@ -1,24 +0,0 @@ -# parametres de déploiement au Makefile - -POSTGRES_HOST=localhost -POSTGRES_PORT=5432 -POSTGRES_DB=yavsc -POSTGRES_USER=yavsc -POSTGRES_PASSWORD= - -HTTP_HOST=localhost - -Org_PORT=83 -Blogs_PORT=85 -Api_PORT=87 - -PostIt_CLIENT_ID=postit - -ASPNETCORE_Smtp__Host="mercure.pschneider.fr" -ASPNETCORE_Smtp__Port=465 -ASPNETCORE_Smtp__SenderName="Paul Schneider" -ASPNETCORE_Smtp__SenderEmail="paul@pschneider.fr" -ASPNETCORE_Smtp__UserName="paul" -ASPNETCORE_Smtp__Password="" - -DESTDIR=/srv/www/yavsc diff --git a/contrib/Makefile b/contrib/Makefile index 79145668f..151045db0 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -1,4 +1,4 @@ -APP_PROJECT_NAMES=Org Blogs Api +APP_PROJECT_NAMES=Org Blogs SLNDIR=.. include $(SLNDIR)/.env @@ -9,11 +9,9 @@ generated/: generated/yavscOrg.service: generated/yavscBlogs.service: -generated/yavscApi.service: generated/yavsc%.service: generated/ template.service $(SLNDIR)/.env @cat template.service | APP_NAME="$*" \ - DESTDIR="$(DESTDIR)" \ HTTP_HOST="$(HTTP_HOST)" \ HTTP_PORT="$*_$(HTTP_PORT)" \ BASEAPPDIR="$(BASEAPPDIR)" \ @@ -35,12 +33,11 @@ generated/yavsc%.service: generated/ template.service $(SLNDIR)/.env @echo Created service file: $@ -copy-services: copy-service-Org copy-service-Blogs copy-service-Api +copy-services: copy-service-Org copy-service-Blogs copy-service-Org: /etc/systemd/system/yavscOrg.service copy-service-Blogs: /etc/systemd/system/yavscBlogs.service -copy-service-Api: /etc/systemd/system/yavscApi.service -copy-binaries: build_publish_Org build_publish_Blogs build_publish_Api stop-services +copy-binaries: build_publish_Org build_publish_Blogs stop-services @for project in $(APP_PROJECT_NAMES); \ do LCAPI=$$(echo $${project}|tr [:upper:] [:lower:]) ; \ echo "$${project} -> $${LCAPI}" ; \ @@ -63,8 +60,6 @@ copy-binaries: build_publish_Org build_publish_Blogs build_publish_Api stop-serv build_publish_%: clean_publish_dir_% @ASPNETCORE_ENV=$(CONFIGURATION) dotnet publish $(SLNDIR)/src/Yavsc.$*/Yavsc.$*.csproj -build_publish: build_publish_Org build_publish_Blogs build_publish_Api - clean_publish_dir_%: @rm -rf $(SLNDIR)/src/Yavsc.$*/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish @@ -89,7 +84,6 @@ stop-services: $(SLNDIR)/src/Yavsc.Org/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish: build_publish $(SLNDIR)/src/Yavsc.Blogs/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish: build_publish -$(SLNDIR)/src/Yavsc.Api/bin/$(CONFIGURATION)/$(DOTNET_FRAMEWORK)/publish: build_publish showConfig: @echo CONFIGURATION: $(CONFIGURATION) @@ -98,3 +92,4 @@ showConfig: clean: @rm -rf generated +.PHONY: build_publish mep showConfig copy-service-Org copy-service-Blogs reinstall clean diff --git a/contrib/README.md b/contrib/README.md deleted file mode 100644 index 011561560..000000000 --- a/contrib/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Read me - -## Note aux icones - -㝉®🅬⛒⛑🩎🩺🞫🞮🞕🞖🞆🔴🔵🔲🖂🔧🔩🔐🔌💾💼💬💭👿👾🏷🎯🏹🌍🎎💩 diff --git a/doc/README.md b/doc/README.md index fb9bea943..912a2e562 100644 --- a/doc/README.md +++ b/doc/README.md @@ -18,9 +18,6 @@ La racine de l'architecture est [Architecture.md](Architecture.md). | [architecture/postit.md](architecture/postit.md) | PostIt — topologie des projets, ViewLocator custo, navigation, DI, conventions de binding | | [architecture/decoupage-organisation.md](architecture/decoupage-organisation.md) | Découpage des projets .NET (Abstract, Server, Org, Api, Blogs, Web, Org.Tests) | | [testing.md](testing.md) | Stratégie de test : conventions des dossiers, EF Core in-memory, auth stubs, scaffold partagé | -| [onboarding-agents.md](onboarding-agents.md) | Parcours pas-à-pas pour prise en main agents IA + architecture + tests | -| [agent-playbook.md](agent-playbook.md) | Playbook d'usage de Copilot, Plan, Explore avec scénarios et anti-patterns | -| [agent-intent-matrix.md](agent-intent-matrix.md) | Matrice intentions développeur -> agent -> preuves attendues | ## Roadmap & design exploration diff --git a/doc/agent-intent-matrix.md b/doc/agent-intent-matrix.md deleted file mode 100644 index 975cd7b79..000000000 --- a/doc/agent-intent-matrix.md +++ /dev/null @@ -1,20 +0,0 @@ -# Matrice intentions -> agent -> preuves - -Cette matrice aide a choisir rapidement l'agent adapte et a exiger -une sortie verifiable. - -| Intention developpeur | Agent principal | Entrees minimales | Sortie minimale attendue | Verification | -|---|---|---|---|---| -| Comprendre un BC avant changement | Explore | BC cible, profondeur, contrainte de perimetre | Composants, points d'entree, tests relies, risques | Lire les fichiers cites + confirmer tests proposes | -| Decomposer une tache transverse | Plan | Objectif, contraintes, definition of done | Etapes ordonnees, dependances, criteres de verif | Verifier que chaque etape a une preuve observable | -| Implementer une modif locale | Copilot | Fichier cible, comportement attendu, conventions | Patch minimal, justification courte | Build/test du projet impacte | -| Ajouter un test smoke | Copilot (+Explore) | Route/endpoint, projet de test cible | Test + commande cible | Execution test cible | -| Corriger une regression | Plan + Copilot | Symptome, zone suspecte, test attendu | Fix + test NonRegression | Test rouge avant, vert apres | -| Diagnostiquer flux PostIt/OIDC | Explore + Plan | Flux, symptome, plateforme | Carte du flux + hypotheses testables | Verification manuelle + tests existants | - -## Regles d'arbitrage - -- Si l'intention est "comprendre": commencer par Explore. -- Si l'intention est "orchestrer": commencer par Plan. -- Si l'intention est "produire": utiliser Copilot apres cadrage. -- Si une sortie n'inclut pas de preuve, elle est incomplete. diff --git a/doc/agent-playbook.md b/doc/agent-playbook.md deleted file mode 100644 index ecc14f1d2..000000000 --- a/doc/agent-playbook.md +++ /dev/null @@ -1,101 +0,0 @@ -# Playbook d'usage des agents IA (Yavsc) - -Ce playbook normalise l'usage de Copilot, Plan et Explore dans le depot. -Il privilegie des sorties verifiables: fichiers, commandes tests, risques. - -## Quand utiliser quel agent - -- Plan: quand la tache est ambigue, transverse ou risquee. -- Explore: quand il faut cartographier rapidement des zones du code. -- Copilot: quand les specifications sont claires et localisees. - -## Prompt type (base) - -Utiliser ce squelette avant toute tache non triviale: - -```text -Contexte: -Objectif: -Contraintes: -Verification: -Sortie attendue: -``` - -## 4 scenarios de reference - -## 1) Explorer un bounded context - -Intention: -- Comprendre ou implementer un changement dans un BC sans regression laterale. - -Prompt minimal: -```text -Explore le BC avec profondeur medium. -Retour: composants touches, points d'entree, tests existants et risques. -``` - -Preuves attendues: -- Carte des fichiers a modifier. -- Test(s) smoke/mandatory proposes. - -## 2) Ajouter un smoke test - -Intention: -- Couvrir rapidement un endpoint ou une route publique. - -Prompt minimal: -```text -Propose un smoke test pour dans le projet de test approprie. -Respecte les conventions de doc/testing.md. -``` - -Preuves attendues: -- Fichier test cree/modifie. -- Commande precise pour executer le test cible. - -## 3) Corriger une regression backend API - -Intention: -- Corriger un bug sans casser un flux voisin. - -Prompt minimal: -```text -Planifie puis implemente un fix de dans . -Ajoute/ajuste un test NonRegression rouge puis vert. -``` - -Preuves attendues: -- Explication cause racine. -- Test non-regression associe. -- Commande d'execution et resultat attendu. - -## 4) Tracer un flux PostIt/OIDC - -Intention: -- Localiser une cassure d'authentification entre client et serveur. - -Prompt minimal: -```text -Cartographie le flux OIDC PostIt: entrypoints, callback, stockage token, -refresh. Donne points de rupture probables et tests/verification proposes. -``` - -Preuves attendues: -- Liste ordonnee des etapes du flux. -- Fichiers critiques. -- Hypotheses testables. - -## Anti-patterns a eviter - -- Prompt sans objectif verifiable. -- Demande trop large sans perimetre de fichiers. -- Validation basee uniquement sur "ca semble correct". -- Pas de lien entre changement et niveau de test. - -## Gate PR minimale (agent-assiste) - -Avant validation: -- Impact architecture explicite. -- Rationale de choix agent explicite. -- Test(s) executes et justifies. -- Risques residuels documentes. diff --git a/doc/onboarding-agents.md b/doc/onboarding-agents.md deleted file mode 100644 index 14a727a17..000000000 --- a/doc/onboarding-agents.md +++ /dev/null @@ -1,73 +0,0 @@ -# Onboarding guide: agents IA + architecture + tests - -Ce guide est optimise pour accelerer la prise en main des agents IA -(Copilot, Plan, Explore) dans Yavsc, avec une verification rapide -par les tests. - -## Resultat attendu - -A la fin du parcours, un contributeur doit pouvoir: -- Identifier les projets impactes par une modification. -- Choisir l'agent adapte a l'intention de travail. -- Produire une proposition de changement verifiable par les tests. - -## Parcours en 3 modules - -## Module A - Comprendre le terrain (30-45 min) - -Objectif: acquerir une lecture fiable de l'architecture. - -1. Lire [README.md](../README.md) puis [Architecture.md](Architecture.md). -2. Lire [architecture/decoupage-organisation.md](architecture/decoupage-organisation.md). -3. Selon le domaine: - - Backend/API: [architecture/workflow-multi-parties.md](architecture/workflow-multi-parties.md) - - PostIt: [architecture/postit.md](architecture/postit.md) puis [architecture/postit-oidc.md](architecture/postit-oidc.md) - -Definition of done: -- Expliquer en 5 phrases quelles couches sont touchees. -- Citer le ou les points d'entree applicatifs a verifier. - -## Module B - Boucle tests rapide (20-30 min) - -Objectif: verifier rapidement sans lancer toute la suite. - -1. Lire [testing.md](testing.md). -2. Lancer les smoke tests d'abord, puis mandatory selon le projet. -3. N'elargir au test complet que si le scope depasse le BC touche. - -Definition of done: -- Fournir la commande test executee. -- Expliquer pourquoi ce niveau de test est suffisant. - -## Module C - Usage agentique en production (30-40 min) - -Objectif: utiliser les agents comme accelerateurs, pas comme boites noires. - -1. Plan: decomposer la tache en etapes verifiables. -2. Explore: collecter le contexte code/doc precise. -3. Copilot: implementer localement et verifier. - -Regles: -- Toujours donner un contexte explicite (fichier, but, contrainte). -- Demander des preuves observables (fichiers modifies, tests, risques). -- Refuser toute sortie non verifiable. - -Definition of done: -- Une tache simple est livree avec: - - Plan - - Changement local - - Preuve par test - -## Routine continue (sans echeance fixe) - -Rituels recommandes: -- Hebdo: revue des prompts qui ont bien fonctionne. -- Mensuel: mise a jour du present guide et du playbook. -- A chaque incident: ajouter un anti-pattern dans le playbook. - -## Check-list de validation - -- Le changement indique son impact architecture. -- Le choix de l'agent est justifie. -- La preuve test est incluse. -- Les risques residuels sont explicitement listes. diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 2f27340e9..0d5fa50cc 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -8,6 +8,8 @@ 12.1.1 + + @@ -16,17 +18,19 @@ - - + + + + diff --git a/src/PostIt/PostIt.Android/Application.cs b/src/PostIt/PostIt.Android/Application.cs index 040b01ca3..f5a7908dd 100644 --- a/src/PostIt/PostIt.Android/Application.cs +++ b/src/PostIt/PostIt.Android/Application.cs @@ -1,5 +1,4 @@ using Android.App; -using Android; using Android.Runtime; using Avalonia; using Avalonia.Android; @@ -10,9 +9,6 @@ using Avalonia.Controls; using Avalonia.Styling; using Yavsc.Api.Client; -[assembly: UsesPermission(Manifest.Permission.AccessFineLocation)] -[assembly: UsesPermission(Manifest.Permission.AccessCoarseLocation)] - namespace PostIt.Android { [Application] diff --git a/src/PostIt/PostIt.Android/MainActivity.cs b/src/PostIt/PostIt.Android/MainActivity.cs index 549100804..ad8455ef0 100644 --- a/src/PostIt/PostIt.Android/MainActivity.cs +++ b/src/PostIt/PostIt.Android/MainActivity.cs @@ -57,17 +57,6 @@ public class MainActivity : AvaloniaMainActivity } - public override void OnRequestPermissionsResult(int requestCode, string[]? permissions, Permission[]? grantResults) - { - if (PostIt.Android.Services.AndroidCurrentLocationProvider - .HandlePermissionResult(requestCode, grantResults)) - { - return; - } - - base.OnRequestPermissionsResult(requestCode, permissions, grantResults); - } - internal static class AndroidOidcCallbackSink { private static System.Threading.Tasks.TaskCompletionSource? _pending; diff --git a/src/PostIt/PostIt.Android/PlatformBootstrap.cs b/src/PostIt/PostIt.Android/PlatformBootstrap.cs index 5b90267f0..f208f9ce9 100644 --- a/src/PostIt/PostIt.Android/PlatformBootstrap.cs +++ b/src/PostIt/PostIt.Android/PlatformBootstrap.cs @@ -14,12 +14,11 @@ internal static class PlatformBootstrap { internal static void InitPlatform() { + Platform.CreateBrowser = () => { var activity = MainActivity.Current; return activity is null ? null : new AndroidSystemBrowser(activity); }; - - Platform.TryGetCurrentLocationAsync = AndroidCurrentLocationProvider.TryGetCurrentLocationAsync; } } diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index 3d8be385c..7f44645d0 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -11,7 +11,7 @@ false 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+183.Branch.release-1.0.8-rc8.Sha.6cff3db32ecf72c0d2d430b7002fa7816a34e070 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 diff --git a/src/PostIt/PostIt.Android/Services/AndroidCurrentLocationProvider.cs b/src/PostIt/PostIt.Android/Services/AndroidCurrentLocationProvider.cs deleted file mode 100644 index f710cacc7..000000000 --- a/src/PostIt/PostIt.Android/Services/AndroidCurrentLocationProvider.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Android; -using Android.App; -using Android.Content.PM; -using Android.Locations; -using AndroidX.Core.App; -using AndroidX.Core.Content; -using PostIt.Services; - -namespace PostIt.Android.Services; - -internal static class AndroidCurrentLocationProvider -{ - public static async Task TryGetCurrentLocationAsync(CancellationToken cancellationToken) - { - var activity = MainActivity.Current; - if (activity is null) - { - return CurrentLocationResult.Unavailable("L'activité Android n'est pas encore prête."); - } - - var permissionGranted = await LocationPermissionBroker.EnsureGrantedAsync(activity, cancellationToken).ConfigureAwait(false); - if (!permissionGranted) - { - return CurrentLocationResult.PermissionDenied(); - } - - var locationManager = activity.GetSystemService(global::Android.Content.Context.LocationService) as LocationManager; - if (locationManager is null) - { - return CurrentLocationResult.Unavailable("Le service de localisation Android est indisponible."); - } - - var location = locationManager.GetProviders(enabledOnly: true)? - .Select(provider => locationManager.GetLastKnownLocation(provider)) - .Where(candidate => candidate is not null) - .OrderByDescending(candidate => candidate!.Time) - .ThenBy(candidate => candidate!.Accuracy) - .FirstOrDefault(); - - if (location is null) - { - return CurrentLocationResult.Unavailable("Aucune position n'est disponible. Activez la localisation du système puis réessayez."); - } - - return CurrentLocationResult.Success(location.Latitude, location.Longitude); - } - - public static bool HandlePermissionResult(int requestCode, Permission[]? grantResults) - => LocationPermissionBroker.HandleResult(requestCode, grantResults); - - private static class LocationPermissionBroker - { - private const int RequestCode = 4042; - private static readonly string[] RequestedPermissions = - { - Manifest.Permission.AccessFineLocation, - Manifest.Permission.AccessCoarseLocation, - }; - - private static readonly object SyncRoot = new(); - private static TaskCompletionSource? _pendingRequest; - - public static Task EnsureGrantedAsync(Activity activity, CancellationToken cancellationToken) - { - if (HasLocationPermission(activity)) - { - return Task.FromResult(true); - } - - lock (SyncRoot) - { - if (_pendingRequest is null) - { - _pendingRequest = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - ActivityCompat.RequestPermissions(activity, RequestedPermissions, RequestCode); - } - - if (!cancellationToken.CanBeCanceled) - { - return _pendingRequest.Task; - } - - return WaitAsync(_pendingRequest.Task, cancellationToken); - } - } - - public static bool HandleResult(int requestCode, Permission[]? grantResults) - { - if (requestCode != RequestCode) - { - return false; - } - - var granted = grantResults is { Length: > 0 } && grantResults.All(result => result == Permission.Granted); - TaskCompletionSource? pendingRequest; - lock (SyncRoot) - { - pendingRequest = _pendingRequest; - _pendingRequest = null; - } - - pendingRequest?.TrySetResult(granted); - return true; - } - - private static bool HasLocationPermission(Activity activity) - { - return ContextCompat.CheckSelfPermission(activity, Manifest.Permission.AccessFineLocation) == Permission.Granted - || ContextCompat.CheckSelfPermission(activity, Manifest.Permission.AccessCoarseLocation) == Permission.Granted; - } - - private static async Task WaitAsync(Task task, CancellationToken cancellationToken) - { - using var registration = cancellationToken.Register(() => - { - lock (SyncRoot) - { - _pendingRequest?.TrySetCanceled(cancellationToken); - _pendingRequest = null; - } - }); - - return await task.ConfigureAwait(false); - } - } -} diff --git a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj index 4534a2940..67600a727 100644 --- a/src/PostIt/PostIt.Browser/PostIt.Browser.csproj +++ b/src/PostIt/PostIt.Browser/PostIt.Browser.csproj @@ -6,7 +6,7 @@ enable 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+183.Branch.release-1.0.8-rc8.Sha.6cff3db32ecf72c0d2d430b7002fa7816a34e070 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 diff --git a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj index 948e726c5..30ba65c41 100644 --- a/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj +++ b/src/PostIt/PostIt.Desktop/PostIt.Desktop.csproj @@ -7,7 +7,7 @@ enable 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+183.Branch.release-1.0.8-rc8.Sha.6cff3db32ecf72c0d2d430b7002fa7816a34e070 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 diff --git a/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs deleted file mode 100644 index 04cc0f314..000000000 --- a/src/PostIt/PostIt.Tests/ActivitiesPageViewModelTests.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System.Net.Http; -using PostIt.ViewModels; -using Yavsc.Abstract.Workflow; -using Yavsc.Api.Client; - -namespace PostIt.Tests; - -public class ActivitiesPageViewModelTests -{ - [Fact] - public void ActivityApiClient_uses_avatar_authority_when_provided() - { - var api = new StubActivityApi(); - var client = new ActivityApiClient( - api, - "https://api.pschneider.fr/api/v1/", - "https://yavsc.pschneider.fr/"); - - var url = client.BuildAvatarXsUrl("paul"); - - Assert.Equal("https://yavsc.pschneider.fr/avatars/paul.xs.png", url); - } - - [Fact] - public async Task ActivityApiClient_uses_business_absolute_paths() - { - var api = new StubActivityApi(); - var client = new ActivityApiClient(api, "https://business.example/api/v1/"); - var billingClient = new BillingApiClient(api, "https://business.example/api/v1/"); - - await client.GetCatalogAsync("brush", TestContext.Current.CancellationToken); - await client.GetUsersAsync("brush-pro", TestContext.Current.CancellationToken); - await billingClient.CreateAsync("Rdv", new { Foo = "Bar" }, TestContext.Current.CancellationToken); - await billingClient.GetQuerySummariesAsync("Rdv", TestContext.Current.CancellationToken); - - Assert.Equal("https://business.example/api/v1/activity/catalog?parentCode=brush", api.Paths[0]); - Assert.Equal("https://business.example/api/v1/activity/brush-pro/users", api.Paths[1]); - Assert.Equal("https://business.example/api/v1/billing/Rdv", api.Paths[2]); - Assert.Equal("https://business.example/api/v1/billing/Rdv", api.Paths[3]); - } - - [Fact] - public async Task ActivityApiClient_uses_updated_business_base_without_restart() - { - var api = new StubActivityApi(); - var baseUrl = "https://business-a.example/api/v1/"; - var client = new ActivityApiClient(api, () => baseUrl); - - await client.GetCatalogAsync(ct: TestContext.Current.CancellationToken); - - baseUrl = "https://business-b.example/api/v1/"; - await client.GetUsersAsync("brush", TestContext.Current.CancellationToken); - - Assert.Equal("https://business-a.example/api/v1/activity/catalog", api.Paths[0]); - Assert.Equal("https://business-b.example/api/v1/activity/brush/users", api.Paths[1]); - } - - [Fact] - public async Task RefreshAsync_loads_first_activity_then_specialization_performers() - { - var api = new StubActivityApi(); - var client = new ActivityApiClient(api, "https://business.example/api/v1/"); - var billingClient = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = new ActivitiesPageViewModel(client, billingClient); - - await vm.RefreshAsync(); - - Assert.Equal("brush", vm.SelectedActivity?.Code); - Assert.Single(vm.Specializations); - Assert.Equal("brush", vm.CurrentActivity?.Code); - Assert.Single(vm.Performers); - Assert.Equal("Alice", vm.Performers[0].UserName); - Assert.Equal("https://business.example/avatars/Alice.xs.png", vm.Performers[0].AvatarXsUrl); - Assert.True(vm.Performers[0].HasPerformerProfile); - Assert.True(vm.Performers[0].IsPerformerActive); - Assert.Equal("Actif", vm.Performers[0].PerformerStatusBadgeLabel); - Assert.Equal("Pas d'autre activité", vm.Performers[0].ExtraActivityLabel); - - await vm.ShowSpecializationAsync(vm.Specializations[0]); - - Assert.Equal("brush-pro", vm.CurrentActivity?.Code); - Assert.Single(vm.Performers); - Assert.Equal("Bob", vm.Performers[0].UserName); - Assert.Equal("https://business.example/avatars/Bob.xs.png", vm.Performers[0].AvatarXsUrl); - Assert.True(vm.Performers[0].HasPerformerProfile); - Assert.False(vm.Performers[0].IsPerformerActive); - Assert.Equal("Inactif", vm.Performers[0].PerformerStatusBadgeLabel); - Assert.Equal("Autres spécialisations: 2", vm.Performers[0].ExtraActivityLabel); - Assert.Contains("brush pro", vm.StatusMessage, StringComparison.OrdinalIgnoreCase); - - await vm.ShowSpecializationAsync(null); - - Assert.Equal("brush", vm.CurrentActivity?.Code); - Assert.Single(vm.Performers); - Assert.Equal("Alice", vm.Performers[0].UserName); - } - - private sealed class StubActivityApi : IYavscApiClient - { - public HttpClient Http { get; } = new(); - public List Paths { get; } = new(); - - public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) - { - Paths.Add(path); - - if (typeof(T) == typeof(List)) - { - var activities = new List - { - new() - { - Code = "brush", - Name = "Brush", - Description = "Coiffure à domicile", - PerformerCount = 1, - Forms = new List - { - new() { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" } - }, - Children = new List - { - new() - { - Code = "brush-pro", - Name = "Brush Pro", - Description = "Spécialisation premium", - ParentCode = "brush", - PerformerCount = 1, - Forms = new List - { - new() { Id = 2, ActionName = "Rdv", Title = "Rendez-vous premium" } - } - } - } - } - }; - return Task.FromResult((T)(object)activities); - } - - if (typeof(T) == typeof(List)) - { - var performers = path.EndsWith("brush-pro/users", StringComparison.Ordinal) - ? new List - { - new() { PerformerId = "pro-2", HasPerformerProfile = true, Active = false, UserName = "Bob", ActivityCode = "brush-pro", ActivityName = "Brush Pro", ExtraActivityCount = 2 } - } - : new List - { - new() { PerformerId = "pro-1", HasPerformerProfile = true, Active = true, UserName = "Alice", ActivityCode = "brush", ActivityName = "Brush", ExtraActivityCount = 0 } - }; - - return Task.FromResult((T)(object)performers); - } - - return Task.FromResult(default(T)!); - } - - public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) - { - Paths.Add(path); - return Task.CompletedTask; - } - - public ValueTask DisposeAsync() => ValueTask.CompletedTask; - } -} diff --git a/src/PostIt/PostIt.Tests/AndroidAppLaunchTests.cs b/src/PostIt/PostIt.Tests/AndroidAppLaunchTests.cs index 25630c836..d4980d268 100644 --- a/src/PostIt/PostIt.Tests/AndroidAppLaunchTests.cs +++ b/src/PostIt/PostIt.Tests/AndroidAppLaunchTests.cs @@ -12,7 +12,6 @@ namespace PostIt.Tests; /// Skip conditions: the package is not installed on the connected device, /// or no device is connected via adb. /// -[Trait("Category", "Platform-Android")] public class AndroidAppLaunchTests { private const string PackageName = "fr.pschneider.postit"; @@ -24,8 +23,8 @@ public class AndroidAppLaunchTests _output = output; } - // TODO https://twosixtech.com/blog/integrating-docker-and-adb/ - [Fact] + // https://twosixtech.com/blog/integrating-docker-and-adb/ + // FIXME ala hosted shared resource adb server - [Fact] public void PostIt_starts_and_draws_a_first_frame_on_the_emulator() { if (!IsPackageInstalledOnAnyDevice()) diff --git a/src/PostIt/PostIt.Tests/BearerScopeTests.cs b/src/PostIt/PostIt.Tests/BearerScopeTests.cs index 984483fc7..c6bf7d56e 100644 --- a/src/PostIt/PostIt.Tests/BearerScopeTests.cs +++ b/src/PostIt/PostIt.Tests/BearerScopeTests.cs @@ -67,7 +67,7 @@ public class BearerScopeTests Scopes = userScopes, RedirectUri = "postit://callback", }, - ApiUrl = "https://example.invalid/api/v1/", + BusinessApiUrl = "https://example.invalid/api/v1/", }; var tokensPath = Path.Combine( @@ -266,7 +266,7 @@ public class BearerScopeTests // private HttpClient is independent, so we resolve the // absolute URI ourselves from Settings.BusinessApiUrl — // the same URL BlogApiClient would have set as BaseAddress. - var absolute = new Uri(new Uri(Settings.ApiUrl), path); + var absolute = new Uri(new Uri(Settings.BusinessApiUrl), path); using var req = new HttpRequestMessage(method, absolute); req.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken); diff --git a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs deleted file mode 100644 index 17fcc968c..000000000 --- a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs +++ /dev/null @@ -1,357 +0,0 @@ -using System.Text.Json; -using PostIt.Helpers; -using PostIt.Services; -using PostIt.ViewModels; -using PostIt.ViewModels.Commands; -using Yavsc; -using Yavsc.Abstract.Workflow; -using Yavsc.Api.Client; -using Yavsc.Models.Haircut; - -namespace PostIt.Tests; - -public class BillingCommandPageViewModelTests -{ - [Fact] - public async Task SubmitAsync_posts_rdv_payload_to_selected_billing_route() - { - var api = new RecordingApi(); - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "dev", Name = "Développement" }, - new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - client) as RdvViewModel; - - vm!.EventDate = DateTime.Parse("2026-09-02 14:30"); - vm!.Reason = "Point de cadrage"; - vm!.Address = "1 rue du Test"; - vm!.Latitude = 48.8566; - vm!.Longitude = 2.3522; - vm!.Consent = true; - - await vm.SubmitCommand.ExecuteAsync(null); - - Assert.Equal("https://business.example/api/v1/billing/Rdv", api.LastPath); - Assert.NotNull(api.LastBody); - - using var json = JsonDocument.Parse(JsonSerializer.Serialize(api.LastBody)); - Assert.Equal("dev", json.RootElement.GetProperty("ActivityCode").GetString()); - Assert.Equal("perf-1", json.RootElement.GetProperty("PerformerId").GetString()); - Assert.Equal("Point de cadrage", json.RootElement.GetProperty("Reason").GetString()); - Assert.Equal((int)QueryStatus.Inserted, json.RootElement.GetProperty("Status").GetInt32()); - } - - [Fact] - public async Task SubmitAsync_refuses_unsupported_billing_code() - { - var api = new RecordingApi(); - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - - var vm = - new CommandFormSummary { Id = 13, ActionName = "Book", Title = "Réservation" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "book", Name = "Book" }, - new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, - client); - - Assert.Null(vm); - } - - [Fact] - public async Task SubmitAsync_allows_missing_coordinates_and_omits_them_from_payload() - { - var api = new RecordingApi(); - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - - new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "dev", Name = "Développement" }, - new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - client) as RdvViewModel; - vm!.EventDate = DateTime.Parse("2026-09-02 14:30"); - vm!.Reason = "Point de cadrage"; - vm!.Address = "1 rue du Test"; - vm!.Latitude = null; - vm!.Longitude = null; - vm!.Consent = true; - - await vm.SubmitCommand.ExecuteAsync(null); - - using var json = JsonDocument.Parse(JsonSerializer.Serialize(api.LastBody)); - var location = json.RootElement.GetProperty("Location"); - Assert.Equal("1 rue du Test", location.GetProperty("Address").GetString()); - Assert.False(location.TryGetProperty("Latitude", out _)); - Assert.False(location.TryGetProperty("Longitude", out _)); - } - - [Fact] - public async Task UseCurrentLocationAsync_prefills_coordinates_from_platform_provider() - { - var original = Platform.TryGetCurrentLocationAsync; - try - { - Platform.TryGetCurrentLocationAsync = _ => Task.FromResult(CurrentLocationResult.Success(48.8566, 2.3522)); - - var api = new RecordingApi(); - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "dev", Name = "Développement" }, - new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - client) as RdvViewModel; - - await vm!.UseCurrentLocationCommand.ExecuteAsync(null); - - Assert.Equal(48.8566, vm!.Latitude); - Assert.Equal(2.3522, vm!.Longitude); - } - finally - { - Platform.TryGetCurrentLocationAsync = original; - } - } - - [Fact] - public void ApplyLocationFromMap_sets_coordinates_and_updates_status() - { - var api = new RecordingApi(); - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "dev", Name = "Développement" }, - new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - client) as RdvViewModel; - - vm!.Address = string.Empty; - vm.ApplyLocationFromMap(48.85661234, 2.35224567); - - Assert.Equal(48.856612, vm.Latitude); - Assert.Equal(2.352246, vm.Longitude); - Assert.Contains("Position sélectionnée", vm.StatusMessage, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public void EventDateSelection_round_trips_with_EventDate_for_DatePicker_binding() - { - var api = new RecordingApi(); - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "dev", Name = "Développement" }, - new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - client) as RdvViewModel; - - var selected = new DateTimeOffset(2026, 9, 7, 14, 30, 0, TimeSpan.FromHours(2)); - vm!.EventDateSelection = selected; - - Assert.Equal(selected.LocalDateTime, vm.EventDate); - Assert.Equal(vm.EventDate, vm.EventDateSelection!.Value.LocalDateTime); - } - - [Fact] - public void ApplyResolvedAddress_populates_empty_address_directly() - { - var api = new RecordingApi(); - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "dev", Name = "Développement" }, - new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - client) as RdvViewModel; - - vm!.Address = string.Empty; - vm.ApplyResolvedAddress("10 rue de Rivoli, 75001 Paris"); - - Assert.Equal("10 rue de Rivoli, 75001 Paris", vm.Address); - Assert.False(vm.HasSuggestedAddress); - } - - [Fact] - public void ApplyResolvedAddress_preserves_manual_address_and_exposes_suggestion() - { - var api = new RecordingApi(); - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - new CommandFormSummary { Id = 12, ActionName = "Rdv", Title = "Rendez-vous" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "dev", Name = "Développement" }, - new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - client) as RdvViewModel; - - vm!.Address = "Saisie manuelle"; - vm.ApplyResolvedAddress("10 rue de Rivoli, 75001 Paris"); - - Assert.Equal("Saisie manuelle", vm.Address); - Assert.True(vm.HasSuggestedAddress); - Assert.Equal("10 rue de Rivoli, 75001 Paris", vm.SuggestedAddress); - - vm.ApplySuggestedAddressCommand.Execute(null); - - Assert.Equal("10 rue de Rivoli, 75001 Paris", vm.Address); - Assert.False(vm.HasSuggestedAddress); - } - - [Fact] - public async Task InitializeAsync_loads_prestations_for_brush_and_submit_posts_selected_prestation() - { - var api = new RecordingApi - { - HairPrestations = new List - { - new() { Id = 10, Title = "Femme · Cheveux mi-longs", Details = "Coupe · Brushing" }, - new() { Id = 11, Title = "Homme · Cheveux courts", Details = "Coupe · Coiffage" }, - } - }; - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - new CommandFormSummary { Id = 13, ActionName = "Brush", Title = "Coupe" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "brush", Name = "Brush" }, - new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, - client) as BrushViewModel; - vm!.EventDate = DateTime.Parse("2026-09-02 14:30"); - vm!.Address = "1 rue du Test"; - vm!.Latitude = 48.8566; - vm!.Longitude = 2.3522; - vm!.Consent = true; - vm!.AdditionalInfo = "Prévoir shampoing"; - - await vm.InitializeAsync(); - vm.SelectedPrestation = vm.AvailablePrestations[1]; - await vm.SubmitCommand.ExecuteAsync(null); - - Assert.Equal("https://business.example/api/v1/billing/Brush", api.LastPath); - using var json = JsonDocument.Parse(JsonSerializer.Serialize(api.LastBody)); - Assert.Equal(11, json.RootElement.GetProperty("PrestationId").GetInt32()); - Assert.Equal("Prévoir shampoing", json.RootElement.GetProperty("AdditionalInfo").GetString()); - } - - [Fact] - public async Task InitializeAsync_loads_prestations_for_mbrush_and_submit_posts_selected_prestations() - { - var api = new RecordingApi - { - HairPrestations = new List - { - new() { Id = 21, Title = "Femme · Cheveux longs", Details = "Coupe · Couleur" }, - new() { Id = 22, Title = "Enfant · Cheveux courts", Details = "Coupe · Sans technique" }, - } - }; - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - new CommandFormSummary { Id = 14, ActionName = "MBrush", Title = "Coupe groupée" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "mbrush", Name = "MBrush" }, - new ActivityUserDisplayItem { PerformerId = "perf-3", UserName = "Cara" }, - client) as MBrushViewModel; - vm!.EventDate = DateTime.Parse("2026-09-03 10:00"); - vm!.Address = "2 rue du Test"; - vm!.Latitude = 48.8567; - vm!.Longitude = 2.3523; - vm!.Consent = true; - - await vm.InitializeAsync(); - vm!.MultiPrestations[0].IsSelected = true; - vm!.MultiPrestations[1].IsSelected = true; - await vm!.SubmitCommand.ExecuteAsync(null); - - Assert.Equal("https://business.example/api/v1/billing/MBrush", api.LastPath); - using var json = JsonDocument.Parse(JsonSerializer.Serialize(api.LastBody)); - var prestations = json.RootElement.GetProperty("Prestations"); - Assert.Equal(2, prestations.GetArrayLength()); - Assert.Equal(21, prestations[0].GetProperty("PrestationId").GetInt32()); - Assert.Equal(22, prestations[1].GetProperty("PrestationId").GetInt32()); - } - - [Fact] - public async Task InitializeAsync_with_existing_brush_query_prefills_and_submit_updates_query() - { - var api = new RecordingApi - { - HairPrestations = new List - { - new() { Id = 30, Title = "Femme · Cheveux longs", Details = "Coupe · Brushing" }, - new() { Id = 31, Title = "Homme · Cheveux courts", Details = "Coupe" }, - } - }; - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = - new CommandFormSummary { Id = 13, ActionName = "Brush", Title = "Coupe" } - .CreateCommandPageViewModel( - new ActivityInfo { Code = "brush", Name = "Brush" }, - new ActivityUserDisplayItem { PerformerId = "perf-2", UserName = "Bob" }, - client) as BrushViewModel; - await vm!.InitializeAsync(new BillingQueryDetailsDto - { - Id = 77, - BillingCode = "Brush", - ActivityCode = "brush", - PerformerId = "perf-2", - ClientId = "cli-1", - EventDate = new DateTime(2026, 9, 2, 14, 30, 0, DateTimeKind.Utc), - Consent = true, - Status = QueryStatus.Accepted, - PrestationId = 30, - AdditionalInfo = "Ancienne note", - Location = new BillingLocationDto - { - Address = "1 rue du Test", - Latitude = 48.8566, - Longitude = 2.3522, - } - }); - - vm!.SelectedPrestation = vm!.AvailablePrestations[1]; - vm!.AdditionalInfo = "Note mise à jour"; - await vm!.SubmitCommand.ExecuteAsync(null); - - Assert.Equal(HttpMethod.Put, api.LastMethod); - Assert.Equal("https://business.example/api/v1/billing/Brush/77", api.LastPath); - Assert.True(vm.IsEditingExisting); - Assert.Equal("Mettre à jour la commande", vm.SubmitLabel); - - using var json = JsonDocument.Parse(JsonSerializer.Serialize(api.LastBody)); - Assert.Equal(77, json.RootElement.GetProperty("Id").GetInt32()); - Assert.Equal(31, json.RootElement.GetProperty("PrestationId").GetInt32()); - Assert.Equal("Note mise à jour", json.RootElement.GetProperty("AdditionalInfo").GetString()); - Assert.Equal((int)QueryStatus.Accepted, json.RootElement.GetProperty("Status").GetInt32()); - } - - private sealed class RecordingApi : IYavscApiClient - { - public HttpClient Http { get; } = new(); - public HttpMethod? LastMethod { get; private set; } - public string? LastPath { get; private set; } - public object? LastBody { get; private set; } - public List? HairPrestations { get; init; } - - public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) - { - LastMethod = method; - LastPath = path; - LastBody = body; - if (typeof(T) == typeof(List)) - { - return Task.FromResult((T)(object)(HairPrestations ?? new List())); - } - return Task.FromResult(default(T)!); - } - - public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) - { - LastMethod = method; - LastPath = path; - LastBody = body; - return Task.CompletedTask; - } - - public ValueTask DisposeAsync() => ValueTask.CompletedTask; - } -} diff --git a/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs deleted file mode 100644 index d3d1d5694..000000000 --- a/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Net.Http; -using PostIt.ViewModels; -using Yavsc; -using Yavsc.Abstract.Workflow; -using Yavsc.Api.Client; - -namespace PostIt.Tests; - -public class BillingQueriesPageViewModelTests -{ - [Fact] - public async Task RefreshAsync_filters_queries_by_selected_activity_and_performer() - { - var api = new StubBillingApi(); - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = new BillingQueriesPageViewModel( - new ActivityInfo { Code = "dev", Name = "Développement" }, - new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - new CommandFormSummary { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" }, - client); - - await vm.InitializeAsync(); - - Assert.Equal("https://business.example/api/v1/billing/Rdv", api.Paths.Single()); - Assert.Equal(3, vm.Queries.Count); - Assert.Contains(vm.Queries, q => q.Description == "Rendez-vous #1"); - Assert.Contains("3 commande", vm.StatusMessage, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public async Task RefreshAsync_in_readonly_ongoing_mode_keeps_only_ongoing_statuses_and_disables_open() - { - var api = new StubBillingApi(); - var client = new BillingApiClient(api, "https://business.example/api/v1/"); - var vm = new BillingQueriesPageViewModel( - new ActivityInfo { Code = "dev", Name = "Développement" }, - new ActivityUserDisplayItem { PerformerId = "perf-1", UserName = "Alice" }, - new CommandFormSummary { Id = 1, ActionName = "Rdv", Title = "Rendez-vous" }, - client, - isReadOnly: true, - ongoingOnly: true); - - await vm.InitializeAsync(); - - Assert.Equal(2, vm.Queries.Count); - Assert.All(vm.Queries, q => Assert.DoesNotContain("Rejected", q.StatusLabel, StringComparison.OrdinalIgnoreCase)); - Assert.Contains("lecture seule", vm.StatusMessage, StringComparison.OrdinalIgnoreCase); - - Assert.True(vm.Queries.Count > 0); - } - - private sealed class StubBillingApi : IYavscApiClient - { - public HttpClient Http { get; } = new(); - public List Paths { get; } = new(); - - public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) - { - Paths.Add(path); - - if (typeof(T) == typeof(List)) - { - var data = new List - { - new() - { - Id = 11, - ActivityCode = "dev", - PerformerId = "perf-1", - ClientId = "cli-1", - Status = QueryStatus.Inserted, - Description = "Rendez-vous #1", - Reason = "Point de cadrage", - EventDate = new DateTime(2026, 9, 1, 10, 0, 0, DateTimeKind.Utc), - }, - new() - { - Id = 12, - ActivityCode = "other", - PerformerId = "perf-1", - ClientId = "cli-1", - Status = QueryStatus.Accepted, - Description = "Autre activité", - EventDate = new DateTime(2026, 9, 2, 10, 0, 0, DateTimeKind.Utc), - }, - new() - { - Id = 13, - ActivityCode = "dev", - PerformerId = "perf-2", - ClientId = "cli-1", - Status = QueryStatus.Accepted, - Description = "Autre performer", - EventDate = new DateTime(2026, 9, 3, 10, 0, 0, DateTimeKind.Utc), - }, - new() - { - Id = 14, - ActivityCode = "dev", - PerformerId = "perf-1", - ClientId = "cli-1", - Status = QueryStatus.InProgress, - Description = "En cours", - EventDate = new DateTime(2026, 9, 4, 10, 0, 0, DateTimeKind.Utc), - }, - new() - { - Id = 15, - ActivityCode = "dev", - PerformerId = "perf-1", - ClientId = "cli-1", - Status = QueryStatus.Rejected, - Description = "Rejetée", - EventDate = new DateTime(2026, 9, 5, 10, 0, 0, DateTimeKind.Utc), - } - }; - - return Task.FromResult((T)(object)data); - } - - return Task.FromResult(default(T)!); - } - - public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) - { - Paths.Add(path); - return Task.CompletedTask; - } - - public ValueTask DisposeAsync() => ValueTask.CompletedTask; - } -} diff --git a/src/PostIt/PostIt.Tests/BlogPostAuthorDtoTests.cs b/src/PostIt/PostIt.Tests/BlogPostAuthorDtoTests.cs index daabdf59b..895f220ec 100644 --- a/src/PostIt/PostIt.Tests/BlogPostAuthorDtoTests.cs +++ b/src/PostIt/PostIt.Tests/BlogPostAuthorDtoTests.cs @@ -166,51 +166,4 @@ public class BlogPostAuthorDtoTests Assert.True(root.TryGetProperty("userName", out _)); Assert.True(root.TryGetProperty("avatar", out _)); } - - [Fact] - public void BlogPostDto_deserialises_acl_from_detail_payload() - { - // Detail payload shape emitted by BlogApiController.GetBlog: - // ACL entries are included under "acl"/"ACL". - var json = """ - { - "id": 99, - "title": "ACL test", - "authorId": "u-alice", - "acl": [ - { "circleId": 12, "blogPostId": 99 }, - { "circleId": 34, "blogPostId": 99 } - ] - } - """; - - var post = JsonSerializer.Deserialize(json, CaseInsensitiveJson); - - Assert.NotNull(post); - var acl = post!.GetACL(); - Assert.Equal(2, acl.Length); - Assert.Contains(acl, a => a.CircleId == 12); - Assert.Contains(acl, a => a.CircleId == 34); - } - - [Fact] - public void BlogPostDto_does_not_emit_acl_when_serialized_for_write() - { - var post = new BlogPostDto - { - Id = 77, - Title = "Write payload" - }; - post.AuthorizeCircle(11); - - // The client should not send ACL through POST/PUT blog payloads. - // ACL mutations have their own dedicated /blogacl endpoint. - var json = JsonSerializer.Serialize(post, - new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - - using var doc = JsonDocument.Parse(json); - var root = doc.RootElement; - Assert.False(root.TryGetProperty("acl", out _)); - Assert.False(root.TryGetProperty("wireAcl", out _)); - } } diff --git a/src/PostIt/PostIt.Tests/MainPageSaveTests.cs b/src/PostIt/PostIt.Tests/MainPageSaveTests.cs index 0fd5627c8..5baae6581 100644 --- a/src/PostIt/PostIt.Tests/MainPageSaveTests.cs +++ b/src/PostIt/PostIt.Tests/MainPageSaveTests.cs @@ -79,9 +79,9 @@ public class MainPageSaveTests // whose Title is exactly what the user typed. The bug // fails this assertion with Title == string.Empty. Assert.NotEmpty(recorder.Calls); - var (method, path, body) = recorder.Calls[1]; + var (method, path, body) = recorder.FirstCall; Assert.Equal(HttpMethod.Post, method); - Assert.Equal("blogspot", path); + Assert.Equal("blog", path); var sent = Assert.IsType(body); Assert.Equal(typed, sent.Title); } diff --git a/src/PostIt/PostIt.Tests/NominatimReverseGeocodingServiceTests.cs b/src/PostIt/PostIt.Tests/NominatimReverseGeocodingServiceTests.cs deleted file mode 100644 index 4eceb7a2f..000000000 --- a/src/PostIt/PostIt.Tests/NominatimReverseGeocodingServiceTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Net; -using System.Text; -using PostIt.Services; - -namespace PostIt.Tests; - -public class NominatimReverseGeocodingServiceTests -{ - [Fact] - public async Task TryResolveAddressAsync_formats_compact_street_address_from_nominatim_payload() - { - var handler = new StubHandler(""" - { - "display_name": "6, Place de l'Hôtel-de-Ville - Esplanade de la Libération, Paris, 75004, France", - "address": { - "house_number": "6", - "road": "Place de l'Hôtel-de-Ville - Esplanade de la Libération", - "postcode": "75004", - "city": "Paris" - } - } - """); - - var service = new NominatimReverseGeocodingService(new HttpClient(handler) - { - BaseAddress = new Uri("https://nominatim.openstreetmap.org/") - }); - - var result = await service.TryResolveAddressAsync(48.8566, 2.3522); - - Assert.Equal("6, Place de l'Hôtel-de-Ville - Esplanade de la Libération, 75004, Paris", result); - Assert.NotNull(handler.LastRequest); - Assert.Contains("reverse?format=jsonv2", handler.LastRequest!.RequestUri!.ToString(), StringComparison.Ordinal); - } - - [Fact] - public async Task TryResolveAddressAsync_returns_null_on_unsuccessful_response() - { - var handler = new StubHandler("{}", HttpStatusCode.TooManyRequests); - var service = new NominatimReverseGeocodingService(new HttpClient(handler) - { - BaseAddress = new Uri("https://nominatim.openstreetmap.org/") - }); - - var result = await service.TryResolveAddressAsync(48.8566, 2.3522); - - Assert.Null(result); - } - - private sealed class StubHandler : HttpMessageHandler - { - private readonly string _payload; - private readonly HttpStatusCode _statusCode; - - public HttpRequestMessage? LastRequest { get; private set; } - - public StubHandler(string payload, HttpStatusCode statusCode = HttpStatusCode.OK) - { - _payload = payload; - _statusCode = statusCode; - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - LastRequest = request; - return Task.FromResult(new HttpResponseMessage(_statusCode) - { - Content = new StringContent(_payload, Encoding.UTF8, "application/json") - }); - } - } -} \ No newline at end of file diff --git a/src/PostIt/PostIt.Tests/PostAclDialogTests.cs b/src/PostIt/PostIt.Tests/PostAclDialogTests.cs index e7175203c..ccb33ec7a 100644 --- a/src/PostIt/PostIt.Tests/PostAclDialogTests.cs +++ b/src/PostIt/PostIt.Tests/PostAclDialogTests.cs @@ -9,7 +9,6 @@ using PostIt.Services; using PostIt.ViewModels; using PostIt.Views; using Yavsc.Api.Client; -using Yavsc.Api.Client.Dtos; using Yavsc.Blogspot; namespace PostIt.Tests; @@ -95,7 +94,7 @@ public class PostAclDialogTests HttpMethod method, string path, object? body = null, CancellationToken ct = default) { - var absolute = new Uri(new Uri(Settings.ApiUrl), path); + var absolute = new Uri(new Uri(Settings.BusinessApiUrl), path); using var req = new HttpRequestMessage(method, absolute); using var resp = _http.SendAsync(req, ct).GetAwaiter().GetResult(); resp.EnsureSuccessStatusCode(); @@ -123,8 +122,8 @@ public class PostAclDialogTests var handler = new CountingHttpHandler(); var settings = new Settings(); var api = new TestableYavscApiClient(settings, new TokenStore(System.IO.Path.GetTempFileName()), handler); - var aclClient = new BlogAclApiClient(api, settings.ApiUrl); - var circleClient = new CircleApiClient(api, settings.ApiUrl); + var aclClient = new BlogAclApiClient(api, settings.BusinessApiUrl); + var circleClient = new CircleApiClient(api, settings.BusinessApiUrl); var services = new ServiceCollection(); services.AddSingleton(settings); @@ -191,8 +190,9 @@ public class PostAclDialogTests await Task.Delay(20); } - // Assert: one GET went out (for /circle) from LoadAsync. - Assert.Equal(1, handler.RequestCount); + // Assert: exactly two GETs went out (one to /blogacl, + // one to /circle), both from the LoadAsync call. + Assert.Equal(2, handler.RequestCount); // And the VM's idempotency gate has flipped. Assert.True(vm.Loaded); @@ -218,58 +218,7 @@ public class PostAclDialogTests await vm.LoadAsync(); // Assert: the second call short-circuited on _loaded. - Assert.Equal(1, handler.RequestCount); + Assert.Equal(2, handler.RequestCount); Assert.True(vm.Loaded); } - - [Fact] - public async Task LoadAsync_keeps_acl_from_blogpostdto_and_only_loads_circles() - { - var post = new BlogPostDto { Id = 42, Title = "ACL hydration" }; - post.AuthorizeCircle(12); - post.AuthorizeCircle(34); - - var api = new StubAclApiClient(); - var aclClient = new BlogAclApiClient(api, "http://localhost/"); - var circleClient = new CircleApiClient(api, "http://localhost/"); - var vm = new PostAclDialogViewModel(post, aclClient, circleClient); - - await vm.LoadAsync(); - - Assert.Equal(1, api.CallCount); - Assert.Equal(2, vm.AclEntries.Count); - Assert.Contains(vm.AclEntries, a => a.CircleId == 12); - Assert.Contains(vm.AclEntries, a => a.CircleId == 34); - } - - private sealed class StubAclApiClient : IYavscApiClient - { - public HttpClient Http { get; } = new(); - public int CallCount { get; private set; } - - public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) - { - CallCount++; - - if (typeof(T) == typeof(List)) - { - var circles = new List - { - new() { Id = 12, Name = "A", OwnerId = "owner", Public = false }, - new() { Id = 34, Name = "B", OwnerId = "owner", Public = false }, - }; - return Task.FromResult((T)(object)circles); - } - - return Task.FromResult(default(T)!); - } - - public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) - { - CallCount++; - return Task.CompletedTask; - } - - public ValueTask DisposeAsync() => ValueTask.CompletedTask; - } } diff --git a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj index 2c4f954d9..86a7998af 100644 --- a/src/PostIt/PostIt.Tests/PostIt.Tests.csproj +++ b/src/PostIt/PostIt.Tests/PostIt.Tests.csproj @@ -8,7 +8,7 @@ true 1.1.0.0 1.1.0.0 - 1.1.0-beta.1+183.Branch.release-1.0.8-rc8.Sha.6cff3db32ecf72c0d2d430b7002fa7816a34e070 + 1.1.0-beta.1+148.Branch.release-1.0.8-rc4.Sha.6b161b0fb509fec0ce467aa94f1a450b202af2f0 1.1.0-beta.1 diff --git a/src/PostIt/PostIt.Tests/PostItViewModelTests.cs b/src/PostIt/PostIt.Tests/PostItViewModelTests.cs index 1a867bd61..d2c5d78e9 100644 --- a/src/PostIt/PostIt.Tests/PostItViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/PostItViewModelTests.cs @@ -55,21 +55,6 @@ public class PostItViewModelTests Assert.Equal("Hello", posts[0].Title); } - [Fact] - public async Task TogglePublishCommand_uses_the_current_checked_state_without_inverting_it() - { - var api = new RecordingPublishApi(); - var blog = new BlogApiClient(api, "http://localhost/"); - var viewModel = new MainViewModel(blog); - - viewModel.SelectedPost = new BlogPostDto { Id = 42, IsPublished = false }; - - await viewModel.SetPublishStateAsync(true); - - Assert.True(api.LastPublishValue); - Assert.True(viewModel.DraftIsPublished); - } - /// Test fake that always throws if the API is invoked. private sealed class ThrowingYavscApiClient : YavscApiClient { @@ -118,34 +103,4 @@ public class PostItViewModelTests return Task.FromResult(default(T)!); } } - - private sealed class RecordingPublishApi : IYavscApiClient - { - public bool LastPublishValue { get; private set; } - public HttpClient Http { get; } = new(); - - public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) - { - if (method == HttpMethod.Put && path.Contains("/publish", StringComparison.OrdinalIgnoreCase)) - { - var publish = body?.GetType().GetProperty("publish")?.GetValue(body) is bool value && value; - LastPublishValue = publish; - } - - return Task.FromResult(default(T)!); - } - - public Task CallAsync(HttpMethod method, string path, object? body = null, CancellationToken ct = default) - { - if (method == HttpMethod.Put && path.Contains("/publish", StringComparison.OrdinalIgnoreCase)) - { - var publish = body?.GetType().GetProperty("publish")?.GetValue(body) is bool value && value; - LastPublishValue = publish; - } - - return Task.CompletedTask; - } - - public ValueTask DisposeAsync() => ValueTask.CompletedTask; - } } diff --git a/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs b/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs deleted file mode 100644 index fdf8689ab..000000000 --- a/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Headless.XUnit; -using PostIt.ViewModels; -using PostIt.ViewModels.Commands; -using PostIt.Views.Commands; -using Yavsc.Abstract.Workflow; -using Yavsc.Api.Client; - -namespace PostIt.Tests; - -public class RdvPageHeadlessTests -{ - [AvaloniaFact] - public void Suggested_address_panel_is_hidden_by_default() - { - var page = CreatePage(out _); - - var panel = page.FindControl("SuggestedAddressPanel"); - var progress = page.FindControl("SuggestedAddressProgress"); - - Assert.NotNull(panel); - Assert.NotNull(progress); - Assert.False(panel!.IsVisible); - Assert.False(progress!.IsVisible); - } - - [AvaloniaFact] - public async Task Suggested_address_panel_and_spinner_follow_viewmodel_state() - { - var page = CreatePage(out var vm); - var panel = page.FindControl("SuggestedAddressPanel")!; - var progress = page.FindControl("SuggestedAddressProgress")!; - var applyButton = page.FindControl