Quota update in controller, audit log (serilog) + ai translator + Unit tests ! (to be tested) + migration update

This commit is contained in:
Thomas Fransolet 2026-04-10 16:28:33 +02:00
parent eff4f7ba5c
commit e0ff0eeba6
59 changed files with 10041 additions and 104 deletions

3
.gitignore vendored
View File

@ -57,3 +57,6 @@ ManagerService/obj
# Firebase service account key (sensitive — never commit)
*firebase-adminsdk*.json
# MongoDB export data (données sensibles — ne jamais committer)
migration-data/

View File

@ -0,0 +1,176 @@
using ManagerService.Controllers;
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 System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class AiControllerTests
{
private static readonly AiChatResponse FakeResponse = new AiChatResponse { Reply = "OK" };
private AiController BuildController(MyInfoMateDbContext db, Mock<IAssistantService>? mockService = null)
{
mockService ??= new Mock<IAssistantService>();
mockService
.Setup(s => s.ChatAsync(It.IsAny<AiChatRequest>()))
.ReturnsAsync(FakeResponse);
return new AiController(mockService.Object, db, NullLogger<AiController>.Instance);
}
private static AiChatRequest MakeRequest(string instanceId, AppType appType = AppType.Tablet) =>
new AiChatRequest { InstanceId = instanceId, AppType = appType, Message = "Bonjour" };
// ── FORBID CASES ─────────────────────────────────────────────────────
[Fact]
public async Task Chat_InstanceNotFound_ReturnsForbid()
{
using var db = DbContextFactory.Create();
var result = await BuildController(db).Chat(MakeRequest("unknown"));
Assert.IsType<ForbidResult>(result);
}
[Fact]
public async Task Chat_InstanceAssistantDisabled_ReturnsForbid()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = false, DateCreation = DateTime.UtcNow
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true,
Languages = new List<string>()
});
db.SaveChanges();
var result = await BuildController(db).Chat(MakeRequest("i1"));
Assert.IsType<ForbidResult>(result);
}
[Fact]
public async Task Chat_AppInstanceAssistantDisabled_ReturnsForbid()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = false,
Languages = new List<string>()
});
db.SaveChanges();
var result = await BuildController(db).Chat(MakeRequest("i1"));
Assert.IsType<ForbidResult>(result);
}
[Fact]
public async Task Chat_NoAppInstance_ReturnsForbid()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow
});
db.SaveChanges();
var result = await BuildController(db).Chat(MakeRequest("i1"));
Assert.IsType<ForbidResult>(result);
}
// ── QUOTA COUNTER ────────────────────────────────────────────────────
[Fact]
public async Task Chat_FirstRequestOfMonth_ResetsCounterAndSets1()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow,
AiRequestsThisMonth = 99, AiUsageMonthKey = "2020-01"
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true,
Languages = new List<string>()
});
db.SaveChanges();
await BuildController(db).Chat(MakeRequest("i1"));
var inst = db.Instances.First();
Assert.Equal(1, inst.AiRequestsThisMonth);
Assert.Equal(DateTime.UtcNow.ToString("yyyy-MM"), inst.AiUsageMonthKey);
}
[Fact]
public async Task Chat_SameMonth_IncrementsCounter()
{
using var db = DbContextFactory.Create();
var monthKey = DateTime.UtcNow.ToString("yyyy-MM");
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow,
AiRequestsThisMonth = 3, AiUsageMonthKey = monthKey
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true,
Languages = new List<string>()
});
db.SaveChanges();
await BuildController(db).Chat(MakeRequest("i1"));
Assert.Equal(4, db.Instances.First().AiRequestsThisMonth);
}
// ── NOMINAL ──────────────────────────────────────────────────────────
[Fact]
public async Task Chat_Success_CallsAssistantServiceAndReturns200()
{
using var db = DbContextFactory.Create();
var mockService = new Mock<IAssistantService>();
mockService.Setup(s => s.ChatAsync(It.IsAny<AiChatRequest>())).ReturnsAsync(FakeResponse);
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow,
AiUsageMonthKey = DateTime.UtcNow.ToString("yyyy-MM")
});
db.ApplicationInstances.Add(new ApplicationInstance
{
Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true,
Languages = new List<string>()
});
db.SaveChanges();
var result = await BuildController(db, mockService).Chat(MakeRequest("i1"));
var ok = Assert.IsType<OkObjectResult>(result);
Assert.Equal(FakeResponse, ok.Value);
mockService.Verify(s => s.ChatAsync(It.IsAny<AiChatRequest>()), Times.Once);
}
}
}

View File

@ -0,0 +1,87 @@
using ManagerService.Controllers;
using ManagerService.Data;
using ManagerService.Services;
using static ManagerService.Services.ApiKeyDatabaseService;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class ApiKeyControllerTests
{
private static ApiKeyController BuildController(MyInfoMateDbContext db, string instanceId = "inst-test")
{
var service = new ApiKeyDatabaseService(db);
var controller = new ApiKeyController(service);
FakeUser.SetUser(controller, FakeUser.Create("Manager.instanceadmin", instanceId));
return controller;
}
[Fact]
public async Task GetApiKeys_ReturnsKeysForInstance()
{
using var db = DbContextFactory.Create();
db.ApiKeys.AddRange(
new ApiKey { Id = "k1", InstanceId = "inst-test", Name = "Key1", KeyHash = "h1", IsActive = true, AppType = ApiKeyAppType.VisitApp },
new ApiKey { Id = "k2", InstanceId = "other-inst", Name = "Key2", KeyHash = "h2", IsActive = true, AppType = ApiKeyAppType.VisitApp }
);
db.SaveChanges();
var result = await BuildController(db).GetApiKeys();
var ok = Assert.IsType<OkObjectResult>(result);
var keys = Assert.IsAssignableFrom<System.Collections.Generic.IEnumerable<ApiKeyDTO>>(ok.Value);
Assert.Single(keys);
}
[Fact]
public async Task CreateApiKey_ValidRequest_ReturnsPlainKey()
{
using var db = DbContextFactory.Create();
var result = await BuildController(db).CreateApiKey(new CreateApiKeyRequest
{
Name = "My Key",
AppType = ApiKeyAppType.VisitApp
});
var ok = Assert.IsType<OkObjectResult>(result);
Assert.NotNull(ok.Value);
// La clé plain text doit commencer par "ak_"
var keyProp = ok.Value!.GetType().GetProperty("key");
Assert.NotNull(keyProp);
var plainKey = keyProp!.GetValue(ok.Value) as string;
Assert.StartsWith("ak_", plainKey);
// La clé est stockée hachée, pas en clair
Assert.Equal(1, db.ApiKeys.Count());
Assert.Null(db.ApiKeys.First().Key);
}
[Fact]
public async Task RevokeApiKey_ExistingKey_Returns204()
{
using var db = DbContextFactory.Create();
db.ApiKeys.Add(new ApiKey { Id = "k1", InstanceId = "inst-test", Name = "Key1", KeyHash = "h1", IsActive = true, AppType = ApiKeyAppType.VisitApp });
db.SaveChanges();
var result = await BuildController(db).RevokeApiKey("k1");
Assert.IsType<NoContentResult>(result);
Assert.Equal(0, db.ApiKeys.Count(k => k.IsActive));
}
[Fact]
public async Task RevokeApiKey_UnknownKey_Returns404()
{
using var db = DbContextFactory.Create();
var result = await BuildController(db).RevokeApiKey("unknown");
Assert.IsType<NotFoundResult>(result);
}
}
}

View File

@ -0,0 +1,139 @@
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.Collections.Generic;
using System.Linq;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class ApplicationInstanceControllerTests
{
private static ApplicationInstanceController BuildController(MyInfoMateDbContext db)
{
var cfg = FakeMongoConfig.Create();
var controller = new ApplicationInstanceController(
NullLogger<ApplicationInstanceController>.Instance,
new InstanceDatabaseService(cfg),
new UserDatabaseService(cfg),
new ProfileLogic(NullLogger<ProfileLogic>.Instance),
db);
FakeUser.SetUser(controller, FakeUser.Create("Manager.instanceadmin", "inst-test"));
return controller;
}
// ── CREATE ───────────────────────────────────────────────────────────
[Fact]
public void Create_ValidDto_Persists()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Create(new ApplicationInstanceDTO
{
instanceId = "inst-test",
appType = AppType.Tablet
});
Assert.IsType<OkObjectResult>(result);
Assert.Equal(1, db.ApplicationInstances.Count());
}
// ── UPDATE ───────────────────────────────────────────────────────────
[Fact]
public void Update_ExistingAppInstance_UpdatesAppType()
{
using var db = DbContextFactory.Create();
db.ApplicationInstances.Add(new ApplicationInstance { Id = "ai-1", InstanceId = "inst-test", AppType = AppType.Tablet });
db.SaveChanges();
var result = BuildController(db).Update(new ApplicationInstanceDTO
{
id = "ai-1",
instanceId = "inst-test",
appType = AppType.Web
});
Assert.IsType<OkObjectResult>(result);
Assert.Equal(AppType.Web, db.ApplicationInstances.First().AppType);
}
[Fact]
public void Update_UnknownId_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Update(new ApplicationInstanceDTO { id = "unknown" });
Assert.IsType<NotFoundObjectResult>(result);
}
// ── DELETE ───────────────────────────────────────────────────────────
[Fact]
public void Delete_ExistingAppInstance_Returns202()
{
using var db = DbContextFactory.Create();
db.ApplicationInstances.Add(new ApplicationInstance { Id = "ai-1", InstanceId = "inst-test", AppType = AppType.Tablet });
db.SaveChanges();
var result = BuildController(db).Delete("ai-1");
var obj = Assert.IsType<ObjectResult>(result);
Assert.Equal(202, obj.StatusCode);
Assert.Equal(0, db.ApplicationInstances.Count());
}
// ── CONFIG LINK ──────────────────────────────────────────────────────
[Fact]
public void AddConfigLink_DuplicatePrevented_Returns409()
{
using var db = DbContextFactory.Create();
db.ApplicationInstances.Add(new ApplicationInstance { Id = "ai-1", InstanceId = "inst-test", AppType = AppType.Tablet, Configurations = new List<AppConfigurationLink>() });
db.Configurations.Add(new Configuration { Id = "c1", InstanceId = "inst-test", Label = "C", Title = new List<TranslationDTO>() });
db.AppConfigurationLinks.Add(new AppConfigurationLink { Id = "lnk-1", ApplicationInstanceId = "ai-1", ConfigurationId = "c1" });
db.SaveChanges();
var result = BuildController(db).AddConfigurationToApplicationInstance("ai-1", new AppConfigurationLinkDTO
{
applicationInstanceId = "ai-1",
configurationId = "c1"
});
Assert.IsType<ConflictObjectResult>(result);
}
[Fact]
public void UpdateApplicationLinkOrder_ReordersCorrectly()
{
using var db = DbContextFactory.Create();
db.Configurations.AddRange(
new Configuration { Id = "c1", InstanceId = "inst-test", Label = "C1", Title = new List<TranslationDTO>() },
new Configuration { Id = "c2", InstanceId = "inst-test", Label = "C2", Title = new List<TranslationDTO>() }
);
db.ApplicationInstances.Add(new ApplicationInstance { Id = "ai-1", InstanceId = "inst-test", AppType = AppType.Tablet });
db.AppConfigurationLinks.AddRange(
new AppConfigurationLink { Id = "lnk-1", ApplicationInstanceId = "ai-1", ConfigurationId = "c1", Order = 0 },
new AppConfigurationLink { Id = "lnk-2", ApplicationInstanceId = "ai-1", ConfigurationId = "c2", Order = 1 }
);
db.SaveChanges();
var result = BuildController(db).UpdateApplicationLinkOrder(new List<AppConfigurationLinkDTO>
{
new AppConfigurationLinkDTO { id = "lnk-1", order = 1, configurationId = "c1" },
new AppConfigurationLinkDTO { id = "lnk-2", order = 0, configurationId = "c2" }
});
Assert.IsType<OkObjectResult>(result);
Assert.Equal(1, db.AppConfigurationLinks.First(l => l.Id == "lnk-1").Order);
Assert.Equal(0, db.AppConfigurationLinks.First(l => l.Id == "lnk-2").Order);
}
}
}

View File

@ -0,0 +1,80 @@
using Manager.Interfaces.Models;
using Manager.Services;
using ManagerService.Data;
using ManagerService.Helpers;
using ManagerService.Service.Controllers;
using ManagerService.Service.Services;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class AuthenticationControllerTests
{
private static AuthenticationController BuildController(MyInfoMateDbContext db)
{
var settings = Options.Create(new TokensSettings
{
Secret = "test-secret-key-32-chars-minimum!!",
AccessTokenExpiration = 30
});
var profileLogic = new ProfileLogic(NullLogger<ProfileLogic>.Instance);
var tokensService = new TokensService(
NullLogger<TokensService>.Instance,
settings,
profileLogic,
db);
return new AuthenticationController(
NullLogger<AuthenticationController>.Instance,
tokensService,
db);
}
// Note: en mode DEBUG, email est toujours surchargé en "test@email.be"
// et password en "kljqsdkljqsd".
[Fact]
public void Authenticate_UserNotFound_ReturnsProblem()
{
using var db = DbContextFactory.Create();
// Aucun utilisateur "test@email.be" en base
var result = BuildController(db).AuthenticateWithJson(
new ManagerService.DTOs.LoginDTO { email = "anyone@test.be", password = "any" });
// KeyNotFoundException → catch(Exception) → Problem (500)
var obj = Assert.IsType<ObjectResult>(result);
Assert.Equal(500, obj.StatusCode);
}
[Fact]
public void Authenticate_WrongPassword_Returns401()
{
using var db = DbContextFactory.Create();
// Mot de passe incorrect en base (hash invalide → PasswordUtils.Compare lance une exception
// qui n'est pas UnauthorizedAccessException → retourne Problem 500).
// On met un utilisateur avec un hash valide pour un autre mot de passe.
var profileLogic = new ProfileLogic(NullLogger<ProfileLogic>.Instance);
db.Users.Add(new User
{
Id = "u1",
Email = "test@email.be",
Password = profileLogic.HashPassword("differentpassword"),
LastName = "Test",
Token = "t1",
InstanceId = "inst-test"
});
db.SaveChanges();
var result = BuildController(db).AuthenticateWithJson(
new ManagerService.DTOs.LoginDTO { email = "test@email.be", password = "kljqsdkljqsd" });
// UnauthorizedAccessException → Unauthorized (401)
Assert.IsType<UnauthorizedObjectResult>(result);
}
}
}

View File

