diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index 140472838..cf4100eda 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -50,20 +50,9 @@ jobs: echo "đŸ—ïž Building the solution..." cd /src/_src && dotnet build --verbosity normal - name: Test - artifacts: - name: test-results - path: test-results.html run: | echo "🚀 Lancement des tests..." cd /src/_src && dotnet test \ --report-xunit \ --report-xunit-html \ --report-xunit-html-filename "test-results.html" - TEST_EXIT_CODE=$? - echo "Test exit code: $TEST_EXIT_CODE" - if [ $TEST_EXIT_CODE -ne 0 ]; then - echo "❌ Tests failed with exit code $TEST_EXIT_CODE" - exit $TEST_EXIT_CODE - else - echo "✅ All tests passed successfully." - fi diff --git a/.gitignore b/.gitignore index 32d9c064d..c1b184a87 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ DataDir/ # gitleaks scan output (local secret-scan report, not for the repo) /gitleaks-report.json +/test-report.html +/test-report.xml + -# Exclude test reports (local test report, not for the repo) -/test-reports diff --git a/.vscode/launch.json b/.vscode/launch.json index 87cf40dbb..f3411003c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,9 +22,11 @@ }, { "name": "Android Debug", - "type": "maui", - "request": "launch", - "project": "${workspaceFolder}/src/PostIt/PostIt.Android/PostIt.Android.csproj" + "type": "mono", + "preLaunchTask": "run-debug-android", + "request": "attach", + "address": "localhost", + "port": 55555 }, { "name": "Android Attach - Debug", diff --git a/.vscode/settings.json b/.vscode/settings.json index 7732a77b1..83a17ae38 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -45,6 +45,5 @@ ] } }, - "dotnet.defaultSolution": "yavsc.sln", - "axaml.selectedSolution": "/home/paul/Workspace/yavsc/yavsc.sln" + "dotnet.defaultSolution": "yavsc.sln" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0b0bb63a5..15ee3405b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,13 +4,9 @@ "tasks": [ { - "label": "build-debug-android", + "label": "run-debug-android", "command": "dotnet", - "detail": "Build the PostIt Android project in debug mode", "type": "shell", - "runOptions": { - - }, "problemMatcher": ["$msCompile"], "options": { "cwd": "${workspaceFolder}/src/PostIt/PostIt.Android", diff --git a/CHANGELOG.md b/CHANGELOG.md index 011dfe449..885b95854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,101 +1,5 @@ # Changelog -## [1.0.8-rc18] - unstable - -AccĂšs aux fichiers personnels PostIt rendu fonctionnel de bout en bout -(arbre de stockage indexĂ© par le login, pas par le `sub`), boutons -d'action des listes rĂ©parĂ©s (binding `$parent`), revendication `name` -enfin prĂ©sente dans les access tokens (branchement du `ProfileService`), -et nĂ©gociation HTTP/3 cĂŽtĂ© client PostIt. - -### Added - -* [PostIt] NĂ©gociation HTTP/3 (QUIC) pour les appels API Yavsc : - `DefaultRequestVersion = 3.0`, `DefaultVersionPolicy = - RequestVersionOrLower` sur le `HttpClient` partagĂ© de `YavscApiClient`, - avec repli h2/h1.1 oĂč QUIC n'est pas disponible (WASM fetch, Android/ - desktop sans libmsquic). -* [Tests] `MyFilesPageHeadlessTests` : monte `MyFilesPage` via - `NavigationPage` + `Window.Show()` et vĂ©rifie que chaque bouton de - ligne (TĂ©lĂ©charger / Supprimer) a sa `Command` liĂ©e, est armĂ© - (`CanExecute`) et dĂ©clenche l'appel API au clic. -* [Tests] `Remoting.PasswordToken_carries_name_claim_equal_to_login` : - mint un vrai JWT via le token endpoint IdentityServer (password grant, - `openid profile`) et asserte que l'access token porte `name`=login et - que `GetUserName()` rĂ©sout vers ce login. -* [Tests] `FileSystemApiTests.Get_fs_lists_files_stored_under_the_user_login` : - dĂ©couple `sub` et login, tĂ©lĂ©verse sous le login, et asserte que - `GET /api/v1/fs` retourne le fichier (et que le rĂ©pertoire nommĂ© d'aprĂšs - le `sub` n'existe pas). - -### Changed - -* [Yavsc.Org] Branchement du `ProfileService` custom - (`AddProfileService()` aprĂšs - `AddAspNetIdentity()`). La classe Ă©tait du code mort : - IdentityServer8 retombait sur son profile service par dĂ©faut, qui - n'Ă©met que les claims listĂ©s dans les `UserClaims` des API resources. - `name` n'en faisait jamais partie, donc tout access token Ă©tait sans - `name` et `GetUserName()` rĂ©solvait Ă  `null` sur les hosts ressources - (Blogs/Api) — cause du 500 sur l'upload MyFiles et du - `User.Identity.Name` injoignable. Le service Ă©met dĂ©sormais - `name`=`user.UserName`, `email` et les rĂŽles pour tout client dont - les `AllowedScopes` contiennent `profile`, indĂ©pendamment du caller. -* [Yavsc.Server] `ServiceExtensions` : `NameClaimType = - JwtClaimTypes.Name`, `MapInboundClaims = false` (conserver les noms - courts de claims tels qu'Ă©mis). -* [Yavsc.Blogs] `FileSystemHelpers.EnsureDestinationDirectory` utilise - `GetUserName()` (claim `name`) au lieu de `User.Identity.Name` pour - la racine de stockage par utilisateur. -* [Tests] `WebServerFixture` : `AccessTokenType = 0` (Jwt dans IS8 ; - 1 = Reference, l'inverse de Duende) ; `openid`+`profile` sur le client - de test ; rĂ©ensemencement des identity resources openid/profile dans - `ReseedAuthTestData` (un `ResetAndMigrateDatabase` vide le store - in-memory partagĂ© et les laissait absentes → `invalid_scope`). -* [Tests] `TestTokenIssuer.Issue` gagne un paramĂštre `name` optionnel - (dĂ©faut = subject) pour reflĂ©ter la forme production oĂč - `sub` (GUID) ≠ `name` (login). -* [Tests] `BillingServiceTests` : `[Collection("Yavsc Server")]` pour - sĂ©rialiser avec le build d'hĂŽte qui mute les registres statiques - `BillingService`/`Config` (race parallĂšle prĂ©-existante). - -### Fixed - -* [Yavsc.Blogs] La liste `GET /api/v1/fs` cherchait sous - `{Blog}/{sub-GUID}` (`User.GetUserId()`), alors que l'arbre de - stockage perso est indexĂ© par le login partout ailleurs - (`EnsureDestinationDirectory` Ă©crit sous `{Blog}/{name-claim}`, - le endpoint de tĂ©lĂ©chargement et les avatars utilisent - `user.UserName`). Une fois l'upload corrigĂ©, les fichiers - atterrissaient sous `{Blog}/{login}` tandis que la liste cherchait - sous `{Blog}/{GUID}` — un arbre vide — d'oĂč la page PostIt - « Mes fichiers » qui ne listait rien. `GetDir` rĂ©sout dĂ©sormais - l'utilisateur en base et liste sous `user.UserName`. - `UploadedFile.OwnerId` reste `sub`, donc `EnrichWithFileIds` - interpole toujours par `uid`. -* [PostIt] Boutons d'action des lignes de liste grisĂ©s et inactifs au - clic (TĂ©lĂ©charger / Supprimer / Joindre sur MyFiles ; download/detach - sur les listes de piĂšces jointes de `BillingQueryDetailsPage` et - `EstimateEditionPage`). Le binding `$parent[vm:XxxViewModel].Cmd` - cherchait un ancĂȘtre CONTRÔLE de type `XxxViewModel` — un viewmodel, - pas un contrĂŽle — donc rĂ©solvait Ă  `null` et le `Button` restait - dĂ©sactivĂ© (grisĂ©) et inerte au clic. CorrigĂ© en - `$parent[ContentPage].((vm:XxxViewModel)DataContext).Cmd` (syntaxe - dĂ©jĂ  en vigueur dans `CirclesPage` / `EstimateListPage`). -* [PostIt] Les messages de la barre de statut (`StatusBar`) n'Ă©taient - pas sĂ©lectionnables/copiables : le texte Ă©tait un `TextBlock` (non - sĂ©lectionnable dans Avalonia). RemplacĂ© par `SelectableTextBlock`. -* [Yavsc.Blogs] La suppression d'un fichier (`DELETE api/v1/fs/{name}`) - renvoyait un 40x alors que le fichier Ă©tait bien supprimĂ© du disque. - `DeleteUserDirOrFile` (et `DeleteUserFile`) lisaient - `FileInfo.Length` **aprĂšs** `fi.Delete()` ; or `FileInfo.Length` - lĂšve `FileNotFoundException` une fois le fichier disparu — le `catch` - du contrĂŽleur transformait cette exception en 400, d'oĂč le message - perçu « le fichier n'existe pas sur le serveur » tandis que la - suppression avait rĂ©ussi. La taille est dĂ©sormais capturĂ©e avant la - suppression. -* PostIt default settings - ## [1.0.8-rc17] - unstable Fichiers utilisateur et piĂšces jointes sur les devis/demandes (PostIt + @@ -605,8 +509,6 @@ the ACL now comes along with the article, [Unreleased]: https://forgejo.pschneider.fr/notazof/yavsc/compare/HEAD [1.0.8-rc16]: https://forgejo.pschneider.fr/notazof/yavsc/compare/1.0.8-rc15...1.0.8-rc16 -[1.0.8-rc17]: https://forgejo.pschneider.fr/notazof/yavsc/compare/1.0.8-rc16...1.0.8-rc17 -[1.0.8-rc18]: https://forgejo.pschneider.fr/notazof/yavsc/compare/1.0.8-rc17...1.0.8-rc18 [1.0.8-rc15]: https://forgejo.pschneider.fr/notazof/yavsc/compare/1.0.8-rc14...1.0.8-rc15 [1.0.8-rc1]: https://forgejo.pschneider.fr/notazof/yavsc/compare/1.0.7...1.0.8-rc1 [1.0.7]: https://forgejo.pschneider.fr/notazof/yavsc/compare/1.0.6...1.0.7 diff --git a/Makefile b/Makefile index 29fd47efa..e883088c2 100644 --- a/Makefile +++ b/Makefile @@ -21,30 +21,7 @@ src/Yavsc/bin/output/wwwroot: dotnet --project src/Yavsc.Org/Yavsc.Org.csproj publish test: - ASPNETCORE_ENVIRONMENT=Development dotnet test --report-xunit \ - --report-xunit-html \ - --results-directory "test-reports" \ - --report-xunit-html-filename "test-results.html" - -test-postit: - ASPNETCORE_ENVIRONMENT=Development dotnet test \ - --report-xunit \ - --report-xunit-html \ - --results-directory "test-reports" \ - --report-xunit-html-filename "postit-test-results.html" \ - --project src/PostIt/PostIt.Tests/PostIt.Tests.csproj - -backend-tests: Blogs-backend-test Org-backend-test Api-backend-test -Api-backend-test: -Blogs-backend-test: -Org-backend-test: -%-backend-test: - ASPNETCORE_ENVIRONMENT=Development dotnet test \ - --report-xunit \ - --report-xunit-html \ - --results-directory "test-reports" \ - --report-xunit-html-filename "$*-backend-test-results.html" \ - --project src/test/Yavsc.$*.Tests/Yavsc.$*.Tests.csproj + ASPNETCORE_ENVIRONMENT=Development dotnet test watch: dotnet watch -p:Configuration=$(CONFIG) --project src/Yavsc/Yavsc.csproj diff --git a/src/PostIt/PostIt.Android/PostIt.Android.csproj b/src/PostIt/PostIt.Android/PostIt.Android.csproj index 18bfec459..14516758d 100644 --- a/src/PostIt/PostIt.Android/PostIt.Android.csproj +++ b/src/PostIt/PostIt.Android/PostIt.Android.csproj @@ -27,14 +27,4 @@ - - - - - - - + \ No newline at end of file diff --git a/src/PostIt/PostIt.Tests/MyFilesPageHeadlessTests.cs b/src/PostIt/PostIt.Tests/MyFilesPageHeadlessTests.cs deleted file mode 100644 index 72619f3c5..000000000 --- a/src/PostIt/PostIt.Tests/MyFilesPageHeadlessTests.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System.Net.Http; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Headless.XUnit; -using Avalonia.VisualTree; -using CommunityToolkit.Mvvm.Input; -using PostIt.ViewModels; -using PostIt.Views; -using Yavsc.Api.Client; - -namespace PostIt.Tests; - -/// -/// Headless UI coverage for the "Mes fichiers" page -/// (). The user reported that once a file -/// is listed, "l'item est inerte au click, et ses boutons restent -/// grisĂ©s, dĂ©sactivĂ©s" — neither the row nor its action buttons -/// (TĂ©lĂ©charger / Supprimer) respond to a click. -/// -/// These tests mount a real under -/// the Avalonia headless platform with a stub -/// that lists one file, then walk the -/// realised visual tree to find the row's action buttons and -/// assert the two things that would silently break and reproduce -/// the "grayed / inert" symptom: -/// -/// The Command binding -/// ($parent[vm:MyFilesViewModel].DownloadFileCommand) -/// resolved to a non-null command — a null command leaves the -/// button disabled (grayed) and click-silent. -/// The command is armed -/// (CanExecute true) for the listed file — an -/// AsyncRelayCommand with -/// AllowConcurrentExecutions=false (the -/// -/// default) disables itself while running; a button stuck -/// "running" is exactly the grayed-and-inert report. -/// Executing the command actually invokes the -/// file download / delete on the API client — the row -/// "activates" on click instead of being inert. -/// -/// The OS save picker is absent under headless, so -/// FileSaveHelpers.SaveAsync throws or cancels and the -/// download command's catch sets an error status — but the -/// API download is awaited before the picker, so the call -/// is recorded regardless. We do not await the command's -/// ExecutionTask to completion, to avoid blocking on the -/// (platform-dependent) picker. -/// -public class MyFilesPageHeadlessTests -{ - private static (MyFilesPage page, MyFilesViewModel vm, StubFsApi api) CreatePage() - { - var api = new StubFsApi(); - var fsClient = new UserFilesApiClient(api, "https://blogs.example/api/v1/"); - var vm = new MyFilesViewModel(fsClient); - var page = new MyFilesPage { DataContext = vm }; - return (page, vm, api); - } - - /// - /// Mount the page the way the app does — pushed onto a - /// shown in a - /// (a realizes its content template via - /// the navigation host, cf. RdvPageHeadlessTests) — after - /// listing a single file by awaiting - /// . Populating - /// before the first layout - /// pass lets the ItemsControl realise its item templates - /// under headless. - /// - private static async Task<(MyFilesPage page, MyFilesViewModel vm, StubFsApi api)> MountAsync() - { - var (page, vm, api) = CreatePage(); - await vm.InitializeAsync(); - var nav = new NavigationPage(); - nav.PushAsync(page).GetAwaiter().GetResult(); - var window = new Window { Content = nav }; - window.Show(); - // Let bindings / the ItemsControl item templates apply. - await Task.Delay(50); - return (page, vm, api); - } - - /// - /// Find the first action button in a file row whose content - /// matches . The file rows live in an - /// ItemsControl DataTemplate (no x:Name), so we - /// walk the visual tree. - /// - private static Button? FindRowButton(MyFilesPage page, string content) - => page.GetVisualDescendants() - .OfType