Le switch de collecte des ressources était commenté : 157 lignes mortes dans Export, 126 dans Import. Une visite téléchargée n'embarquait donc que l'image de la configuration, celle du loader et l'image de chaque section — ni contenus d'articles, ni audios, ni icônes de carte, ni images de quiz. Export matérialise désormais les entités, pas seulement leurs DTO, et appelle GetReferencedResourceIds(language) : la méthode existait déjà sur les 13 sous-types et n'attendait que d'être appelée. Elle filtre aussi les audios par langue, ce que le switch ne faisait pas. Import cesse de redécouvrir les ressources section par section : l'export embarquant tout ce qui est référencé, une passe unique sur la charge suffit. createResource étant idempotente, les quelques appels ciblés qui subsistent sont sans effet. 4 tests, dont un qui vérifie par réflexion que les 13 sous-types implémentent la collecte : le trou venait d'un switch où un type oublié passait dans le default sans bruit, et c'est ce qu'il ne faut plus pouvoir refaire. dotnet test 148/148. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
127 lines
5.0 KiB
C#
127 lines
5.0 KiB
C#
using Manager.DTOs;
|
|
using ManagerService.Data;
|
|
using ManagerService.Data.SubSection;
|
|
using ManagerService.DTOs;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Xunit;
|
|
|
|
namespace ManagerService.Tests.Controllers
|
|
{
|
|
/// <summary>
|
|
/// L'export d'une configuration doit embarquer **toutes** les ressources que ses
|
|
/// sections référencent — c'est ce qui rend une visite lisible hors ligne.
|
|
///
|
|
/// Le `switch` qui les collectait était commenté depuis un moment : seules l'image
|
|
/// de la configuration, celle du loader et l'image de chaque section partaient, donc
|
|
/// une visite téléchargée n'avait ni contenus d'articles ni audios. Ces tests portent
|
|
/// sur `GetReferencedResourceIds`, à qui la collecte a été confiée : c'est elle qui
|
|
/// garantit qu'un nouveau type de section n'ouvrira pas le même trou en silence.
|
|
/// </summary>
|
|
public class ConfigurationExportTests
|
|
{
|
|
private static SectionArticle ArticleWithMedia() => new SectionArticle
|
|
{
|
|
Id = "sec-article",
|
|
ConfigurationId = "cfg-1",
|
|
InstanceId = "inst-1",
|
|
Label = "Article",
|
|
Type = SectionType.Article,
|
|
Title = new List<TranslationDTO>(),
|
|
Description = new List<TranslationDTO>(),
|
|
ImageId = "res-image-section",
|
|
ArticleContent = new List<TranslationDTO>(),
|
|
ArticleAudioIds = new List<TranslationDTO>
|
|
{
|
|
new TranslationDTO { language = "FR", value = "res-audio-fr" },
|
|
new TranslationDTO { language = "NL", value = "res-audio-nl" },
|
|
},
|
|
ArticleContents = new List<ContentDTO>
|
|
{
|
|
new ContentDTO { resourceId = "res-content-1" },
|
|
new ContentDTO { resourceId = "res-content-2" },
|
|
},
|
|
};
|
|
|
|
[Fact]
|
|
public void Article_ExposesItsContentAndAudioResources()
|
|
{
|
|
var ids = ArticleWithMedia().GetReferencedResourceIds().ToList();
|
|
|
|
// Sans ces trois familles, la visite hors ligne s'ouvre sans ses médias.
|
|
Assert.Contains("res-image-section", ids);
|
|
Assert.Contains("res-content-1", ids);
|
|
Assert.Contains("res-content-2", ids);
|
|
Assert.Contains("res-audio-fr", ids);
|
|
}
|
|
|
|
[Fact]
|
|
public void Article_FiltersAudioByLanguage_WhenOneIsAsked()
|
|
{
|
|
var ids = ArticleWithMedia().GetReferencedResourceIds("FR").ToList();
|
|
|
|
Assert.Contains("res-audio-fr", ids);
|
|
// Télécharger une visite en français ne doit pas embarquer les audios NL :
|
|
// c'est du poids sur le device du visiteur, pas du contenu.
|
|
Assert.DoesNotContain("res-audio-nl", ids);
|
|
}
|
|
|
|
[Fact]
|
|
public void Map_ExposesIconCategoriesAndPointResources()
|
|
{
|
|
var map = new SectionMap
|
|
{
|
|
Id = "sec-map",
|
|
ConfigurationId = "cfg-1",
|
|
InstanceId = "inst-1",
|
|
Label = "Map",
|
|
Type = SectionType.Map,
|
|
Title = new List<TranslationDTO>(),
|
|
Description = new List<TranslationDTO>(),
|
|
IconResourceId = "res-map-icon",
|
|
MapCategories = new List<CategorieDTO>
|
|
{
|
|
new CategorieDTO { resourceDTO = new ResourceDTO { id = "res-categorie" } },
|
|
},
|
|
MapPoints = new List<GeoPoint>
|
|
{
|
|
new GeoPoint
|
|
{
|
|
Id = 1,
|
|
ImageResourceId = "res-point-image",
|
|
Contents = new List<ContentDTO> { new ContentDTO { resourceId = "res-point-content" } },
|
|
},
|
|
},
|
|
};
|
|
|
|
var ids = map.GetReferencedResourceIds().ToList();
|
|
|
|
Assert.Contains("res-map-icon", ids);
|
|
Assert.Contains("res-categorie", ids);
|
|
Assert.Contains("res-point-image", ids);
|
|
Assert.Contains("res-point-content", ids);
|
|
}
|
|
|
|
[Fact]
|
|
public void EverySectionType_ImplementsTheCollection()
|
|
{
|
|
// Le trou d'origine venait d'un `switch` : un type oublié passait dans le
|
|
// `default` sans bruit. La méthode étant abstraite sur Section, le compilateur
|
|
// impose désormais l'implémentation — ce test le constate plutôt qu'il ne
|
|
// l'espère, et échouera si quelqu'un réintroduit une collecte par switch.
|
|
var sectionType = typeof(Section);
|
|
var subTypes = sectionType.Assembly.GetTypes()
|
|
.Where(t => t.IsSubclassOf(sectionType) && !t.IsAbstract)
|
|
.ToList();
|
|
|
|
Assert.NotEmpty(subTypes);
|
|
foreach (var t in subTypes)
|
|
{
|
|
var method = t.GetMethod(nameof(Section.GetReferencedResourceIds));
|
|
Assert.NotNull(method);
|
|
Assert.Equal(t, method.DeclaringType);
|
|
}
|
|
}
|
|
}
|
|
}
|