manager-service/ManagerService.Tests/Controllers/SectionParcoursControllerTests.cs
Thomas Fransolet af6b5b76ef Self-service onboarding (Stripe + Resend) + Postgres v3 schema (pgvector, media, visitor questions)
75 fichiers, ~1 mois de travail depuis f222861 (17/07). Contenu :

Onboarding self-service — jamais exécuté de bout en bout
  OnboardingController, StripeWebhookController, StripeService, ResendEmailService
  + IEmailService (10 templates), EmailTemplates/, TrialLifecycleService (Hangfire),
  PasswordTokenHelper, SlugHelper, 5 DTOs. Essai 14 j, Stripe customer/Checkout/Tax,
  mot de passe oublié + invitation user, plafond IA d'essai.

Schéma Postgres v3 — passe pendant que la base est vide
  ContentEmbedding + index HNSW (vector_cosine_ops), IEmbeddingService +
  GoogleEmbeddingService (gemini-embedding-001, 768 dims), Deployment/Dockerfile.postgres
  (postgis 3.4.3 + pgvector 0.8.6, épinglé par digest — un tag mobile rejouerait le
  warning de collation glibc). Colonnes Resource : StoragePath, FileName,
  IncludeInAiKnowledge, AiIndexStatus + nouveaux ResourceType ajoutés EN FIN d'enum.

