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>
298 lines
12 KiB
C#
298 lines
12 KiB
C#
using Manager.Services;
|
|
using ManagerService.Controllers;
|
|
using ManagerService.Data;
|
|
using ManagerService.DTOs;
|
|
using ManagerService.Tests.Infrastructure;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using System.Linq;
|
|
using Xunit;
|
|
|
|
namespace ManagerService.Tests.Controllers
|
|
{
|
|
public class DeviceControllerTests
|
|
{
|
|
private static DeviceController BuildController(MyInfoMateDbContext db)
|
|
{
|
|
var cfg = FakeMongoConfig.Create();
|
|
var controller = new DeviceController(
|
|
NullLogger<DeviceController>.Instance,
|
|
new DeviceDatabaseService(cfg),
|
|
new ConfigurationDatabaseService(cfg),
|
|
db);
|
|
FakeUser.SetUser(controller, FakeUser.Create("Manager.instanceadmin", "inst-test"));
|
|
return controller;
|
|
}
|
|
|
|
private static void SeedPrerequisites(MyInfoMateDbContext db)
|
|
{
|
|
db.Configurations.Add(new Configuration { Id = "conf-1", InstanceId = "inst-test", Label = "Conf", Title = new System.Collections.Generic.List<TranslationDTO>() });
|
|
db.ApplicationInstances.Add(new ApplicationInstance { Id = "ai-1", InstanceId = "inst-test", AppType = AppType.Tablet });
|
|
db.ApplicationInstances.Add(new ApplicationInstance { Id = "ai-vr", InstanceId = "inst-test", AppType = AppType.VR });
|
|
db.SaveChanges();
|
|
}
|
|
|
|
// ── HEARTBEAT (XR-5) ─────────────────────────────────────────────────
|
|
|
|
private static Device SeedDevice(MyInfoMateDbContext db, string instanceId = "inst-test")
|
|
{
|
|
var device = new Device
|
|
{
|
|
Id = "dev-1",
|
|
Identifier = "headset-abc",
|
|
InstanceId = instanceId,
|
|
ConfigurationId = "conf-1",
|
|
AppType = AppType.VR,
|
|
Connected = false
|
|
};
|
|
db.Devices.Add(device);
|
|
db.SaveChanges();
|
|
return device;
|
|
}
|
|
|
|
[Fact]
|
|
public void Heartbeat_WritesBatteryVersionAndLastSeen()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedPrerequisites(db);
|
|
SeedDevice(db);
|
|
|
|
var result = BuildController(db).Heartbeat("dev-1", new DeviceHeartbeatDTO
|
|
{
|
|
batteryLevel = "87",
|
|
appVersion = "1.4.2"
|
|
});
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
|
|
var device = db.Devices.First();
|
|
Assert.Equal("87", device.BatteryLevel);
|
|
Assert.Equal("1.4.2", device.AppVersion);
|
|
Assert.NotNull(device.LastSeen);
|
|
|
|
// Recevoir le battement est la preuve que l'appareil est en ligne : l'app
|
|
// n'a pas à le déclarer.
|
|
Assert.True(device.Connected);
|
|
}
|
|
|
|
[Fact]
|
|
public void Heartbeat_CannotRenameOrMoveTheDevice()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedPrerequisites(db);
|
|
SeedDevice(db);
|
|
|
|
BuildController(db).Heartbeat("dev-1", new DeviceHeartbeatDTO { batteryLevel = "50" });
|
|
|
|
var device = db.Devices.First();
|
|
Assert.Equal("inst-test", device.InstanceId);
|
|
Assert.Equal("conf-1", device.ConfigurationId);
|
|
Assert.Equal("headset-abc", device.Identifier);
|
|
}
|
|
|
|
[Fact]
|
|
public void Heartbeat_OtherInstance_Returns403()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedPrerequisites(db);
|
|
SeedDevice(db, instanceId: "autre-instance");
|
|
|
|
var result = BuildController(db).Heartbeat("dev-1", new DeviceHeartbeatDTO { batteryLevel = "10" });
|
|
|
|
Assert.Equal(403, result.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public void Heartbeat_UnknownDevice_Returns404()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedPrerequisites(db);
|
|
|
|
var result = BuildController(db).Heartbeat("nope", new DeviceHeartbeatDTO());
|
|
|
|
Assert.IsType<NotFoundObjectResult>(result);
|
|
}
|
|
|
|
// ── CREATE ───────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Create_NewIdentifier_CreatesDevice()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedPrerequisites(db);
|
|
|
|
var result = BuildController(db).Create(new DeviceDetailDTO
|
|
{
|
|
identifier = "device-abc",
|
|
instanceId = "inst-test",
|
|
configurationId = "conf-1",
|
|
name = "Tablet 1"
|
|
});
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
Assert.Equal(1, db.Devices.Count());
|
|
Assert.Equal("Tablet 1", db.Devices.First().Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_ExistingIdentifier_UpdatesDeviceNoDuplicate()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedPrerequisites(db);
|
|
db.Devices.Add(new Device { Id = "d1", Identifier = "device-abc", InstanceId = "inst-test", ConfigurationId = "conf-1" });
|
|
db.SaveChanges();
|
|
|
|
var result = BuildController(db).Create(new DeviceDetailDTO
|
|
{
|
|
identifier = "device-abc",
|
|
instanceId = "inst-test",
|
|
configurationId = "conf-1",
|
|
name = "Updated Name"
|
|
});
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
Assert.Equal(1, db.Devices.Count()); // pas de doublon
|
|
Assert.Equal("Updated Name", db.Devices.First().Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_NewDevice_AutoCreatesAppConfigurationLink()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedPrerequisites(db);
|
|
|
|
BuildController(db).Create(new DeviceDetailDTO
|
|
{
|
|
identifier = "device-abc",
|
|
instanceId = "inst-test",
|
|
configurationId = "conf-1",
|
|
name = "Tablet 1"
|
|
});
|
|
|
|
Assert.Equal(1, db.AppConfigurationLinks.Count());
|
|
}
|
|
|
|
// ── UPDATE ───────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Update_ExistingDevice_UpdatesName()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.Configurations.Add(new Configuration { Id = "conf-1", InstanceId = "inst-test", Label = "Conf", Title = new System.Collections.Generic.List<TranslationDTO>() });
|
|
db.Devices.Add(new Device { Id = "d1", Identifier = "dev-1", InstanceId = "inst-test", Name = "Old Name", ConfigurationId = "conf-1" });
|
|
db.SaveChanges();
|
|
|
|
var result = BuildController(db).Update(new DeviceDetailDTO { id = "d1", name = "New Name", instanceId = "inst-test", identifier = "dev-1" });
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
Assert.Equal("New Name", db.Devices.First().Name);
|
|
}
|
|
|
|
// ── DELETE ───────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Delete_ExistingDevice_Returns202()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.Configurations.Add(new Configuration { Id = "conf-1", InstanceId = "inst-test", Label = "Conf", Title = new System.Collections.Generic.List<TranslationDTO>() });
|
|
db.Devices.Add(new Device { Id = "d1", Identifier = "dev-1", InstanceId = "inst-test", ConfigurationId = "conf-1" });
|
|
db.SaveChanges();
|
|
|
|
var result = BuildController(db).Delete("d1");
|
|
|
|
var obj = Assert.IsType<ObjectResult>(result);
|
|
Assert.Equal(202, obj.StatusCode);
|
|
Assert.Equal(0, db.Devices.Count());
|
|
}
|
|
|
|
[Fact]
|
|
public void Delete_UnknownId_Returns404()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
|
|
var result = BuildController(db).Delete("unknown");
|
|
|
|
Assert.IsType<NotFoundObjectResult>(result);
|
|
}
|
|
|
|
// ── CANAL (AppType) ──────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Create_VrDevice_LinksToVrApplicationInstance()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedPrerequisites(db);
|
|
|
|
var result = BuildController(db).Create(new DeviceDetailDTO
|
|
{
|
|
identifier = "quest-1",
|
|
instanceId = "inst-test",
|
|
configurationId = "conf-1",
|
|
name = "Casque 1",
|
|
appType = AppType.VR
|
|
});
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
Assert.Equal(AppType.VR, db.Devices.Single().AppType);
|
|
// Le rattachement se faisait sur AppType.Tablet en dur : un casque atterrissait
|
|
// dans l'onglet Kiosk.
|
|
Assert.Equal("ai-vr", db.AppConfigurationLinks.Single().ApplicationInstanceId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_WithoutAppType_DefaultsToTablet()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedPrerequisites(db);
|
|
|
|
BuildController(db).Create(new DeviceDetailDTO
|
|
{
|
|
identifier = "device-abc",
|
|
instanceId = "inst-test",
|
|
configurationId = "conf-1",
|
|
name = "Tablet 1"
|
|
});
|
|
|
|
Assert.Equal(AppType.Tablet, db.Devices.Single().AppType);
|
|
Assert.Equal("ai-1", db.AppConfigurationLinks.Single().ApplicationInstanceId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_VrDevice_WithoutVrApplicationInstance_Returns404()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.Configurations.Add(new Configuration { Id = "conf-1", InstanceId = "inst-test", Label = "Conf", Title = new System.Collections.Generic.List<TranslationDTO>() });
|
|
db.ApplicationInstances.Add(new ApplicationInstance { Id = "ai-1", InstanceId = "inst-test", AppType = AppType.Tablet });
|
|
db.SaveChanges();
|
|
|
|
var result = BuildController(db).Create(new DeviceDetailDTO
|
|
{
|
|
identifier = "quest-1",
|
|
instanceId = "inst-test",
|
|
configurationId = "conf-1",
|
|
appType = AppType.VR
|
|
});
|
|
|
|
Assert.IsType<NotFoundObjectResult>(result);
|
|
Assert.Equal(0, db.Devices.Count());
|
|
}
|
|
|
|
[Fact]
|
|
public void Get_FiltersByAppType()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
SeedPrerequisites(db);
|
|
db.Devices.Add(new Device { Id = "d1", Identifier = "tab-1", InstanceId = "inst-test", ConfigurationId = "conf-1", AppType = AppType.Tablet });
|
|
db.Devices.Add(new Device { Id = "d2", Identifier = "quest-1", InstanceId = "inst-test", ConfigurationId = "conf-1", AppType = AppType.VR });
|
|
db.SaveChanges();
|
|
|
|
var vr = Assert.IsType<OkObjectResult>(BuildController(db).Get(null, AppType.VR));
|
|
Assert.Equal("quest-1", Assert.Single((System.Collections.Generic.IEnumerable<DeviceDTO>)vr.Value).identifier);
|
|
|
|
// Sans filtre : comportement historique, tous les appareils.
|
|
var all = Assert.IsType<OkObjectResult>(BuildController(db).Get(null));
|
|
Assert.Equal(2, ((System.Collections.Generic.IEnumerable<DeviceDTO>)all.Value).Count());
|
|
}
|
|
}
|
|
}
|