@ -0,0 +1,104 @@
using ManagerService.Controllers;
using ManagerService.Data;
using ManagerService.DTOs;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class ConfigurationControllerTests
{
private static IConfiguration BuildConfig() =>
new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["ConnectionStrings:TabletDb"] = "mongodb://localhost:27017",
["OpenWeatherApiKey"] = ""
})
.Build();
private static ConfigurationController BuildController(MyInfoMateDbContext db) =>
new ConfigurationController(BuildConfig(), NullLogger<ConfigurationController>.Instance, db);
// ── GET ──────────────────────────────────────────────────────────────
[Fact]
public void Get_FiltersToInstance()
{
using var db = DbContextFactory.Create();
db.Configurations.AddRange(
new Configuration { Id = "c1", InstanceId = "inst-a", Label = "A", Title = new List<TranslationDTO>() },
new Configuration { Id = "c2", InstanceId = "inst-b", Label = "B", Title = new List<TranslationDTO>() }
);
db.SaveChanges();
var result = BuildController(db).Get("inst-a");
var ok = Assert.IsType<OkObjectResult>(result);
var list = Assert.IsAssignableFrom<System.Collections.IEnumerable>(ok.Value);
Assert.Single(list.Cast<object>());
}
// ── CREATE ───────────────────────────────────────────────────────────
[Fact]
public void Create_ValidDto_Persists()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Create(new ConfigurationDTO
{
instanceId = "inst-a",
label = "My Config",
languages = new List<string> { "FR" }
});
Assert.IsType<OkObjectResult>(result);
Assert.Equal(1, db.Configurations.Count());
Assert.Equal("My Config", db.Configurations.First().Label);
}
// ── UPDATE ───────────────────────────────────────────────────────────
[Fact]
public void Update_UnknownId_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Update(new ConfigurationDTO { id = "unknown", label = "X" });
Assert.IsType<NotFoundObjectResult>(result);
}
// ── DELETE ───────────────────────────────────────────────────────────
[Fact]
public void Delete_ExistingConfig_Returns202()
{
using var db = DbContextFactory.Create();
db.Configurations.Add(new Configuration { Id = "c1", InstanceId = "inst-a", Label = "A", Title = new List<TranslationDTO>() });
db.SaveChanges();
var result = BuildController(db).Delete("c1");
var obj = Assert.IsType<ObjectResult>(result);
Assert.Equal(202, obj.StatusCode);
Assert.Equal(0, db.Configurations.Count());
}
[Fact]
public void Delete_UnknownId_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Delete("unknown");
Assert.IsType<NotFoundObjectResult>(result);
}
}
}

View File

@ -0,0 +1,136 @@
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.SaveChanges();
}
// ── 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);
}
}
}

View File

@ -0,0 +1,247 @@
using Manager.Services;
using ManagerService.Controllers;
using ManagerService.Data;
using ManagerService.DTOs;
using ManagerService.Helpers;
using ManagerService.Services;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class InstanceControllerTests
{
private InstanceController BuildController(MyInfoMateDbContext db)
{
var cfg = FakeMongoConfig.Create();
var instanceService = new InstanceDatabaseService(cfg);
var userService = new UserDatabaseService(cfg);
var profileLogic = new ProfileLogic(NullLogger<ProfileLogic>.Instance);
var apiKeyService = new ApiKeyDatabaseService(db);
return new InstanceController(
NullLogger<InstanceController>.Instance,
instanceService,
userService,
profileLogic,
db,
apiKeyService);
}
// ── CREATE ───────────────────────────────────────────────────────────
[Fact]
public void CreateInstance_DuplicateName_Returns409()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance { Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow });
db.SaveChanges();
var result = BuildController(db).CreateInstance(new InstanceDTO { name = "Musée" });
Assert.IsType<ConflictObjectResult>(result);
}
[Fact]
public void CreateInstance_ValidDto_PersistsAndReturnsDto()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).CreateInstance(new InstanceDTO { name = "Nouveau" });
Assert.IsType<OkObjectResult>(result);
Assert.Equal(1, db.Instances.Count());
}
[Fact]
public void CreateInstance_NullDto_Returns400()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).CreateInstance(null!);
Assert.IsType<BadRequestObjectResult>(result);
}
// ── UPDATE ───────────────────────────────────────────────────────────
[Fact]
public void Updateinstance_ClearSubscriptionPlan_SetsNull()
{
using var db = DbContextFactory.Create();
db.SubscriptionPlans.Add(new SubscriptionPlan { Id = "p1", Name = "Starter" });
db.Instances.Add(new Instance { Id = "i1", Name = "Musée", SubscriptionPlanId = "p1", DateCreation = DateTime.UtcNow });
db.SaveChanges();
// subscriptionPlanId = "" → doit effacer le plan
var result = BuildController(db).Updateinstance(new InstanceDTO { id = "i1", subscriptionPlanId = "" });
Assert.IsType<OkObjectResult>(result);
Assert.Null(db.Instances.First().SubscriptionPlanId);
}
[Fact]
public void Updateinstance_SetSubscriptionPlan_Updates()
{
using var db = DbContextFactory.Create();
db.SubscriptionPlans.Add(new SubscriptionPlan { Id = "p1", Name = "Starter" });
db.Instances.Add(new Instance { Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow });
db.SaveChanges();
var result = BuildController(db).Updateinstance(new InstanceDTO { id = "i1", subscriptionPlanId = "p1" });
Assert.IsType<OkObjectResult>(result);
Assert.Equal("p1", db.Instances.First().SubscriptionPlanId);
}
[Fact]
public void Updateinstance_UnknownId_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Updateinstance(new InstanceDTO { id = "unknown", name = "X" });
Assert.IsType<NotFoundObjectResult>(result);
}
// ── GET QUOTA ────────────────────────────────────────────────────────
[Fact]
public void GetQuota_UnknownInstance_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).GetQuota("unknown");
Assert.IsType<NotFoundObjectResult>(result);
}
[Fact]
public void GetQuota_NoSubscriptionPlan_ReturnsZeroQuotas()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance { Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow });
db.SaveChanges();
var result = BuildController(db).GetQuota("i1");
var ok = Assert.IsType<OkObjectResult>(result);
var dto = Assert.IsType<InstanceQuotaDTO>(ok.Value);
Assert.Equal(0, dto.storageQuotaBytes);
Assert.Equal(0, dto.aiRequestsPerMonth);
}
[Fact]
public void GetQuota_WithPlan_ReturnsQuotaFromPlan()
{
using var db = DbContextFactory.Create();
db.SubscriptionPlans.Add(new SubscriptionPlan
{
Id = "p1", Name = "Standard",
StorageQuotaBytes = 10_000_000_000L,
AiRequestsPerMonth = 100
});
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", SubscriptionPlanId = "p1", DateCreation = DateTime.UtcNow
});
db.SaveChanges();
var result = BuildController(db).GetQuota("i1");
var ok = Assert.IsType<OkObjectResult>(result);
var dto = Assert.IsType<InstanceQuotaDTO>(ok.Value);
Assert.Equal(10_000_000_000L, dto.storageQuotaBytes);
Assert.Equal(100, dto.aiRequestsPerMonth);
}
[Fact]
public void GetQuota_SumsResourceSizeBytes()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance { Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow });
db.Resources.AddRange(
new Resource { Id = "r1", InstanceId = "i1", Label = "img1", Type = ResourceType.Image, SizeBytes = 1000 },
new Resource { Id = "r2", InstanceId = "i1", Label = "img2", Type = ResourceType.Image, SizeBytes = 2000 },
new Resource { Id = "r3", InstanceId = "other", Label = "img3", Type = ResourceType.Image, SizeBytes = 9999 }
);
db.SaveChanges();
var result = BuildController(db).GetQuota("i1");
var ok = Assert.IsType<OkObjectResult>(result);
var dto = Assert.IsType<InstanceQuotaDTO>(ok.Value);
Assert.Equal(3000, dto.storageUsedBytes);
}
[Fact]
public void GetQuota_AiUsage_CurrentMonth_ReturnsCount()
{
using var db = DbContextFactory.Create();
var monthKey = DateTime.UtcNow.ToString("yyyy-MM");
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow,
AiRequestsThisMonth = 7, AiUsageMonthKey = monthKey
});
db.SaveChanges();
var result = BuildController(db).GetQuota("i1");
var ok = Assert.IsType<OkObjectResult>(result);
var dto = Assert.IsType<InstanceQuotaDTO>(ok.Value);
Assert.Equal(7, dto.aiRequestsUsed);
}
[Fact]
public void GetQuota_AiUsage_PreviousMonth_ReturnsZero()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance
{
Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow,
AiRequestsThisMonth = 7, AiUsageMonthKey = "2020-01"
});
db.SaveChanges();
var result = BuildController(db).GetQuota("i1");
var ok = Assert.IsType<OkObjectResult>(result);
var dto = Assert.IsType<InstanceQuotaDTO>(ok.Value);
Assert.Equal(0, dto.aiRequestsUsed);
}
// ── DELETE ───────────────────────────────────────────────────────────
[Fact]
public void DeleteInstance_CascadesUsers()
{
using var db = DbContextFactory.Create();
db.Instances.Add(new Instance { Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow });
db.Users.AddRange(
new User { Id = "u1", Email = "a@test.be", Password = "x", LastName = "A", Token = "t1", InstanceId = "i1" },
new User { Id = "u2", Email = "b@test.be", Password = "x", LastName = "B", Token = "t2", InstanceId = "i1" }
);
db.SaveChanges();
var result = BuildController(db).DeleteInstance("i1");
var status = Assert.IsType<ObjectResult>(result);
Assert.Equal(202, status.StatusCode);
Assert.Equal(0, db.Instances.Count());
Assert.Equal(0, db.Users.Count());
}
[Fact]
public void DeleteInstance_UnknownId_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).DeleteInstance("unknown");
Assert.IsType<NotFoundObjectResult>(result);
}
}
}

View File

@ -0,0 +1,144 @@
using Manager.Services;
using ManagerService.Controllers;
using ManagerService.Data;
using ManagerService.DTOs;
using ManagerService.Services;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class ResourceControllerTests
{
private static ResourceController BuildController(MyInfoMateDbContext db)
{
var cfg = FakeMongoConfig.Create();
var controller = new ResourceController(
NullLogger<ResourceController>.Instance,
new ResourceDatabaseService(cfg),
new SectionDatabaseService(cfg),
new ConfigurationDatabaseService(cfg),
db);
FakeUser.SetUser(controller, FakeUser.Create("Manager.contenteditor", "inst-test"));
return controller;
}
// ── GET ──────────────────────────────────────────────────────────────
[Fact]
public void Get_FiltersToInstance()
{
using var db = DbContextFactory.Create();
db.Resources.AddRange(
new Resource { Id = "r1", InstanceId = "inst-test", Label = "A", Type = ResourceType.Image },
new Resource { Id = "r2", InstanceId = "other-inst", Label = "B", Type = ResourceType.Image }
);
db.SaveChanges();
var result = BuildController(db).Get("inst-test", new List<ResourceType>());
var ok = Assert.IsType<OkObjectResult>(result);
var resources = (ok.Value as System.Collections.IEnumerable)!.Cast<object>().ToList();
Assert.Single(resources);
}
[Fact]
public void Get_WithTypeFilter_FiltersType()
{
using var db = DbContextFactory.Create();
db.Resources.AddRange(
new Resource { Id = "r1", InstanceId = "inst-test", Label = "A", Type = ResourceType.Image },
new Resource { Id = "r2", InstanceId = "inst-test", Label = "B", Type = ResourceType.Video }
);
db.SaveChanges();
var result = BuildController(db).Get("inst-test", new List<ResourceType> { ResourceType.Image });
var ok = Assert.IsType<OkObjectResult>(result);
var resources = (ok.Value as System.Collections.IEnumerable)!.Cast<object>().ToList();
Assert.Single(resources);
}
// ── UPDATE ───────────────────────────────────────────────────────────
[Fact]
public void Update_ExistingResource_UpdatesLabel()
{
using var db = DbContextFactory.Create();
db.Resources.Add(new Resource { Id = "r1", InstanceId = "inst-test", Label = "Old", Type = ResourceType.Image });
db.SaveChanges();
var result = BuildController(db).Update(new ResourceDTO { id = "r1", label = "New Label" });
Assert.IsType<OkObjectResult>(result);
Assert.Equal("New Label", db.Resources.First().Label);
}
[Fact]
public void Update_UnknownResource_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Update(new ResourceDTO { id = "unknown", label = "X" });
Assert.IsType<NotFoundObjectResult>(result);
}
// ── DELETE ───────────────────────────────────────────────────────────
[Fact]
public void Delete_NullifiesConfigurationImageId()
{
using var db = DbContextFactory.Create();
db.Resources.Add(new Resource { Id = "r1", InstanceId = "inst-test", Label = "Img", Type = ResourceType.Image });
db.Configurations.Add(new Configuration { Id = "c1", InstanceId = "inst-test", Label = "C", ImageId = "r1", Title = new List<TranslationDTO>() });
db.SaveChanges();
BuildController(db).Delete("r1");
Assert.Null(db.Configurations.First().ImageId);
}
[Fact]
public void Delete_NullifiesSectionImageId()
{
using var db = DbContextFactory.Create();
db.Resources.Add(new Resource { Id = "r1", InstanceId = "inst-test", Label = "Img", Type = ResourceType.Image });
db.Configurations.Add(new Configuration { Id = "c1", InstanceId = "inst-test", Label = "C", Title = new List<TranslationDTO>() });
db.Sections.Add(new Section { Id = "s1", InstanceId = "inst-test", Label = "S", ImageId = "r1", Type = SectionType.Article, ConfigurationId = "c1", Title = new List<TranslationDTO>(), Description = new List<TranslationDTO>() });
db.SaveChanges();
BuildController(db).Delete("r1");
Assert.Null(db.Sections.First().ImageId);
}
[Fact]
public void Delete_UnknownResource_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Delete("unknown");
Assert.IsType<NotFoundObjectResult>(result);
}
[Fact]
public void Delete_ExistingResource_Returns202()
{
using var db = DbContextFactory.Create();
db.Resources.Add(new Resource { Id = "r1", InstanceId = "inst-test", Label = "A", Type = ResourceType.Image });
db.SaveChanges();
var result = BuildController(db).Delete("r1");
var obj = Assert.IsType<ObjectResult>(result);
Assert.Equal(202, obj.StatusCode);
Assert.Equal(0, db.Resources.Count());
}
}
}

View File

