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.
195 lines
7.6 KiB
C#
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());
|
|
}
|
|
}
|
|
}
|