diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddTenantCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddTenantCommandTests.cs deleted file mode 100644 index 2653c6ee1..000000000 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddTenantCommandTests.cs +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System; -using System.IO; -using System.Text.Json; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using EdFi.Ods.AdminApi.Infrastructure.Helpers; -using NUnit.Framework; -using Shouldly; - -namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; - -[TestFixture] -public class AddTenantCommandTests -{ - private string _testFilePath; - - private static string GetTestFilePath(string testName) - { - var dir = Path.Combine(Path.GetTempPath(), "AdminApiTests"); - Directory.CreateDirectory(dir); - return Path.Combine(dir, $"{testName}_{Guid.NewGuid()}.json"); - } - - private void CopyTestSettings(string destPath, string sourceFile = "testsappsettings.json") - { - var sourcePath = Path.Combine(TestContext.CurrentContext.TestDirectory, sourceFile); - File.Copy(sourcePath, destPath, true); - } - - [TearDown] - public void Cleanup() - { - if (!string.IsNullOrEmpty(_testFilePath) && File.Exists(_testFilePath)) - { - File.Delete(_testFilePath); - } - } - - [Test] - public void ShouldThrowWhenAppSettingsIsEmpty() - { - _testFilePath = GetTestFilePath(nameof(ShouldThrowWhenAppSettingsIsEmpty)); - File.WriteAllText(_testFilePath, string.Empty); - - var provider = new FileSystemAppSettingsFileProvider(_testFilePath); - var command = new AddTenantCommand(provider); - - var model = new TestAddTenantModel("newtenant", "sec", "adm"); - - var ex = Should.Throw(() => command.Execute(model)); - ex.Message.ShouldBe("appsettings.json contains invalid JSON."); - } - - [Test] - public void ShouldThrowWhenTenantsSectionMissing() - { - _testFilePath = GetTestFilePath(nameof(ShouldThrowWhenTenantsSectionMissing)); - var json = @"{ ""ConnectionStrings"": { ""EdFi_Admin"": ""a"", ""EdFi_Security"": ""b"" } }"; - File.WriteAllText(_testFilePath, json); - - var provider = new FileSystemAppSettingsFileProvider(_testFilePath); - var command = new AddTenantCommand(provider); - - var model = new TestAddTenantModel("newtenant", "sec", "adm"); - - var ex = Should.Throw(() => command.Execute(model)); - ex.Message.ShouldBe("Tenants section missing in appsettings.json."); - } - - [Test] - public void ShouldThrowWhenTenantAlreadyExists() - { - _testFilePath = GetTestFilePath(nameof(ShouldThrowWhenTenantAlreadyExists)); - CopyTestSettings(_testFilePath); - - var provider = new FileSystemAppSettingsFileProvider(_testFilePath); - var command = new AddTenantCommand(provider); - - var model = new TestAddTenantModel("tenant1", "sec", "adm"); - - var ex = Should.Throw(() => command.Execute(model)); - ex.Message.ShouldBe("Tenant 'tenant1' already exists."); - } - - [Test] - public void ShouldThrowWhenAppSettingsContainsInvalidJson() - { - _testFilePath = GetTestFilePath(nameof(ShouldThrowWhenAppSettingsContainsInvalidJson)); - File.WriteAllText(_testFilePath, "{ invalid json }"); - - var provider = new FileSystemAppSettingsFileProvider(_testFilePath); - var command = new AddTenantCommand(provider); - - var model = new TestAddTenantModel("newtenant", "sec", "adm"); - - var ex = Should.Throw(() => command.Execute(model)); - ex.Message.ShouldBe("appsettings.json contains invalid JSON."); - } - - [Test] - public void ShouldAddNewTenantWhenValid() - { - _testFilePath = GetTestFilePath(nameof(ShouldAddNewTenantWhenValid)); - CopyTestSettings(_testFilePath); - - var provider = new FileSystemAppSettingsFileProvider(_testFilePath); - var command = new AddTenantCommand(provider); - - var model = new TestAddTenantModel("newtenant", "sec-conn", "adm-conn"); - - command.Execute(model); - - var updatedJson = File.ReadAllText(_testFilePath); - using var doc = JsonDocument.Parse(updatedJson); - var tenants = doc.RootElement.GetProperty("Tenants"); - tenants.TryGetProperty("newtenant", out var newTenant).ShouldBeTrue(); - var connStrings = newTenant.GetProperty("ConnectionStrings"); - connStrings.GetProperty("EdFi_Security").GetString().ShouldBe("sec-conn"); - connStrings.GetProperty("EdFi_Admin").GetString().ShouldBe("adm-conn"); - } - - private class TestAddTenantModel(string tenantName, string sec, string adm) : IAddTenantModel - { - public string TenantName { get; } = tenantName; - public string EdFiSecurityConnectionString { get; } = sec; - public string EdFiAdminConnectionString { get; } = adm; - } -} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteTenantCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteTenantCommandTests.cs deleted file mode 100644 index 84e42267d..000000000 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteTenantCommandTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System; -using System.IO; -using System.Text.Json; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using EdFi.Ods.AdminApi.Infrastructure.Helpers; -using NUnit.Framework; -using Shouldly; - -namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; - -[TestFixture] -public class DeleteTenantCommandTests -{ - private string _testFilePath = string.Empty; - - private static string GetTestFilePath(string testName) - { - var dir = Path.Combine(Path.GetTempPath(), "AdminApiTests"); - Directory.CreateDirectory(dir); - return Path.Combine(dir, $"{testName}_{Guid.NewGuid()}.json"); - } - - private void CopyTestSettings(string destPath, string sourceFile = "testsappsettings.json") - { - var sourcePath = Path.Combine(TestContext.CurrentContext.TestDirectory, sourceFile); - File.Copy(sourcePath, destPath, true); - } - - [TearDown] - public void Cleanup() - { - if (_testFilePath is not null && File.Exists(_testFilePath)) - { - File.Delete(_testFilePath); - } - } - - [Test] - public void Should_throw_when_appsettings_is_empty() - { - _testFilePath = GetTestFilePath(nameof(Should_throw_when_appsettings_is_empty)); - File.WriteAllText(_testFilePath, string.Empty); - - var provider = new FileSystemAppSettingsFileProvider(_testFilePath); - var command = new DeleteTenantCommand(provider); - - var ex = Should.Throw(() => command.Execute("tenant1")); - ex.Message.ShouldBe("appsettings.json contains invalid JSON."); - } - - [Test] - public void Should_throw_when_tenants_section_missing() - { - _testFilePath = GetTestFilePath(nameof(Should_throw_when_tenants_section_missing)); - var json = @"{ ""ConnectionStrings"": { ""EdFi_Admin"": ""a"", ""EdFi_Security"": ""b"" } }"; - File.WriteAllText(_testFilePath, json); - - var provider = new FileSystemAppSettingsFileProvider(_testFilePath); - var command = new DeleteTenantCommand(provider); - - var ex = Should.Throw(() => command.Execute("tenant1")); - ex.Message.ShouldBe("Tenants section missing in appsettings.json."); - } - - [Test] - public void Should_throw_when_tenant_does_not_exist() - { - _testFilePath = GetTestFilePath(nameof(Should_throw_when_tenant_does_not_exist)); - CopyTestSettings(_testFilePath); - - var provider = new FileSystemAppSettingsFileProvider(_testFilePath); - var command = new DeleteTenantCommand(provider); - - var ex = Should.Throw(() => command.Execute("notarealtenant")); - ex.Message.ShouldBe("Tenant 'notarealtenant' does not exist."); - } - - [Test] - public void Should_throw_when_appsettings_contains_invalid_json() - { - _testFilePath = GetTestFilePath(nameof(Should_throw_when_appsettings_contains_invalid_json)); - File.WriteAllText(_testFilePath, "{ invalid json }"); - - var provider = new FileSystemAppSettingsFileProvider(_testFilePath); - var command = new DeleteTenantCommand(provider); - - var ex = Should.Throw(() => command.Execute("tenant1")); - ex.Message.ShouldBe("appsettings.json contains invalid JSON."); - } - - [Test] - public void Should_delete_tenant_when_valid() - { - _testFilePath = GetTestFilePath(nameof(Should_delete_tenant_when_valid)); - CopyTestSettings(_testFilePath); - - var provider = new FileSystemAppSettingsFileProvider(_testFilePath); - var command = new DeleteTenantCommand(provider); - - command.Execute("tenant1"); - - var updatedJson = File.ReadAllText(_testFilePath); - using var doc = JsonDocument.Parse(updatedJson); - var tenants = doc.RootElement.GetProperty("Tenants"); - tenants.TryGetProperty("tenant1", out _).ShouldBeFalse(); - tenants.TryGetProperty("tenant2", out _).ShouldBeTrue(); - } -} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/AddTenantTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/AddTenantTests.cs deleted file mode 100644 index db9aeb241..000000000 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/AddTenantTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System; -using System.Threading.Tasks; -using AutoMapper; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Features.Tenants; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using EdFi.Ods.AdminApi.Infrastructure.Helpers; -using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; -using FakeItEasy; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using NUnit.Framework; -using Shouldly; - -namespace EdFi.Ods.AdminApi.UnitTests.Features.Tenants; - -[TestFixture] -public class AddTenantTests -{ - [Test] - public async Task Handle_ReturnsBadRequest_WhenNotMultiTenant() - { - var options = A.Fake>(); - A.CallTo(() => options.Value).Returns(new AppSettings { MultiTenancy = false, DatabaseEngine = "Postgres" }); - - var addTenantCommand = new TestAddTenantCommand(); - var mapper = A.Fake(); - var request = new AddTenant.AddTenantRequest { TenantName = "tenant1" }; - var serviceScopeFactory = A.Fake(); - var tenantsService = A.Fake(); - var scope = A.Fake(); - var scopedServiceProvider = A.Fake(); - - A.CallTo(() => scopedServiceProvider.GetService(typeof(ITenantsService))).Returns(tenantsService); - - A.CallTo(() => serviceScopeFactory.CreateScope()).Returns(scope); - A.CallTo(() => scope.ServiceProvider).Returns(scopedServiceProvider); - - var validator = new TestValidator(tenantsService, options); - - var result = await AddTenant.Handle(validator, addTenantCommand, mapper, request, serviceScopeFactory, options); - - // Assert - var badRequest = result as IResult; - badRequest.ShouldNotBeNull(); - badRequest.GetType().Name.ShouldStartWith("BadRequest"); - var valueProperty = badRequest.GetType().GetProperty("Value"); - valueProperty.ShouldNotBeNull(); - var value = valueProperty.GetValue(badRequest); - value.ShouldNotBeNull(); - value.ToString().ShouldContain("Not multitenant environment."); - } - - [Test] - public async Task Handle_CallsValidatorAndCommand_AndReturnsCreated_WhenValid() - { - var options = A.Fake>(); - A.CallTo(() => options.Value).Returns(new AppSettings { MultiTenancy = true, DatabaseEngine = "Postgres" }); - - var addTenantCommand = new TestAddTenantCommand(); - var mapper = A.Fake(); - var model = A.Fake(); - var request = new AddTenant.AddTenantRequest { TenantName = "tenant2" }; - var serviceScopeFactory = A.Fake(); - var tenantsService = A.Fake(); - var scope = A.Fake(); - var scopedServiceProvider = A.Fake(); - - A.CallTo(() => scopedServiceProvider.GetService(typeof(ITenantsService))).Returns(tenantsService); - A.CallTo(() => mapper.Map(request)).Returns(model); - - A.CallTo(() => serviceScopeFactory.CreateScope()).Returns(scope); - A.CallTo(() => scope.ServiceProvider).Returns(scopedServiceProvider); - - var validator = new TestValidator(tenantsService, options); - - var result = await AddTenant.Handle(validator, addTenantCommand, mapper, request, serviceScopeFactory, options); - - result.ShouldNotBeNull(); - result.GetType().Name.ShouldBe("Created"); - } - - private class TestValidator(ITenantsService service, IOptions options) : AddTenant.Validator(service, options) - { - public Task GuardAsync(AddTenant.AddTenantRequest request) - { - // Always succeed - return Task.CompletedTask; - } - } - - private class TestAddTenantCommand : AddTenantCommand - { - public string ExecutedTenantName { get; private set; } - - public TestAddTenantCommand() : base(A.Fake()) - { - } - - public override void Execute(IAddTenantModel model) - { - ExecutedTenantName = model.TenantName; - } - } -} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/AddTenantValidatorTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/AddTenantValidatorTests.cs deleted file mode 100644 index 4252d3eb4..000000000 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/AddTenantValidatorTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Features.Tenants; -using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; -using FakeItEasy; -using Microsoft.Extensions.Options; -using NUnit.Framework; -using Shouldly; - -namespace EdFi.Ods.AdminApi.UnitTests.Features.Tenants; - -[TestFixture] -public class AddTenantValidatorTests -{ - private AddTenant.Validator _validator; - private ITenantsService _tenantsService; - private IOptions _options; - - [SetUp] - public void SetUp() - { - _tenantsService = A.Fake(); - _options = Options.Create(new AppSettings { DatabaseEngine = "SqlServer" }); - _validator = new AddTenant.Validator(_tenantsService, _options); - } - - [Test] - public void Should_Have_Error_When_TenantName_Is_Empty() - { - var model = new AddTenant.AddTenantRequest { TenantName = string.Empty }; - var result = _validator.Validate(model); - result.Errors.Any(x => x.PropertyName == nameof(model.TenantName)).ShouldBeTrue(); - } - - [Test] - public void Should_Have_Error_When_TenantName_Exceeds_Max_Length() - { - var model = new AddTenant.AddTenantRequest { TenantName = new string('A', 101) }; - var result = _validator.Validate(model); - result.Errors.Any(x => x.PropertyName == nameof(model.TenantName)).ShouldBeTrue(); - } - - [Test] - public async Task Should_Have_Error_When_TenantName_Is_Not_Unique() - { - var existingTenants = new List - { - new() { TenantName = "ExistingTenant" } - }; - A.CallTo(() => _tenantsService.GetTenantsAsync(true)).Returns(Task.FromResult(existingTenants)); - - var model = new AddTenant.AddTenantRequest { TenantName = "ExistingTenant" }; - var result = await _validator.ValidateAsync(model); - result.Errors.Any(x => x.PropertyName == nameof(model.TenantName)).ShouldBeTrue(); - } - - [Test] - public void Should_Have_Error_When_EdFiAdminConnectionString_Exceeds_Max_Length() - { - var model = new AddTenant.AddTenantRequest - { - TenantName = "UniqueTenant", - EdFiAdminConnectionString = new string('C', 501) - }; - var result = _validator.Validate(model); - result.Errors.Any(x => x.PropertyName == nameof(model.EdFiAdminConnectionString)).ShouldBeTrue(); - } - - [Test] - public void Should_Have_Error_When_EdFiAdminConnectionString_Is_Invalid() - { - var model = new AddTenant.AddTenantRequest - { - TenantName = "UniqueTenant", - EdFiAdminConnectionString = "invalid-connection-string" - }; - var result = _validator.Validate(model); - result.Errors.Any(x => x.PropertyName == nameof(model.EdFiAdminConnectionString)).ShouldBeTrue(); - } - - [Test] - public void Should_Have_Error_When_EdFiSecurityConnectionString_Exceeds_Max_Length() - { - var model = new AddTenant.AddTenantRequest - { - TenantName = "UniqueTenant", - EdFiSecurityConnectionString = new string('C', 501) - }; - var result = _validator.Validate(model); - result.Errors.Any(x => x.PropertyName == nameof(model.EdFiSecurityConnectionString)).ShouldBeTrue(); - } - - [Test] - public void Should_Have_Error_When_EdFiSecurityConnectionString_Is_Invalid() - { - var model = new AddTenant.AddTenantRequest - { - TenantName = "UniqueTenant", - EdFiSecurityConnectionString = "invalid-connection-string" - }; - - var result = _validator.Validate(model); - result.Errors.Any(x => x.PropertyName == nameof(model.EdFiSecurityConnectionString)).ShouldBeTrue(); - } - - [Test] - public async Task Should_Not_Have_Error_For_Valid_Model() - { - // Setup empty tenants list to ensure uniqueness check passes - A.CallTo(() => _tenantsService.GetTenantsAsync(true)).Returns(Task.FromResult(new List())); - - var model = new AddTenant.AddTenantRequest - { - TenantName = "UniqueTenant", - EdFiAdminConnectionString = "Server=.;Database=Admin;Trusted_Connection=True;", - EdFiSecurityConnectionString = "Server=.;Database=Security;Trusted_Connection=True;" - }; - - var result = await _validator.ValidateAsync(model); - result.IsValid.ShouldBeTrue(); - } -} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/DeleteTenantTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/DeleteTenantTests.cs deleted file mode 100644 index 4173a0bcd..000000000 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/DeleteTenantTests.cs +++ /dev/null @@ -1,153 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System; -using System.Threading.Tasks; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Features.Tenants; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using EdFi.Ods.AdminApi.Infrastructure.Helpers; -using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; -using FakeItEasy; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using NUnit.Framework; -using Shouldly; - -namespace EdFi.Ods.AdminApi.UnitTests.Features.Tenants; - -[TestFixture] -public class DeleteTenantTests -{ - private ITenantsService _tenantsService = null!; - private DeleteTenantCommand _deleteTenantCommand = null!; - private IServiceScopeFactory _serviceScopeFactory = null!; - private IOptions _options = null!; - - [SetUp] - public void SetUp() - { - _tenantsService = A.Fake(); - _deleteTenantCommand = A.Fake(); - _serviceScopeFactory = A.Fake(); - _options = Options.Create(new AppSettings { MultiTenancy = true }); - } - - [Test] - public async Task Handle_ShouldReturnBadRequest_WhenMultiTenancyIsDisabled() - { - // Arrange - var options = Options.Create(new AppSettings { MultiTenancy = false }); - - // Act - var result = await DeleteTenant.Handle( - _tenantsService, - _deleteTenantCommand, - _serviceScopeFactory, - options, - "tenant1"); - - // Assert - var badRequest = result as IResult; - badRequest.ShouldNotBeNull(); - - badRequest.GetType().Name.ShouldStartWith("BadRequest"); - var valueProperty = badRequest.GetType().GetProperty("Value"); - valueProperty.ShouldNotBeNull(); - var value = valueProperty.GetValue(badRequest); - value.ShouldNotBeNull(); - value.ToString().ShouldContain("Not multitenant environment."); - } - - [Test] - public async Task Handle_ShouldReturnNotFound_WhenTenantDoesNotExist() - { - // Arrange - A.CallTo(() => _tenantsService.GetTenantByTenantIdAsync(A._)).Returns(Task.FromResult(null)); - - // Act - var result = await DeleteTenant.Handle( - _tenantsService, - _deleteTenantCommand, - _serviceScopeFactory, - _options, - "tenant1"); - - // Assert - var notFound = result as IResult; - notFound.ShouldNotBeNull(); - var httpResult = notFound as Microsoft.AspNetCore.Http.HttpResults.NotFound; - httpResult.ShouldNotBeNull(); - } - - [Test] - public async Task Handle_ShouldDeleteTenantAndReturnOk_WhenTenantExists() - { - // Arrange - var tenant = new TenantModel { TenantName = "tenant1" }; - A.CallTo(() => _tenantsService.GetTenantByTenantIdAsync("tenant1")).Returns(Task.FromResult(tenant)); - - var scope = A.Fake(); - var scopedServiceProvider = A.Fake(); - var scopedTenantsService = A.Fake(); - - A.CallTo(() => _serviceScopeFactory.CreateScope()).Returns(scope); - A.CallTo(() => scope.ServiceProvider).Returns(scopedServiceProvider); - A.CallTo(() => scopedServiceProvider.GetService(typeof(ITenantsService))).Returns(scopedTenantsService); - A.CallTo(() => scopedTenantsService.InitializeTenantsAsync()).Returns(Task.CompletedTask); - - // Use a test double for DeleteTenantCommand - var testDeleteTenantCommand = new TestDeleteTenantCommand(); - - // Act - var result = await DeleteTenant.Handle( - _tenantsService, - testDeleteTenantCommand, - _serviceScopeFactory, - _options, - "tenant1"); - - // Assert - testDeleteTenantCommand.ExecutedTenantName.ShouldBe("tenant1"); - A.CallTo(() => scopedTenantsService.InitializeTenantsAsync()).MustHaveHappened(); - - var okResult = result as IResult; - okResult.ShouldNotBeNull(); - var httpResult = okResult as Microsoft.AspNetCore.Http.HttpResults.Ok; - httpResult.ShouldNotBeNull(); - } - - [Test] - public void Handle_ShouldThrow_WhenGetTenantByTenantIdAsyncThrows() - { - // Arrange - A.CallTo(() => _tenantsService.GetTenantByTenantIdAsync(A._)).Throws(); - - // Act & Assert - Should.ThrowAsync(async () => - await DeleteTenant.Handle( - _tenantsService, - _deleteTenantCommand, - _serviceScopeFactory, - _options, - "tenant1")); - } - - private class TestDeleteTenantCommand : DeleteTenantCommand - { - public string ExecutedTenantName { get; private set; } - - public TestDeleteTenantCommand() : base(A.Fake()) - { - } - - public override void Execute(string tenantName) - { - ExecutedTenantName = tenantName; - } - } -} - diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddTenantCommandTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddTenantCommandTests.cs deleted file mode 100644 index 43adf7c76..000000000 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddTenantCommandTests.cs +++ /dev/null @@ -1,142 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System; -using System.Text.Json.Nodes; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using EdFi.Ods.AdminApi.Infrastructure.Helpers; -using FakeItEasy; -using NUnit.Framework; -using Shouldly; - -namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Database.Commands; - -[TestFixture] -public class AddTenantCommandTests -{ - private StubFileSystemAppSettingsFileProvider _fileProvider = null!; - private AddTenantCommand _command = null!; - - [SetUp] - public void SetUp() - { - _fileProvider = new StubFileSystemAppSettingsFileProvider(); - _command = new AddTenantCommand(_fileProvider); - } - - private class StubFileSystemAppSettingsFileProvider : FileSystemAppSettingsFileProvider, IAppSettingsFileProvider - { - public StubFileSystemAppSettingsFileProvider() : base("defaultFilePath") - { - } - - public string ReadText { get; set; } - public string WrittenText { get; private set; } - - public new string ReadAllText() - { - return ReadText ?? string.Empty; - } - - public new void WriteAllText(string text) - { - WrittenText = text; - } - } - - // Update tests to use the stub - [Test] - public void Execute_ShouldAddTenant_WhenTenantDoesNotExist() - { - // Arrange - var stubProvider = _fileProvider; - stubProvider.ReadText = @"{ - ""Tenants"": { - ""existingTenant"": { - ""ConnectionStrings"": { - ""EdFi_Security"": ""sec"", - ""EdFi_Admin"": ""admin"" - } - } - } - }"; - - var model = A.Fake(); - A.CallTo(() => model.TenantName).Returns("newTenant"); - A.CallTo(() => model.EdFiSecurityConnectionString).Returns("sec2"); - A.CallTo(() => model.EdFiAdminConnectionString).Returns("admin2"); - - // Act - _command.Execute(model); - - // Assert - stubProvider.WrittenText.ShouldNotBeNull(); - var root = JsonNode.Parse(stubProvider.WrittenText!)!; - var tenants = root["Tenants"]!.AsObject(); - tenants.ContainsKey("newTenant").ShouldBeTrue(); - tenants["newTenant"]!["ConnectionStrings"]!["EdFi_Security"]!.ToString().ShouldBe("sec2"); - tenants["newTenant"]!["ConnectionStrings"]!["EdFi_Admin"]!.ToString().ShouldBe("admin2"); - } - - [Test] - public void Execute_ShouldThrow_WhenTenantAlreadyExists() - { - // Arrange - var stubProvider = _fileProvider; - stubProvider.ReadText = @"{ - ""Tenants"": { - ""existingTenant"": { - ""ConnectionStrings"": { - ""EdFi_Security"": ""sec"", - ""EdFi_Admin"": ""admin"" - } - } - } - }"; - - var model = A.Fake(); - A.CallTo(() => model.TenantName).Returns("existingTenant"); - A.CallTo(() => model.EdFiSecurityConnectionString).Returns("sec"); - A.CallTo(() => model.EdFiAdminConnectionString).Returns("admin"); - - // Act & Assert - var ex = Should.Throw(() => _command.Execute(model)); - ex.Message.ShouldContain("already exists"); - } - - [Test] - public void Execute_ShouldThrow_WhenTenantsSectionMissing() - { - // Arrange - var stubProvider = _fileProvider; - stubProvider.ReadText = @"{ ""SomeOtherSection"": {} }"; - - var model = A.Fake(); - A.CallTo(() => model.TenantName).Returns("tenantX"); - A.CallTo(() => model.EdFiSecurityConnectionString).Returns("secX"); - A.CallTo(() => model.EdFiAdminConnectionString).Returns("adminX"); - - // Act & Assert - var ex = Should.Throw(() => _command.Execute(model)); - ex.Message.ShouldContain("Tenants section missing"); - } - - [Test] - public void Execute_ShouldThrow_WhenAppSettingsIsEmptyOrInvalid() - { - // Arrange - var stubProvider = _fileProvider; - stubProvider.ReadText = string.Empty; - - var model = A.Fake(); - A.CallTo(() => model.TenantName).Returns("tenantY"); - A.CallTo(() => model.EdFiSecurityConnectionString).Returns("secY"); - A.CallTo(() => model.EdFiAdminConnectionString).Returns("adminY"); - - // Act & Assert - var ex = Should.Throw(() => _command.Execute(model)); - ex.Message.ShouldContain("appsettings.json contains invalid JSON."); - } -} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteTenantCommandTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteTenantCommandTests.cs deleted file mode 100644 index 812140e0d..000000000 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteTenantCommandTests.cs +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System; -using System.Text.Json.Nodes; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using EdFi.Ods.AdminApi.Infrastructure.Helpers; -using NUnit.Framework; -using Shouldly; - -namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Database.Commands; - -[TestFixture] -public class DeleteTenantCommandTests -{ - private StubFileSystemAppSettingsFileProvider _fileProvider = null!; - private DeleteTenantCommand _command = null!; - - [SetUp] - public void SetUp() - { - _fileProvider = new StubFileSystemAppSettingsFileProvider(); - _command = new DeleteTenantCommand(_fileProvider); - } - - private class StubFileSystemAppSettingsFileProvider : FileSystemAppSettingsFileProvider, IAppSettingsFileProvider - { - public StubFileSystemAppSettingsFileProvider() : base("defaultFilePath") - { - } - - public string ReadText { get; set; } - public string WrittenText { get; private set; } - - public new string ReadAllText() - { - return ReadText ?? string.Empty; - } - - public new void WriteAllText(string text) - { - WrittenText = text; - } - } - - [Test] - public void Execute_ShouldRemoveTenant_WhenTenantExists() - { - // Arrange - var stubProvider = _fileProvider; - stubProvider.ReadText = @"{ - ""Tenants"": { - ""tenant1"": { - ""ConnectionStrings"": { - ""EdFi_Security"": ""sec1"", - ""EdFi_Admin"": ""admin1"" - } - }, - ""tenant2"": { - ""ConnectionStrings"": { - ""EdFi_Security"": ""sec2"", - ""EdFi_Admin"": ""admin2"" - } - } - } - }"; - - // Act - _command.Execute("tenant1"); - - // Assert - stubProvider.WrittenText.ShouldNotBeNull(); - var root = JsonNode.Parse(stubProvider.WrittenText!)!; - var tenants = root["Tenants"]!.AsObject(); - tenants.ContainsKey("tenant1").ShouldBeFalse(); - tenants.ContainsKey("tenant2").ShouldBeTrue(); - } - - [Test] - public void Execute_ShouldThrow_WhenTenantDoesNotExist() - { - // Arrange - var stubProvider = _fileProvider; - stubProvider.ReadText = @"{ - ""Tenants"": { - ""tenant2"": { - ""ConnectionStrings"": { - ""EdFi_Security"": ""sec2"", - ""EdFi_Admin"": ""admin2"" - } - } - } - }"; - - // Act & Assert - var ex = Should.Throw(() => _command.Execute("tenant1")); - ex.Message.ShouldContain("does not exist"); - } - - [Test] - public void Execute_ShouldThrow_WhenTenantsSectionMissing() - { - // Arrange - var stubProvider = _fileProvider; - stubProvider.ReadText = @"{ ""SomeOtherSection"": {} }"; - - // Act & Assert - var ex = Should.Throw(() => _command.Execute("tenant1")); - ex.Message.ShouldContain("Tenants section missing"); - } - - [Test] - public void Execute_ShouldThrow_WhenAppSettingsIsEmptyOrInvalid() - { - // Arrange - var stubProvider = _fileProvider; - stubProvider.ReadText = string.Empty; - - // Act & Assert - var ex = Should.Throw(() => _command.Execute("tenant1")); - ex.Message.ShouldContain("appsettings.json contains invalid JSON."); - } -} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Tenants.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Tenants.postman_collection.json index 89dfee891..7f4110607 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Tenants.postman_collection.json +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Tenants.postman_collection.json @@ -1,9 +1,9 @@ { "info": { - "_postman_id": "d327aadc-5703-41da-9ee8-113331168025", + "_postman_id": "170362e2-1d87-42e5-9732-d6fbe264d0ba", "name": "Admin API E2E 2.0 - Tenants", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "22794466" + "_exporter_id": "29458798" }, "item": [ { @@ -12,388 +12,6 @@ { "name": "Tenants", "item": [ - { - "name": "Tenants", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (pm.variables.get(\"isMultitenant\") == \"true\") {", - " pm.test(\"POST Tenants: Status code is Created\", function () {", - " pm.response.to.have.status(201);", - " });", - "}", - "else {", - " pm.test(\"Status code is Bad Request\", function () {", - " pm.response.to.have.status(400);", - " });", - "", - " const response = pm.response.json();", - "", - " pm.test(\"Response matches error format\", function () {", - " pm.expect(response).to.have.property(\"message\");", - " });", - "", - " pm.test(\"Response title is helpful and accurate\", function () {", - " pm.expect(response.message.toLowerCase()).to.contain(\"not multitenant environment.\");", - " });", - "}" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "pm.collectionVariables.set(\"CreatedTenantName\", \"Tenant-\" + pm.variables.replaceIn('{{$guid}}'));" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"TenantName\": \"{{CreatedTenantName}}\",\r\n \"EdFiSecurityConnectionString\": \"{{securityconnectionString}}\",\r\n \"EdFiAdminConnectionString\": \"{{connectionString}}\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{API_URL}}/v2/tenants", - "host": [ - "{{API_URL}}" - ], - "path": [ - "v2", - "tenants" - ] - } - }, - "response": [] - }, - { - "name": "Tenants Just Name", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (pm.variables.get(\"isMultitenant\") == \"true\") {", - " pm.test(\"POST Tenants: Status code is Created\", function () {", - " pm.response.to.have.status(201);", - " });", - "}", - "else {", - " pm.test(\"Status code is Bad Request\", function () {", - " pm.response.to.have.status(400);", - " });", - "", - " const response = pm.response.json();", - "", - " pm.test(\"Response matches error format\", function () {", - " pm.expect(response).to.have.property(\"message\");", - " });", - "", - " pm.test(\"Response title is helpful and accurate\", function () {", - " pm.expect(response.message.toLowerCase()).to.contain(\"not multitenant environment.\");", - " });", - "}" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "pm.collectionVariables.set(\"TenantGUID2\", pm.variables.replaceIn('{{$guid}}'));\r", - "" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"TenantName\": \"Tenant-{{TenantGUID2}}\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{API_URL}}/v2/tenants", - "host": [ - "{{API_URL}}" - ], - "path": [ - "v2", - "tenants" - ] - } - }, - "response": [] - }, - { - "name": "Tenants - Invalid Api Mode", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is Bad Request\", function () {", - " pm.response.to.have.status(400);", - "});", - "", - "const response = pm.response.json();", - "", - "pm.test(\"Response matches error format\", function () {", - " pm.expect(response).to.have.property(\"message\");", - "});", - "", - "pm.test(\"Response title is helpful and accurate\", function () {", - " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");", - "});" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"TenantName\": \"Tenant-{{TenantGUID}}\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{API_URL}}/v1/tenants", - "host": [ - "{{API_URL}}" - ], - "path": [ - "v1", - "tenants" - ] - } - }, - "response": [] - }, - { - "name": "Tenants - Invalid Admin connection string", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (pm.variables.get(\"isMultitenant\") == \"true\") {", - " pm.test(\"POST Tenants Invalid EdFiAdminConnectionString: Status code is Bad Request\", function () {", - " pm.response.to.have.status(400);", - " });", - "", - " const response = pm.response.json();", - "", - " pm.test(\"POST Tenants Invalid EdFiAdminConnectionString: Response matches error format\", function () {", - " pm.expect(response).to.have.property(\"title\");", - " pm.expect(response).to.have.property(\"errors\");", - " });", - "", - " pm.test(\"POST Tenants Invalid EdFiAdminConnectionString: Response title is helpful and accurate\", function () {", - " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");", - " });", - "", - " pm.test(\"POST Tenants Invalid ConnectionString: Response errors include messages by property\", function () {", - " pm.expect(response.errors[\"EdFiAdminConnectionString\"].length).to.equal(1);", - " });", - "", - " pm.test(\"POST Tenants Invalid ConnectionString: Response errors include messages with wrong elements\", function () {", - " pm.expect(response.errors[\"EdFiAdminConnectionString\"][0]).to.contain(\"is not valid\");", - " });", - "}", - "else {", - " pm.test(\"Status code is Bad Request\", function () {", - " pm.response.to.have.status(400);", - " });", - "", - " const response = pm.response.json();", - "", - " pm.test(\"Response matches error format\", function () {", - " pm.expect(response).to.have.property(\"message\");", - " });", - "", - " pm.test(\"Response title is helpful and accurate\", function () {", - " pm.expect(response.message.toLowerCase()).to.contain(\"not multitenant environment.\");", - " });", - "}" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"TenantName\": \"Tenant-{{TenantGUID}}\",\r\n \"EdFiSecurityConnectionString\": \"{{securityconnectionString}}\",\r\n \"EdFiAdminConnectionString\": \"not-valid-connection-string\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{API_URL}}/v2/tenants", - "host": [ - "{{API_URL}}" - ], - "path": [ - "v2", - "tenants" - ] - } - }, - "response": [] - }, - { - "name": "Tenants - Invalid Security connection string", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (pm.variables.get(\"isMultitenant\") == \"true\") {", - " pm.test(\"POST Tenants Invalid EdFiSecurityConnectionString: Status code is Bad Request\", function () {", - " pm.response.to.have.status(400);", - " });", - "", - " const response = pm.response.json();", - "", - " pm.test(\"POST Tenants Invalid EdFiSecurityConnectionString: Response matches error format\", function () {", - " pm.expect(response).to.have.property(\"title\");", - " pm.expect(response).to.have.property(\"errors\");", - " });", - "", - " pm.test(\"POST Tenants Invalid EdFiSecurityConnectionString: Response title is helpful and accurate\", function () {", - " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");", - " });", - "", - " pm.test(\"POST Tenants Invalid ConnectionString: Response errors include messages by property\", function () {", - " pm.expect(response.errors[\"EdFiSecurityConnectionString\"].length).to.equal(1);", - " });", - "", - " pm.test(\"POST Tenants Invalid ConnectionString: Response errors include messages with wrong elements\", function () {", - " pm.expect(response.errors[\"EdFiSecurityConnectionString\"][0]).to.contain(\"is not valid\");", - " });", - "}", - "else {", - " pm.test(\"Status code is Bad Request\", function () {", - " pm.response.to.have.status(400);", - " });", - "", - " const response = pm.response.json();", - "", - " pm.test(\"Response matches error format\", function () {", - " pm.expect(response).to.have.property(\"message\");", - " });", - "", - " pm.test(\"Response title is helpful and accurate\", function () {", - " pm.expect(response.message.toLowerCase()).to.contain(\"not multitenant environment.\");", - " });", - "}" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - }, - { - "listen": "prerequest", - "script": { - "exec": [ - "" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - } - ], - "request": { - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\r\n \"TenantName\": \"Tenant-{{TenantGUID}}\",\r\n \"EdFiSecurityConnectionString\": \"not-valid-connection-string\",\r\n \"EdFiAdminConnectionString\": \"{{connectionString}}\"\r\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{API_URL}}/v2/tenants", - "host": [ - "{{API_URL}}" - ], - "path": [ - "v2", - "tenants" - ] - } - }, - "response": [] - }, { "name": "Tenants", "event": [ @@ -515,7 +133,7 @@ "response": [] }, { - "name": "Tenants Invalid", + "name": "Tenant without 's'", "event": [ { "listen": "test", @@ -563,9 +181,7 @@ " const result = pm.response.json();", "", " pm.test(\"GET Tenant Name: Response result matches tenant\", function () {", - " const tenantName = pm.collectionVariables.get(\"CreatedTenantName\");", - "", - " pm.expect(result.tenantName).to.equal(tenantName);", + " pm.expect(result.tenantName).to.equal(\"tenant1\");", " pm.expect(result.adminConnectionString.host).to.not.equal(null);", " pm.expect(result.adminConnectionString.database).to.not.equal(null);", " pm.expect(result.securityConnectionString.host).to.not.equal(null);", @@ -626,14 +242,14 @@ "method": "GET", "header": [], "url": { - "raw": "{{API_URL}}/v2/tenants/{{CreatedTenantName}}", + "raw": "{{API_URL}}/v2/tenants/tenant1", "host": [ "{{API_URL}}" ], "path": [ "v2", "tenants", - "{{CreatedTenantName}}" + "tenant1" ] } }, @@ -757,101 +373,6 @@ } }, "response": [] - }, - { - "name": "Tenants - Invalid Api Mode", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"Status code is Bad Request\", function () {", - " pm.response.to.have.status(400);", - "});", - "", - "const response = pm.response.json();", - "", - "pm.test(\"Response matches error format\", function () {", - " pm.expect(response).to.have.property(\"message\");", - "});", - "", - "pm.test(\"Response title is helpful and accurate\", function () {", - " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");", - "});" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - } - ], - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "{{API_URL}}/v1/tenants/{{CreatedTenantName}}", - "host": [ - "{{API_URL}}" - ], - "path": [ - "v1", - "tenants", - "{{CreatedTenantName}}" - ] - } - }, - "response": [] - }, - { - "name": "Tenants", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (pm.variables.get(\"isMultitenant\") == \"true\") {", - " pm.test(\"DELETE Tenants: Status code is OK\", function () {", - " pm.response.to.have.status(200);", - " });", - "}", - "else {", - " pm.test(\"Status code is Bad Request\", function () {", - " pm.response.to.have.status(400);", - " });", - "", - " const response = pm.response.json();", - "", - " pm.test(\"Response matches error format\", function () {", - " pm.expect(response).to.have.property(\"message\");", - " });", - "", - " pm.test(\"Response title is helpful and accurate\", function () {", - " pm.expect(response.message.toLowerCase()).to.contain(\"not multitenant environment.\");", - " });", - "}" - ], - "type": "text/javascript", - "packages": {}, - "requests": {} - } - } - ], - "request": { - "method": "DELETE", - "header": [], - "url": { - "raw": "{{API_URL}}/v2/tenants/{{CreatedTenantName}}", - "host": [ - "{{API_URL}}" - ], - "path": [ - "v2", - "tenants", - "{{CreatedTenantName}}" - ] - } - }, - "response": [] } ] } diff --git a/Application/EdFi.Ods.AdminApi/Features/Tenants/AddTenant.cs b/Application/EdFi.Ods.AdminApi/Features/Tenants/AddTenant.cs deleted file mode 100644 index bdab671a9..000000000 --- a/Application/EdFi.Ods.AdminApi/Features/Tenants/AddTenant.cs +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using AutoMapper; -using EdFi.Ods.AdminApi.Common.Features; -using EdFi.Ods.AdminApi.Common.Infrastructure; -using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; -using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; -using FluentValidation; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Swashbuckle.AspNetCore.Annotations; - -namespace EdFi.Ods.AdminApi.Features.Tenants; - -public class AddTenant : IFeature -{ - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder - .MapPost(endpoints, "/tenants", Handle) - .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponseCode(201)) - .BuildForVersions(AdminApiVersions.V2); - } - - public static async Task Handle( - Validator validator, - AddTenantCommand addTenantCommand, - IMapper mapper, - AddTenantRequest request, - IServiceScopeFactory serviceScopeFactory, - IOptions options) - { - if (!options.Value.MultiTenancy) - return Results.BadRequest(new { message = "Not multitenant environment." }); - - await validator.GuardAsync(request); - - var model = mapper.Map(request); - addTenantCommand.Execute(model); - - await InitializeTenantsAsync(serviceScopeFactory); - - return Results.Created($"/tenants/{request.TenantName}", null); - } - - [SwaggerSchema(Title = "AddTenantRequest")] - public class AddTenantRequest : IAddTenantModel - { - [SwaggerSchema(Description = "The unique name of the tenant.", Nullable = false)] - public string TenantName { get; set; } = string.Empty; - - [SwaggerSchema(Description = "The connection string for EdFi_Security.", Nullable = true)] - public string? EdFiSecurityConnectionString { get; set; } - - [SwaggerSchema(Description = "The connection string for EdFi_Admin.", Nullable = true)] - public string? EdFiAdminConnectionString { get; set; } - } - - public class Validator : AbstractValidator - { - private readonly ITenantsService _tenantsService; - private readonly string _databaseEngine; - - public Validator([FromServices] ITenantsService tenantsService, IOptions options) - { - _tenantsService = tenantsService; - _databaseEngine = options.Value.DatabaseEngine ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); - - RuleFor(x => x.TenantName) - .NotEmpty() - .MaximumLength(100) - .Must(BeAUniqueName) - .WithMessage(FeatureConstants.TenantAlreadyExistsMessage); - - RuleFor(m => m.EdFiAdminConnectionString) - .MaximumLength(500) - .Must(BeAValidConnectionString) - .WithMessage(FeatureConstants.TenantConnectionStringInvalid) - .When(m => !string.IsNullOrEmpty(m.EdFiAdminConnectionString)); - - RuleFor(m => m.EdFiSecurityConnectionString) - .MaximumLength(500) - .Must(BeAValidConnectionString) - .WithMessage(FeatureConstants.TenantConnectionStringInvalid) - .When(m => !string.IsNullOrEmpty(m.EdFiSecurityConnectionString)); - } - - private bool BeAUniqueName(string name) - { - var tenants = _tenantsService.GetTenantsAsync(true).Result; - return tenants.TrueForAll(x => x.TenantName != name); - } - - private bool BeAValidConnectionString(string? connectionString) - { - return ConnectionStringHelper.ValidateConnectionString(_databaseEngine, connectionString); - } - } - - private static async Task InitializeTenantsAsync(IServiceScopeFactory serviceScopeFactory) - { - using IServiceScope scope = serviceScopeFactory.CreateScope(); - - ITenantsService scopedProcessingService = - scope.ServiceProvider.GetRequiredService(); - - await scopedProcessingService.InitializeTenantsAsync(); - } -} diff --git a/Application/EdFi.Ods.AdminApi/Features/Tenants/DeleteTenant.cs b/Application/EdFi.Ods.AdminApi/Features/Tenants/DeleteTenant.cs deleted file mode 100644 index 586c2670a..000000000 --- a/Application/EdFi.Ods.AdminApi/Features/Tenants/DeleteTenant.cs +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using EdFi.Ods.AdminApi.Common.Features; -using EdFi.Ods.AdminApi.Common.Infrastructure; -using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Swashbuckle.AspNetCore.Annotations; - -namespace EdFi.Ods.AdminApi.Features.Tenants; - -public class DeleteTenant : IFeature -{ - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder - .MapDelete(endpoints, "/tenants/{tenantName}", Handle) - .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponseCode(200, FeatureCommonConstants.DeletedSuccessResponseDescription)) - .BuildForVersions(AdminApiVersions.V2); - } - - public static async Task Handle( - [FromServices] ITenantsService iTenantsService, - DeleteTenantCommand deleteTenantCommand, - IServiceScopeFactory serviceScopeFactory, - IOptions options, - string tenantName) - { - if (!options.Value.MultiTenancy) - return Results.BadRequest(new { message = "Not multitenant environment." }); - - var tenant = await iTenantsService.GetTenantByTenantIdAsync(tenantName); - if (tenant == null) - return Results.NotFound(); - - deleteTenantCommand.Execute(tenantName); - - await InitializeTenantsAsync(serviceScopeFactory); - - return Results.Ok("Tenant".ToJsonObjectResponseDeleted()); - } - - [SwaggerSchema(Title = "DeleteTenantRequest")] - public class DeleteTenantRequest - { - [SwaggerSchema(Description = "The unique name of the tenant to delete.", Nullable = false)] - public string TenantName { get; set; } = string.Empty; - } - - private static async Task InitializeTenantsAsync(IServiceScopeFactory serviceScopeFactory) - { - using IServiceScope scope = serviceScopeFactory.CreateScope(); - - ITenantsService scopedProcessingService = - scope.ServiceProvider.GetRequiredService(); - - await scopedProcessingService.InitializeTenantsAsync(); - } -} diff --git a/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs b/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs index 4258b03f5..7ac784734 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs @@ -19,71 +19,99 @@ public class ReadTenants : IFeature { public void MapEndpoints(IEndpointRouteBuilder endpoints) { - AdminApiEndpointBuilder.MapGet(endpoints, "/tenants", GetTenantsAsync) + AdminApiEndpointBuilder + .MapGet(endpoints, "/tenants", GetTenantsAsync) .BuildForVersions(AdminApiVersions.V2); - AdminApiEndpointBuilder.MapGet(endpoints, "/tenants/{tenantName}", GetTenantsByTenantIdAsync) + AdminApiEndpointBuilder + .MapGet(endpoints, "/tenants/{tenantName}", GetTenantsByTenantIdAsync) .BuildForVersions(AdminApiVersions.V2); } - public static async Task GetTenantsAsync([FromServices] ITenantsService tenantsService, IMemoryCache memoryCache, IOptions options) + public static async Task GetTenantsAsync( + [FromServices] ITenantsService tenantsService, + IMemoryCache memoryCache, + IOptions options + ) { - var _databaseEngine = options.Value.DatabaseEngine ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); + var _databaseEngine = + options.Value.DatabaseEngine + ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); var tenants = await tenantsService.GetTenantsAsync(true); var response = tenants - .Select(t => - { - var adminHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase(_databaseEngine, t.ConnectionStrings.EdFiAdminConnectionString); - var securityHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase(_databaseEngine, t.ConnectionStrings.EdFiSecurityConnectionString); - - return new TenantsResponse + .Select(t => { - TenantName = t.TenantName, - AdminConnectionString = new EdfiConnectionString() - { - host = adminHostAndDatabase.Host, - database = adminHostAndDatabase.Database - }, - SecurityConnectionString = new EdfiConnectionString() + var adminHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase( + _databaseEngine, + t.ConnectionStrings.EdFiAdminConnectionString + ); + var securityHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase( + _databaseEngine, + t.ConnectionStrings.EdFiSecurityConnectionString + ); + + return new TenantsResponse { - host = securityHostAndDatabase.Host, - database = securityHostAndDatabase.Database - } - }; - }) - .ToList(); + TenantName = t.TenantName, + AdminConnectionString = new EdfiConnectionString() + { + host = adminHostAndDatabase.Host, + database = adminHostAndDatabase.Database + }, + SecurityConnectionString = new EdfiConnectionString() + { + host = securityHostAndDatabase.Host, + database = securityHostAndDatabase.Database + } + }; + }) + .ToList(); return Results.Ok(response); } - public static async Task GetTenantsByTenantIdAsync([FromServices] ITenantsService tenantsService, - IMemoryCache memoryCache, string tenantName, IOptions options) + public static async Task GetTenantsByTenantIdAsync( + [FromServices] ITenantsService tenantsService, + IMemoryCache memoryCache, + string tenantName, + IOptions options + ) { - var _databaseEngine = options.Value.DatabaseEngine ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); + var _databaseEngine = + options.Value.DatabaseEngine + ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); var tenant = await tenantsService.GetTenantByTenantIdAsync(tenantName); if (tenant == null) return Results.NotFound(); - var adminHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase(_databaseEngine, tenant.ConnectionStrings.EdFiAdminConnectionString); - var securityHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase(_databaseEngine, tenant.ConnectionStrings.EdFiSecurityConnectionString); + var adminHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase( + _databaseEngine, + tenant.ConnectionStrings.EdFiAdminConnectionString + ); + var securityHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase( + _databaseEngine, + tenant.ConnectionStrings.EdFiSecurityConnectionString + ); - return Results.Ok(new TenantsResponse - { - TenantName = tenant.TenantName, - AdminConnectionString = new EdfiConnectionString() + return Results.Ok( + new TenantsResponse { - host = adminHostAndDatabase.Host, - database = adminHostAndDatabase.Database - }, - SecurityConnectionString = new EdfiConnectionString() - { - host = securityHostAndDatabase.Host, - database = securityHostAndDatabase.Database + TenantName = tenant.TenantName, + AdminConnectionString = new EdfiConnectionString() + { + host = adminHostAndDatabase.Host, + database = adminHostAndDatabase.Database + }, + SecurityConnectionString = new EdfiConnectionString() + { + host = securityHostAndDatabase.Host, + database = securityHostAndDatabase.Database + } } - }); + ); } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddTenantCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddTenantCommand.cs deleted file mode 100644 index deb15ee54..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddTenantCommand.cs +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System.Text.Json; -using System.Text.Json.Nodes; -using EdFi.Ods.AdminApi.Infrastructure.Helpers; - -namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; - -public class AddTenantCommand(IAppSettingsFileProvider fileProvider) -{ - private static readonly object _fileLock = new(); - private readonly IAppSettingsFileProvider _fileProvider = fileProvider; - - public virtual void Execute(IAddTenantModel model) - { - lock (_fileLock) - { - var json = _fileProvider.ReadAllText(); - try - { - var root = JsonNode.Parse(json) ?? throw new InvalidOperationException("appsettings.json is empty or invalid."); - - var tenantsNode = root["Tenants"] as JsonObject ?? throw new InvalidOperationException("Tenants section missing in appsettings.json."); - - if (tenantsNode.ContainsKey(model.TenantName)) - { - throw new InvalidOperationException($"Tenant '{model.TenantName}' already exists."); - } - - var tenantObj = new JsonObject - { - ["ConnectionStrings"] = new JsonObject - { - ["EdFi_Security"] = model.EdFiSecurityConnectionString, - ["EdFi_Admin"] = model.EdFiAdminConnectionString - } - }; - - tenantsNode[model.TenantName] = tenantObj; - - var options = new JsonSerializerOptions { WriteIndented = true }; - var updatedJson = root.ToJsonString(options); - - _fileProvider.WriteAllText(updatedJson); - } - catch (JsonException ex) - { - throw new InvalidOperationException("appsettings.json contains invalid JSON.", ex); - } - } - } - -} - -public interface IAddTenantModel -{ - public string TenantName { get; } - public string? EdFiSecurityConnectionString { get; } - public string? EdFiAdminConnectionString { get; } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteTenantCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteTenantCommand.cs deleted file mode 100644 index a20166d76..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteTenantCommand.cs +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Licensed to the Ed-Fi Alliance under one or more agreements. -// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. -// See the LICENSE and NOTICES files in the project root for more information. - -using System.Text.Json; -using System.Text.Json.Nodes; -using EdFi.Ods.AdminApi.Infrastructure.Helpers; - -namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; - -public class DeleteTenantCommand(IAppSettingsFileProvider fileProvider) -{ - private static readonly object _fileLock = new(); - - private readonly IAppSettingsFileProvider _fileProvider = fileProvider; - - public virtual void Execute(string tenantName) - { - lock (_fileLock) - { - var json = _fileProvider.ReadAllText(); - try - { - var root = JsonNode.Parse(json) ?? throw new InvalidOperationException("appsettings.json is empty or invalid."); - - var tenantsNode = root["Tenants"] as JsonObject ?? throw new InvalidOperationException("Tenants section missing in appsettings.json."); - - if (!tenantsNode.ContainsKey(tenantName)) - { - throw new InvalidOperationException($"Tenant '{tenantName}' does not exist."); - } - - tenantsNode.Remove(tenantName); - - var options = new JsonSerializerOptions { WriteIndented = true }; - var updatedJson = root.ToJsonString(options); - - _fileProvider.WriteAllText(updatedJson); - } - catch (JsonException ex) - { - throw new InvalidOperationException("appsettings.json contains invalid JSON.", ex); - } - } - } -} - -public interface IDeleteTenantModel -{ - public string TenantName { get; set; } -} diff --git a/Docker/Settings/V1/gateway/Dockerfile b/Docker/Settings/V1/gateway/Dockerfile index 4718eb3e3..74c939269 100644 --- a/Docker/Settings/V1/gateway/Dockerfile +++ b/Docker/Settings/V1/gateway/Dockerfile @@ -3,8 +3,8 @@ # The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. # See the LICENSE and NOTICES files in the project root for more information. -# Tag nginx:alpine -FROM nginx@sha256:da9c94bec1da829ebd52431a84502ec471c8e548ffb2cedbf36260fd9bd1d4d3 +# Tag nginx:alpine3.20 +FROM nginx@sha256:2140dad235c130ac861018a4e13a6bc8aea3a35f3a40e20c1b060d51a7efd250 LABEL maintainer="Ed-Fi Alliance, LLC and Contributors " ENV ADMIN_API_VIRTUAL_NAME=${ADMIN_API_VIRTUAL_NAME:-adminapi} diff --git a/Docker/api.mssql.Dockerfile b/Docker/api.mssql.Dockerfile index 51c239d1a..c14386e32 100644 --- a/Docker/api.mssql.Dockerfile +++ b/Docker/api.mssql.Dockerfile @@ -5,7 +5,7 @@ FROM mcr.microsoft.com/dotnet/aspnet:8.0.8-alpine3.20-amd64@sha256:98fa594b91cda6cac28d2aae25567730db6f8857367fab7646bdda91bc784b5f AS base RUN apk upgrade --no-cache && \ - apk add --no-cache unzip=~6 dos2unix=~7 bash=~5 gettext=~0 jq=~1 icu=~74 openssl=3.3.4-r0 musl=~1.2.5-r1 && \ + apk add --no-cache unzip=~6 dos2unix=~7 bash=~5 gettext=~0 jq=~1 icu=~74 openssl=3.3.5-r0 musl=~1.2.5-r1 && \ addgroup -S edfi && adduser -S edfi -G edfi FROM base AS build diff --git a/Docker/api.pgsql.Dockerfile b/Docker/api.pgsql.Dockerfile index 870b3ea88..75ebc1a38 100644 --- a/Docker/api.pgsql.Dockerfile +++ b/Docker/api.pgsql.Dockerfile @@ -6,7 +6,7 @@ #tag 8.0-alpine FROM mcr.microsoft.com/dotnet/aspnet:8.0.10-alpine3.20-amd64@sha256:1659f678b93c82db5b42fb1fb12d98035ce482b85747c2c54e514756fa241095 AS base RUN apk upgrade --no-cache && \ - apk add --no-cache bash=~5 dos2unix=~7 gettext=~0 icu=~74 jq=~1 musl=~1.2.5-r1 openssl=3.3.4-r0 postgresql14-client=~14 unzip=~6 && \ + apk add --no-cache bash=~5 dos2unix=~7 gettext=~0 icu=~74 jq=~1 musl=~1.2.5-r1 openssl=3.3.5-r0 postgresql14-client=~14 unzip=~6 && \ rm -rf /var/cache/apk/* && \ addgroup -S edfi && adduser -S edfi -G edfi diff --git a/Docker/dev.pgsql.Dockerfile b/Docker/dev.pgsql.Dockerfile index 6e83ffcca..4a1df9b3c 100644 --- a/Docker/dev.pgsql.Dockerfile +++ b/Docker/dev.pgsql.Dockerfile @@ -30,7 +30,7 @@ RUN dotnet publish -c Release /p:EnvironmentName=$ASPNETCORE_ENVIRONMENT --no-bu FROM mcr.microsoft.com/dotnet/aspnet:8.0.10-alpine3.20-amd64@sha256:1659f678b93c82db5b42fb1fb12d98035ce482b85747c2c54e514756fa241095 AS runtimebase RUN apk upgrade --no-cache && \ - apk add --no-cache bash=~5 dos2unix=~7 gettext=~0 icu=~74 musl=~1.2.5-r1 openssl=3.3.4-r0 postgresql14-client=~14 && \ + apk add --no-cache bash=~5 dos2unix=~7 gettext=~0 icu=~74 musl=~1.2.5-r1 openssl=3.3.5-r0 postgresql14-client=~14 && \ addgroup -S edfi && adduser -S edfi -G edfi FROM runtimebase AS setup