@ -0,0 +1,195 @@
using ManagerService.Controllers;
using ManagerService.Data;
using ManagerService.Data.SubSection;
using ManagerService.DTOs;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class SectionAgendaControllerTests
{
private SectionAgendaController BuildController(MyInfoMateDbContext db)
{
var cfg = new ConfigurationBuilder().Build();
return new SectionAgendaController(cfg, NullLogger<SectionAgendaController>.Instance, db);
}
private SectionAgenda MakeSection(MyInfoMateDbContext db, string id = "sa1")
{
var section = new SectionAgenda
{
Id = id,
Label = "Agenda test",
Title = new List<TranslationDTO>(),
Description = new List<TranslationDTO>(),
ConfigurationId = "conf1",
InstanceId = "inst1",
Type = SectionType.Agenda,
AgendaResourceIds = new List<TranslationDTO>(),
EventAgendas = new List<EventAgenda>()
};
db.Sections.Add(section);
return section;
}
private EventAgenda MakeEvent(string sectionId, DateTime? dateFrom)
{
return new EventAgenda
{
Label = new List<TranslationDTO>(),
Description = new List<TranslationDTO>(),
SectionAgendaId = sectionId,
DateFrom = dateFrom
};
}
// ── GET ALL ──────────────────────────────────────────────────────────
[Fact]
public void GetAllEventAgendaFromSection_UnknownSection_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).GetAllEventAgendaFromSection("unknown");
Assert.IsType<NotFoundObjectResult>(result);
}
[Fact]
public void GetAllEventAgendaFromSection_ReturnsAllEvents()
{
using var db = DbContextFactory.Create();
MakeSection(db);
db.EventAgendas.Add(MakeEvent("sa1", DateTime.Today.AddDays(-5)));
db.EventAgendas.Add(MakeEvent("sa1", DateTime.Today.AddDays(5)));
db.SaveChanges();
var result = BuildController(db).GetAllEventAgendaFromSection("sa1");
var ok = Assert.IsType<OkObjectResult>(result);
var events = Assert.IsAssignableFrom<IEnumerable<EventAgendaDTO>>(ok.Value);
Assert.Equal(2, events.Count());
}
// ── GET UPCOMING ─────────────────────────────────────────────────────
[Fact]
public void GetUpcomingEventAgendas_FiltersOutPastEvents()
{
using var db = DbContextFactory.Create();
MakeSection(db);
db.EventAgendas.Add(MakeEvent("sa1", DateTime.Today.AddDays(-1))); // passé → filtré
db.EventAgendas.Add(MakeEvent("sa1", DateTime.Today)); // aujourd'hui → inclus
db.EventAgendas.Add(MakeEvent("sa1", DateTime.Today.AddDays(1))); // futur → inclus
db.SaveChanges();
var result = BuildController(db).GetUpcomingEventAgendas("sa1");
var ok = Assert.IsType<OkObjectResult>(result);
var events = Assert.IsAssignableFrom<IEnumerable<EventAgendaDTO>>(ok.Value);
Assert.Equal(2, events.Count());
}
[Fact]
public void GetUpcomingEventAgendas_NullDateFrom_IsIncluded()
{
using var db = DbContextFactory.Create();
MakeSection(db);
db.EventAgendas.Add(MakeEvent("sa1", null)); // sans date → inclus
db.EventAgendas.Add(MakeEvent("sa1", DateTime.Today.AddDays(-1))); // passé → filtré
db.SaveChanges();
var result = BuildController(db).GetUpcomingEventAgendas("sa1");
var ok = Assert.IsType<OkObjectResult>(result);
var events = Assert.IsAssignableFrom<IEnumerable<EventAgendaDTO>>(ok.Value);
Assert.Equal(1, events.Count());
}
[Fact]
public void GetUpcomingEventAgendas_UnknownSection_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).GetUpcomingEventAgendas("unknown");
Assert.IsType<NotFoundObjectResult>(result);
}
// ── CREATE ───────────────────────────────────────────────────────────
[Fact]
public void CreateEventAgenda_ValidDto_Persists()
{
using var db = DbContextFactory.Create();
MakeSection(db);
db.SaveChanges();
var dto = new EventAgendaDTO
{
label = new List<TranslationDTO>(),
description = new List<TranslationDTO>(),
sectionAgendaId = "sa1"
};
var result = BuildController(db).CreateEventAgenda("sa1", dto);
Assert.IsType<OkObjectResult>(result);
Assert.Equal(1, db.EventAgendas.Count());
}
[Fact]
public void CreateEventAgenda_UnknownSection_Returns500()
{
using var db = DbContextFactory.Create();
var dto = new EventAgendaDTO
{
label = new List<TranslationDTO>(),
description = new List<TranslationDTO>(),
sectionAgendaId = "unknown"
};
// La section n'existe pas → KeyNotFoundException → ObjectResult 500
var result = BuildController(db).CreateEventAgenda("unknown", dto);
var status = Assert.IsType<ObjectResult>(result);
Assert.Equal(500, status.StatusCode);
}
// ── DELETE ───────────────────────────────────────────────────────────
[Fact]
public void DeleteEventAgenda_ExistingEvent_Returns202()
{
using var db = DbContextFactory.Create();
MakeSection(db);
var ev = MakeEvent("sa1", null);
db.EventAgendas.Add(ev);
db.SaveChanges();
var id = ev.Id;
var result = BuildController(db).DeleteEventAgenda(id);
var status = Assert.IsType<ObjectResult>(result);
Assert.Equal(202, status.StatusCode);
Assert.Equal(0, db.EventAgendas.Count());
}
[Fact]
public void DeleteEventAgenda_UnknownId_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).DeleteEventAgenda(9999);
Assert.IsType<NotFoundObjectResult>(result);
}
}
}

View File

@ -0,0 +1,125 @@
using Manager.Services;
using ManagerService.Controllers;
using ManagerService.Data;
using ManagerService.DTOs;
using ManagerService.Services;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class SectionControllerTests
{
private static IConfiguration BuildConfig() =>
new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["ConnectionStrings:TabletDb"] = "mongodb://localhost:27017",
["SupportedLanguages:0"] = "FR",
["SupportedLanguages:1"] = "EN"
})
.Build();
private static SectionController BuildController(MyInfoMateDbContext db)
{
var cfg = FakeMongoConfig.Create();
var controller = new SectionController(
BuildConfig(),
NullLogger<SectionController>.Instance,
new SectionDatabaseService(cfg),
new ConfigurationDatabaseService(cfg),
db);
FakeUser.SetUser(controller, FakeUser.Create("Manager.contenteditor", "inst-test"));
return controller;
}
// ── GET ──────────────────────────────────────────────────────────────
[Fact]
public void Get_FiltersToInstance()
{
using var db = DbContextFactory.Create();
db.Sections.AddRange(
new Section { Id = "s1", InstanceId = "inst-test", Label = "A", Type = SectionType.Article, ConfigurationId = "c1", Title = new List<TranslationDTO>(), Description = new List<TranslationDTO>() },
new Section { Id = "s2", InstanceId = "other-inst", Label = "B", Type = SectionType.Article, ConfigurationId = "c2", Title = new List<TranslationDTO>(), Description = new List<TranslationDTO>() }
);
db.SaveChanges();
var result = BuildController(db).Get("inst-test");
var ok = Assert.IsType<OkObjectResult>(result);
var sections = (ok.Value as System.Collections.IEnumerable)!.Cast<object>().ToList();
Assert.Single(sections);
}
// ── CREATE ───────────────────────────────────────────────────────────
[Fact]
public void Create_ValidDto_Persists()
{
using var db = DbContextFactory.Create();
db.Configurations.Add(new Configuration { Id = "c1", InstanceId = "inst-test", Label = "Conf", Title = new List<TranslationDTO>() });
db.SaveChanges();
var result = BuildController(db).Create(new SectionDTO
{
instanceId = "inst-test",
configurationId = "c1",
label = "Section 1",
type = SectionType.Video
});
Assert.IsType<OkObjectResult>(result);
Assert.Equal(1, db.Sections.Count());
}
[Fact]
public void Create_UnknownConfiguration_ReturnsError()
{
using var db = DbContextFactory.Create();
// KeyNotFoundException → caught by catch(Exception) → 500
var result = BuildController(db).Create(new SectionDTO
{
instanceId = "inst-test",
configurationId = "nonexistent",
label = "Section 1",
type = SectionType.Video
});
var obj = Assert.IsType<ObjectResult>(result);
Assert.Equal(500, obj.StatusCode);
}
// ── DELETE ───────────────────────────────────────────────────────────
[Fact]
public void Delete_ExistingSection_Returns202()
{
using var db = DbContextFactory.Create();
db.Sections.Add(new Section { Id = "s1", InstanceId = "inst-test", Label = "A", Type = SectionType.Article, ConfigurationId = "c1", Title = new List<TranslationDTO>(), Description = new List<TranslationDTO>() });
db.SaveChanges();
var result = BuildController(db).Delete("s1");
var obj = Assert.IsType<ObjectResult>(result);
Assert.Equal(202, obj.StatusCode);
Assert.Equal(0, db.Sections.Count());
}
[Fact]
public void Delete_UnknownSection_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Delete("unknown");
Assert.IsType<NotFoundObjectResult>(result);
}
}
}

View File

@ -0,0 +1,186 @@
using Manager.DTOs;
using ManagerService.Controllers;
using ManagerService.Data;
using ManagerService.Data.SubSection;
using ManagerService.DTOs;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
using static ManagerService.Data.SubSection.SectionEvent;
namespace ManagerService.Tests.Controllers
{
public class SectionEventControllerTests
{
private static SectionEventController BuildController(MyInfoMateDbContext db)
{
var controller = new SectionEventController(
FakeMongoConfig.Create(),
NullLogger<SectionEventController>.Instance,
db);
FakeUser.SetUser(controller, FakeUser.Create("Manager.contenteditor", "inst-test"));
return controller;
}
private static List<TranslationDTO> EmptyTranslations() => new List<TranslationDTO>();
// ── PROGRAMME BLOCK ──────────────────────────────────────────────────
[Fact]
public void CreateProgrammeBlock_ValidRequest_Persists()
{
using var db = DbContextFactory.Create();
db.Sections.Add(new SectionEvent
{
Id = "se-1",
InstanceId = "inst-test",
Label = "Event",
Type = SectionType.Event,
ConfigurationId = "c1",
Title = EmptyTranslations(),
Description = EmptyTranslations(),
Programme = new List<ProgrammeBlock>(),
ParcoursIds = new List<string>()
});
db.SaveChanges();
var result = BuildController(db).CreateProgrammeBlock("se-1", new ProgrammeBlockDTO
{
title = EmptyTranslations(),
description = EmptyTranslations(),
startTime = DateTime.UtcNow,
endTime = DateTime.UtcNow.AddHours(1)
});
Assert.IsType<OkObjectResult>(result);
Assert.Equal(1, db.ProgrammeBlocks.Count());
}
[Fact]
public void CreateProgrammeBlock_UnknownSection_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).CreateProgrammeBlock("unknown", new ProgrammeBlockDTO
{
title = EmptyTranslations(),
startTime = DateTime.UtcNow,
endTime = DateTime.UtcNow.AddHours(1)
});
// KeyNotFoundException attrapée par catch(Exception) → 500
var obj = Assert.IsType<ObjectResult>(result);
Assert.Equal(500, obj.StatusCode);
}
[Fact]
public void UpdateProgrammeBlock_ExistingBlock_UpdatesFields()
{
using var db = DbContextFactory.Create();
db.ProgrammeBlocks.Add(new ProgrammeBlock
{
Id = "pb-1",
Title = EmptyTranslations(),
Description = EmptyTranslations(),
StartTime = DateTime.UtcNow,
EndTime = DateTime.UtcNow.AddHours(1),
MapAnnotations = new List<MapAnnotation>()
});
db.SaveChanges();
var newStart = DateTime.UtcNow.AddHours(2);
var result = BuildController(db).UpdateProgrammeBlock(new ProgrammeBlockDTO
{
id = "pb-1",
title = EmptyTranslations(),
description = EmptyTranslations(),
startTime = newStart,
endTime = newStart.AddHours(1)
});
Assert.IsType<OkObjectResult>(result);
Assert.Equal(newStart, db.ProgrammeBlocks.First().StartTime);
}
[Fact]
public void DeleteProgrammeBlock_ExistingBlock_Returns202()
{
using var db = DbContextFactory.Create();
db.ProgrammeBlocks.Add(new ProgrammeBlock
{
Id = "pb-1",
Title = EmptyTranslations(),
Description = EmptyTranslations(),
StartTime = DateTime.UtcNow,
EndTime = DateTime.UtcNow.AddHours(1),
MapAnnotations = new List<MapAnnotation>()
});
db.SaveChanges();
var result = BuildController(db).DeleteProgrammeBlock("pb-1");
var obj = Assert.IsType<ObjectResult>(result);
Assert.Equal(202, obj.StatusCode);
Assert.Equal(0, db.ProgrammeBlocks.Count());
}
// ── MAP ANNOTATION ───────────────────────────────────────────────────
[Fact]
public void CreateMapAnnotation_ValidRequest_Persists()
{
using var db = DbContextFactory.Create();
db.ProgrammeBlocks.Add(new ProgrammeBlock
{
Id = "pb-1",
Title = EmptyTranslations(),
Description = EmptyTranslations(),
StartTime = DateTime.UtcNow,
EndTime = DateTime.UtcNow.AddHours(1),
MapAnnotations = new List<MapAnnotation>()
});
db.SaveChanges();
var result = BuildController(db).CreateMapAnnotation("pb-1", new MapAnnotationDTO
{
label = EmptyTranslations(),
type = EmptyTranslations(),
geometryType = GeometryType.Point,
geometry = new GeometryDTO { type = "Point", coordinates = new List<double> { 4.35, 50.85 } }
});
Assert.IsType<OkObjectResult>(result);
Assert.Equal(1, db.MapAnnotations.Count());
}
[Fact]
public void UpdateMapAnnotation_ExistingAnnotation_UpdatesLabel()
{
using var db = DbContextFactory.Create();
db.MapAnnotations.Add(new MapAnnotation
{
Id = "ma-1",
Label = EmptyTranslations(),
Type = EmptyTranslations()
});
db.SaveChanges();
var newLabel = new List<TranslationDTO> { new TranslationDTO { language = "FR", value = "Updated" } };
var result = BuildController(db).UpdateMapAnnotation(new MapAnnotationDTO
{
id = "ma-1",
label = newLabel,
type = EmptyTranslations(),
geometryType = GeometryType.Point,
geometry = null // UpdateMapAnnotation handles null geometry
});
Assert.IsType<OkObjectResult>(result);
Assert.Equal("Updated", db.MapAnnotations.First().Label.First().value);
}
}
}

View File

@ -0,0 +1,142 @@
using Manager.DTOs;
using ManagerService.Controllers;
using ManagerService.Data;
using ManagerService.Data.SubSection;
using ManagerService.DTOs;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class SectionMapControllerTests
{
private static SectionMapController BuildController(MyInfoMateDbContext db)
{
var controller = new SectionMapController(
FakeMongoConfig.Create(),
NullLogger<SectionMapController>.Instance,
db);
FakeUser.SetUser(controller, FakeUser.Create("Manager.contenteditor", "inst-test"));
return controller;
}
private static GeoPoint EmptyGeoPoint() => new GeoPoint
{
Title = new List<TranslationDTO>(),
Description = new List<TranslationDTO>(),
Contents = new List<ContentDTO>(),
Schedules = new List<TranslationDTO>(),
Prices = new List<TranslationDTO>(),
Phone = new List<TranslationDTO>(),
Email = new List<TranslationDTO>(),
Site = new List<TranslationDTO>()
};
private static GeoPointDTO EmptyGeoPointDTO() => new GeoPointDTO
{
title = new List<TranslationDTO>(),
description = new List<TranslationDTO>(),
contents = new List<ContentDTO>(),
schedules = new List<TranslationDTO>(),
prices = new List<TranslationDTO>(),
phone = new List<TranslationDTO>(),
email = new List<TranslationDTO>(),
site = new List<TranslationDTO>()
};
// ── GET POINTS ───────────────────────────────────────────────────────
[Fact]
public void GetAllGeoPoints_ReturnsPointsForSection()
{
using var db = DbContextFactory.Create();
var section = new SectionMap
{
Id = "sm-1",
InstanceId = "inst-test",
Label = "Map",
Type = SectionType.Map,
ConfigurationId = "c1",
Title = new List<TranslationDTO>(),
Description = new List<TranslationDTO>(),
MapPoints = new List<GeoPoint> { EmptyGeoPoint(), EmptyGeoPoint() },
MapCategories = new List<CategorieDTO>()
};
db.Sections.Add(section);
db.SaveChanges();
var result = BuildController(db).GetAllGeoPointsFromSection("sm-1");
var ok = Assert.IsType<OkObjectResult>(result);
var points = Assert.IsAssignableFrom<IEnumerable<GeoPointDTO>>(ok.Value);
Assert.Equal(2, points.Count());
}
[Fact]
public void GetAllGeoPoints_UnknownSection_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).GetAllGeoPointsFromSection("unknown");
Assert.IsType<NotFoundObjectResult>(result);
}
// ── CREATE POINT ─────────────────────────────────────────────────────
[Fact]
public void Create_ValidPoint_Persists()
{
using var db = DbContextFactory.Create();
db.Sections.Add(new SectionMap
{
Id = "sm-1",
InstanceId = "inst-test",
Label = "Map",
Type = SectionType.Map,
ConfigurationId = "c1",
Title = new List<TranslationDTO>(),
Description = new List<TranslationDTO>(),
MapPoints = new List<GeoPoint>(),
MapCategories = new List<CategorieDTO>()
});
db.SaveChanges();
var result = BuildController(db).Create("sm-1", EmptyGeoPointDTO());
Assert.IsType<OkObjectResult>(result);
Assert.Equal(1, db.GeoPoints.Count());
}
// ── DELETE POINT ─────────────────────────────────────────────────────
[Fact]
public void Delete_ExistingGeoPoint_Returns202()
{
using var db = DbContextFactory.Create();
db.GeoPoints.Add(EmptyGeoPoint());
db.SaveChanges();
int id = (int)db.GeoPoints.First().Id!;
var result = BuildController(db).Delete(id);
var obj = Assert.IsType<ObjectResult>(result);
Assert.Equal(202, obj.StatusCode);
Assert.Equal(0, db.GeoPoints.Count());
}
[Fact]
public void Delete_UnknownGeoPoint_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Delete(999);
Assert.IsType<NotFoundObjectResult>(result);
}
}
}

