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 <noreply@anthropic.com>
283 lines
11 KiB
C#
283 lines
11 KiB
C#
using Manager.Services;
|
|
using ManagerService.Controllers;
|
|
using ManagerService.Data;
|
|
using ManagerService.DTOs;
|
|
using ManagerService.Helpers;
|
|
using ManagerService.Tests.Infrastructure;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Xunit;
|
|
|
|
namespace ManagerService.Tests.Controllers
|
|
{
|
|
public class UserControllerTests
|
|
{
|
|
private UserController BuildController(MyInfoMateDbContext db, string callerRole = "Manager.superadmin", string callerInstanceId = "inst-test",
|
|
FakeEmailService emailService = null)
|
|
{
|
|
var cfg = FakeMongoConfig.Create();
|
|
var userService = new UserDatabaseService(cfg);
|
|
var profileLogic = new ProfileLogic(NullLogger<ProfileLogic>.Instance);
|
|
|
|
var controller = new UserController(
|
|
NullLogger<UserController>.Instance,
|
|
userService,
|
|
profileLogic,
|
|
db,
|
|
emailService ?? new FakeEmailService(),
|
|
FakeConfiguration.Create());
|
|
|
|
FakeUser.SetUser(controller, FakeUser.Create(callerRole, callerInstanceId));
|
|
return controller;
|
|
}
|
|
|
|
private static UserDetailDTO MakeNewUser(string email, string instanceId = "inst-test", UserRole role = UserRole.ContentEditor) =>
|
|
new UserDetailDTO
|
|
{
|
|
email = email,
|
|
firstName = "Test",
|
|
lastName = "User",
|
|
instanceId = instanceId,
|
|
password = "password123",
|
|
role = role
|
|
};
|
|
|
|
// ── GET ──────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Get_SuperAdmin_SeesAllInstances()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.Users.AddRange(
|
|
new User { Id = "u1", Email = "a@a.be", Password = "x", LastName = "A", Token = "t1", InstanceId = "inst1" },
|
|
new User { Id = "u2", Email = "b@b.be", Password = "x", LastName = "B", Token = "t2", InstanceId = "inst2" }
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var result = BuildController(db, "Manager.superadmin").Get();
|
|
|
|
var ok = Assert.IsType<OkObjectResult>(result);
|
|
var users = Assert.IsAssignableFrom<IEnumerable<UserDetailDTO>>(ok.Value);
|
|
Assert.Equal(2, users.Count());
|
|
}
|
|
|
|
[Fact]
|
|
public void Get_InstanceAdmin_SeesOnlyOwnInstance()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.Users.AddRange(
|
|
new User { Id = "u1", Email = "a@a.be", Password = "x", LastName = "A", Token = "t1", InstanceId = "inst-test" },
|
|
new User { Id = "u2", Email = "b@b.be", Password = "x", LastName = "B", Token = "t2", InstanceId = "other-inst" }
|
|
);
|
|
db.SaveChanges();
|
|
|
|
var result = BuildController(db, "Manager.instanceadmin", "inst-test").Get();
|
|
|
|
var ok = Assert.IsType<OkObjectResult>(result);
|
|
var users = Assert.IsAssignableFrom<IEnumerable<UserDetailDTO>>(ok.Value);
|
|
Assert.Equal(1, users.Count());
|
|
Assert.All(users, u => Assert.Equal("inst-test", u.instanceId));
|
|
}
|
|
|
|
// ── CREATE ───────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task CreateUser_DuplicateEmail_Returns409()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.Users.Add(new User { Id = "u1", Email = "dup@a.be", Password = "x", LastName = "A", Token = "t1", InstanceId = "inst-test" });
|
|
db.SaveChanges();
|
|
|
|
var result = await BuildController(db).CreateUser(MakeNewUser("dup@a.be"));
|
|
|
|
Assert.IsType<ConflictObjectResult>(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateUser_InstanceAdminCreatingInstanceAdmin_Returns403()
|
|
{
|
|
// InstanceAdmin (role=1) ne peut pas créer SuperAdmin (role=0)
|
|
using var db = DbContextFactory.Create();
|
|
|
|
var result = await BuildController(db, "Manager.instanceadmin")
|
|
.CreateUser(MakeNewUser("new@a.be", role: UserRole.SuperAdmin));
|
|
|
|
var statusResult = Assert.IsType<ObjectResult>(result);
|
|
Assert.Equal(403, statusResult.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateUser_ValidDto_HashesPassword()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
|
|
var result = await BuildController(db).CreateUser(MakeNewUser("new@a.be"));
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
var user = db.Users.First();
|
|
Assert.NotEqual("password123", user.Password); // doit être haché
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateUser_NullPassword_InvitesUserByEmail()
|
|
{
|
|
// Sans mot de passe, l'utilisateur n'est plus refusé : il est créé avec un
|
|
// mot de passe aléatoire et reçoit un lien d'invitation pour définir le sien.
|
|
using var db = DbContextFactory.Create();
|
|
var emails = new FakeEmailService();
|
|
|
|
var dto = new UserDetailDTO { email = "x@x.be", lastName = "X", instanceId = "inst-test", password = null };
|
|
var result = await BuildController(db, emailService: emails).CreateUser(dto);
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
|
|
var user = db.Users.Single();
|
|
Assert.False(string.IsNullOrEmpty(user.Password));
|
|
Assert.False(string.IsNullOrEmpty(user.PasswordTokenHash));
|
|
Assert.NotNull(user.PasswordTokenExpiresAt);
|
|
|
|
var invitation = Assert.Single(emails.Sent);
|
|
Assert.Equal(nameof(FakeEmailService.SendUserInvitationEmailAsync), invitation.Kind);
|
|
Assert.Equal("x@x.be", invitation.To);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateUser_WithPassword_DoesNotSendInvitation()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
var emails = new FakeEmailService();
|
|
|
|
await BuildController(db, emailService: emails).CreateUser(MakeNewUser("with-pwd@a.be"));
|
|
|
|
Assert.Empty(emails.Sent);
|
|
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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(result);
|
|
Assert.Equal(6, db.Users.Count(u => u.InstanceId == "inst-test"));
|
|
}
|
|
|
|
// ── UPDATE ───────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void UpdateUser_UnknownId_Returns404()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
|
|
var result = BuildController(db).UpdateUser(new UserDetailDTO { id = "unknown", firstName = "X", lastName = "Y" });
|
|
|
|
Assert.IsType<NotFoundObjectResult>(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void UpdateUser_ExistingUser_UpdatesName()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.Users.Add(new User { Id = "u1", Email = "a@a.be", Password = "x", LastName = "Old", Token = "t1", InstanceId = "inst-test" });
|
|
db.SaveChanges();
|
|
|
|
var result = BuildController(db).UpdateUser(new UserDetailDTO { id = "u1", firstName = "New", lastName = "Name" });
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
var user = db.Users.First();
|
|
Assert.Equal("New", user.FirstName);
|
|
Assert.Equal("Name", user.LastName);
|
|
}
|
|
|
|
// ── DELETE ───────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void DeleteUser_ExistingUser_Returns202()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
db.Users.Add(new User { Id = "u1", Email = "a@a.be", Password = "x", LastName = "A", Token = "t1", InstanceId = "inst-test" });
|
|
db.SaveChanges();
|
|
|
|
var result = BuildController(db).DeleteUser("u1");
|
|
|
|
var statusResult = Assert.IsType<ObjectResult>(result);
|
|
Assert.Equal(202, statusResult.StatusCode);
|
|
Assert.Equal(0, db.Users.Count());
|
|
}
|
|
|
|
[Fact]
|
|
public void DeleteUser_UnknownId_Returns404()
|
|
{
|
|
using var db = DbContextFactory.Create();
|
|
|
|
var result = BuildController(db).DeleteUser("unknown");
|
|
|
|
Assert.IsType<NotFoundObjectResult>(result);
|
|
}
|
|
}
|
|
}
|