Quota update in controller, audit log (serilog) + ai translator + Unit tests ! (to be tested) + migration update
This commit is contained in:
parent
eff4f7ba5c
commit
e0ff0eeba6
3
.gitignore
vendored
3
.gitignore
vendored
@ -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/
|
||||
|
||||
176
ManagerService.Tests/Controllers/AiControllerTests.cs
Normal file
176
ManagerService.Tests/Controllers/AiControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
87
ManagerService.Tests/Controllers/ApiKeyControllerTests.cs
Normal file
87
ManagerService.Tests/Controllers/ApiKeyControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
104
ManagerService.Tests/Controllers/ConfigurationControllerTests.cs
Normal file
104
ManagerService.Tests/Controllers/ConfigurationControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
136
ManagerService.Tests/Controllers/DeviceControllerTests.cs
Normal file
136
ManagerService.Tests/Controllers/DeviceControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
247
ManagerService.Tests/Controllers/InstanceControllerTests.cs
Normal file
247
ManagerService.Tests/Controllers/InstanceControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
144
ManagerService.Tests/Controllers/ResourceControllerTests.cs
Normal file
144
ManagerService.Tests/Controllers/ResourceControllerTests.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
195
ManagerService.Tests/Controllers/SectionAgendaControllerTests.cs
Normal file
195
ManagerService.Tests/Controllers/SectionAgendaControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
125
ManagerService.Tests/Controllers/SectionControllerTests.cs
Normal file
125
ManagerService.Tests/Controllers/SectionControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
186
ManagerService.Tests/Controllers/SectionEventControllerTests.cs
Normal file
186
ManagerService.Tests/Controllers/SectionEventControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
142
ManagerService.Tests/Controllers/SectionMapControllerTests.cs
Normal file
142
ManagerService.Tests/Controllers/SectionMapControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
195
ManagerService.Tests/Controllers/SectionQuizControllerTests.cs
Normal file
195
ManagerService.Tests/Controllers/SectionQuizControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
179
ManagerService.Tests/Controllers/StatsControllerTests.cs
Normal file
179
ManagerService.Tests/Controllers/StatsControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
185
ManagerService.Tests/Controllers/UserControllerTests.cs
Normal file
185
ManagerService.Tests/Controllers/UserControllerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
ManagerService.Tests/Infrastructure/DbContextFactory.cs
Normal file
17
ManagerService.Tests/Infrastructure/DbContextFactory.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
ManagerService.Tests/Infrastructure/FakeMongoConfig.cs
Normal file
22
ManagerService.Tests/Infrastructure/FakeMongoConfig.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
61
ManagerService.Tests/Infrastructure/FakeUser.cs
Normal file
61
ManagerService.Tests/Infrastructure/FakeUser.cs
Normal 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 }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
33
ManagerService.Tests/ManagerService.Tests.csproj
Normal file
33
ManagerService.Tests/ManagerService.Tests.csproj
Normal 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>
|
||||
7
ManagerService.Tests/nuget.config
Normal file
7
ManagerService.Tests/nuget.config
Normal 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>
|
||||
@ -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
|
||||
|
||||
3
ManagerService/AssemblyInfo.cs
Normal file
3
ManagerService/AssemblyInfo.cs
Normal file
@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("ManagerService.Tests")]
|
||||
@ -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();
|
||||
|
||||
|
||||
64
ManagerService/Controllers/AuditController.cs
Normal file
64
ManagerService/Controllers/AuditController.cs
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
@ -373,7 +387,8 @@ namespace ManagerService.Controllers
|
||||
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
|
||||
if (updatedResource.sizeBytes > 0)
|
||||
resource.SizeBytes = updatedResource.sizeBytes;
|
||||
|
||||
_myInfoMateDbContext.SaveChanges();
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
17
ManagerService/Data/AuditLog.cs
Normal file
17
ManagerService/Data/AuditLog.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
1557
ManagerService/Migrations/20260410122319_AddStatsHistoryToSubscriptionPlan.Designer.cs
generated
Normal file
1557
ManagerService/Migrations/20260410122319_AddStatsHistoryToSubscriptionPlan.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
1557
ManagerService/Migrations/20260410122549_FixPremiumStatsSettings.Designer.cs
generated
Normal file
1557
ManagerService/Migrations/20260410122549_FixPremiumStatsSettings.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
1578
ManagerService/Migrations/20260410123835_AddQuotaFieldsToInstance.Designer.cs
generated
Normal file
1578
ManagerService/Migrations/20260410123835_AddQuotaFieldsToInstance.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
1578
ManagerService/Migrations/20260410124619_FixInstanceQuotaColumnsWritable.Designer.cs
generated
Normal file
1578
ManagerService/Migrations/20260410124619_FixInstanceQuotaColumnsWritable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,23 +1,37 @@
|
||||
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
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
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>();
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
11
ManagerService/Services/IAssistantService.cs
Normal file
11
ManagerService/Services/IAssistantService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,13 +8,20 @@
|
||||
//"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",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"Microsoft.EntityFrameworkCore": "Warning"
|
||||
}
|
||||
},
|
||||
"WriteTo": [
|
||||
{ "Name": "Console" }
|
||||
],
|
||||
"Enrich": [ "FromLogContext" ]
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Tokens": {
|
||||
"Secret": "WVD[&vAwis9=#883bM$FRc0kdKJdfkkjlksd4554qsasadd54df875ds4fqsf5Mw8h",
|
||||
|
||||
210
docker-compose-myinfomate.yml
Normal file
210
docker-compose-myinfomate.yml
Normal 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
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user