View File

@ -0,0 +1,195 @@
using Manager.DTOs;
using ManagerService.Controllers;
using ManagerService.Data;
using ManagerService.Data.SubSection;
using ManagerService.DTOs;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class SectionQuizControllerTests
{
private SectionQuizController BuildController(MyInfoMateDbContext db)
{
var cfg = new ConfigurationBuilder().Build();
return new SectionQuizController(cfg, NullLogger<SectionQuizController>.Instance, db);
}
private SectionQuiz MakeQuizSection(string id = "sq1")
{
return new SectionQuiz
{
Id = id,
Label = "Quiz test",
Title = new List<TranslationDTO>(),
Description = new List<TranslationDTO>(),
ConfigurationId = "conf1",
InstanceId = "inst1",
Type = SectionType.Quiz,
QuizQuestions = new List<QuizQuestion>(),
QuizBadLevel = new List<TranslationAndResourceDTO>(),
QuizMediumLevel = new List<TranslationAndResourceDTO>(),
QuizGoodLevel = new List<TranslationAndResourceDTO>(),
QuizGreatLevel = new List<TranslationAndResourceDTO>()
};
}
private QuizQuestion MakeQuestion(string sectionId, int order, string? label = null)
{
return new QuizQuestion
{
SectionQuizId = sectionId,
Order = order,
Label = new List<TranslationAndResourceDTO>(),
Responses = new List<ResponseDTO>()
};
}
// ── CREATE ───────────────────────────────────────────────────────────
[Fact]
public void Create_AutoSetsOrderToCount()
{
using var db = DbContextFactory.Create();
var section = MakeQuizSection();
db.Sections.Add(section);
// 2 questions déjà dans la section
db.QuizQuestions.AddRange(
MakeQuestion("sq1", 0),
MakeQuestion("sq1", 1)
);
db.SaveChanges();
var dto = new QuestionDTO
{
label = new List<TranslationAndResourceDTO>(),
responses = new List<ResponseDTO>()
};
var result = BuildController(db).Create("sq1", dto);
var ok = Assert.IsType<OkObjectResult>(result);
// La nouvelle question doit avoir l'ordre = 2 (count avant ajout)
var question = db.QuizQuestions.OrderByDescending(q => q.Order).First();
Assert.Equal(2, question.Order);
}
[Fact]
public void Create_UnknownSection_Returns500()
{
using var db = DbContextFactory.Create();
var dto = new QuestionDTO
{
label = new List<TranslationAndResourceDTO>(),
responses = new List<ResponseDTO>()
};
var result = BuildController(db).Create("unknown", dto);
var status = Assert.IsType<ObjectResult>(result);
Assert.Equal(500, status.StatusCode);
}
// ── UPDATE / REORDER ─────────────────────────────────────────────────
[Fact]
public void Update_ChangeOrder_ReordersAllQuestionsFrom0()
{
using var db = DbContextFactory.Create();
var section = MakeQuizSection();
db.Sections.Add(section);
var q0 = MakeQuestion("sq1", 0);
var q1 = MakeQuestion("sq1", 1);
var q2 = MakeQuestion("sq1", 2);
db.QuizQuestions.AddRange(q0, q1, q2);
db.SaveChanges();
// Déplace q2 (order=2) à la position 0
var dto = new QuestionDTO
{
id = q2.Id,
order = 0,
label = new List<TranslationAndResourceDTO>(),
responses = new List<ResponseDTO>()
};
BuildController(db).Update(dto);
var questions = db.QuizQuestions.OrderBy(q => q.Order).ToList();
// Après réordonnancement : q2 doit être en position 0
Assert.Equal(q2.Id, questions[0].Id);
// Les ordres doivent être continus depuis 0
Assert.Equal(new[] { 0, 1, 2 }, questions.Select(q => q.Order).ToArray());
}
[Fact]
public void Update_SameOrder_UpdatesFieldsWithoutReorder()
{
using var db = DbContextFactory.Create();
var section = MakeQuizSection();
db.Sections.Add(section);
var q0 = MakeQuestion("sq1", 0);
db.QuizQuestions.Add(q0);
db.SaveChanges();
var dto = new QuestionDTO
{
id = q0.Id,
order = 0, // même ordre
label = new List<TranslationAndResourceDTO> { new TranslationAndResourceDTO { language = "fr", value = "Nouvelle question" } },
responses = new List<ResponseDTO>()
};
BuildController(db).Update(dto);
// Le label est mis à jour (via l'objet retourné — la valeur est dans le DTO en retour)
// On vérifie juste que ça s'est exécuté sans erreur
Assert.Equal(1, db.QuizQuestions.Count());
}
[Fact]
public void Update_UnknownQuestion_Returns404()
{
using var db = DbContextFactory.Create();
var dto = new QuestionDTO { id = 9999, order = 0, label = new List<TranslationAndResourceDTO>(), responses = new List<ResponseDTO>() };
var result = BuildController(db).Update(dto);
Assert.IsType<NotFoundObjectResult>(result);
}
// ── DELETE ───────────────────────────────────────────────────────────
[Fact]
public void Delete_ExistingQuestion_Returns202()
{
using var db = DbContextFactory.Create();
var section = MakeQuizSection();
db.Sections.Add(section);
var q = MakeQuestion("sq1", 0);
db.QuizQuestions.Add(q);
db.SaveChanges();
var qId = q.Id;
var result = BuildController(db).Delete(qId);
var status = Assert.IsType<ObjectResult>(result);
Assert.Equal(202, status.StatusCode);
Assert.Equal(0, db.QuizQuestions.Count());
}
[Fact]
public void Delete_UnknownId_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Delete(9999);
Assert.IsType<NotFoundObjectResult>(result);
}
}
}

View File

@ -0,0 +1,179 @@
using ManagerService.Controllers;
using ManagerService.Data;
using ManagerService.DTOs;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class StatsControllerTests
{
private StatsController BuildController(MyInfoMateDbContext db) => new StatsController(db);
// ── TRACK EVENT ──────────────────────────────────────────────────────
[Fact]
public void TrackEvent_ValidDto_PersistsAndReturns204()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).TrackEvent(new VisitEventDTO
{
instanceId = "i1",
sessionId = "s1",
eventType = "SectionView",
appType = "Mobile"
});
Assert.IsType<NoContentResult>(result);
Assert.Equal(1, db.VisitEvents.Count());
}
[Fact]
public void TrackEvent_MissingInstanceId_Returns400()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).TrackEvent(new VisitEventDTO
{
instanceId = "",
eventType = "SectionView",
appType = "Mobile"
});
Assert.IsType<BadRequestObjectResult>(result);
}
[Fact]
public void TrackEvent_UnknownEventType_Returns400()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).TrackEvent(new VisitEventDTO
{
instanceId = "i1",
eventType = "InvalidType",
appType = "Mobile"
});
Assert.IsType<BadRequestObjectResult>(result);
}
// ── GET SUMMARY ──────────────────────────────────────────────────────
[Fact]
public void GetSummary_MissingInstanceId_Returns400()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).GetSummary("", null, null, null);
Assert.IsType<BadRequestObjectResult>(result);
}
[Fact]
public void GetSummary_FiltersToInstance()
{
using var db = DbContextFactory.Create();
db.VisitEvents.AddRange(
new VisitEvent { Id = "e1", InstanceId = "i1", SessionId = "s1", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow },
new VisitEvent { Id = "e2", InstanceId = "other", SessionId = "s2", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow }
);
db.SaveChanges();
var result = BuildController(db).GetSummary("i1", null, null, null);
var ok = Assert.IsType<OkObjectResult>(result);
var summary = Assert.IsType<StatsSummaryDTO>(ok.Value);
Assert.Equal(1, summary.TotalSessions);
}
[Fact]
public void GetSummary_CountsDistinctSessions()
{
using var db = DbContextFactory.Create();
db.VisitEvents.AddRange(
new VisitEvent { Id = "e1", InstanceId = "i1", SessionId = "session-A", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow },
new VisitEvent { Id = "e2", InstanceId = "i1", SessionId = "session-A", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow },
new VisitEvent { Id = "e3", InstanceId = "i1", SessionId = "session-B", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow }
);
db.SaveChanges();
var result = BuildController(db).GetSummary("i1", null, null, null);
var ok = Assert.IsType<OkObjectResult>(result);
var summary = Assert.IsType<StatsSummaryDTO>(ok.Value);
Assert.Equal(2, summary.TotalSessions);
}
[Fact]
public void GetSummary_AppliesDateRange()
{
using var db = DbContextFactory.Create();
var inRange = DateTime.UtcNow.AddDays(-5);
var outOfRange = DateTime.UtcNow.AddDays(-40);
db.VisitEvents.AddRange(
new VisitEvent { Id = "e1", InstanceId = "i1", SessionId = "s1", EventType = VisitEventType.SectionView, Timestamp = inRange },
new VisitEvent { Id = "e2", InstanceId = "i1", SessionId = "s2", EventType = VisitEventType.SectionView, Timestamp = outOfRange }
);
db.SaveChanges();
var from = DateTime.UtcNow.AddDays(-10);
var to = DateTime.UtcNow;
var result = BuildController(db).GetSummary("i1", from, to, null);
var ok = Assert.IsType<OkObjectResult>(result);
var summary = Assert.IsType<StatsSummaryDTO>(ok.Value);
Assert.Equal(1, summary.TotalSessions);
}
[Fact]
public void GetSummary_DefaultsTo30Days()
{
using var db = DbContextFactory.Create();
// Event dans les 30 jours
db.VisitEvents.Add(new VisitEvent
{
Id = "e1", InstanceId = "i1", SessionId = "s1",
EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow.AddDays(-5)
});
// Event au-delà des 30 jours
db.VisitEvents.Add(new VisitEvent
{
Id = "e2", InstanceId = "i1", SessionId = "s2",
EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow.AddDays(-35)
});
db.SaveChanges();
// Sans dates → filtre 30 derniers jours
var result = BuildController(db).GetSummary("i1", null, null, null);
var ok = Assert.IsType<OkObjectResult>(result);
var summary = Assert.IsType<StatsSummaryDTO>(ok.Value);
Assert.Equal(1, summary.TotalSessions);
}
[Fact]
public void GetSummary_TopSections_AggregatesByViewCount()
{
using var db = DbContextFactory.Create();
db.VisitEvents.AddRange(
new VisitEvent { Id = "e1", InstanceId = "i1", SessionId = "s1", EventType = VisitEventType.SectionView, SectionId = "sect-a", Timestamp = DateTime.UtcNow },
new VisitEvent { Id = "e2", InstanceId = "i1", SessionId = "s2", EventType = VisitEventType.SectionView, SectionId = "sect-a", Timestamp = DateTime.UtcNow },
new VisitEvent { Id = "e3", InstanceId = "i1", SessionId = "s3", EventType = VisitEventType.SectionView, SectionId = "sect-b", Timestamp = DateTime.UtcNow }
);
db.SaveChanges();
var result = BuildController(db).GetSummary("i1", null, null, null);
var ok = Assert.IsType<OkObjectResult>(result);
var summary = Assert.IsType<StatsSummaryDTO>(ok.Value);
Assert.Equal(2, summary.TopSections.Count);
Assert.Equal("sect-a", summary.TopSections.First().SectionId);
Assert.Equal(2, summary.TopSections.First().Views);
}
}
}

View File

@ -0,0 +1,169 @@
using ManagerService.Controllers;
using ManagerService.Data;
using ManagerService.DTOs;
using ManagerService.Tests.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace ManagerService.Tests.Controllers
{
public class SubscriptionPlanControllerTests
{
private SubscriptionPlanController BuildController(MyInfoMateDbContext db) =>
new SubscriptionPlanController(NullLogger<SubscriptionPlanController>.Instance, db);
// ── GET ──────────────────────────────────────────────────────────────
[Fact]
public void Get_ReturnsAllPlans()
{
using var db = DbContextFactory.Create();
db.SubscriptionPlans.AddRange(
new SubscriptionPlan { Id = "p1", Name = "Starter" },
new SubscriptionPlan { Id = "p2", Name = "Standard" }
);
db.SaveChanges();
var result = BuildController(db).Get();
var ok = Assert.IsType<OkObjectResult>(result);
var plans = Assert.IsAssignableFrom<IEnumerable<SubscriptionPlanDTO>>(ok.Value);
Assert.Equal(2, plans.Count());
}
[Fact]
public void GetById_ExistingId_ReturnsDto()
{
using var db = DbContextFactory.Create();
db.SubscriptionPlans.Add(new SubscriptionPlan
{
Id = "p1", Name = "Starter", StorageQuotaBytes = 1024, AiRequestsPerMonth = 50
});
db.SaveChanges();
var result = BuildController(db).GetById("p1");
var ok = Assert.IsType<OkObjectResult>(result);
var dto = Assert.IsType<SubscriptionPlanDTO>(ok.Value);
Assert.Equal("Starter", dto.name);
Assert.Equal(1024, dto.storageQuotaBytes);
Assert.Equal(50, dto.aiRequestsPerMonth);
}
[Fact]
public void GetById_UnknownId_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).GetById("unknown");
Assert.IsType<NotFoundObjectResult>(result);
}
// ── CREATE ───────────────────────────────────────────────────────────
[Fact]
public void Create_ValidDto_PersistsAndReturnsDto()
{
using var db = DbContextFactory.Create();
var dto = new SubscriptionPlanDTO { name = "Premium", storageQuotaBytes = 5000, aiRequestsPerMonth = 100 };
var result = BuildController(db).Create(dto);
var ok = Assert.IsType<OkObjectResult>(result);
var returned = Assert.IsType<SubscriptionPlanDTO>(ok.Value);
Assert.Equal("Premium", returned.name);
Assert.NotNull(returned.id);
Assert.Equal(1, db.SubscriptionPlans.Count());
}
[Fact]
public void Create_NullDto_Returns400()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Create(null!);
Assert.IsType<BadRequestObjectResult>(result);
}
// ── UPDATE ───────────────────────────────────────────────────────────
[Fact]
public void Update_ExistingPlan_UpdatesName()
{
using var db = DbContextFactory.Create();
db.SubscriptionPlans.Add(new SubscriptionPlan { Id = "p1", Name = "Old" });
db.SaveChanges();
var result = BuildController(db).Update(new SubscriptionPlanDTO { id = "p1", name = "New", storageQuotaBytes = 0, aiRequestsPerMonth = 0 });
Assert.IsType<OkObjectResult>(result);
Assert.Equal("New", db.SubscriptionPlans.First().Name);
}
[Fact]
public void Update_UnknownId_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Update(new SubscriptionPlanDTO { id = "unknown", name = "X" });
Assert.IsType<NotFoundObjectResult>(result);
}
[Fact]
public void Update_NullDto_Returns400()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Update(null!);
Assert.IsType<BadRequestObjectResult>(result);
}
// ── DELETE ───────────────────────────────────────────────────────────
[Fact]
public void Delete_UnusedPlan_Returns202()
{
using var db = DbContextFactory.Create();
db.SubscriptionPlans.Add(new SubscriptionPlan { Id = "p1", Name = "Starter" });
db.SaveChanges();
var result = BuildController(db).Delete("p1");
var statusResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(202, statusResult.StatusCode);
Assert.Equal(0, db.SubscriptionPlans.Count());
}
[Fact]
public void Delete_PlanUsedByInstance_Returns409()
{
using var db = DbContextFactory.Create();
db.SubscriptionPlans.Add(new SubscriptionPlan { Id = "p1", Name = "Starter" });
db.Instances.Add(new Instance
{
Id = "inst1", Name = "Musée", SubscriptionPlanId = "p1",
DateCreation = DateTime.UtcNow
});
db.SaveChanges();
var result = BuildController(db).Delete("p1");
Assert.IsType<ConflictObjectResult>(result);
}
[Fact]
public void Delete_UnknownId_Returns404()
{
using var db = DbContextFactory.Create();
var result = BuildController(db).Delete("unknown");
Assert.IsType<NotFoundObjectResult>(result);
}
}
}

