Securite
- RequireAppKey : filtre qui exige une cle d'API valide ou un utilisateur du
manager. Pose sur les routes de contenu consommees par les apps visiteur
(Configuration, Section, Resource, SectionMap, SectionEvent, SectionAgenda,
SectionParcours, SectionQuiz, ApplicationInstance). Un [Authorize] d'action
ne peut pas assouplir celui de la classe ; seul [AllowAnonymous] le
court-circuite, et le filtre redevient le controle d'acces. Il ferme
l'enumeration par identifiant, pas la confidentialite : la cle s'obtient
par le slug ou le pincode.
- Instance/slug/{slug} rendait le pinCode, l'adresse de facturation, la TVA et
les quotas a qui lit l'URL du site visiteur. StripCommercialFields est
desormais applique sur slug et byPin ; isTrialActive reste expose pour le
filigrane d'essai.
- Device.Create et Device/{id}/detail repondaient 403 a toute tablette depuis
a452f4a (13/03) : la classe exige InstanceAdmin, une cle ne porte que
AppRead. Ouverts a la cle, le cloisonnement par instance etait deja ecrit.
Canal VR
- Device.AppType (defaut Tablet, backfill a 1) ; Create resout
l'ApplicationInstance sur ce type au lieu de Tablet en dur.
- Get filtre optionnellement par appType ; DeviceDetailDTO expose appVersion
et lastSeen.
- PUT Device/{id}/heartbeat : batterie, version, connexion. Volontairement
etroit, une app ne peut ni se renommer ni changer d'instance.
- ApiKeyAppType.VrApp en fin d'enum.
Contenu immersif
- ResourceType : Image360 (11), Video360 (12), Model3D (13), en fin d'enum.
- Section Scene3D : un modele GLB et ses points d'interet, en objet manipule
ou en decor habite. Les points sont des GeoPoint, avec une LocalTransform
en jsonb (convention glTF) ; CRUD dans SectionScene3DController.
- Instance.HasImmersiveContent : l'add-on ajoute 100 Go au quota de stockage,
repose apres un changement de plan et retire a la desactivation.
- ImmersiveBackground (owned) sur Configuration et ApplicationInstance, avec
une image de repli pour les canaux qui ne rendent pas l'immersif.
Export de configuration
- exportVersion (1) et generatedAt : le JSON devient un contrat, lu tel quel
par l'app Unity.
- Section.ToDTO() n'etant pas virtuelle, l'export ne portait aucun champ
specifique de sous-type. Passe par SectionFactory.ToDTO, et charge les
points des Map et des Scene3D.
- Le fond immersif et son repli partent avec les ressources, URL resolues.
Migrations : AddAppTypeToDevice, AddScene3DSectionAndImmersiveAddon,
AddImmersiveBackground.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
171 lines
6.5 KiB
C#
171 lines
6.5 KiB
C#
using Hangfire;
|
|
using Manager.Services;
|
|
using Manager.Interfaces.Models;
|
|
using ManagerService.Controllers;
|
|
using ManagerService.Helpers;
|
|
using ManagerService.Data;
|
|
using ManagerService.DTOs;
|
|
using ManagerService.Services;
|
|
using ManagerService.Tests.Infrastructure;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace ManagerService.Tests.Controllers
|
|
{
|
|
/// <summary>
|
|
/// L'add-on « Contenu immersif » — §6 de <c>DOCS/v2/vr-quest-unity-plan.md</c>.
|
|
///
|
|
/// ⚠️ Ces tests existent pour une raison précise : la migration pose un
|
|
/// <c>defaultValue</c>, ce qui amène EF Core à traiter la colonne comme
|
|
/// <c>ValueGeneratedOnAdd</c> — <b>read-only après insert</b>. Le piège est déjà
|
|
/// documenté dans <c>MyInfoMateDbContext</c> pour les quotas, et il a coûté une
|
|
/// migration corrigée à la main sur <c>Device.AppType</c>. Sans
|
|
/// <c>ValueGeneratedNever</c>, l'add-on ne s'activerait jamais — silencieusement.
|
|
/// </summary>
|
|
public class InstanceAddonTests
|
|
{
|
|
private InstanceController BuildController(MyInfoMateDbContext db)
|
|
{
|
|
var cfg = FakeMongoConfig.Create();
|
|
return new InstanceController(
|
|
NullLogger<InstanceController>.Instance,
|
|
new InstanceDatabaseService(cfg),
|
|
new UserDatabaseService(cfg),
|
|
new ProfileLogic(NullLogger<ProfileLogic>.Instance),
|
|
db,
|
|
new ApiKeyDatabaseService(db),
|
|
new Mock<IBackgroundJobClient>().Object);
|
|
}
|
|
|
|
[Fact]
|
|
public void ImmersiveContent_IsOffByDefault()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.Instances.Add(new Instance { Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow });
|
|
db.SaveChanges();
|
|
|
|
Assert.False(db.Instances.First().HasImmersiveContent);
|
|
}
|
|
|
|
[Fact]
|
|
public void ImmersiveContent_CanBeTurnedOn()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.Instances.Add(new Instance { Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow });
|
|
db.SaveChanges();
|
|
|
|
var result = BuildController(db).Updateinstance(
|
|
new InstanceDTO { id = "i1", hasImmersiveContent = true });
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
Assert.True(db.Instances.First().HasImmersiveContent);
|
|
}
|
|
|
|
[Fact]
|
|
public void ImmersiveContent_AbsentFromPayload_KeepsPreviousValue()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.Instances.Add(new Instance
|
|
{
|
|
Id = "i1",
|
|
Name = "Musée",
|
|
DateCreation = DateTime.UtcNow,
|
|
HasImmersiveContent = true
|
|
});
|
|
db.SaveChanges();
|
|
|
|
// Un écran qui ne parle pas de l'add-on ne doit pas le désactiver au passage :
|
|
// le contrôleur applique `?? valeur actuelle`, comme pour les autres drapeaux.
|
|
BuildController(db).Updateinstance(new InstanceDTO { id = "i1", name = "Musée" });
|
|
|
|
Assert.True(db.Instances.First().HasImmersiveContent);
|
|
}
|
|
|
|
private const long Envelope = 100L * 1024 * 1024 * 1024;
|
|
private const long PlanQuota = 10L * 1024 * 1024 * 1024;
|
|
|
|
private static MyInfoMateDbContext WithPlan(long quota = PlanQuota)
|
|
{
|
|
var db = DbContextFactory.Create();
|
|
db.SubscriptionPlans.Add(new SubscriptionPlan
|
|
{
|
|
Id = "pro",
|
|
Name = "Pro",
|
|
StorageQuotaBytes = quota
|
|
});
|
|
db.Instances.Add(new Instance
|
|
{
|
|
Id = "i1",
|
|
Name = "Musée",
|
|
DateCreation = DateTime.UtcNow,
|
|
StorageQuotaBytes = quota
|
|
});
|
|
db.SaveChanges();
|
|
return db;
|
|
}
|
|
|
|
[Fact]
|
|
public void ActivatingImmersive_RaisesStorageQuota()
|
|
{
|
|
using var db = WithPlan();
|
|
|
|
BuildController(db).Updateinstance(
|
|
new InstanceDTO { id = "i1", hasImmersiveContent = true });
|
|
|
|
// Sans ce relèvement, un client Pro activait la 360 et saturait son quota à la
|
|
// première vidéo : c'est le point de marge du §6 du plan.
|
|
Assert.Equal(PlanQuota + Envelope, db.Instances.First().StorageQuotaBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public void DeactivatingImmersive_LowersStorageQuotaBack()
|
|
{
|
|
using var db = WithPlan();
|
|
var controller = BuildController(db);
|
|
|
|
controller.Updateinstance(new InstanceDTO { id = "i1", hasImmersiveContent = true });
|
|
controller.Updateinstance(new InstanceDTO { id = "i1", hasImmersiveContent = false });
|
|
|
|
Assert.Equal(PlanQuota, db.Instances.First().StorageQuotaBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public void ActivatingImmersiveTwice_DoesNotStackEnvelopes()
|
|
{
|
|
using var db = WithPlan();
|
|
var controller = BuildController(db);
|
|
|
|
controller.Updateinstance(new InstanceDTO { id = "i1", hasImmersiveContent = true });
|
|
controller.Updateinstance(new InstanceDTO { id = "i1", hasImmersiveContent = true });
|
|
|
|
// Un écran qui renvoie tout le formulaire à chaque enregistrement repasse `true`
|
|
// sans rien changer : le quota ne doit pas grimper d'une enveloppe par clic.
|
|
Assert.Equal(PlanQuota + Envelope, db.Instances.First().StorageQuotaBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public void ChangingPlan_KeepsTheImmersiveEnvelope()
|
|
{
|
|
using var db = WithPlan();
|
|
db.SubscriptionPlans.Add(new SubscriptionPlan
|
|
{
|
|
Id = "premium",
|
|
Name = "Premium",
|
|
StorageQuotaBytes = 50L * 1024 * 1024 * 1024
|
|
});
|
|
db.SaveChanges();
|
|
|
|
var controller = BuildController(db);
|
|
controller.Updateinstance(new InstanceDTO { id = "i1", hasImmersiveContent = true });
|
|
controller.Updateinstance(new InstanceDTO { id = "i1", subscriptionPlanId = "premium" });
|
|
|
|
// ApplyPlanQuotas réécrit le quota depuis le plan : sans reposer l'enveloppe
|
|
// par-dessus, faire monter un client en gamme lui retirerait son add-on.
|
|
Assert.Equal(50L * 1024 * 1024 * 1024 + Envelope,
|
|
db.Instances.First().StorageQuotaBytes);
|
|
}
|
|
}
|
|
}
|