diff --git a/ManagerService.Tests/Controllers/ConfigurationExportTests.cs b/ManagerService.Tests/Controllers/ConfigurationExportTests.cs
new file mode 100644
index 0000000..1961e86
--- /dev/null
+++ b/ManagerService.Tests/Controllers/ConfigurationExportTests.cs
@@ -0,0 +1,126 @@
+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" },
+ },
+ };
+
+ [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);
+ }
+ }
+ }
+}
diff --git a/ManagerService/Controllers/ConfigurationController.cs b/ManagerService/Controllers/ConfigurationController.cs
index 164420e..7fc4f98 100644
--- a/ManagerService/Controllers/ConfigurationController.cs
+++ b/ManagerService/Controllers/ConfigurationController.cs
@@ -398,7 +398,10 @@ namespace ManagerService.Controllers
if (configuration == null)
throw new KeyNotFoundException("Configuration does not exist");
- List sectionDTOs = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == configuration.Id).Select(s => s.ToDTO()).ToList();
+ // Les entités, pas seulement leurs DTO : la collecte des ressources passe
+ // par GetReferencedResourceIds, qui vit sur le sous-type.
+ List sections = _myInfoMateDbContext.Sections.Where(s => s.ConfigurationId == configuration.Id).ToList();
+ List sectionDTOs = sections.Select(s => s.ToDTO()).ToList();
List resourceDTOs = new List();
if (configuration.ImageId != null)
@@ -411,170 +414,17 @@ namespace ManagerService.Controllers
addResourceToList(resourceDTOs, configuration.LoaderImageId);
}
- foreach (var section in sectionDTOs)
+ foreach (var section in sections)
{
- if (section.imageId != null) {
- addResourceToList(resourceDTOs, section.imageId);
- }
-
- /*switch (section.type) {
- case SectionType.Map:
- MapDTO mapDTO = JsonConvert.DeserializeObject(section.data);
- if (mapDTO.iconResourceId != null)
- {
- addResourceToList(resourceDTOs, mapDTO.iconResourceId);
- }
-
- foreach (var point in mapDTO.points) {
- if (point.imageResourceId != null)
- {
- addResourceToList(resourceDTOs, point.imageResourceId);
- }
- foreach (var content in point.contents) {
- if (content.resourceId != null)
- {
- addResourceToList(resourceDTOs, content.resourceId);
- }
- }
- }
-
- if (mapDTO.categories != null && mapDTO.categories.Count > 0)
- {
- foreach (var categorie in mapDTO.categories)
- {
- if (categorie.iconResourceId != null)
- {
- addResourceToList(resourceDTOs, categorie.iconResourceId);
- }
- }
- }
-
- break;
- case SectionType.Slider:
- SliderDTO sliderDTO = JsonConvert.DeserializeObject(section.data);
- foreach (var content in sliderDTO.contents)
- {
- if (content.resourceId != null)
- {
- addResourceToList(resourceDTOs, content.resourceId);
- }
- }
- break;
- case SectionType.Quizz:
- QuizzDTO quizzDTO = JsonConvert.DeserializeObject(section.data);
- foreach (var question in quizzDTO.questions)
- {
- if (question.label != null)
- {
- foreach (var questionLabel in question.label)
- {
- addResourceToList(resourceDTOs, questionLabel.resourceId);
- }
- }
- if (question.imageBackgroundResourceId != null)
- {
- addResourceToList(resourceDTOs, question.imageBackgroundResourceId);
- }
- foreach (var response in question.responses)
- {
- if (response.label != null)
- {
- foreach (var responseLabel in response.label)
- {
- addResourceToList(resourceDTOs, responseLabel.resourceId);
- }
- }
- }
- }
- if (quizzDTO.bad_level != null)
- {
- if (quizzDTO.bad_level.label != null)
- {
- foreach (var badLevelLabel in quizzDTO.bad_level.label)
- {
- addResourceToList(resourceDTOs, badLevelLabel.resourceId);
- }
- }
- }
- if (quizzDTO.medium_level != null)
- {
- if (quizzDTO.medium_level.label != null)
- {
- foreach (var medium_levelLabel in quizzDTO.medium_level.label)
- {
- addResourceToList(resourceDTOs, medium_levelLabel.resourceId);
- }
- }
- }
- if (quizzDTO.good_level != null)
- {
- if (quizzDTO.good_level.label != null)
- {
- foreach (var good_levelLabel in quizzDTO.good_level.label)
- {
- addResourceToList(resourceDTOs, good_levelLabel.resourceId);
- }
- }
- }
- if (quizzDTO.great_level != null)
- {
- if (quizzDTO.great_level.label != null)
- {
- foreach (var great_levelLabel in quizzDTO.great_level.label)
- {
- addResourceToList(resourceDTOs, great_levelLabel.resourceId);
- }
- }
- }
- break;
- case SectionType.Article:
- ArticleDTO articleDTO = JsonConvert.DeserializeObject(section.data);
- foreach (var content in articleDTO.contents)
- {
- if (content.resourceId != null)
- {
- addResourceToList(resourceDTOs, content.resourceId);
- }
- }
-
- // If not a language is used for export in manager, if one is the myvisit app
- var audios = language != null ? articleDTO.audioIds.Where(a => a.language == language) : articleDTO.audioIds;
- foreach (var audio in audios)
- {
- if (audio.value != null)
- {
- addResourceToList(resourceDTOs, audio.value);
- }
- }
- break;
- case SectionType.PDF:
- PdfDTO pdfDTO = JsonConvert.DeserializeObject(section.data);
- foreach (var pdf in pdfDTO.pdfs)
- {
- foreach (var pdfFileAndTitle in pdf.pdfFilesAndTitles)
- {
- addResourceToList(resourceDTOs, pdfFileAndTitle.resourceId);
- }
- }
- break;
- case SectionType.Agenda:
- AgendaDTO agendaDTO = JsonConvert.DeserializeObject(section.data);
- var resourceIds = language != null ? agendaDTO.resourceIds.Where(a => a.language == language) : agendaDTO.resourceIds;
- foreach (var resourceId in resourceIds)
- {
- if (resourceId.value != null && resourceId.value.Trim().Length > 0)
- {
- addResourceToList(resourceDTOs, resourceId.value);
- }
- }
- break;
- case SectionType.Menu:
- case SectionType.Web:
- case SectionType.Video:
- case SectionType.Weather:
- default:
- break;
- }*/
+ // Remplace 157 lignes de `switch` commenté qui n'ont jamais tourné :
+ // une visite téléchargée n'embarquait 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.
+ // `GetReferencedResourceIds` est déjà implémentée sur les 13 sous-types
+ // et connaît chacun ses propres médias ; c'est aussi elle qui garantit
+ // qu'un nouveau type de section n'ouvre pas un trou silencieux ici.
+ foreach (var resourceId in section.GetReferencedResourceIds(language))
+ addResourceToList(resourceDTOs, resourceId);
}
ExportConfigurationDTO toDownload = configuration.ToExportDTO(sectionDTOs, resourceDTOs);
string jsonString = JsonConvert.SerializeObject(toDownload);
@@ -655,6 +505,12 @@ namespace ManagerService.Controllers
var sectionsAlreadyInDB = _myInfoMateDbContext.Sections.Where(s => !exportConfiguration.sections.Select(s => s.id).Contains(s.Id)).Select(s => s.Id).ToList();
+ // Toutes les ressources de la charge, en une passe. `createResource` est
+ // idempotente, donc les quelques appels ciblés qui subsistent plus haut sont
+ // sans effet — et un nouveau type de section ne peut plus ouvrir de trou ici.
+ foreach (var resourceExport in exportConfiguration.resources ?? new List())
+ createResource(resourceExport);
+
foreach (var section in exportConfiguration.sections.Where(s => !sectionsAlreadyInDB.Contains(s.id)))
{
Section newSection = SectionFactory.CreateEmpty(section.type);
@@ -686,133 +542,11 @@ namespace ManagerService.Controllers
_myInfoMateDbContext.Sections.Add(newSection);
//_sectionService.Create(newSection);
- /*switch (section.type)
- {
- case SectionType.Map:
- MapDTO mapDTO = JsonConvert.DeserializeObject(section.data);
- if (mapDTO.iconResourceId != null)
- {
- createResource(exportConfiguration.resources.Where(r => r.id == mapDTO.iconResourceId).FirstOrDefault());
- }
-
- foreach (var point in mapDTO.points)
- {
- if (point.imageResourceId != null)
- {
- createResource(exportConfiguration.resources.Where(r => r.id == point.imageResourceId).FirstOrDefault());
- }
- foreach (var content in point.contents)
- {
- if (content.resourceId != null)
- {
- createResource(exportConfiguration.resources.Where(r => r.id == content.resourceId).FirstOrDefault());
- }
- }
- }
-
- foreach (var categorie in mapDTO.categories)
- {
- if (categorie.iconResourceId != null)
- {
- createResource(exportConfiguration.resources.Where(r => r.id == categorie.iconResourceId).FirstOrDefault());
- }
- }
-
- break;
- case SectionType.Slider:
- SliderDTO sliderDTO = JsonConvert.DeserializeObject(section.data);
- foreach (var content in sliderDTO.contents)
- {
- if (content.resourceId != null)
- {
- createResource(exportConfiguration.resources.Where(r => r.id == content.resourceId).FirstOrDefault());
- }
- }
- break;
- case SectionType.Quizz:
- QuizDTO quizzDTO = JsonConvert.DeserializeObject(section.data);
- foreach (var question in quizzDTO.questions)
- {
- if (question.label != null)
- {
- foreach (var questionLabel in question.label)
- {
- createResource(exportConfiguration.resources.Where(r => r.id == questionLabel.resourceId).FirstOrDefault());
- }
- }
- if (question.imageBackgroundResourceId != null)
- {
- createResource(exportConfiguration.resources.Where(r => r.id == question.imageBackgroundResourceId).FirstOrDefault());
- }
- foreach (var response in question.responses)
- {
- if (response.label != null)
- {
- foreach (var responseLabel in response.label)
- {
- createResource(exportConfiguration.resources.Where(r => r.id == responseLabel.resourceId).FirstOrDefault());
- }
- }
- }
- }
- if (quizzDTO.bad_level != null)
- {
- if(quizzDTO.bad_level.label != null)
- {
- foreach (var balLevelLabel in quizzDTO.bad_level.label)
- {
- createResource(exportConfiguration.resources.Where(r => r.id == balLevelLabel.resourceId).FirstOrDefault());
- }
- }
- }
- if (quizzDTO.medium_level != null)
- {
- if (quizzDTO.medium_level.label != null)
- {
- foreach (var medium_levelLabel in quizzDTO.medium_level.label)
- {
- createResource(exportConfiguration.resources.Where(r => r.id == medium_levelLabel.resourceId).FirstOrDefault());
- }
- }
- }
- if (quizzDTO.good_level != null)
- {
- if (quizzDTO.good_level.label != null)
- {
- foreach (var good_levelLabel in quizzDTO.good_level.label)
- {
- createResource(exportConfiguration.resources.Where(r => r.id == good_levelLabel.resourceId).FirstOrDefault());
- }
- }
- }
- if (quizzDTO.great_level != null)
- {
- if (quizzDTO.great_level.label != null)
- {
- foreach (var great_levelLabel in quizzDTO.great_level.label)
- {
- createResource(exportConfiguration.resources.Where(r => r.id == great_levelLabel.resourceId).FirstOrDefault());
- }
- }
- }
- break;
- case SectionType.Article:
- ArticleDTO articleDTO = JsonConvert.DeserializeObject(section.data);
- foreach (var content in articleDTO.contents)
- {
- if (content.resourceId != null)
- {
- createResource(exportConfiguration.resources.Where(r => r.id == content.resourceId).FirstOrDefault());
- }
- }
- break;
- case SectionType.Menu:
- case SectionType.Web:
- case SectionType.Video:
- case SectionType.Weather:
- default:
- break;
- }*/
+ // Le `switch` de 126 lignes qui vivait ici cherchait, section par
+ // section, quelles ressources de la charge créer — et il était commenté,
+ // donc seule l'image de la section arrivait. Remplacé par la boucle
+ // unique plus haut : l'export embarque désormais toutes les ressources
+ // référencées, l'import n'a plus à les redécouvrir.
}
return new ObjectResult("The configuration has been successfully imported") { StatusCode = 202 };