View File

@ -0,0 +1,185 @@
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 Xunit;
namespace ManagerService.Tests.Controllers
{
public class UserControllerTests
{
private UserController BuildController(MyInfoMateDbContext db, string callerRole = "Manager.superadmin", string callerInstanceId = "inst-test")
{
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);
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 void 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 = BuildController(db).CreateUser(MakeNewUser("dup@a.be"));
Assert.IsType<ConflictObjectResult>(result);
}
[Fact]
public void CreateUser_InstanceAdminCreatingInstanceAdmin_Returns403()
{
// InstanceAdmin (role=1) ne peut pas créer SuperAdmin (role=0)
using var db = DbContextFactory.Create();
var result = 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 void CreateUser_ValidDto_HashesPassword()
{
using var db = DbContextFactory.Create();
var result = 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 void CreateUser_NullPassword_Returns400()
{
using var db = DbContextFactory.Create();
var dto = new UserDetailDTO { email = "x@x.be", lastName = "X", instanceId = "inst-test", password = null };
var result = BuildController(db).CreateUser(dto);
Assert.IsType<BadRequestObjectResult>(result);
}
// ── 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);
}
}
}

View File

@ -0,0 +1,17 @@
using ManagerService.Data;
using Microsoft.EntityFrameworkCore;
namespace ManagerService.Tests.Infrastructure
{
public static class DbContextFactory
{
public static MyInfoMateDbContext Create()
{
var options = new DbContextOptionsBuilder<MyInfoMateDbContext>()
.UseInMemoryDatabase(System.Guid.NewGuid().ToString())
.Options;
return new MyInfoMateDbContext(options);
}
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
namespace ManagerService.Tests.Infrastructure
{
/// <summary>
/// IConfiguration avec une fausse connexion MongoDB pour instancier les
/// DatabaseService dont le code actif des contrôleurs ne se sert plus
/// (remplacé par EF Core), mais dont le constructeur en a besoin.
/// MongoClient est lazy : aucune connexion réelle ne sera tentée.
/// </summary>
public static class FakeMongoConfig
{
public static IConfiguration Create() =>
new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["ConnectionStrings:TabletDb"] = "mongodb://localhost:27017"
})
.Build();
}
}

View File

@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Security.Claims;
namespace ManagerService.Tests.Infrastructure
{
// Constantes reproduisant ManagerService.Service.Security (internal)
public static class Permissions
{
public const string SuperAdmin = "Manager.superadmin";
public const string InstanceAdmin = "Manager.instanceadmin";
public const string ContentEditor = "Manager.contenteditor";
public const string Viewer = "Manager.viewer";
}
public static class ClaimTypeKeys
{
public const string Permission = "Permission";
public const string InstanceId = "InstanceId";
}
public static class FakeUser
{
public static ClaimsPrincipal Create(string role, string instanceId = "inst-test")
{
var claims = new List<Claim>
{
new Claim(ClaimTypeKeys.Permission, role),
new Claim(ClaimTypeKeys.InstanceId, instanceId)
};
// Rôles cumulatifs : SuperAdmin hérite de tout
if (role == Permissions.SuperAdmin)
{
claims.Add(new Claim(ClaimTypeKeys.Permission, Permissions.InstanceAdmin));
claims.Add(new Claim(ClaimTypeKeys.Permission, Permissions.ContentEditor));
claims.Add(new Claim(ClaimTypeKeys.Permission, Permissions.Viewer));
}
else if (role == Permissions.InstanceAdmin)
{
claims.Add(new Claim(ClaimTypeKeys.Permission, Permissions.ContentEditor));
claims.Add(new Claim(ClaimTypeKeys.Permission, Permissions.Viewer));
}
else if (role == Permissions.ContentEditor)
{
claims.Add(new Claim(ClaimTypeKeys.Permission, Permissions.Viewer));
}
return new ClaimsPrincipal(new ClaimsIdentity(claims, "test"));
}
public static void SetUser(ControllerBase controller, ClaimsPrincipal user)
{
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext { User = user }
};
}
}
}

View File

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.2" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.3" />
<PackageReference Include="MongoDB.Driver" Version="2.19.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ManagerService\ManagerService.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>

View File

@ -5,6 +5,8 @@ VisualStudioVersion = 17.12.35527.113 d17.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagerService", "ManagerService\ManagerService.csproj", "{042E0BC4-8DCF-4EEC-8420-C71AA85D4D99}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagerService.Tests", "ManagerService.Tests\ManagerService.Tests.csproj", "{C95DA377-C16F-457E-AC51-C8CCC7BC562D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -15,6 +17,10 @@ Global
{042E0BC4-8DCF-4EEC-8420-C71AA85D4D99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{042E0BC4-8DCF-4EEC-8420-C71AA85D4D99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{042E0BC4-8DCF-4EEC-8420-C71AA85D4D99}.Release|Any CPU.Build.0 = Release|Any CPU
{C95DA377-C16F-457E-AC51-C8CCC7BC562D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C95DA377-C16F-457E-AC51-C8CCC7BC562D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C95DA377-C16F-457E-AC51-C8CCC7BC562D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C95DA377-C16F-457E-AC51-C8CCC7BC562D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("ManagerService.Tests")]

View File

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NSwag.Annotations;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;
@ -16,12 +17,12 @@ namespace ManagerService.Controllers
[OpenApiTag("AI", Description = "Assistant IA")]
public class AiController : ControllerBase
{
private readonly AssistantService _assistantService;
private readonly IAssistantService _assistantService;
private readonly MyInfoMateDbContext _context;
private readonly ILogger<AiController> _logger;
public AiController(
AssistantService assistantService,
IAssistantService assistantService,
MyInfoMateDbContext context,
ILogger<AiController> logger)
{
@ -30,6 +31,31 @@ namespace ManagerService.Controllers
_logger = logger;
}
/// <summary>
/// Traduit un texte HTML vers plusieurs langues via IA
/// </summary>
[HttpPost("translate")]
[ProducesResponseType(typeof(AiTranslateResponse), 200)]
[ProducesResponseType(403)]
[ProducesResponseType(typeof(string), 500)]
public async Task<IActionResult> Translate([FromBody] AiTranslateRequest request, [FromQuery] string instanceId)
{
try
{
var instance = _context.Instances.FirstOrDefault(i => i.Id == instanceId);
if (instance == null || !instance.IsAssistant)
return Forbid();
var result = await _assistantService.TranslateAsync(request);
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Erreur lors de la traduction IA");
return new ObjectResult("Une erreur est survenue") { StatusCode = 500 };
}
}
/// <summary>
/// Envoie un message à l'assistant IA, scopé à l'instance et optionnellement à une configuration
/// </summary>
@ -42,7 +68,8 @@ namespace ManagerService.Controllers
try
{
// Vérifie que l'instance a activé la fonctionnalité assistant
var instance = _context.Instances.FirstOrDefault(i => i.Id == request.InstanceId);
var instance = _context.Instances
.FirstOrDefault(i => i.Id == request.InstanceId);
if (instance == null || !instance.IsAssistant)
return Forbid();
@ -59,6 +86,11 @@ namespace ManagerService.Controllers
instance.AiRequestsThisMonth = 0;
instance.AiUsageMonthKey = monthKey;
}
var quota = instance.AiRequestsPerMonth;
if (quota > 0 && instance.AiRequestsThisMonth >= quota)
return StatusCode(429, "Quota IA mensuel dépassé");
instance.AiRequestsThisMonth++;
_context.SaveChanges();

View File

@ -0,0 +1,64 @@
using ManagerService.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using NSwag.Annotations;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace ManagerService.Controllers
{
[Authorize(Policy = ManagerService.Service.Security.Policies.SuperAdmin)]
[ApiController, Route("api/[controller]")]
[OpenApiTag("Audit", Description = "Audit trail — SuperAdmin only")]
public class AuditController : ControllerBase
{
private readonly MyInfoMateDbContext _db;
private readonly ILogger<AuditController> _logger;
public AuditController(MyInfoMateDbContext db, ILogger<AuditController> logger)
{
_db = db;
_logger = logger;
}
[HttpGet]
public async Task<IActionResult> GetAuditLogs(
[FromQuery] string? instanceId,
[FromQuery] string? entityType,
[FromQuery] string? userId,
[FromQuery] DateTime? from,
[FromQuery] DateTime? to,
[FromQuery] int page = 1,
[FromQuery] int limit = 50)
{
var query = _db.AuditLogs.AsQueryable();
if (!string.IsNullOrEmpty(instanceId))
query = query.Where(a => a.InstanceId == instanceId);
if (!string.IsNullOrEmpty(entityType))
query = query.Where(a => a.EntityType == entityType);
if (!string.IsNullOrEmpty(userId))
query = query.Where(a => a.UserId == userId);
if (from.HasValue)
query = query.Where(a => a.Timestamp >= from.Value);
if (to.HasValue)
query = query.Where(a => a.Timestamp <= to.Value);
var total = await query.CountAsync();
var items = await query
.OrderByDescending(a => a.Timestamp)
.Skip((page - 1) * limit)
.Take(limit)
.ToListAsync();
return Ok(new { total, page, limit, items });
}
}
}

View File

@ -116,6 +116,20 @@ namespace ManagerService.Controllers
instance.DateCreation = DateTime.Now.ToUniversalTime();
instance.Id = idService.GenerateHexId();
// Copier les valeurs du plan comme valeurs par défaut
if (instance.SubscriptionPlanId != null)
{
var plan = _myInfoMateDbContext.SubscriptionPlans.FirstOrDefault(p => p.Id == instance.SubscriptionPlanId);
if (plan != null)
{
instance.StorageQuotaBytes = plan.StorageQuotaBytes;
instance.AiRequestsPerMonth = plan.AiRequestsPerMonth;
instance.HasStats = plan.HasStats;
instance.StatsHistoryDays = plan.StatsHistoryDays;
instance.HasAdvancedStats = plan.HasAdvancedStats;
}
}
/*List<OldInstance> instances = _instanceService.GetAll();
Instance instance = _myInfoMateDbContext.Instances.FirstOrDefault(i => i.Id == id);*/
@ -168,9 +182,16 @@ namespace ManagerService.Controllers
if (instance == null)
throw new KeyNotFoundException("instance does not exist");
instance.DateCreation = updatedInstance.dateCreation != null ? updatedInstance.dateCreation.Value : instance.DateCreation;
instance.Name= updatedInstance.name != null ? updatedInstance.name : instance.Name;
instance.PinCode = updatedInstance.pinCode != null ? updatedInstance.pinCode : instance.PinCode;
instance.DateCreation = updatedInstance.dateCreation ?? instance.DateCreation;
instance.Name = updatedInstance.name ?? instance.Name;
instance.PinCode = updatedInstance.pinCode ?? instance.PinCode;
instance.IsPushNotification = updatedInstance.isPushNotification ?? instance.IsPushNotification;
instance.IsStatistic = updatedInstance.isStatistic ?? instance.IsStatistic;
instance.IsMobile = updatedInstance.isMobile ?? instance.IsMobile;
instance.IsTablet = updatedInstance.isTablet ?? instance.IsTablet;
instance.IsWeb = updatedInstance.isWeb ?? instance.IsWeb;
instance.IsVR = updatedInstance.isVR ?? instance.IsVR;
instance.IsAssistant = updatedInstance.isAssistant ?? instance.IsAssistant;
if (updatedInstance.subscriptionPlanId == "")
instance.SubscriptionPlanId = null;
else if (updatedInstance.subscriptionPlanId != null)
@ -272,7 +293,6 @@ namespace ManagerService.Controllers
try
{
var instance = _myInfoMateDbContext.Instances
.Include(i => i.SubscriptionPlan)
.FirstOrDefault(i => i.Id == id);
if (instance == null)
@ -288,9 +308,9 @@ namespace ManagerService.Controllers
return new OkObjectResult(new InstanceQuotaDTO
{
storageUsedBytes = storageUsed,
storageQuotaBytes = instance.SubscriptionPlan?.StorageQuotaBytes ?? 0,
storageQuotaBytes = instance.StorageQuotaBytes,
aiRequestsUsed = aiUsed,
aiRequestsPerMonth = instance.SubscriptionPlan?.AiRequestsPerMonth ?? 0
aiRequestsPerMonth = instance.AiRequestsPerMonth
});
}
catch (Exception ex)

View File

@ -11,6 +11,7 @@ using NSwag.Annotations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
@ -28,6 +29,7 @@ namespace ManagerService.Controllers
private readonly ResourceDatabaseService _resourceSvc;
private readonly UserDatabaseService _userSvc;
private readonly DeviceDatabaseService _deviceSvc;
private readonly IHttpClientFactory _httpClientFactory;
public MigrationController(
MyInfoMateDbContext db,
@ -36,7 +38,8 @@ namespace ManagerService.Controllers
SectionDatabaseService sectionSvc,
ResourceDatabaseService resourceSvc,
UserDatabaseService userSvc,
DeviceDatabaseService deviceSvc)
DeviceDatabaseService deviceSvc,
IHttpClientFactory httpClientFactory)
{
_db = db;
_instanceSvc = instanceSvc;
@ -45,6 +48,7 @@ namespace ManagerService.Controllers
_resourceSvc = resourceSvc;
_userSvc = userSvc;
_deviceSvc = deviceSvc;
_httpClientFactory = httpClientFactory;
}
/// <summary>
@ -131,6 +135,30 @@ namespace ManagerService.Controllers
: _instanceSvc.GetAll().Select(i => i.Id).ToList();
var source = instanceIds.SelectMany(id => _resourceSvc.GetAll(id)).ToList();
// Fetch sizes en parallèle par batches de 30
var httpClient = _httpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(10);
var sizemap = new System.Collections.Concurrent.ConcurrentDictionary<string, long>();
var urlSource = source.Where(r => !string.IsNullOrEmpty(r.Url)).ToList();
const int batchSize = 30;
for (int i = 0; i < urlSource.Count; i += batchSize)
{
var batch = urlSource.Skip(i).Take(batchSize);
await Task.WhenAll(batch.Select(async r =>
{
try
{
var req = new HttpRequestMessage(HttpMethod.Head, r.Url);
var resp = await httpClient.SendAsync(req);
if (resp.Content.Headers.ContentLength.HasValue)
sizemap[r.Id] = resp.Content.Headers.ContentLength.Value;
}
catch { /* URL inaccessible ou expirée → SizeBytes restera 0 */ }
}));
}
foreach (var old in source)
{
try
@ -148,8 +176,8 @@ namespace ManagerService.Controllers
Label = old.Label,
DateCreation = old.DateCreation,
InstanceId = old.InstanceId,
Url = old.Url, // NOTE: MongoDB field is "URL", mapped to Url in OldResource
SizeBytes = 0,
Url = old.Url,
SizeBytes = sizemap.TryGetValue(old.Id, out var size) ? size : 0,
};
if (!dryRun)

View File

@ -234,6 +234,20 @@ namespace ManagerService.Controllers
var resourceType = (ResourceType)Enum.Parse(typeof(ResourceType), type);
List<Resource> resources = new List<Resource>();
var instance = _myInfoMateDbContext.Instances
.Include(i => i.SubscriptionPlan)
.FirstOrDefault(i => i.Id == instanceId);
var storageQuota = instance?.SubscriptionPlan?.StorageQuotaBytes ?? 0;
if (storageQuota > 0)
{
var storageUsed = _myInfoMateDbContext.Resources
.Where(r => r.InstanceId == instanceId)
.Sum(r => (long?)r.SizeBytes) ?? 0;
var incomingBytes = Request.Form.Files.Sum(f => f.Length);
if (storageUsed + incomingBytes > storageQuota)
return new ObjectResult("Quota de stockage dépassé") { StatusCode = 413 };
}
foreach (var file in Request.Form.Files)
{
if (file.Length > 0)
@ -372,8 +386,9 @@ namespace ManagerService.Controllers
resource.InstanceId = updatedResource.instanceId != null ? updatedResource.instanceId : resource.InstanceId;
resource.Label = updatedResource.label != null ? updatedResource.label : resource.Label;
resource.Type = updatedResource.type != null ? updatedResource.type : resource.Type;
resource.Url = updatedResource.url != null ? updatedResource.url: resource.Url;
//resource.Data = updatedResource.data; // NOT ALLOWED
resource.Url = updatedResource.url != null ? updatedResource.url : resource.Url;
if (updatedResource.sizeBytes > 0)
resource.SizeBytes = updatedResource.sizeBytes;
_myInfoMateDbContext.SaveChanges();

View File

@ -8,6 +8,7 @@ using ManagerService.Helpers;
using ManagerService.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using NSwag.Annotations;
namespace ManagerService.Controllers
@ -80,7 +81,21 @@ namespace ManagerService.Controllers
if (string.IsNullOrEmpty(instanceId))
return BadRequest("instanceId is required");
var fromDate = (from ?? DateTime.UtcNow.AddDays(-30)).ToUniversalTime();
var instance = _db.Instances
.FirstOrDefault(i => i.Id == instanceId);
if (instance != null && !instance.HasStats)
return Ok(new StatsSummaryDTO());
var historyDays = instance?.StatsHistoryDays ?? 30;
var hasAdvancedStats = instance?.HasAdvancedStats ?? false;
var maxFrom = historyDays > 0
? DateTime.UtcNow.AddDays(-historyDays)
: DateTime.MinValue;
var requestedFrom = (from ?? DateTime.UtcNow.AddDays(-30)).ToUniversalTime();
var fromDate = historyDays > 0 && requestedFrom < maxFrom ? maxFrom : requestedFrom;
var toDate = (to ?? DateTime.UtcNow).ToUniversalTime();
var eventsQuery = _db.VisitEvents
@ -106,6 +121,18 @@ namespace ManagerService.Controllers
summary.AvgVisitDurationSeconds = (int)sessionsWithDuration.Average();
}
// Section title lookup (FR preferred, fallback to first available)
var allSectionIds = events.Where(e => e.SectionId != null).Select(e => e.SectionId!).Distinct().ToList();
var sectionTitles = _db.Sections
.Where(s => allSectionIds.Contains(s.Id))
.Select(s => new { s.Id, s.Title })
.ToList()
.ToDictionary(
s => s.Id,
s => s.Title?.FirstOrDefault(t => t.language == "fr")?.value
?? s.Title?.FirstOrDefault()?.value
);
// Top sections
var sectionViews = events
.Where(e => e.EventType == VisitEventType.SectionView && e.SectionId != null)
@ -119,7 +146,7 @@ namespace ManagerService.Controllers
return new SectionStatDTO
{
SectionId = g.Key,
SectionTitle = g.Key, // apps can enrich with section titles client-side
SectionTitle = sectionTitles.GetValueOrDefault(g.Key),
Views = g.Count(),
AvgDurationSeconds = leaveDurations.Any() ? (int)leaveDurations.Average() : 0
};
@ -143,15 +170,19 @@ namespace ManagerService.Controllers
.OrderBy(d => d.Date)
.ToList();
// Language distribution
// Language distribution (one entry per distinct session)
summary.LanguageDistribution = events
.Where(e => e.Language != null)
.GroupBy(e => e.Language!)
.GroupBy(e => e.SessionId)
.Select(g => g.First().Language!)
.GroupBy(lang => lang)
.ToDictionary(g => g.Key, g => g.Count());
// AppType distribution
// AppType distribution (one entry per distinct session)
summary.AppTypeDistribution = events
.GroupBy(e => e.AppType.ToString())
.GroupBy(e => e.SessionId)
.Select(g => g.First().AppType.ToString())
.GroupBy(t => t)
.ToDictionary(g => g.Key, g => g.Count());
// Top POIs
@ -227,7 +258,7 @@ namespace ManagerService.Controllers
return new QuizStatDTO
{
SectionId = g.Key,
SectionTitle = g.Key,
SectionTitle = sectionTitles.GetValueOrDefault(g.Key),
Completions = g.Count(),
AvgScore = scores.Any() ? scores.Average(s => s.score) : 0,
TotalQuestions = scores.Any() ? (int)scores.Average(s => s.total) : 0
@ -316,6 +347,18 @@ namespace ManagerService.Controllers
}
summary.QrScans = new QrScanStatDTO { TotalScans = qrEvents.Count, ValidScans = validQr, InvalidScans = invalidQr };
if (!hasAdvancedStats)
{
summary.LanguageDistribution = new Dictionary<string, int>();
summary.TopPois = new List<PoiStatDTO>();
summary.TopAgendaEvents = new List<AgendaEventStatDTO>();
summary.QuizStats = new List<QuizStatDTO>();
summary.GameStats = new List<GameStatDTO>();
summary.TopArticles = new List<ArticleStatDTO>();
summary.TopMenuItems = new List<MenuItemStatDTO>();
summary.QrScans = new QrScanStatDTO { TotalScans = 0, ValidScans = 0, InvalidScans = 0 };
}
return Ok(summary);
}
catch (Exception ex)

View File

@ -27,6 +27,7 @@ namespace ManagerService.Controllers
_myInfoMateDbContext = myInfoMateDbContext;
}
[Authorize(Policy = ManagerService.Service.Security.Policies.Viewer)]
[ProducesResponseType(typeof(List<SubscriptionPlanDTO>), 200)]
[ProducesResponseType(typeof(string), 500)]
[HttpGet]

View File

@ -40,4 +40,16 @@ namespace ManagerService.DTOs
public List<AiCardDTO>? Cards { get; set; }
public NavigationActionDTO? Navigation { get; set; }
}
public class AiTranslateRequest
{
public string Text { get; set; }
public string SourceLang { get; set; }
public List<string> TargetLangs { get; set; } = new();
}
public class AiTranslateResponse
{
public Dictionary<string, string> Translations { get; set; } = new();
}
}

