diff --git a/.forgejo/workflows/buildAndTest.yml b/.forgejo/workflows/buildAndTest.yml index ea58d2fef..2f14e449e 100644 --- a/.forgejo/workflows/buildAndTest.yml +++ b/.forgejo/workflows/buildAndTest.yml @@ -48,4 +48,4 @@ jobs: --verbosity normal \ --filter="Category!=Platform-Android" \ --logger "xunit;LogFileName=test-results.xml" \ - && echo "✅ Success !" || echo "❌ Fail ($?)!" + && echo "✅ Success !" || { echo "❌ Fail ($?)!"; exit 1; } diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index a72f92bd6..911fc831d 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -175,6 +175,12 @@ 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: | @@ -200,6 +206,7 @@ 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/.gitignore b/.gitignore index b7813f604..056cf69d2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ appsettings-*.*.json generated/ *.tmp +tmp/ DataDir/ *.tests.trx diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c7c92595..111c1491a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,58 @@ # Changelog +## [1.0.8-rc14] - 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`. + +## [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 diff --git a/src/PostIt/Directory.Packages.props b/src/PostIt/Directory.Packages.props index 4dd4b2889..2f27340e9 100644 --- a/src/PostIt/Directory.Packages.props +++ b/src/PostIt/Directory.Packages.props @@ -16,6 +16,8 @@ + + diff --git a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs index c7b5b926b..17fcc968c 100644 --- a/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/BillingCommandPageViewModelTests.cs @@ -115,6 +115,89 @@ public class BillingCommandPageViewModelTests } } + [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() { diff --git a/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs b/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs index a7f2b0844..d3d1d5694 100644 --- a/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs +++ b/src/PostIt/PostIt.Tests/BillingQueriesPageViewModelTests.cs @@ -45,10 +45,8 @@ public class BillingQueriesPageViewModelTests 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.False(vm.CanOpenDetails); - vm.SelectedQuery = vm.Queries[0]; - Assert.False(vm.OpenSelectedQueryCommand.CanExecute(null)); + Assert.True(vm.Queries.Count > 0); } private sealed class StubBillingApi : IYavscApiClient diff --git a/src/PostIt/PostIt.Tests/NominatimReverseGeocodingServiceTests.cs b/src/PostIt/PostIt.Tests/NominatimReverseGeocodingServiceTests.cs new file mode 100644 index 000000000..4eceb7a2f --- /dev/null +++ b/src/PostIt/PostIt.Tests/NominatimReverseGeocodingServiceTests.cs @@ -0,0 +1,72 @@ +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/RdvPageHeadlessTests.cs b/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs new file mode 100644 index 000000000..fdf8689ab --- /dev/null +++ b/src/PostIt/PostIt.Tests/RdvPageHeadlessTests.cs @@ -0,0 +1,79 @@ +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