From a3e0b4df715839afa3bb2981a6779e49e59b8d06 Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Wed, 12 Aug 2026 14:55:24 +0200 Subject: [PATCH] Le plafond de 5 utilisateurs existe enfin cote serveur MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CreateUser ne comptait rien. Ce qui a ete livre le 12/08 dans manager-app -- compteur « X / 5 » et bouton d'ajout desactive -- est un garde-fou d'interface : un POST direct sur l'API passait toujours. Le controle est desormais fait la ou il est opposable, et rend 422. Cote front, rien a retoucher : le resultat d'invokeAPI est deja lu depuis le correctif du 409 e-mail deja pris, donc le message remontera tel quel. Deux choix, tous deux documentes dans le code : - 5 en dur. Le faire varier par plan serait une colonne sur SubscriptionPlan, qui n'en porte aucune sur les utilisateurs -- donc une migration apres le gel du schema (lot B). Dette V1 assumee. - Le SuperAdmin n'y est pas soumis. C'est la seule porte de service qui reste tant qu'aucun champ ne permet de relever la limite d'un client, et ca s'aligne sur le front, qui ne lui montre deja pas le compteur. L'inscription self-service (OnboardingController) n'est pas concernee : elle cree le premier utilisateur d'une instance neuve. 4 tests : au plafond, sous le plafond, plafond par instance, SuperAdmin exempte. dotnet test 175/175. Co-Authored-By: Claude Opus 5 --- .../Controllers/UserControllerTests.cs | 69 +++++++++++++++++++ ManagerService/Controllers/UserController.cs | 16 +++++ 2 files changed, 85 insertions(+) diff --git a/ManagerService.Tests/Controllers/UserControllerTests.cs b/ManagerService.Tests/Controllers/UserControllerTests.cs index 10b0ec7..aaee9d8 100644 --- a/ManagerService.Tests/Controllers/UserControllerTests.cs +++ b/ManagerService.Tests/Controllers/UserControllerTests.cs @@ -157,6 +157,75 @@ namespace ManagerService.Tests.Controllers Assert.Null(db.Users.Single().PasswordTokenHash); } + // ── PLAFOND DE 5 UTILISATEURS ──────────────────────────────────────── + + private static void SeedUsers(MyInfoMateDbContext db, int count, string instanceId) + { + for (var i = 0; i < count; i++) + db.Users.Add(new User + { + Id = $"{instanceId}-u{i}", + Email = $"{instanceId}-{i}@a.be", + Password = "x", + LastName = "A", + Token = $"{instanceId}-t{i}", + InstanceId = instanceId + }); + db.SaveChanges(); + } + + [Fact] + public async Task CreateUser_AtCap_Returns422() + { + // Le compteur de manager-app est un garde-fou d'interface : un POST direct + // passait toujours. C'est ce trou-là que le 422 ferme. + using var db = DbContextFactory.Create(); + SeedUsers(db, 5, "inst-test"); + + var result = await BuildController(db, "Manager.instanceadmin").CreateUser(MakeNewUser("sixth@a.be")); + + Assert.Equal(422, result.StatusCode); + Assert.Equal(5, db.Users.Count(u => u.InstanceId == "inst-test")); + } + + [Fact] + public async Task CreateUser_UnderCap_Succeeds() + { + using var db = DbContextFactory.Create(); + SeedUsers(db, 4, "inst-test"); + + var result = await BuildController(db, "Manager.instanceadmin").CreateUser(MakeNewUser("fifth@a.be")); + + Assert.IsType(result); + Assert.Equal(5, db.Users.Count(u => u.InstanceId == "inst-test")); + } + + [Fact] + public async Task CreateUser_CapIsPerInstance() + { + // Une autre instance pleine ne doit pas bloquer celle de l'appelant. + using var db = DbContextFactory.Create(); + SeedUsers(db, 5, "other-inst"); + + var result = await BuildController(db, "Manager.instanceadmin").CreateUser(MakeNewUser("first@a.be")); + + Assert.IsType(result); + } + + [Fact] + public async Task CreateUser_SuperAdmin_IsNotCapped() + { + // Porte de service interne : tant qu'aucun champ ne permet de relever la + // limite d'un client, le SuperAdmin doit pouvoir ajouter un siège. + using var db = DbContextFactory.Create(); + SeedUsers(db, 5, "inst-test"); + + var result = await BuildController(db, "Manager.superadmin").CreateUser(MakeNewUser("sixth@a.be")); + + Assert.IsType(result); + Assert.Equal(6, db.Users.Count(u => u.InstanceId == "inst-test")); + } + // ── UPDATE ─────────────────────────────────────────────────────────── [Fact] diff --git a/ManagerService/Controllers/UserController.cs b/ManagerService/Controllers/UserController.cs index 7c692d7..2fe25c0 100644 --- a/ManagerService/Controllers/UserController.cs +++ b/ManagerService/Controllers/UserController.cs @@ -30,6 +30,18 @@ namespace ManagerService.Controllers private readonly IConfiguration _configuration; IHexIdGeneratorService idService = new HexIdGeneratorService(); + /// + /// Plafond d'utilisateurs par instance, tous plans confondus. + /// + /// + /// ⚠️ Dette assumee pour la V1 : le chiffre est en dur. Le faire varier par plan + /// demanderait une colonne sur SubscriptionPlan, qui n'en porte aucune sur les + /// utilisateurs, donc une migration apres le gel du schema (lot B). + /// Le SuperAdmin n'est pas soumis au plafond : c'est la porte de service interne + /// tant qu'aucun champ ne permet de relever la limite d'un client. + /// + private const int MaxUsersPerInstance = 5; + public UserController(ILogger logger, UserDatabaseService userService, ProfileLogic profileLogic, MyInfoMateDbContext myInfoMateDbContext, IEmailService emailService, IConfiguration configuration) { _logger = logger; @@ -113,6 +125,7 @@ namespace ManagerService.Controllers [ProducesResponseType(typeof(UserDetailDTO), 200)] [ProducesResponseType(typeof(string), 400)] [ProducesResponseType(typeof(string), 409)] + [ProducesResponseType(typeof(string), 422)] [ProducesResponseType(typeof(string), 500)] [HttpPost] public async Task CreateUser([FromBody] UserDetailDTO newUserDTO) @@ -131,6 +144,9 @@ namespace ManagerService.Controllers User newUser = new User(); newUser.InstanceId = IsSuperAdmin() ? newUserDTO.instanceId : GetCallerInstanceId(); + + if (!IsSuperAdmin() && _myInfoMateDbContext.Users.Count(u => u.InstanceId == newUser.InstanceId) >= MaxUsersPerInstance) + return UnprocessableEntity($"This instance has reached its limit of {MaxUsersPerInstance} users"); newUser.Email = newUserDTO.email; newUser.FirstName = newUserDTO.firstName; newUser.LastName = newUserDTO.lastName;