View File

@ -9,19 +9,24 @@ namespace ManagerService.DTOs
public string name { get; set; }
public DateTime? dateCreation { get; set; }
public string pinCode { get; set; }
public bool isPushNotification { get; set; }
public bool isStatistic { get; set; }
public bool isMobile { get; set; }
public bool isTablet { get; set; }
public bool isWeb { get; set; }
public bool isVR { get; set; }
public bool? isPushNotification { get; set; }
public bool? isStatistic { get; set; }
public bool? isMobile { get; set; }
public bool? isTablet { get; set; }
public bool? isWeb { get; set; }
public bool? isVR { get; set; }
public bool isAssistant { get; set; }
public bool? isAssistant { get; set; }
public string? subscriptionPlanId { get; set; }
public SubscriptionPlanDTO? subscriptionPlan { get; set; }
public int aiRequestsThisMonth { get; set; }
public int? aiRequestsThisMonth { get; set; }
public string? aiUsageMonthKey { get; set; }
public long? storageQuotaBytes { get; set; }
public int? aiRequestsPerMonth { get; set; }
public bool? hasStats { get; set; }
public int? statsHistoryDays { get; set; }
public bool? hasAdvancedStats { get; set; }
public List<ApplicationInstanceDTO> applicationInstanceDTOs { get; set; }
}

View File

@ -6,5 +6,8 @@ namespace ManagerService.DTOs
public string name { get; set; }
public long storageQuotaBytes { get; set; }
public int aiRequestsPerMonth { get; set; }
public bool hasStats { get; set; }
public int statsHistoryDays { get; set; }
public bool hasAdvancedStats { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System;
namespace ManagerService.Data
{
public class AuditLog
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string EntityType { get; set; }
public string EntityId { get; set; }
public string Action { get; set; }
public string? UserId { get; set; }
public string? InstanceId { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public string? OldValues { get; set; }
public string? NewValues { get; set; }
}
}

View File

@ -71,6 +71,10 @@ namespace ManagerService.Data
[Required]
public string InstanceId { get; set; }
public string? AppVersion { get; set; }
public DateTime? LastSeen { get; set; }
public DeviceDTO ToDTO()
{
return new DeviceDTO()

View File

@ -45,6 +45,16 @@ namespace ManagerService.Data
public string AiUsageMonthKey { get; set; } = "";
public long StorageQuotaBytes { get; set; } = 0;
public int AiRequestsPerMonth { get; set; } = 0;
public bool HasStats { get; set; } = false;
public int StatsHistoryDays { get; set; } = 30;
public bool HasAdvancedStats { get; set; } = false;
public InstanceDTO ToDTO(List<ApplicationInstanceDTO> applicationInstanceDTOs)
{
@ -65,6 +75,11 @@ namespace ManagerService.Data
subscriptionPlan = SubscriptionPlan?.ToDTO(),
aiRequestsThisMonth = AiRequestsThisMonth,
aiUsageMonthKey = AiUsageMonthKey,
storageQuotaBytes = StorageQuotaBytes,
aiRequestsPerMonth = AiRequestsPerMonth,
hasStats = HasStats,
statsHistoryDays = StatsHistoryDays,
hasAdvancedStats = HasAdvancedStats,
applicationInstanceDTOs = applicationInstanceDTOs
};
}
@ -74,15 +89,25 @@ namespace ManagerService.Data
Name = instanceDTO.name;
DateCreation = instanceDTO.dateCreation != null ? instanceDTO.dateCreation.Value : DateTime.Now.ToUniversalTime();
PinCode = instanceDTO.pinCode;
IsPushNotification = instanceDTO.isPushNotification;
IsStatistic = instanceDTO.isStatistic;
IsMobile = instanceDTO.isMobile;
IsTablet = instanceDTO.isTablet;
IsWeb = instanceDTO.isWeb;
IsVR = instanceDTO.isVR;
IsAssistant = instanceDTO.isAssistant;
IsPushNotification = instanceDTO.isPushNotification ?? false;
IsStatistic = instanceDTO.isStatistic ?? false;
IsMobile = instanceDTO.isMobile ?? false;
IsTablet = instanceDTO.isTablet ?? false;
IsWeb = instanceDTO.isWeb ?? false;
IsVR = instanceDTO.isVR ?? false;
IsAssistant = instanceDTO.isAssistant ?? false;
if (instanceDTO.subscriptionPlanId != null)
SubscriptionPlanId = instanceDTO.subscriptionPlanId;
if (instanceDTO.storageQuotaBytes.HasValue)
StorageQuotaBytes = instanceDTO.storageQuotaBytes.Value;
if (instanceDTO.aiRequestsPerMonth.HasValue)
AiRequestsPerMonth = instanceDTO.aiRequestsPerMonth.Value;
if (instanceDTO.hasStats.HasValue)
HasStats = instanceDTO.hasStats.Value;
if (instanceDTO.statsHistoryDays.HasValue)
StatsHistoryDays = instanceDTO.statsHistoryDays.Value;
if (instanceDTO.hasAdvancedStats.HasValue)
HasAdvancedStats = instanceDTO.hasAdvancedStats.Value;
return this;
}

View File

