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 { /// /// 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. /// 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(), Description = new List(), ImageId = "res-image-section", ArticleContent = new List(), ArticleAudioIds = new List { new TranslationDTO { language = "FR", value = "res-audio-fr" }, new TranslationDTO { language = "NL", value = "res-audio-nl" }, }, ArticleContents = new List { new ContentDTO { resourceId = "res-content-1" }, new ContentDTO { resourceId = "res-content-2" }, }, }; /// /// ⚠️ Le second trou du même endroit, trouvé le 12/09. Les ressources /// partaient bien, mais pas les champs qui les référencent : /// Section.ToDTO() n'est pas virtuelle, et l'export appelait /// s.ToDTO() sur une variable de type Section. Le JSON ne portait /// donc que les champs communs — une galerie sans ses images, une carte sans ses /// points, une maquette sans son modèle. L'export est censé être « tout le /// contenu en un appel » ; il l'est depuis que SectionFactory.ToDTO fait /// le sous-type réel. /// [Theory] [InlineData(typeof(SectionMap), typeof(MapDTO))] [InlineData(typeof(SectionSlider), typeof(SliderDTO))] [InlineData(typeof(SectionVideo), typeof(VideoDTO))] [InlineData(typeof(SectionScene3D), typeof(Scene3DDTO))] [InlineData(typeof(SectionArticle), typeof(ArticleDTO))] public void Export_ProducesTheRealSubTypeDto(System.Type sectionType, System.Type expectedDto) { var section = (Section)System.Activator.CreateInstance(sectionType)!; section.Title = new List(); section.Description = new List(); var dto = ManagerService.Services.SectionFactory.ToDTO(section); Assert.IsType(expectedDto, dto); Assert.IsAssignableFrom(dto); } [Fact] public void Export_KeepsModel3DFields() { var section = new SectionScene3D { Id = "s1", Title = new List(), Description = new List(), Model3DResourceId = "res-glb", Model3DSource = "https://cdn/model.glb", Points = new List { new GeoPoint { Id = 1, Title = new List(), Description = new List(), Contents = new List(), LocalTransform = new Position3D { x = 1.5f, y = 0.2f, z = -3f } } } }; var dto = Assert.IsType(ManagerService.Services.SectionFactory.ToDTO(section)); Assert.Equal("res-glb", dto.model3DResourceId); Assert.Equal("https://cdn/model.glb", dto.model3DSource); var point = Assert.Single(dto.points); Assert.Equal(1.5f, point.localTransform.x); Assert.Equal(-3f, point.localTransform.z); } [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(), Description = new List(), IconResourceId = "res-map-icon", MapCategories = new List { new CategorieDTO { resourceDTO = new ResourceDTO { id = "res-categorie" } }, }, MapPoints = new List { new GeoPoint { Id = 1, ImageResourceId = "res-point-image", Contents = new List { 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); } } } }