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.
188 lines
7.6 KiB
C#
188 lines
7.6 KiB
C#
using ManagerService.Controllers;
|
|
using ManagerService.Data;
|
|
using ManagerService.DTOs;
|
|
using ManagerService.Tests.Infrastructure;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Xunit;
|
|
|
|
namespace ManagerService.Tests.Controllers
|
|
{
|
|
public class StatsControllerTests
|
|
{
|
|
// GetSummary vérifie les droits de l'appelant (IsSuperAdmin / instance du token) :
|
|
// sans utilisateur, User est null et le contrôleur renvoie un 500.
|
|
private StatsController BuildController(MyInfoMateDbContext db,
|
|
string callerRole = Permissions.SuperAdmin, string callerInstanceId = "i1")
|
|
{
|
|
var controller = new StatsController(db);
|
|
FakeUser.SetUser(controller, FakeUser.Create(callerRole, callerInstanceId));
|
|
return controller;
|
|
}
|
|
|
|
// ── TRACK EVENT ──────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void TrackEvent_ValidDto_PersistsAndReturns204()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
|
|
var result = BuildController(db).TrackEvent(new VisitEventDTO
|
|
{
|
|
instanceId = "i1",
|
|
sessionId = "s1",
|
|
eventType = "SectionView",
|
|
appType = "Mobile"
|
|
});
|
|
|
|
Assert.IsType<NoContentResult>(result);
|
|
Assert.Equal(1, db.VisitEvents.Count());
|
|
}
|
|
|
|
[Fact]
|
|
public void TrackEvent_MissingInstanceId_Returns400()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
|
|
var result = BuildController(db).TrackEvent(new VisitEventDTO
|
|
{
|
|
instanceId = "",
|
|
eventType = "SectionView",
|
|
appType = "Mobile"
|
|
});
|
|
|
|
Assert.IsType<BadRequestObjectResult>(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void TrackEvent_UnknownEventType_Returns400()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
|
|
var result = BuildController(db).TrackEvent(new VisitEventDTO
|
|
{
|
|
instanceId = "i1",
|
|
eventType = "InvalidType",
|
|
appType = "Mobile"
|
|
});
|
|
|
|
Assert.IsType<BadRequestObjectResult>(result);
|
|
}
|
|
|
|
// ── GET SUMMARY ──────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void GetSummary_MissingInstanceId_Returns400()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
|
|
var result = BuildController(db).GetSummary("", null, null, null);
|
|
|
|
Assert.IsType<BadRequestObjectResult>(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_FiltersToInstance()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.VisitEvents.AddRange(
|
|
new VisitEvent { Id = "e1", InstanceId = "i1", SessionId = "s1", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow },
|
|
new VisitEvent { Id = "e2", InstanceId = "other", SessionId = "s2", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow }
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var result = BuildController(db).GetSummary("i1", null, null, null);
|
|
|
|
var ok = Assert.IsType<OkObjectResult>(result);
|
|
var summary = Assert.IsType<StatsSummaryDTO>(ok.Value);
|
|
Assert.Equal(1, summary.TotalSessions);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_CountsDistinctSessions()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.VisitEvents.AddRange(
|
|
new VisitEvent { Id = "e1", InstanceId = "i1", SessionId = "session-A", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow },
|
|
new VisitEvent { Id = "e2", InstanceId = "i1", SessionId = "session-A", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow },
|
|
new VisitEvent { Id = "e3", InstanceId = "i1", SessionId = "session-B", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow }
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var result = BuildController(db).GetSummary("i1", null, null, null);
|
|
|
|
var ok = Assert.IsType<OkObjectResult>(result);
|
|
var summary = Assert.IsType<StatsSummaryDTO>(ok.Value);
|
|
Assert.Equal(2, summary.TotalSessions);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_AppliesDateRange()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
var inRange = DateTime.UtcNow.AddDays(-5);
|
|
var outOfRange = DateTime.UtcNow.AddDays(-40);
|
|
|
|
db.VisitEvents.AddRange(
|
|
new VisitEvent { Id = "e1", InstanceId = "i1", SessionId = "s1", EventType = VisitEventType.SectionView, Timestamp = inRange },
|
|
new VisitEvent { Id = "e2", InstanceId = "i1", SessionId = "s2", EventType = VisitEventType.SectionView, Timestamp = outOfRange }
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var from = DateTime.UtcNow.AddDays(-10);
|
|
var to = DateTime.UtcNow;
|
|
|
|
var result = BuildController(db).GetSummary("i1", from, to, null);
|
|
|
|
var ok = Assert.IsType<OkObjectResult>(result);
|
|
var summary = Assert.IsType<StatsSummaryDTO>(ok.Value);
|
|
Assert.Equal(1, summary.TotalSessions);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_DefaultsTo30Days()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
// Event dans les 30 jours
|
|
db.VisitEvents.Add(new VisitEvent
|
|
{
|
|
Id = "e1", InstanceId = "i1", SessionId = "s1",
|
|
EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow.AddDays(-5)
|
|
});
|
|
// Event au-delà des 30 jours
|
|
db.VisitEvents.Add(new VisitEvent
|
|
{
|
|
Id = "e2", InstanceId = "i1", SessionId = "s2",
|
|
EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow.AddDays(-35)
|
|
});
|
|
db.SaveChanges();
|
|
|
|
// Sans dates → filtre 30 derniers jours
|
|
var result = BuildController(db).GetSummary("i1", null, null, null);
|
|
|
|
var ok = Assert.IsType<OkObjectResult>(result);
|
|
var summary = Assert.IsType<StatsSummaryDTO>(ok.Value);
|
|
Assert.Equal(1, summary.TotalSessions);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_TopSections_AggregatesByViewCount()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.VisitEvents.AddRange(
|
|
new VisitEvent { Id = "e1", InstanceId = "i1", SessionId = "s1", EventType = VisitEventType.SectionView, SectionId = "sect-a", Timestamp = DateTime.UtcNow },
|
|
new VisitEvent { Id = "e2", InstanceId = "i1", SessionId = "s2", EventType = VisitEventType.SectionView, SectionId = "sect-a", Timestamp = DateTime.UtcNow },
|
|
new VisitEvent { Id = "e3", InstanceId = "i1", SessionId = "s3", EventType = VisitEventType.SectionView, SectionId = "sect-b", Timestamp = DateTime.UtcNow }
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var result = BuildController(db).GetSummary("i1", null, null, null);
|
|
|
|
var ok = Assert.IsType<OkObjectResult>(result);
|
|
var summary = Assert.IsType<StatsSummaryDTO>(ok.Value);
|
|
Assert.Equal(2, summary.TopSections.Count);
|
|
Assert.Equal("sect-a", summary.TopSections.First().SectionId);
|
|
Assert.Equal(2, summary.TopSections.First().Views);
|
|
}
|
|
}
|
|
}
|