@ -1,16 +1,28 @@
using Manager.DTOs;
using ManagerService.Data.SubSection;
using ManagerService.DTOs;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using static ManagerService.Data.SubSection.SectionEvent;
namespace ManagerService.Data
{
public class MyInfoMateDbContext : DbContext
{
public MyInfoMateDbContext(DbContextOptions<MyInfoMateDbContext> options) : base(options) { }
private readonly IHttpContextAccessor _httpContextAccessor;
public MyInfoMateDbContext(DbContextOptions<MyInfoMateDbContext> options, IHttpContextAccessor httpContextAccessor)
: base(options)
{
_httpContextAccessor = httpContextAccessor;
}
public DbSet<Instance> Instances { get; set; }
public DbSet<SubscriptionPlan> SubscriptionPlans { get; set; }
@ -49,6 +61,82 @@ namespace ManagerService.Data
// Push Notifications
public DbSet<PushNotification> PushNotifications { get; set; }
// Audit
public DbSet<AuditLog> AuditLogs { get; set; }
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
var auditEntries = BuildAuditEntries();
var result = await base.SaveChangesAsync(cancellationToken);
if (auditEntries.Any())
await base.SaveChangesAsync(cancellationToken);
return result;
}
private static readonly HashSet<Type> AuditedTypes = new()
{
typeof(Section), typeof(Resource), typeof(Configuration),
typeof(Device), typeof(User), typeof(Instance)
};
private List<AuditLog> BuildAuditEntries()
{
var userId = _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
var entries = new List<AuditLog>();
foreach (var entry in ChangeTracker.Entries()
.Where(e => AuditedTypes.Contains(e.Entity.GetType())
&& e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted))
{
var action = entry.State switch
{
EntityState.Added => "Create",
EntityState.Modified => "Update",
EntityState.Deleted => "Delete",
_ => null
};
var entityId = entry.Properties
.FirstOrDefault(p => p.Metadata.IsPrimaryKey())?.CurrentValue?.ToString();
var instanceId = entry.Properties
.FirstOrDefault(p => p.Metadata.Name == "InstanceId")?.CurrentValue?.ToString();
string? oldValues = null;
string? newValues = null;
if (entry.State == EntityState.Modified)
{
oldValues = JsonSerializer.Serialize(
entry.Properties.Where(p => p.IsModified)
.ToDictionary(p => p.Metadata.Name, p => p.OriginalValue));
newValues = JsonSerializer.Serialize(
entry.Properties.Where(p => p.IsModified)
.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue));
}
else if (entry.State == EntityState.Added)
{
newValues = JsonSerializer.Serialize(
entry.Properties.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue));
}
var log = new AuditLog
{
EntityType = entry.Entity.GetType().Name,
EntityId = entityId ?? "",
Action = action!,
UserId = userId,
InstanceId = instanceId,
OldValues = oldValues,
NewValues = newValues
};
AuditLogs.Add(log);
entries.Add(log);
}
return entries;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@ -59,6 +147,20 @@ namespace ManagerService.Data
base.OnModelCreating(modelBuilder);
// Les types suivants sont utilisés uniquement comme valeurs JSONB, pas comme entités DB.
// Le provider InMemory les découvre par convention et échoue à la validation
// si aucune clé primaire n'est définie. On les exclut explicitement.
modelBuilder.Ignore<ContentDTO>();
modelBuilder.Ignore<ResponseDTO>();
modelBuilder.Ignore<CategorieDTO>();
modelBuilder.Ignore<TranslationDTO>();
modelBuilder.Ignore<TranslationAndResourceDTO>();
modelBuilder.Ignore<OrderedTranslationAndResourceDTO>();
modelBuilder.Ignore<EventAddress>();
modelBuilder.Ignore<Translation>();
modelBuilder.Ignore<TranslationAndResource>();
modelBuilder.Ignore<OrderedTranslationAndResource>();
modelBuilder.Entity<Configuration>()
.Property(s => s.Title)
.HasColumnType("jsonb")
@ -240,6 +342,19 @@ namespace ManagerService.Data
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
// Ces colonnes ont un defaultValue dans la migration, ce qui amène EF Core à les traiter
// comme ValueGeneratedOnAdd (read-only après insert). On force ValueGeneratedNever.
modelBuilder.Entity<Instance>()
.Property(i => i.StorageQuotaBytes).ValueGeneratedNever();
modelBuilder.Entity<Instance>()
.Property(i => i.AiRequestsPerMonth).ValueGeneratedNever();
modelBuilder.Entity<Instance>()
.Property(i => i.HasStats).ValueGeneratedNever();
modelBuilder.Entity<Instance>()
.Property(i => i.StatsHistoryDays).ValueGeneratedNever();
modelBuilder.Entity<Instance>()
.Property(i => i.HasAdvancedStats).ValueGeneratedNever();
// Seed : plans d'abonnement par défaut
modelBuilder.Entity<SubscriptionPlan>().HasData(
new SubscriptionPlan
@ -248,20 +363,29 @@ namespace ManagerService.Data
Name = "Starter",
StorageQuotaBytes = 1L * 1024 * 1024 * 1024, // 1 GB
AiRequestsPerMonth = 0,
HasStats = false,
StatsHistoryDays = 30,
HasAdvancedStats = false,
},
new SubscriptionPlan
{
Id = "plan-standard",
Name = "Standard",
StorageQuotaBytes = 10L * 1024 * 1024 * 1024, // 10 GB
AiRequestsPerMonth = 100,
AiRequestsPerMonth = 500,
HasStats = true,
StatsHistoryDays = 30,
HasAdvancedStats = false,
},
new SubscriptionPlan
{
Id = "plan-premium",
Name = "Premium",
StorageQuotaBytes = 50L * 1024 * 1024 * 1024, // 50 GB
AiRequestsPerMonth = 500,
AiRequestsPerMonth = 2000,
HasStats = true,
StatsHistoryDays = 0,
HasAdvancedStats = true,
}
);
}

View File

@ -16,6 +16,15 @@ namespace ManagerService.Data
public int AiRequestsPerMonth { get; set; } = 0;
/// <summary>Whether the plan includes any stats at all.</summary>
public bool HasStats { get; set; } = false;
/// <summary>Max history in days for stats. 0 = unlimited (Premium).</summary>
public int StatsHistoryDays { get; set; } = 30;
/// <summary>Whether the plan includes advanced stats (POI, quiz, game, articles, QR, etc.).</summary>
public bool HasAdvancedStats { get; set; } = false;
public SubscriptionPlanDTO ToDTO()
{
return new SubscriptionPlanDTO
@ -23,7 +32,10 @@ namespace ManagerService.Data
id = Id,
name = Name,
storageQuotaBytes = StorageQuotaBytes,
aiRequestsPerMonth = AiRequestsPerMonth
aiRequestsPerMonth = AiRequestsPerMonth,
hasStats = HasStats,
statsHistoryDays = StatsHistoryDays,
hasAdvancedStats = HasAdvancedStats,
};
}
@ -32,6 +44,9 @@ namespace ManagerService.Data
Name = dto.name;
StorageQuotaBytes = dto.storageQuotaBytes;
AiRequestsPerMonth = dto.aiRequestsPerMonth;
HasStats = dto.hasStats;
StatsHistoryDays = dto.statsHistoryDays;
HasAdvancedStats = dto.hasAdvancedStats;
return this;
}
}

View File

@ -25,6 +25,9 @@
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.17" />
<PackageReference Include="Hangfire.PostgreSql" Version="1.20.10" />
<PackageReference Include="Scrypt.NET" Version="1.3.0" />
<PackageReference Include="AspNetCore.HealthChecks.NpgSql" Version="8.0.2" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Sinks.Telegram" Version="0.2.1" />
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.2.0" />
</ItemGroup>

View File

@ -17,9 +17,9 @@ namespace ManagerService.Migrations
columns: new[] { "Id", "AiRequestsPerMonth", "Name", "StorageQuotaBytes" },
values: new object[,]
{
{ "plan-premium", 500, "Premium", 53687091200L },
{ "plan-standard", 100, "Standard", 10737418240L },
{ "plan-starter", 0, "Starter", 1073741824L }
{ "plan-premium", 2000, "Premium", 53687091200L },
{ "plan-standard", 500, "Standard", 10737418240L },
{ "plan-starter", 0, "Starter", 2147483648L }
});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,104 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ManagerService.Migrations
{
/// <inheritdoc />
public partial class AddStatsHistoryToSubscriptionPlan : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "HasAdvancedStats",
table: "SubscriptionPlans",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<int>(
name: "StatsHistoryDays",
table: "SubscriptionPlans",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "AppVersion",
table: "Devices",
type: "text",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "LastSeen",
table: "Devices",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.CreateTable(
name: "AuditLogs",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
EntityType = table.Column<string>(type: "text", nullable: true),
EntityId = table.Column<string>(type: "text", nullable: true),
Action = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<string>(type: "text", nullable: true),
InstanceId = table.Column<string>(type: "text", nullable: true),
Timestamp = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
OldValues = table.Column<string>(type: "text", nullable: true),
NewValues = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AuditLogs", x => x.Id);
});
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-premium",
columns: new[] { "HasAdvancedStats", "StatsHistoryDays" },
values: new object[] { false, 30 });
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-standard",
columns: new[] { "HasAdvancedStats", "StatsHistoryDays" },
values: new object[] { false, 30 });
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-starter",
columns: new[] { "HasAdvancedStats", "StatsHistoryDays" },
values: new object[] { false, 30 });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AuditLogs");
migrationBuilder.DropColumn(
name: "HasAdvancedStats",
table: "SubscriptionPlans");
migrationBuilder.DropColumn(
name: "StatsHistoryDays",
table: "SubscriptionPlans");
migrationBuilder.DropColumn(
name: "AppVersion",
table: "Devices");
migrationBuilder.DropColumn(
name: "LastSeen",
table: "Devices");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ManagerService.Migrations
{
/// <inheritdoc />
public partial class FixPremiumStatsSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-premium",
columns: new[] { "HasAdvancedStats", "StatsHistoryDays" },
values: new object[] { true, 0 });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-premium",
columns: new[] { "HasAdvancedStats", "StatsHistoryDays" },
values: new object[] { false, 30 });
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,119 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ManagerService.Migrations
{
/// <inheritdoc />
public partial class AddQuotaFieldsToInstance : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "HasStats",
table: "SubscriptionPlans",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<int>(
name: "AiRequestsPerMonth",
table: "Instances",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<bool>(
name: "HasAdvancedStats",
table: "Instances",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "HasStats",
table: "Instances",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<int>(
name: "StatsHistoryDays",
table: "Instances",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<long>(
name: "StorageQuotaBytes",
table: "Instances",
type: "bigint",
nullable: false,
defaultValue: 0L);
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-premium",
columns: new[] { "AiRequestsPerMonth", "HasStats" },
values: new object[] { 2000, true });
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-standard",
columns: new[] { "AiRequestsPerMonth", "HasStats" },
values: new object[] { 500, true });
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-starter",
column: "HasStats",
value: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "HasStats",
table: "SubscriptionPlans");
migrationBuilder.DropColumn(
name: "AiRequestsPerMonth",
table: "Instances");
migrationBuilder.DropColumn(
name: "HasAdvancedStats",
table: "Instances");
migrationBuilder.DropColumn(
name: "HasStats",
table: "Instances");
migrationBuilder.DropColumn(
name: "StatsHistoryDays",
table: "Instances");
migrationBuilder.DropColumn(
name: "StorageQuotaBytes",
table: "Instances");
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-premium",
column: "AiRequestsPerMonth",
value: 500);
migrationBuilder.UpdateData(
table: "SubscriptionPlans",
keyColumn: "Id",
keyValue: "plan-standard",
column: "AiRequestsPerMonth",
value: 100);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ManagerService.Migrations
{
/// <inheritdoc />
public partial class FixInstanceQuotaColumnsWritable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -180,6 +180,40 @@ namespace ManagerService.Migrations
b.ToTable("ApplicationInstances");
});
modelBuilder.Entity("ManagerService.Data.AuditLog", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Action")
.HasColumnType("text");
b.Property<string>("EntityId")
.HasColumnType("text");
b.Property<string>("EntityType")
.HasColumnType("text");
b.Property<string>("InstanceId")
.HasColumnType("text");
b.Property<string>("NewValues")
.HasColumnType("text");
b.Property<string>("OldValues")
.HasColumnType("text");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("AuditLogs");
});
modelBuilder.Entity("ManagerService.Data.Configuration", b =>
{
b.Property<string>("Id")
@ -243,6 +277,9 @@ namespace ManagerService.Migrations
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("AppVersion")
.HasColumnType("text");
b.Property<string>("BatteryLevel")
.HasColumnType("text");
@ -281,6 +318,9 @@ namespace ManagerService.Migrations
b.Property<DateTime>("LastConnectionLevel")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("LastSeen")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.HasColumnType("text");
@ -296,6 +336,9 @@ namespace ManagerService.Migrations
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AiRequestsPerMonth")
.HasColumnType("integer");
b.Property<int>("AiRequestsThisMonth")
.HasColumnType("integer");
@ -305,6 +348,12 @@ namespace ManagerService.Migrations
b.Property<DateTime>("DateCreation")
.HasColumnType("timestamp with time zone");
b.Property<bool>("HasAdvancedStats")
.HasColumnType("boolean");
b.Property<bool>("HasStats")
.HasColumnType("boolean");
b.Property<bool>("IsAssistant")
.HasColumnType("boolean");
@ -333,6 +382,12 @@ namespace ManagerService.Migrations
b.Property<string>("PinCode")
.HasColumnType("text");
b.Property<int>("StatsHistoryDays")
.HasColumnType("integer");
b.Property<long>("StorageQuotaBytes")
.HasColumnType("bigint");
b.Property<string>("SubscriptionPlanId")
.HasColumnType("text");
@ -879,10 +934,19 @@ namespace ManagerService.Migrations
b.Property<int>("AiRequestsPerMonth")
.HasColumnType("integer");
b.Property<bool>("HasAdvancedStats")
.HasColumnType("boolean");
b.Property<bool>("HasStats")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("StatsHistoryDays")
.HasColumnType("integer");
b.Property<long>("StorageQuotaBytes")
.HasColumnType("bigint");
@ -895,21 +959,30 @@ namespace ManagerService.Migrations
{
Id = "plan-starter",
AiRequestsPerMonth = 0,
HasAdvancedStats = false,
HasStats = false,
Name = "Starter",
StatsHistoryDays = 30,
StorageQuotaBytes = 1073741824L
},
new
{
Id = "plan-standard",
AiRequestsPerMonth = 100,
AiRequestsPerMonth = 500,
HasAdvancedStats = false,
HasStats = true,
Name = "Standard",
StatsHistoryDays = 30,
StorageQuotaBytes = 10737418240L
},
new
{
Id = "plan-premium",
AiRequestsPerMonth = 500,
AiRequestsPerMonth = 2000,
HasAdvancedStats = true,
HasStats = true,
Name = "Premium",
StatsHistoryDays = 0,
StorageQuotaBytes = 53687091200L
});
});

View File

@ -1,11 +1,6 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Serilog;
namespace ManagerService
{
@ -13,11 +8,30 @@ namespace ManagerService
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
try
{
CreateHostBuilder(args).Build().Run();
}
catch (System.Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog((context, services, configuration) => configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext())
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();

View File

@ -13,7 +13,7 @@ using ManagerService.Data.SubSection;
namespace ManagerService.Services
{
public class AssistantService
public class AssistantService : IAssistantService
{
private readonly IChatClient _chatClient;
private readonly MyInfoMateDbContext _context;
@ -26,6 +26,40 @@ namespace ManagerService.Services
_context = context;
}
public async Task<AiTranslateResponse> TranslateAsync(AiTranslateRequest request)
{
var targetList = string.Join(", ", request.TargetLangs);
var exampleJson = "{" + string.Join(", ", request.TargetLangs.Select(l => $"\"{l}\": \"...\"")) + "}";
var prompt = $"""
Tu es un traducteur professionnel. Les codes de langue utilisés sont : FR=français, NL=néerlandais, EN=anglais, DE=allemand, IT=italien, ES=espagnol, PL=polonais, CN=chinois, AR=arabe, UK=ukrainien.
Traduis le texte HTML suivant de la langue "{request.SourceLang}" vers ces langues : {targetList}.
Conserve exactement le formatage HTML (balises, attributs, structure). Ne traduis pas le contenu des attributs HTML.
Réponds UNIQUEMENT avec un objet JSON valide, sans markdown, sans explication, au format :
{exampleJson}
Texte à traduire :
{request.Text}
""";
var messages = new List<ChatMessage>
{
new ChatMessage(ChatRole.User, prompt)
};
var response = await _chatClient.GetResponseAsync(messages);
var json = response.Text?.Trim() ?? "{}";
// Nettoyer les éventuels blocs markdown ```json ... ```
if (json.StartsWith("```"))
{
json = System.Text.RegularExpressions.Regex.Replace(json, @"```[a-z]*\n?", "").Trim();
json = json.TrimEnd('`').Trim();
}
var translations = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(json) ?? new();
return new AiTranslateResponse { Translations = translations };
}
private static string StripHtml(string? html) =>
string.IsNullOrEmpty(html) ? "" : System.Text.RegularExpressions.Regex.Replace(html, "<[^>]+>", "").Trim();

View File

@ -0,0 +1,11 @@
using ManagerService.DTOs;
using System.Threading.Tasks;
namespace ManagerService.Services
{
public interface IAssistantService
{
Task<AiChatResponse> ChatAsync(AiChatRequest request);
Task<AiTranslateResponse> TranslateAsync(AiTranslateRequest request);
}
}

View File

@ -46,6 +46,7 @@ using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using ManagerService.Services;
using Serilog;
namespace ManagerService
{
@ -157,6 +158,7 @@ namespace ManagerService
#if RELEASE
//services.AddMqttClientHostedService();
#endif
services.AddHttpContextAccessor();
services.AddScoped(typeof(ProfileLogic));
services.AddScoped<TokensService>();
services.AddScoped<LanguageInit>();
@ -182,7 +184,7 @@ namespace ManagerService
.AsBuilder()
.UseFunctionInvocation()
.Build());
services.AddScoped<AssistantService>();
services.AddScoped<IAssistantService, AssistantService>();
// Push Notifications
var firebaseCredentialsPath = Configuration["Firebase:CredentialsPath"];
@ -215,9 +217,12 @@ namespace ManagerService
services.AddDbContext<MyInfoMateDbContext>(options =>
options.UseNpgsql(dataSource, o => o.UseNetTopologySuite())
.EnableSensitiveDataLogging() // montre les valeurs des param<61>tres
.EnableSensitiveDataLogging()
.LogTo(Console.WriteLine, LogLevel.Information)
);
services.AddHealthChecks()
.AddNpgSql(connectionString);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@ -233,6 +238,7 @@ namespace ManagerService
}
app.UseExceptionHandler(HandleError);
app.UseSerilogRequestLogging();
//app.UseHttpsRedirection();
@ -280,6 +286,7 @@ namespace ManagerService
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHealthChecks("/health");
});
app.UseOpenApi();

