eventsQuery.ToList() ramenait la fenetre entiere, toutes colonnes comprises,
pour n'en faire que des comptages. La retention est passee de 30 jours a 13
mois (StatsRetentionDays = 395) : la methode charge donc desormais treize
fois ce pour quoi elle avait ete ecrite, et ca grossit tout seul.
Ce qui part en SQL : sessions distinctes, sommes de duree par session,
duree moyenne par section, top sections, visites par jour, distributions
par session, top articles, total des scans QR.
Ce qui ne peut pas y aller : POI, agenda, quiz, jeux, menus et validite des
QR se regroupent sur du JSON dans Metadata. On ne remonte plus que les deux
colonnes utiles, et seulement pour le type d'evenement concerne (MetadataOf).
Deux effets de bord voulus :
- les stats avancees ne sont plus calculees puis effacees pour les plans qui
n'y ont pas droit -- les six requetes ne partent pas ;
- les titres de section ne sont resolus que pour les ids reellement affiches
(top 10 + quiz), plus pour tous les ids vus dans la fenetre.
Un changement de semantique, delibere : « la langue de la session » etait
prise sur le premier evenement rendu par la base, dans un ordre indefini.
C'est desormais le plus ancien horodatage. Meme chose pour AppType.
Le reste est preserve a l'identique, y compris les cas tordus : un scan QR
sans metadata compte dans le total sans etre ni valide ni invalide, une
metadata illisible compte la completion de quiz a zero.
11 tests ajoutes -- la branche « stats avancees » n'etait couverte par
AUCUN test : les cas existants ne seedent pas d'Instance, donc
hasAdvancedStats etait toujours faux et la moitie de la methode n'etait
jamais executee. dotnet test 186/186.
⚠️ Ces tests tournent sur le provider InMemory, qui evalue tout cote client :
ils tiennent la semantique, pas la traduction SQL. La verification sur un
vrai Postgres arrive avec les tests Testcontainers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
466 lines
20 KiB
C#
466 lines
20 KiB
C#
using Manager.DTOs;
|
|
using ManagerService.Controllers;
|
|
using ManagerService.Data;
|
|
using ManagerService.DTOs;
|
|
using ManagerService.Tests.Infrastructure;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
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);
|
|
}
|
|
|
|
// ── STATS AVANCÉES ───────────────────────────────────────────────────
|
|
//
|
|
// Sans Instance en base, hasAdvancedStats est faux et toute cette branche est
|
|
// sautée : c'est le cas des tests ci-dessus. Ceux qui suivent la seedent, sans
|
|
// quoi la moitié de GetSummary ne serait jamais exécutée par la suite.
|
|
|
|
private static void SeedAdvancedInstance(MyInfoMateDbContext db)
|
|
{
|
|
db.Instances.Add(new Instance
|
|
{
|
|
Id = "i1",
|
|
Name = "Instance de test",
|
|
HasStats = true,
|
|
HasAdvancedStats = true,
|
|
StatsHistoryDays = MyInfoMateDbContext.StatsRetentionDays
|
|
});
|
|
db.SaveChanges();
|
|
}
|
|
|
|
private static VisitEvent Event(string id, VisitEventType type, string sessionId = "s1",
|
|
string sectionId = null, string metadata = null, string language = null,
|
|
AppType appType = AppType.Mobile, int? durationSeconds = null, DateTime? timestamp = null) =>
|
|
new VisitEvent
|
|
{
|
|
Id = id,
|
|
InstanceId = "i1",
|
|
SessionId = sessionId,
|
|
EventType = type,
|
|
SectionId = sectionId,
|
|
Metadata = metadata,
|
|
Language = language,
|
|
AppType = appType,
|
|
DurationSeconds = durationSeconds,
|
|
Timestamp = timestamp ?? DateTime.UtcNow
|
|
};
|
|
|
|
private static StatsSummaryDTO Summarize(MyInfoMateDbContext db, StatsController controller = null)
|
|
{
|
|
var result = (controller ?? BuildControllerStatic(db)).GetSummary("i1", null, null, null);
|
|
var ok = Assert.IsType<OkObjectResult>(result);
|
|
return Assert.IsType<StatsSummaryDTO>(ok.Value);
|
|
}
|
|
|
|
private static StatsController BuildControllerStatic(MyInfoMateDbContext db)
|
|
{
|
|
var controller = new StatsController(db);
|
|
FakeUser.SetUser(controller, FakeUser.Create(Permissions.SuperAdmin, "i1"));
|
|
return controller;
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_WithoutAdvancedStats_SkipsAdvancedBlocks()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.Instances.Add(new Instance { Id = "i1", Name = "Basique", HasStats = true, HasAdvancedStats = false });
|
|
db.VisitEvents.AddRange(
|
|
Event("e1", VisitEventType.SectionView, sectionId: "sect-a", language: "fr"),
|
|
Event("e2", VisitEventType.QrScan, metadata: "{\"valid\":true}"),
|
|
Event("e3", VisitEventType.ArticleRead, sectionId: "sect-a")
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var summary = Summarize(db);
|
|
|
|
Assert.Equal(1, summary.TotalSessions);
|
|
Assert.Empty(summary.LanguageDistribution);
|
|
Assert.Empty(summary.TopArticles);
|
|
Assert.Equal(0, summary.QrScans.TotalScans);
|
|
// Les stats de base, elles, restent servies
|
|
Assert.Single(summary.TopSections);
|
|
Assert.NotEmpty(summary.AppTypeDistribution);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_AvgVisitDuration_SumsPerSessionThenAverages()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedAdvancedInstance(db);
|
|
db.VisitEvents.AddRange(
|
|
Event("e1", VisitEventType.SectionLeave, "s1", "sect-a", durationSeconds: 10),
|
|
Event("e2", VisitEventType.SectionLeave, "s1", "sect-b", durationSeconds: 30),
|
|
Event("e3", VisitEventType.SectionLeave, "s2", "sect-a", durationSeconds: 20)
|
|
);
|
|
db.SaveChanges();
|
|
|
|
// s1 = 40, s2 = 20 → moyenne 30. Une moyenne sur les événements donnerait 20.
|
|
Assert.Equal(30, Summarize(db).AvgVisitDurationSeconds);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_TopSections_CarryAvgDurationAndTitle()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedAdvancedInstance(db);
|
|
var section = TestSection.Article("sect-a", "i1", "A", "c1");
|
|
section.Title = new List<TranslationDTO>
|
|
{
|
|
new TranslationDTO { language = "en", value = "Cellar" },
|
|
new TranslationDTO { language = "fr", value = "La cave" }
|
|
};
|
|
db.Sections.Add(section);
|
|
db.VisitEvents.AddRange(
|
|
Event("e1", VisitEventType.SectionView, "s1", "sect-a"),
|
|
Event("e2", VisitEventType.SectionLeave, "s1", "sect-a", durationSeconds: 10),
|
|
Event("e3", VisitEventType.SectionLeave, "s2", "sect-a", durationSeconds: 20)
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var stat = Assert.Single(Summarize(db).TopSections);
|
|
Assert.Equal("La cave", stat.SectionTitle); // FR privilégié
|
|
Assert.Equal(1, stat.Views);
|
|
Assert.Equal(15, stat.AvgDurationSeconds);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_VisitsByDay_GroupsByCalendarDayAndChannel()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedAdvancedInstance(db);
|
|
var day = DateTime.UtcNow.AddDays(-3);
|
|
db.VisitEvents.AddRange(
|
|
Event("e1", VisitEventType.SectionView, "s1", timestamp: day, appType: AppType.Mobile),
|
|
Event("e2", VisitEventType.SectionView, "s2", timestamp: day.AddHours(2), appType: AppType.Tablet),
|
|
Event("e3", VisitEventType.SectionView, "s3", timestamp: day.AddDays(1), appType: AppType.Mobile)
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var days = Summarize(db).VisitsByDay;
|
|
|
|
Assert.Equal(2, days.Count);
|
|
Assert.Equal(day.ToString("yyyy-MM-dd"), days[0].Date);
|
|
Assert.Equal(2, days[0].Total);
|
|
Assert.Equal(1, days[0].Mobile);
|
|
Assert.Equal(1, days[0].Tablet);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_LanguageDistribution_CountsOneEntryPerSession()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedAdvancedInstance(db);
|
|
var start = DateTime.UtcNow.AddHours(-2);
|
|
db.VisitEvents.AddRange(
|
|
Event("e1", VisitEventType.SectionView, "s1", language: "fr", timestamp: start),
|
|
Event("e2", VisitEventType.SectionView, "s1", language: "fr", timestamp: start.AddMinutes(1)),
|
|
// La session bascule en cours de route : c'est sa première langue qui compte
|
|
Event("e3", VisitEventType.SectionView, "s1", language: "nl", timestamp: start.AddMinutes(2)),
|
|
Event("e4", VisitEventType.SectionView, "s2", language: "nl", timestamp: start)
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var languages = Summarize(db).LanguageDistribution;
|
|
|
|
Assert.Equal(1, languages["fr"]);
|
|
Assert.Equal(1, languages["nl"]);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_AppTypeDistribution_CountsOneEntryPerSession()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedAdvancedInstance(db);
|
|
db.VisitEvents.AddRange(
|
|
Event("e1", VisitEventType.SectionView, "s1", appType: AppType.Tablet),
|
|
Event("e2", VisitEventType.SectionView, "s1", appType: AppType.Tablet),
|
|
Event("e3", VisitEventType.SectionView, "s2", appType: AppType.Mobile)
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var appTypes = Summarize(db).AppTypeDistribution;
|
|
|
|
Assert.Equal(1, appTypes["Tablet"]);
|
|
Assert.Equal(1, appTypes["Mobile"]);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_MetadataStats_AreAggregated()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedAdvancedInstance(db);
|
|
db.VisitEvents.AddRange(
|
|
Event("p1", VisitEventType.MapPoiTap, sectionId: "sect-map", metadata: "{\"geoPointId\":7,\"geoPointTitle\":\"Ruche\"}"),
|
|
Event("p2", VisitEventType.MapPoiTap, sectionId: "sect-map", metadata: "{\"geoPointId\":7,\"geoPointTitle\":\"Ruche\"}"),
|
|
Event("a1", VisitEventType.AgendaEventTap, metadata: "{\"eventId\":\"ev1\",\"eventTitle\":\"Concert\"}"),
|
|
Event("g1", VisitEventType.GameComplete, metadata: "{\"gameType\":\"Puzzle\",\"durationSeconds\":60}"),
|
|
Event("g2", VisitEventType.GameComplete, metadata: "{\"gameType\":\"Puzzle\",\"durationSeconds\":40}"),
|
|
Event("m1", VisitEventType.MenuItemTap, metadata: "{\"targetSectionId\":\"sect-a\",\"menuItemTitle\":\"Accueil\"}"),
|
|
Event("r1", VisitEventType.ArticleRead, sectionId: "sect-a")
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var summary = Summarize(db);
|
|
|
|
var poi = Assert.Single(summary.TopPois);
|
|
Assert.Equal(7, poi.GeoPointId);
|
|
Assert.Equal(2, poi.Taps);
|
|
Assert.Equal("sect-map", poi.SectionId);
|
|
|
|
Assert.Equal("Concert", Assert.Single(summary.TopAgendaEvents).EventTitle);
|
|
var game = Assert.Single(summary.GameStats);
|
|
Assert.Equal(2, game.Completions);
|
|
Assert.Equal(50, game.AvgDurationSeconds);
|
|
Assert.Equal("Accueil", Assert.Single(summary.TopMenuItems).MenuItemTitle);
|
|
Assert.Equal(1, Assert.Single(summary.TopArticles).Reads);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_QuizStats_AverageScorePerSection()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedAdvancedInstance(db);
|
|
db.VisitEvents.AddRange(
|
|
Event("q1", VisitEventType.QuizComplete, "s1", "sect-quiz", "{\"score\":4,\"totalQuestions\":5}"),
|
|
Event("q2", VisitEventType.QuizComplete, "s2", "sect-quiz", "{\"score\":2,\"totalQuestions\":5}"),
|
|
// Metadata illisible : compté, à zéro, comme avant
|
|
Event("q3", VisitEventType.QuizComplete, "s3", "sect-quiz", "pas du json")
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var quiz = Assert.Single(Summarize(db).QuizStats);
|
|
Assert.Equal("sect-quiz", quiz.SectionId);
|
|
Assert.Equal(3, quiz.Completions);
|
|
Assert.Equal(2.0, quiz.AvgScore);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_QrScans_CountsScansWithoutMetadataInTotalOnly()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedAdvancedInstance(db);
|
|
db.VisitEvents.AddRange(
|
|
Event("qr1", VisitEventType.QrScan, metadata: "{\"valid\":true}"),
|
|
Event("qr2", VisitEventType.QrScan, metadata: "{\"valid\":false}"),
|
|
Event("qr3", VisitEventType.QrScan) // sans metadata : ni valide ni invalide
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var qr = Summarize(db).QrScans;
|
|
|
|
Assert.Equal(3, qr.TotalScans);
|
|
Assert.Equal(1, qr.ValidScans);
|
|
Assert.Equal(1, qr.InvalidScans);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_HasStatsDisabled_ReturnsEmptySummary()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.Instances.Add(new Instance { Id = "i1", Name = "Sans stats", HasStats = false });
|
|
db.VisitEvents.Add(Event("e1", VisitEventType.SectionView, sectionId: "sect-a"));
|
|
db.SaveChanges();
|
|
|
|
Assert.Equal(0, Summarize(db).TotalSessions);
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSummary_RetentionCapsTheRequestedRange()
|
|
{
|
|
// Le plan borne l'historique : une plage plus large que la rétention est
|
|
// ramenée à la rétention, pas honorée.
|
|
using var db = DbContextFactory.Create();
|
|
db.Instances.Add(new Instance { Id = "i1", Name = "30 jours", HasStats = true, StatsHistoryDays = 30 });
|
|
db.VisitEvents.AddRange(
|
|
Event("e1", VisitEventType.SectionView, "s1", timestamp: DateTime.UtcNow.AddDays(-10)),
|
|
Event("e2", VisitEventType.SectionView, "s2", timestamp: DateTime.UtcNow.AddDays(-90))
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var result = BuildControllerStatic(db).GetSummary("i1", DateTime.UtcNow.AddDays(-365), DateTime.UtcNow, null);
|
|
var summary = Assert.IsType<StatsSummaryDTO>(Assert.IsType<OkObjectResult>(result).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);
|
|
}
|
|
}
|
|
}
|