Guide IA
  Champs Guide* sur Instance + InstanceDTO, AssistantService lit la configuration client
  dans les 4 blocs de prompt (ton codé en dur retiré, règle hors-sujet ajoutée aux deux
  variantes qui n'en avaient pas), IHttpClientFactory à la place des new HttpClient().
  Table VisitorQuestion + ConversationId sur AiChatRequest.

Stats
  Rétention unifiée à 13 mois (instances ET plans), VisitEventPurgeService.

Nettoyage
  IsStepLocked / IsHiddenInitially / FactContent supprimés de GuidedStep — IsStepLocked
  rendait une étape définitivement infranchissable même après réussite.
  SectionMap allégé (-57 lignes).

Tests
  SectionParcoursControllerTests, FakeConfiguration, FakeEmailService.
  ContentEmbedding a cassé 116 tests sur 124 (EF InMemory ne connaît pas Vector) :
  l'entité est exclue quand le provider n'est pas Npgsql. Conséquence assumée —
  le vector store n'est couvert par aucun test. dotnet test 124/124.

10 migrations EF. Base locale à jour, dotnet build 0 erreur.
Rien n'est en prod : la bascule Mongo → Postgres est décrite dans DOCS/STATUS.md §1quinquies.

⚠️ appsettings.json contient les clés Stripe (test) et Resend (prod) en clair — à rotationner.
2026-08-09 22:11:48 +02:00

195 lines
7.6 KiB
C#

using ManagerService.Controllers;
using ManagerService.Data;
using ManagerService.Data.SubSection;
using ManagerService.DTOs;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace ManagerService.Tests.Controllers
{
/// <summary>
/// Couvre la persistance des règles de progression d'un parcours — les 3 booléens
/// derrière la question « Comment le visiteur progresse-t-il ? » de manager-app
/// (Libre / Dans l'ordre / Étape par étape) — et le nettoyage des champs morts.
/// </summary>
public class SectionParcoursControllerTests
{
private SectionParcoursController BuildController(MyInfoMateDbContext db)
{
var controller = new SectionParcoursController(
FakeConfiguration.Create(),
NullLogger<SectionParcoursController>.Instance,
db);
FakeUser.SetUser(controller, FakeUser.Create(Permissions.SuperAdmin));
return controller;
}
private static SectionParcours SeedSection(MyInfoMateDbContext db, string id = "sp1", bool showMap = true)
{
var section = new SectionParcours
{
Id = id,
InstanceId = "inst-test",
ConfigurationId = "cfg1",
Label = "Parcours de test",
Title = new List<TranslationDTO>(),
Description = new List<TranslationDTO>(),
ShowMap = showMap,
};
db.Sections.Add(section);
db.SaveChanges();
return section;
}
private static GuidedPathDTO MakePath(string title = "Parcours") => new GuidedPathDTO
{
instanceId = "inst-test",
title = new List<TranslationDTO> { new TranslationDTO { language = "FR", value = title } },
description = new List<TranslationDTO>(),
steps = new List<GuidedStepDTO>(),
order = 0,
};
// ── Modes de progression ─────────────────────────────────────────────
[Theory]
// Libre : le visiteur choisit ses étapes dans l'ordre qu'il veut
[InlineData(false, false, false)]
// Dans l'ordre : séquence imposée, sans obligation de réussite
[InlineData(true, false, false)]
// Étape par étape : séquence + validation, étapes futures masquées
[InlineData(true, true, true)]
public void CreateGuidedPath_PersistsProgressionFlags(bool isLinear, bool requireSuccess, bool hideNext)
{
using var db = DbContextFactory.Create();
SeedSection(db);
var dto = MakePath();
dto.isLinear = isLinear;
dto.requireSuccessToAdvance = requireSuccess;
dto.hideNextStepsUntilComplete = hideNext;
var result = BuildController(db).CreateGuidedPath("sp1", dto);
Assert.Equal(200, result.StatusCode);
var saved = db.GuidedPaths.Single();
Assert.Equal(isLinear, saved.IsLinear);
Assert.Equal(requireSuccess, saved.RequireSuccessToAdvance);
Assert.Equal(hideNext, saved.HideNextStepsUntilComplete);
}
[Fact]
public void UpdateGuidedPath_SwitchingToFreeNavigation_ClearsSequencingFlags()
{
using var db = DbContextFactory.Create();
SeedSection(db);
var created = BuildController(db).CreateGuidedPath("sp1", MakePath());
var path = Assert.IsType<GuidedPathDTO>(created.Value);
// Passage « Étape par étape » → « Libre » (ce que fait applyProgressionMode côté manager-app)
path.isLinear = false;
path.requireSuccessToAdvance = false;
path.hideNextStepsUntilComplete = false;
var result = BuildController(db).UpdateGuidedPath(path);
Assert.Equal(200, result.StatusCode);
var saved = db.GuidedPaths.Single();
Assert.False(saved.IsLinear);
Assert.False(saved.RequireSuccessToAdvance);
Assert.False(saved.HideNextStepsUntilComplete);
}
[Fact]
public void CreateGuidedPath_KeepsGameModeMessages()
{
using var db = DbContextFactory.Create();
SeedSection(db);
var dto = MakePath();
dto.isGameMode = true;
dto.gameMessageDebut = new List<TranslationAndResourceDTO>
{
new TranslationAndResourceDTO { language = "FR", value = "Bienvenue dans l'escape" }
};
dto.gameMessageFin = new List<TranslationAndResourceDTO>
{
new TranslationAndResourceDTO { language = "FR", value = "Bravo !" }
};
BuildController(db).CreateGuidedPath("sp1", dto);
var saved = db.GuidedPaths.Single();
Assert.True(saved.IsGameMode);
Assert.Equal("Bienvenue dans l'escape", saved.GameMessageDebut.Single().value);
Assert.Equal("Bravo !", saved.GameMessageFin.Single().value);
}
[Fact]
public void CreateGuidedPath_UnknownSection_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).CreateGuidedPath("does-not-exist", MakePath());
Assert.Equal(404, result.StatusCode);
}
// ── Étapes ───────────────────────────────────────────────────────────
[Fact]
public void CreateGuidedStep_PersistsGeoTriggerAndTimer()
{
using var db = DbContextFactory.Create();
SeedSection(db);
var created = BuildController(db).CreateGuidedPath("sp1", MakePath());
var pathId = Assert.IsType<GuidedPathDTO>(created.Value).id;
var step = new GuidedStepDTO
{
title = new List<TranslationDTO>(),
description = new List<TranslationDTO>(),
isGeoTriggered = true,
zoneRadiusMeters = 30,
isStepTimer = true,
timerSeconds = 120,
order = 0,
};
var result = BuildController(db).CreateGuidedStep(pathId, step);
Assert.Equal(200, result.StatusCode);
var saved = db.GuidedSteps.Single();
Assert.True(saved.IsGeoTriggered);
Assert.Equal(30, saved.ZoneRadiusMeters);
Assert.True(saved.IsStepTimer);
Assert.Equal(120, saved.TimerSeconds);
}
[Fact]
public void GetAllGuidedPathFromSection_ReturnsPathsOrderedByOrder()
{
using var db = DbContextFactory.Create();
SeedSection(db);
var controller = BuildController(db);
var second = MakePath("Second"); second.order = 1;
var first = MakePath("Premier"); first.order = 0;
controller.CreateGuidedPath("sp1", second);
controller.CreateGuidedPath("sp1", first);
var result = BuildController(db).GetAllGuidedPathFromSection("sp1");
Assert.Equal(200, result.StatusCode);
var paths = Assert.IsAssignableFrom<IEnumerable<GuidedPathDTO>>(result.Value).ToList();
Assert.Equal(2, paths.Count);
Assert.Equal(new int?[] { 0, 1 }, paths.Select(p => p.order).ToArray());
}
}
}