View File

@ -4,8 +4,8 @@
//"TabletDb": "mongodb://admin:MioTech4ever!@localhost:27017", //PROD - Thomas
//"TabletDb": "mongodb://admin:MioTech4ever!@192.168.31.140:27017" //PROD - Thomas
//"TabletDb": "mongodb://admin:mdlf2021!@localhost:27017" //PROD MDLF
"TabletDb": "mongodb://admin:MyMuseum2022!@51.77.222.154:27017", //PROD MyMuseum
//"TabletDb": "mongodb://admin:MyInfoMate2023!@135.125.232.116:27017" //PROD MyInfoMate
//"TabletDb": "mongodb://admin:MyMuseum2022!@51.77.222.154:27017", //PROD MyMuseum
"TabletDb": "mongodb://admin:MyInfoMate2023!@135.125.232.116:27017", //PROD MyInfoMate
"PostgresConnection": "Host=localhost;Database=my_info_mate;Username=mym;Password=mym"
},
"BrokerHostSettings": {

View File

@ -17,11 +17,17 @@
"UserName": "user1", //admin
"Password": "MyInfoMate2023!" //mdlf2021!
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
"Serilog": {
"WriteTo": [
{ "Name": "Console" },
{
"Name": "Telegram",
"Args": {
"token": "8427411434:AAEp0NnlhKkKZz07P0WftS2KOuRmoA99vKs",
"chatId": "44670399",
"restrictedToMinimumLevel": "Error"
}
}
]
}
}

View File

@ -8,12 +8,19 @@
//"TabletDb": "mongodb://admin:MyInfoMate2023!@135.125.232.116:27017" //PROD MyInfoMate
"PostgresConnection": "Host=localhost;Database=my_info_mate;Username=mym;Password=mym"
},*/
"Logging": {
"LogLevel": {
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"WriteTo": [
{ "Name": "Console" }
],
"Enrich": [ "FromLogContext" ]
},
"AllowedHosts": "*",
"Tokens": {

View File

@ -0,0 +1,210 @@
version: "3.3"
services:
################################################
#### Traefik Proxy Setup #####
###############################################
traefik:
image: traefik:v2.8
restart: always
container_name: traefik
ports:
- "80:80" # <== http
- "8080:8080" # <== :8080 is where the dashboard runs on
- "443:443" # <== https
command:
#### These are the CLI commands that will configure Traefik and tell it how to work! ####
## API Settings - https://docs.traefik.io/operations/api/, endpoints - https://docs.traefik.io/operations/api/#endpoints ##
- --api.insecure=true # <== Enabling insecure api, NOT RECOMMENDED FOR PRODUCTION
- --api.dashboard=true # <== Enabling the dashboard to view services, middlewares, routers, etc...
- --api.debug=true # <== Enabling additional endpoints for debugging and profiling
## Log Settings (options: ERROR, DEBUG, PANIC, FATAL, WARN, INFO) - https://docs.traefik.io/observability/logs/ ##
- --log.level=ERROR # <== Setting the level of the logs from traefik
## Provider Settings - https://docs.traefik.io/providers/docker/#provider-configuration ##
- --providers.docker=true # <== Enabling docker as the provider for traefik
- --providers.docker.exposedbydefault=false # <== Don't expose every container to traefik, only expose enabled ones
- --providers.file.filename=/dynamic.yaml # <== Referring to a dynamic configuration file
- --providers.docker.network=web # <== Operate on the docker network named web
## Entrypoints Settings - https://docs.traefik.io/routing/entrypoints/#configuration ##
- --entrypoints.web.address=:80 # <== Defining an entrypoint for port :80 named web
- --entrypoints.web-secured.address=:443 # <== Defining an entrypoint for https on port :443 named web-secured
## Certificate Settings (Let's Encrypt) - https://docs.traefik.io/https/acme/#configuration-examples ##
- --certificatesresolvers.mytlschallenge.acme.tlschallenge=true # <== Enable TLS-ALPN-01 to generate and renew ACME certs
- --certificatesresolvers.mytlschallenge.acme.email=fransolet.thomas@gmail.com # <== Setting email for certs
- --certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json # <== Defining acme file to store cert information
volumes:
- ./letsencrypt:/letsencrypt # <== Volume for certs (TLS)
- /var/run/docker.sock:/var/run/docker.sock # <== Volume for docker admin
- ./dynamic.yaml:/dynamic.yaml # <== Volume for dynamic conf file, **ref: line 27
networks:
- web # <== Placing traefik on the network named web, to access containers on this network
labels:
#### Labels define the behavior and rules of the traefik proxy for this container ####
- "traefik.enable=true" # <== Enable traefik on itself to view dashboard and assign subdomain to view it
- "traefik.http.routers.api.rule=Host(`monitor.myinfomate.be`)" # <== Setting the domain for the dashboard
- "traefik.http.routers.api.service=api@internal" # <== Enabling the api to be a service to access
- "traefik.http.routers.api.middlewares=redirect@file" # <== This is a middleware to redirect to https
- "traefik.http.routers.api-secured.rule=Host(`monitor.myinfomate.be`)" # <== Your Domain Name for the https rule
- "traefik.http.routers.api-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31
- "traefik.http.routers.api-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https
- "traefik.http.routers.api-secured.service=api@internal" # <== Enabling the api to be a service to access
################################################
#### Site Setup Container #####
##############################################
myinfomate-landing:
container_name: "myinfomate-landing"
image: registry.unov.be/myinfomate/landing:latest
restart: always
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.landing.rule=Host(`myinfomate.be`)"
- "traefik.http.routers.landing.entrypoints=web"
- "traefik.http.routers.landing.middlewares=redirect@file"
- "traefik.http.routers.landing-secured.rule=Host(`myinfomate.be`)"
- "traefik.http.routers.landing-secured.entrypoints=web-secured"
- "traefik.http.routers.landing-secured.tls.certresolver=mytlschallenge"
managerService:
container_name: "manager-service"
image: registry.unov.be/managerservice:version-2.0.0
networks:
- web
- backend
#ports:
# - 5005:5005
volumes:
# - /etc/managerservice
- ~/apps/manager:/app/service-data
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.manager-service.rule=Host(`api.myinfomate.be`)" # <== Your Domain Name goes here for the http rule
- "traefik.http.routers.manager-service.entrypoints=web" # <== Defining the entrypoint for http, **ref: line 30
- "traefik.http.routers.manager-service.middlewares=redirect@file" # <== This is a middleware to redirect to https
- "traefik.http.routers.manager-service-secured.rule=Host(`api.myinfomate.be`)" # <== Your Domain Name for the https rule
- "traefik.http.routers.manager-service-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31
- "traefik.http.routers.manager-service-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https
command: /bin/sh -c "sudo chmod -R 777 /root/service-data/configurations /root/service-data/resources"
managerWeb:
container_name: "manager-web"
image: registry.unov.be/mymuseum/manager:version-2.0.0
networks:
- web
volumes:
- /etc/managerweb
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.manager-web.rule=Host(`manager.myinfomate.be`, `visitnamur.myinfomate.be`, `fortsaintheribert.myinfomate.be`)" # <== Your Domain Name goes here for the http rule
- "traefik.http.routers.manager-web.entrypoints=web" # <== Defining the entrypoint for http, **ref: line 30
- "traefik.http.routers.manager-web.middlewares=redirect@file" # <== This is a middleware to redirect to https
- "traefik.http.routers.manager-web-secured.rule=Host(`manager.myinfomate.be`, `visitnamur.myinfomate.be`, `fortsaintheribert.myinfomate.be`)" # <== Your Domain Name for the https rule
- "traefik.http.routers.manager-web-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31
- "traefik.http.routers.manager-web-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https
- "traefik.http.routers.manager-web-fsh.rule=Host(`visitnamur.myinfomate.be`, `fortsaintheribert.myinfomate.be`)" # <== Your Domain Name goes here for the http rule
- "traefik.http.routers.manager-web-fsh.entrypoints=web" # <== Defining the entrypoint for http, **ref: line 30
- "traefik.http.routers.manager-web-fsh.middlewares=redirect@file" # <== This is a middleware to redirect to https
- "traefik.http.routers.manager-web-fsh-secured.rule=Host(`visitnamur.myinfomate.be`, `fortsaintheribert.myinfomate.be`)" # <== Your Domain Name for the https rule
- "traefik.http.routers.manager-web-fsh-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31
- "traefik.http.routers.manager-web-fsh-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https
demoWeb:
container_name: "demo-web"
image: registry.unov.be/mymuseum/demo:latest
networks:
- web
volumes:
- /etc/demoweb
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.demo-web.rule=Host(`demo.myinfomate.be`)" # <== Your Domain Name goes here for the http rule
- "traefik.http.routers.demo-web.entrypoints=web" # <== Defining the entrypoint for http, **ref: line 30
- "traefik.http.routers.demo-web.middlewares=redirect@file" # <== This is a middleware to redirect to https
- "traefik.http.routers.demo-web-secured.rule=Host(`demo.myinfomate.be`)" # <== Your Domain Name for the https rule
- "traefik.http.routers.demo-web-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31
- "traefik.http.routers.demo-web-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https
- "traefik.http.routers.demo-web-fsh.rule=Host(`demo.myinfomate.be`)" # <== Your Domain Name goes here for the http rule
- "traefik.http.routers.demo-web-fsh.entrypoints=web" # <== Defining the entrypoint for http, **ref: line 30
- "traefik.http.routers.demo-web-fsh.middlewares=redirect@file" # <== This is a middleware to redirect to https
- "traefik.http.routers.demo-web-fsh-secured.rule=Host(`demo.myinfomate.be`)" # <== Your Domain Name for the https rule
- "traefik.http.routers.demo-web-fsh-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31
- "traefik.http.routers.demo-web-fsh-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https
mqtt:
container_name: mqtt
image: eclipse-mosquitto
networks:
- web
ports:
- "1883:1883" #default mqtt port
- "9001:9001" #default mqtt port for websockets
volumes:
- /etc/mqtt/config:/mosquitto/config:rw
- /etc/mqtt/data:/mosquitto/data:rw
- /etc/mqtt/log:/mosquitto/log:rw
restart: always
labels:
- "traefik.enable=true"
- "traefik.docker.network=mqtt"
- "traefik.tcp.services.mqtt.loadbalancer.server.port=1883"
- "traefik.tcp.services.mqtt_websocket.loadbalancer.server.port=9001"
- "traefik.tcp.routers.tcpr_mqtt.entrypoints=mqtt"
- "traefik.tcp.routers.tcpr_mqtt.rule=HostSNI(`myinfomate.be`)"
- "traefik.tcp.routers.tcpr_mqtt.service=mqtt"
- "traefik.tcp.routers.tcpr_mqtt_websocket.entrypoints=websocket"
- "traefik.tcp.routers.tcpr_mqtt_websocket.rule=HostSNI(`myinfomate.be`)"
- "traefik.tcp.routers.tcpr_mqtt_websocket.service=mqtt_websocket"
################################################
#### DB Container not on traefik #####
##############################################
#db:
# image: mysql:8.0
# container_name: db
# restart: unless-stopped
# command: '--default-authentication-plugin=mysql_native_password'
# env_file: .env
# environment:
# - MYSQL_DATABASE=wordpress
# volumes:
# - dbdata:/var/lib/mysql
# networks:
# - backend
mongo:
image: mongo
container_name: "mongodb"
ports:
- 27017:27017
volumes:
- ~/apps/mongo:/data/db
restart: always
labels:
- "traefik.enable=true"
- "traefik.tcp.routers.mongodb.rule=HostSNI(`myinfomate.be`)"
- "traefik.tcp.routers.mongo.entrypoints=mongo"
- "traefik.tcp.routers.mongo.tls=true"
- "traefik.tcp.services.mongo.loadbalancer.server.port=27017"
environment:
MONGO_INITDB_ROOT_USERNAME: $MONGODB_USERNAME
MONGO_INITDB_ROOT_PASSWORD: $MONGODB_PASSWORD
networks:
web:
external: true
backend:
external: false
volumes:
#wordpress:
# external: true
#dbdata:
db:
external: true
mongo:
external: true

View File

@ -52,31 +52,20 @@ services:
################################################
#### Site Setup Container #####
##############################################
wordpress: # <== we aren't going to open :80 here because traefik is going to serve this on entrypoint 'web'
image: wordpress
depends_on:
- db
myinfomate-landing:
container_name: "myinfomate-landing"
image: registry.unov.be/myinfomate/landing:latest
restart: always
container_name: wordpress
environment:
- WORDPRESS_DB_HOST=db:3306
- WORDPRESS_DB_USER=$MYSQL_USER
- WORDPRESS_DB_PASSWORD=$MYSQL_PASSWORD
- WORDPRESS_DB_NAME=wordpress
volumes:
- wordpress:/var/www/html
networks:
- web
- backend
labels:
#### Labels define the behavior and rules of the traefik proxy for this container ####
- "traefik.enable=true" # <== Enable traefik to proxy this container
- "traefik.http.routers.nginx-web.rule=Host(`mymuseum.be`)" # <== Your Domain Name goes here for the http rule
- "traefik.http.routers.nginx-web.entrypoints=web" # <== Defining the entrypoint for http, **ref: line 30
- "traefik.http.routers.nginx-web.middlewares=redirect@file" # <== This is a middleware to redirect to https
- "traefik.http.routers.nginx-secured.rule=Host(`mymuseum.be`)" # <== Your Domain Name for the https rule
- "traefik.http.routers.nginx-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31
- "traefik.http.routers.nginx-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https
- "traefik.enable=true"
- "traefik.http.routers.landing.rule=Host(`myinfomate.be`)"
- "traefik.http.routers.landing.entrypoints=web"
- "traefik.http.routers.landing.middlewares=redirect@file"
- "traefik.http.routers.landing-secured.rule=Host(`myinfomate.be`)"
- "traefik.http.routers.landing-secured.entrypoints=web-secured"
- "traefik.http.routers.landing-secured.tls.certresolver=mytlschallenge"
managerService:
container_name: "manager-service"
@ -119,19 +108,6 @@ services:
################################################
#### DB Container not on traefik #####
##############################################
db:
image: mysql:8.0
container_name: db
restart: unless-stopped
command: '--default-authentication-plugin=mysql_native_password'
env_file: .env
environment:
- MYSQL_DATABASE=wordpress
volumes:
- dbdata:/var/lib/mysql
networks:
- backend
mongo:
image: mongo
container_name: "mongodb"