From 4adc7b0f169a7719445ce9f790dfebf1891779aa Mon Sep 17 00:00:00 2001 From: David Jimenez Date: Mon, 25 May 2026 11:18:23 -0600 Subject: [PATCH 01/35] v3 specification - Adds PUT-request-aligned validation - Cover more scenarios. --- .../ValidatorExtensionsTests.cs | 41 +++++++++++++ .../Constants/Constants.cs | 2 +- .../Infrastructure/ValidatorExtensions.cs | 2 +- .../PUT - ApiClients - ID Mismatch.bru | 2 +- .../PUT - ApiClients - ID Missing in Body.bru | 44 ++++++++++++++ .../PUT - Applications - ID Mismatch.bru | 2 +- ...UT - Applications - ID Missing in Body.bru | 45 +++++++++++++++ .../PUT - ClaimSets - ID Mismatch.bru | 2 +- .../PUT - ClaimSets - ID Missing in Body.bru | 39 +++++++++++++ ...ts - Modify Action ClaimSetId Mismatch.bru | 2 +- ...dify Action ClaimSetId Missing in Body.bru | 57 +++++++++++++++++++ ...Modify Action ResourceClaimId Mismatch.bru | 2 +- ...Action ResourceClaimId Missing in Body.bru | 57 +++++++++++++++++++ .../PUT - DataStoreContexts - ID Mismatch.bru | 2 +- ...DataStoreContexts - ID Missing in Body.bru | 41 +++++++++++++ ...T - DataStoreDerivatives - ID Mismatch.bru | 2 +- ...aStoreDerivatives - ID Missing in Body.bru | 41 +++++++++++++ .../PUT - DataStores - ID Mismatch.bru | 2 +- .../PUT - DataStores - ID Missing in Body.bru | 41 +++++++++++++ .../Profiles/PUT - Profiles - ID Mismatch.bru | 2 +- .../PUT - Profiles - ID Missing in Body.bru | 40 +++++++++++++ .../Vendors/PUT - Vendors - ID Mismatch.bru | 2 +- .../PUT - Vendors - ID Missing in Body.bru | 42 ++++++++++++++ 23 files changed, 500 insertions(+), 12 deletions(-) create mode 100644 Application/EdFi.Ods.AdminApi.Common.UnitTests/Infrastructure/ValidatorExtensionsTests.cs create mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ApiClient/PUT - ApiClients - ID Missing in Body.bru create mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Application/PUT - Applications - ID Missing in Body.bru create mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ClaimSets - ID Missing in Body.bru create mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ClaimSetId Missing in Body.bru create mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ResourceClaimId Missing in Body.bru create mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreContexts/PUT - DataStoreContexts - ID Missing in Body.bru create mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreDerivatives/PUT - DataStoreDerivatives - ID Missing in Body.bru create mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/PUT - DataStores - ID Missing in Body.bru create mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Profiles/PUT - Profiles - ID Missing in Body.bru create mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Vendors/PUT - Vendors - ID Missing in Body.bru diff --git a/Application/EdFi.Ods.AdminApi.Common.UnitTests/Infrastructure/ValidatorExtensionsTests.cs b/Application/EdFi.Ods.AdminApi.Common.UnitTests/Infrastructure/ValidatorExtensionsTests.cs new file mode 100644 index 000000000..53dad4868 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.Common.UnitTests/Infrastructure/ValidatorExtensionsTests.cs @@ -0,0 +1,41 @@ +// 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.Constants; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using FluentValidation; +using Shouldly; + +namespace EdFi.Ods.AdminApi.Common.UnitTests.Infrastructure; + +[TestFixture] +public class ValidatorExtensionsTests +{ + [Test] + public void GuardRouteIdMatchesBodyId_WhenIdsMatch_ShouldNotThrow() + { + Should.NotThrow(() => ValidatorExtensions.GuardRouteIdMatchesBodyId(10, 10, "Id")); + } + + [Test] + public void GuardRouteIdMatchesBodyId_WhenBodyIdIsZeroAndRouteIdDiffers_ShouldThrowValidationException() + { + var exception = Should.Throw(() => + ValidatorExtensions.GuardRouteIdMatchesBodyId(10, 0, "Id")); + + exception.Errors.Single(x => x.PropertyName == "Id").ErrorMessage + .ShouldBe(ErrorMessagesConstants.RequestBodyIdMismatch); + } + + [Test] + public void GuardRouteIdMatchesBodyId_WhenRouteIdIsZeroAndBodyIdDiffers_ShouldThrowValidationException() + { + var exception = Should.Throw(() => + ValidatorExtensions.GuardRouteIdMatchesBodyId(0, 10, "Id")); + + exception.Errors.Single(x => x.PropertyName == "Id").ErrorMessage + .ShouldBe(ErrorMessagesConstants.RequestBodyIdMismatch); + } +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs b/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs index 1613bc05b..9b70b343c 100644 --- a/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs @@ -55,7 +55,7 @@ public class ErrorMessagesConstants /// /// Identifier provided in the request body does not match the identifier provided as a parameter in the request URL. /// - public const string RequestBodyIdMismatch = "The ID in the request body must match the ID in the URL."; + public const string RequestBodyIdMismatch = "Request body id must match the id in the url."; } public enum AdminApiMode diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ValidatorExtensions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ValidatorExtensions.cs index 460302540..be08ea441 100644 --- a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ValidatorExtensions.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/ValidatorExtensions.cs @@ -21,7 +21,7 @@ public static async Task GuardAsync(this IValidator validato public static void GuardRouteIdMatchesBodyId(int routeId, int requestId, string propertyName) { - if (routeId > 0 && requestId > 0 && routeId != requestId) + if (routeId != requestId) { throw new ValidationException([new ValidationFailure(propertyName, ErrorMessagesConstants.RequestBodyIdMismatch)]); } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ApiClient/PUT - ApiClients - ID Mismatch.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ApiClient/PUT - ApiClients - ID Mismatch.bru index 71902cf1a..d81b80793 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ApiClient/PUT - ApiClients - ID Mismatch.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ApiClient/PUT - ApiClients - ID Mismatch.bru @@ -36,7 +36,7 @@ script:post-response { test("PUT ApiClients ID Mismatch: Response includes Id mismatch error", function () { expect(response.errors["Id"].length).to.equal(1); - expect(response.errors["Id"][0]).to.contain("must match the ID in the URL"); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); }); } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ApiClient/PUT - ApiClients - ID Missing in Body.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ApiClient/PUT - ApiClients - ID Missing in Body.bru new file mode 100644 index 000000000..1bf5c2e85 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ApiClient/PUT - ApiClients - ID Missing in Body.bru @@ -0,0 +1,44 @@ +meta { + name: ApiClients - ID Missing in Body + type: http + seq: 18.1 +} + +put { + url: {{API_URL}}/v3/apiclients/{{CreatedApiClientId}} + body: json + auth: inherit +} + +body:json { + { + "name": "Updated Test ApiClient", + "isApproved": false, + "applicationId": {{CreatedApplicationId}}, + "dataStoreIds": [ + {{DataStoreId}} + ] + } +} + +script:post-response { + test("PUT ApiClients ID Missing in Body: Status code is Bad Request", function () { + expect(res.getStatus()).to.equal(400); + }); + + const response = res.getBody(); + + test("PUT ApiClients ID Missing in Body: Response matches error format", function () { + expect(response).to.have.property("title"); + expect(response).to.have.property("errors"); + }); + + test("PUT ApiClients ID Missing in Body: Response includes Id mismatch error", function () { + expect(response.errors["Id"].length).to.equal(1); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Application/PUT - Applications - ID Mismatch.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Application/PUT - Applications - ID Mismatch.bru index 2cb33285e..36bce1894 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Application/PUT - Applications - ID Mismatch.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Application/PUT - Applications - ID Mismatch.bru @@ -37,7 +37,7 @@ script:post-response { test("PUT Applications ID Mismatch: Response includes Id mismatch error", function () { expect(response.errors["Id"].length).to.equal(1); - expect(response.errors["Id"][0]).to.contain("must match the ID in the URL"); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); }); } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Application/PUT - Applications - ID Missing in Body.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Application/PUT - Applications - ID Missing in Body.bru new file mode 100644 index 000000000..a1f61862c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Application/PUT - Applications - ID Missing in Body.bru @@ -0,0 +1,45 @@ +meta { + name: Applications - ID Missing in Body + type: http + seq: 21.6 +} + +put { + url: {{API_URL}}/v3/applications/{{CreatedApplicationId}} + body: json + auth: inherit +} + +body:json { + { + "applicationName": "Updated Application Name", + "vendorId": {{OtherApplicationVendorId}}, + "claimSetName": "Ed-Fi ODS Admin App", + "profileIds": [], + "educationOrganizationIds": [1234], + "dataStoreIds": [ {{DataStoreId}} ], + "enabled": false + } +} + +script:post-response { + test("PUT Applications ID Missing in Body: Status code is Bad Request", function () { + expect(res.getStatus()).to.equal(400); + }); + + const response = res.getBody(); + + test("PUT Applications ID Missing in Body: Response matches error format", function () { + expect(response).to.have.property("title"); + expect(response).to.have.property("errors"); + }); + + test("PUT Applications ID Missing in Body: Response includes Id mismatch error", function () { + expect(response.errors["Id"].length).to.equal(1); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ClaimSets - ID Mismatch.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ClaimSets - ID Mismatch.bru index 864590d28..85c4c036d 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ClaimSets - ID Mismatch.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ClaimSets - ID Mismatch.bru @@ -31,7 +31,7 @@ script:post-response { test("PUT ClaimSets ID Mismatch: Response includes Id mismatch error", function () { expect(response.errors["Id"].length).to.equal(1); - expect(response.errors["Id"][0]).to.contain("must match the ID in the URL"); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); }); } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ClaimSets - ID Missing in Body.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ClaimSets - ID Missing in Body.bru new file mode 100644 index 000000000..6bcf357da --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ClaimSets - ID Missing in Body.bru @@ -0,0 +1,39 @@ +meta { + name: ClaimSets - ID Missing in Body + type: http + seq: 50.6 +} + +put { + url: {{API_URL}}/v3/claimSets/{{CreatedClaimSetId}} + body: json + auth: inherit +} + +body:json { + { + "name": "Updated Test ClaimSet" + } +} + +script:post-response { + test("PUT ClaimSets ID Missing in Body: Status code is Bad Request", function () { + expect(res.getStatus()).to.equal(400); + }); + + const response = res.getBody(); + + test("PUT ClaimSets ID Missing in Body: Response matches error format", function () { + expect(response).to.have.property("title"); + expect(response).to.have.property("errors"); + }); + + test("PUT ClaimSets ID Missing in Body: Response includes Id mismatch error", function () { + expect(response.errors["Id"].length).to.equal(1); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ClaimSetId Mismatch.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ClaimSetId Mismatch.bru index 99653ee61..c8e42f72d 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ClaimSetId Mismatch.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ClaimSetId Mismatch.bru @@ -49,7 +49,7 @@ script:post-response { test("PUT ResourceClaimClaimSets ClaimSetId Mismatch: Response includes ClaimSetId mismatch error", function () { expect(response.errors["ClaimSetId"].length).to.equal(1); - expect(response.errors["ClaimSetId"][0]).to.contain("must match the ID in the URL"); + expect(response.errors["ClaimSetId"][0]).to.contain("Request body id must match the id in the url."); }); } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ClaimSetId Missing in Body.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ClaimSetId Missing in Body.bru new file mode 100644 index 000000000..9a8d7bccb --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ClaimSetId Missing in Body.bru @@ -0,0 +1,57 @@ +meta { + name: ResourceClaimClaimSets - Modify Action ClaimSetId Missing in Body + type: http + seq: 25.3 +} + +put { + url: {{API_URL}}/v3/claimSets/{{CreatedClaimSetId}}/resourceClaimActions/4 + body: json + auth: inherit +} + +body:json { + { + "resourceClaimId": 4, + "resourceClaimActions": [ + { + "name": "read", + "enabled": true + }, + { + "name": "create", + "enabled": true + }, + { + "name": "update", + "enabled": false + }, + { + "name": "delete", + "enabled": false + } + ] + } +} + +script:post-response { + test("PUT ResourceClaimClaimSets ClaimSetId Missing in Body: Status code is Bad Request", function () { + expect(res.getStatus()).to.equal(400); + }); + + const response = res.getBody(); + + test("PUT ResourceClaimClaimSets ClaimSetId Missing in Body: Response matches error format", function () { + expect(response).to.have.property("title"); + expect(response).to.have.property("errors"); + }); + + test("PUT ResourceClaimClaimSets ClaimSetId Missing in Body: Response includes ClaimSetId mismatch error", function () { + expect(response.errors["ClaimSetId"].length).to.equal(1); + expect(response.errors["ClaimSetId"][0]).to.contain("Request body id must match the id in the url."); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ResourceClaimId Mismatch.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ResourceClaimId Mismatch.bru index 9bedf5a0c..7c457abed 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ResourceClaimId Mismatch.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ResourceClaimId Mismatch.bru @@ -49,7 +49,7 @@ script:post-response { test("PUT ResourceClaimClaimSets ResourceClaimId Mismatch: Response includes ResourceClaimId mismatch error", function () { expect(response.errors["ResourceClaimId"].length).to.equal(1); - expect(response.errors["ResourceClaimId"][0]).to.contain("must match the ID in the URL"); + expect(response.errors["ResourceClaimId"][0]).to.contain("Request body id must match the id in the url."); }); } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ResourceClaimId Missing in Body.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ResourceClaimId Missing in Body.bru new file mode 100644 index 000000000..351589a84 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/ClaimSets/PUT - ResourceClaimClaimSets - Modify Action ResourceClaimId Missing in Body.bru @@ -0,0 +1,57 @@ +meta { + name: ResourceClaimClaimSets - Modify Action ResourceClaimId Missing in Body + type: http + seq: 25.4 +} + +put { + url: {{API_URL}}/v3/claimSets/{{CreatedClaimSetId}}/resourceClaimActions/4 + body: json + auth: inherit +} + +body:json { + { + "claimSetId": {{CreatedClaimSetId}}, + "resourceClaimActions": [ + { + "name": "read", + "enabled": true + }, + { + "name": "create", + "enabled": true + }, + { + "name": "update", + "enabled": false + }, + { + "name": "delete", + "enabled": false + } + ] + } +} + +script:post-response { + test("PUT ResourceClaimClaimSets ResourceClaimId Missing in Body: Status code is Bad Request", function () { + expect(res.getStatus()).to.equal(400); + }); + + const response = res.getBody(); + + test("PUT ResourceClaimClaimSets ResourceClaimId Missing in Body: Response matches error format", function () { + expect(response).to.have.property("title"); + expect(response).to.have.property("errors"); + }); + + test("PUT ResourceClaimClaimSets ResourceClaimId Missing in Body: Response includes ResourceClaimId mismatch error", function () { + expect(response.errors["ResourceClaimId"].length).to.equal(1); + expect(response.errors["ResourceClaimId"][0]).to.contain("Request body id must match the id in the url."); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreContexts/PUT - DataStoreContexts - ID Mismatch.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreContexts/PUT - DataStoreContexts - ID Mismatch.bru index c0e6b19c5..ba43bd81a 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreContexts/PUT - DataStoreContexts - ID Mismatch.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreContexts/PUT - DataStoreContexts - ID Mismatch.bru @@ -33,7 +33,7 @@ script:post-response { test("PUT DataStoreContexts ID Mismatch: Response includes Id mismatch error", function () { expect(response.errors["Id"].length).to.equal(1); - expect(response.errors["Id"][0]).to.contain("must match the ID in the URL"); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); }); } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreContexts/PUT - DataStoreContexts - ID Missing in Body.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreContexts/PUT - DataStoreContexts - ID Missing in Body.bru new file mode 100644 index 000000000..216b70003 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreContexts/PUT - DataStoreContexts - ID Missing in Body.bru @@ -0,0 +1,41 @@ +meta { + name: DataStoreContexts - ID Missing in Body + type: http + seq: 21.1 +} + +put { + url: {{API_URL}}/v3/dataStoreContexts/{{CreatedDataStoreContextId}} + body: json + auth: inherit +} + +body:json { + { + "dataStoreId": {{DataStoreId}}, + "contextKey": "ckey", + "contextValue": "cvalue" + } +} + +script:post-response { + test("PUT DataStoreContexts ID Missing in Body: Status code is Bad Request", function () { + expect(res.getStatus()).to.equal(400); + }); + + const response = res.getBody(); + + test("PUT DataStoreContexts ID Missing in Body: Response matches error format", function () { + expect(response).to.have.property("title"); + expect(response).to.have.property("errors"); + }); + + test("PUT DataStoreContexts ID Missing in Body: Response includes Id mismatch error", function () { + expect(response.errors["Id"].length).to.equal(1); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreDerivatives/PUT - DataStoreDerivatives - ID Mismatch.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreDerivatives/PUT - DataStoreDerivatives - ID Mismatch.bru index fded4257b..387ca36af 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreDerivatives/PUT - DataStoreDerivatives - ID Mismatch.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreDerivatives/PUT - DataStoreDerivatives - ID Mismatch.bru @@ -33,7 +33,7 @@ script:post-response { test("PUT DataStoreDerivatives ID Mismatch: Response includes Id mismatch error", function () { expect(response.errors["Id"].length).to.equal(1); - expect(response.errors["Id"][0]).to.contain("must match the ID in the URL"); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); }); } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreDerivatives/PUT - DataStoreDerivatives - ID Missing in Body.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreDerivatives/PUT - DataStoreDerivatives - ID Missing in Body.bru new file mode 100644 index 000000000..daf7c8439 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStoreDerivatives/PUT - DataStoreDerivatives - ID Missing in Body.bru @@ -0,0 +1,41 @@ +meta { + name: DataStoreDerivatives - ID Missing in Body + type: http + seq: 25.1 +} + +put { + url: {{API_URL}}/v3/dataStoreDerivatives/{{CreatedDataStoreDerivativeId}} + body: json + auth: inherit +} + +body:json { + { + "dataStoreId": {{DataStoreId}}, + "derivativeType": "ReadReplica", + "connectionString": "{{connectionString}}" + } +} + +script:post-response { + test("PUT DataStoreDerivatives ID Missing in Body: Status code is Bad Request", function () { + expect(res.getStatus()).to.equal(400); + }); + + const response = res.getBody(); + + test("PUT DataStoreDerivatives ID Missing in Body: Response matches error format", function () { + expect(response).to.have.property("title"); + expect(response).to.have.property("errors"); + }); + + test("PUT DataStoreDerivatives ID Missing in Body: Response includes Id mismatch error", function () { + expect(response.errors["Id"].length).to.equal(1); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/PUT - DataStores - ID Mismatch.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/PUT - DataStores - ID Mismatch.bru index 936c7d52c..a11ad910c 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/PUT - DataStores - ID Mismatch.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/PUT - DataStores - ID Mismatch.bru @@ -33,7 +33,7 @@ script:post-response { test("PUT DataStores ID Mismatch: Response includes Id mismatch error", function () { expect(response.errors["Id"].length).to.equal(1); - expect(response.errors["Id"][0]).to.contain("must match the ID in the URL"); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); }); } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/PUT - DataStores - ID Missing in Body.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/PUT - DataStores - ID Missing in Body.bru new file mode 100644 index 000000000..bd56ab0fb --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/PUT - DataStores - ID Missing in Body.bru @@ -0,0 +1,41 @@ +meta { + name: DataStores - ID Missing in Body + type: http + seq: 20.1 +} + +put { + url: {{API_URL}}/v3/dataStores/{{CreatedDataStoreId}} + body: json + auth: inherit +} + +body:json { + { + "name": "Updated-Test-DataStore-{{DataStoreGUID}}", + "dataStoreType": "Updated-Test-OdsdataStoreType", + "connectionString": "{{connectionString}}" + } +} + +script:post-response { + test("PUT DataStores ID Missing in Body: Status code is Bad Request", function () { + expect(res.getStatus()).to.equal(400); + }); + + const response = res.getBody(); + + test("PUT DataStores ID Missing in Body: Response matches error format", function () { + expect(response).to.have.property("title"); + expect(response).to.have.property("errors"); + }); + + test("PUT DataStores ID Missing in Body: Response includes Id mismatch error", function () { + expect(response.errors["Id"].length).to.equal(1); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Profiles/PUT - Profiles - ID Mismatch.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Profiles/PUT - Profiles - ID Mismatch.bru index adb1a98c7..6a32df26c 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Profiles/PUT - Profiles - ID Mismatch.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Profiles/PUT - Profiles - ID Mismatch.bru @@ -32,7 +32,7 @@ script:post-response { test("PUT Profiles ID Mismatch: Response includes Id mismatch error", function () { expect(response.errors["Id"].length).to.equal(1); - expect(response.errors["Id"][0]).to.contain("must match the ID in the URL"); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); }); } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Profiles/PUT - Profiles - ID Missing in Body.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Profiles/PUT - Profiles - ID Missing in Body.bru new file mode 100644 index 000000000..72166d92a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Profiles/PUT - Profiles - ID Missing in Body.bru @@ -0,0 +1,40 @@ +meta { + name: Profiles - ID Missing in Body + type: http + seq: 21.1 +} + +put { + url: {{API_URL}}/v3/profiles/{{CreatedProfileId}} + body: json + auth: inherit +} + +body:json { + { + "Name": "Updated-Test-Profile", + "Definition": "" + } +} + +script:post-response { + test("PUT Profiles ID Missing in Body: Status code is Bad Request", function () { + expect(res.getStatus()).to.equal(400); + }); + + const response = res.getBody(); + + test("PUT Profiles ID Missing in Body: Response matches error format", function () { + expect(response).to.have.property("title"); + expect(response).to.have.property("errors"); + }); + + test("PUT Profiles ID Missing in Body: Response includes Id mismatch error", function () { + expect(response.errors["Id"].length).to.equal(1); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Vendors/PUT - Vendors - ID Mismatch.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Vendors/PUT - Vendors - ID Mismatch.bru index 6d23ad023..f71176b60 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Vendors/PUT - Vendors - ID Mismatch.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Vendors/PUT - Vendors - ID Mismatch.bru @@ -34,7 +34,7 @@ script:post-response { test("PUT Vendors ID Mismatch: Response includes Id mismatch error", function () { expect(response.errors["Id"].length).to.equal(1); - expect(response.errors["Id"][0]).to.contain("must match the ID in the URL"); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); }); } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Vendors/PUT - Vendors - ID Missing in Body.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Vendors/PUT - Vendors - ID Missing in Body.bru new file mode 100644 index 000000000..33f434c9a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Vendors/PUT - Vendors - ID Missing in Body.bru @@ -0,0 +1,42 @@ +meta { + name: Vendors - ID Missing in Body + type: http + seq: 14.6 +} + +put { + url: {{API_URL}}/v3/vendors/{{CreatedVendorId}} + body: json + auth: inherit +} + +body:json { + { + "company": "Updated Test Company", + "namespacePrefixes": "uri://academicbenchmarks.com", + "contactName": "Updated User", + "contactEmailAddress": "updated@example.com" + } +} + +script:post-response { + test("PUT Vendors ID Missing in Body: Status code is Bad Request", function () { + expect(res.getStatus()).to.equal(400); + }); + + const response = res.getBody(); + + test("PUT Vendors ID Missing in Body: Response matches error format", function () { + expect(response).to.have.property("title"); + expect(response).to.have.property("errors"); + }); + + test("PUT Vendors ID Missing in Body: Response includes Id mismatch error", function () { + expect(response.errors["Id"].length).to.equal(1); + expect(response.errors["Id"][0]).to.contain("Request body id must match the id in the url."); + }); +} + +settings { + encodeUrl: true +} From 44e5b8f3d839c5ca996ffe9b83ddeb4597e72093 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 15:01:14 -0600 Subject: [PATCH 02/35] Add design spec for renaming DbInstances/DbDataStores to OdsInstanceManage Documents the plan to rename the shared DbInstance entity/table to OdsInstanceManage, move v2's DbInstances feature into OdsInstances/Manage (/v2/odsInstances/manage) and v3's DbDataStores feature into DataStores/Manage (/v3/dataStores/manage), and update all affected tests and E2E artifacts. Co-Authored-By: Claude Sonnet 5 --- ...dbinstances-to-odsinstancemanage-design.md | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-27-rename-dbinstances-to-odsinstancemanage-design.md diff --git a/docs/superpowers/specs/2026-07-27-rename-dbinstances-to-odsinstancemanage-design.md b/docs/superpowers/specs/2026-07-27-rename-dbinstances-to-odsinstancemanage-design.md new file mode 100644 index 000000000..4f54684d1 --- /dev/null +++ b/docs/superpowers/specs/2026-07-27-rename-dbinstances-to-odsinstancemanage-design.md @@ -0,0 +1,236 @@ +# Rename DbInstances (v2) / DbDataStores (v3) to OdsInstances/Manage and DataStores/Manage + +Date: 2026-07-27 + +## Summary + +`v2`'s `DbInstances` feature and `v3`'s `DbDataStores` feature are two separate presentation +layers over the exact same physical table (`adminapi.DbInstances`) and the exact same EF Core +entity class (`DbInstance`, defined once in the shared `EdFi.Ods.AdminApi.Common` project and +consumed by both version-specific `AdminApiDbContext` classes). + +This change: + +- Renames the shared entity/table to `OdsInstanceManage` / `adminapi.OdsInstanceManages`. +- Moves and renames the v2 feature folder `Features\DbInstances` into a new + `Features\OdsInstances\Manage` subfolder, changing routes from `/v2/dbInstances` to + `/v2/odsInstances/manage`. +- Moves and renames the v3 feature folder `Features\DbDataStores` into a new + `Features\DataStores\Manage` subfolder, changing routes from `/v3/dbDataStores` to + `/v3/dataStores/manage`. +- Renames all identifiers containing `DbInstance`/`DbDataStore` across production code, tests, + Bruno E2E collections, and `.http` scratch files to match. +- Is a **breaking change**: old routes and old `AppSettings` config keys are removed outright, + with no deprecated aliases or backward-compatibility shims. + +## Background / current state + +- v2's `OdsInstances` feature already depends on `DbInstances` data: it merges `DbInstance` + status/database-name into its "OdsInstance + EdOrgs" response + (`OdsInstanceWithEducationOrganizationsModel.DbInstanceId`, + `ReadEducationOrganizations.MergeDbInstanceData`). +- v3's `DataStores` feature does the analogous merge + (`ReadEducationOrganizations.MergeDbDataStoreData`), but its + `DataStoreWithEducationOrganizationsModel` is missing a field equivalent to v2's + `DbInstanceId` — a pre-existing parity gap this change closes. +- The migration artifact tree is duplicated: `Application\EdFi.Ods.AdminApi\Artifacts\` (MsSql + + PgSql) is the one actually consumed — the `.csproj` includes + ``, and + `eng\run-dbup-migrations.ps1` → `Install-AdminApiTables` runs against that published output. + `Application\EdFi.Ods.AdminApi.V3\Artifacts\` is a documentation-only duplicate (the V3 project + has `IsPublishable=false` and no matching `Content` include) that has historically been kept in + sync by hand at each prior migration step. This change keeps that convention: the new migration + is added to both locations, but only the v2 copy has any functional effect. +- There is no FK constraint between this table and `OdsInstances` — `OdsInstanceId` is a plain + nullable, unenforced column populated once async provisioning completes. + +## Naming scheme + +| Concept | Old (v2) | Old (v3) | New (shared, Common project) | +|---|---|---|---| +| Entity class | `DbInstance` | `DbInstance` | `OdsInstanceManage` | +| Table | `adminapi.DbInstances` | `adminapi.DbInstances` | `adminapi.OdsInstanceManages` | +| DbSet property | `DbInstances` | `DbInstances` | `OdsInstanceManages` | +| Status enum | `DbInstanceStatus` | `DbInstanceStatus` | `OdsInstanceManageStatus` | + +Per-version feature-layer naming stays distinct, matching the existing convention where v3 +already renames DTO-level fields (`OdsInstanceId`/`OdsInstanceName` → `DataStoreId`/ +`DataStoreName`) while sharing the underlying entity: + +| Concept | v2 (new) | v3 (new) | +|---|---|---| +| Feature folder | `Features\OdsInstances\Manage\` | `Features\DataStores\Manage\` | +| Route prefix | `/v2/odsInstances/manage` | `/v3/dataStores/manage` | +| FK-style `Id` field (in the OdsInstance/DataStore + EdOrgs response models) | `OdsInstanceManageId` | `DataStoreManageId` | +| Query interfaces | `IGetOdsInstanceManagesQuery`, `IGetOdsInstanceManageByIdQuery` | `IGetDataStoreManagesQuery`, `IGetDataStoreManageByIdQuery` | +| Command classes | `AddOdsInstanceManageCommand`, `DeleteOdsInstanceManageCommand` | `AddDataStoreManageCommand`, `DeleteDataStoreManageCommand` | +| Dispatcher jobs | `CreatePendingOdsInstanceManagesDispatcherJob`, `DeletePendingOdsInstanceManagesDispatcherJob` | `CreatePendingDataStoreManagesDispatcherJob`, `DeletePendingDataStoreManagesDispatcherJob` | + +Shared `JobConstants`/`AppSettings` keys (Common project) follow the entity name: + +- `JobConstants.DbInstanceIdKey` → `OdsInstanceManageIdKey` +- `JobConstants.CreatePendingDbInstancesDispatcherJobName` / + `DeletePendingDbInstancesDispatcherJobName` → + `CreatePendingOdsInstanceManagesDispatcherJobName` / `DeletePendingOdsInstanceManagesDispatcherJobName` +- `AppSettings.CreateDbInstancesSweepIntervalInMins`, `CreateDbInstancesMaxRetryAttempts`, + `DeleteDbInstancesSweepIntervalInMins`, `DeleteDbInstancesMaxRetryAttempts` → + `CreateOdsInstanceManagesSweepIntervalInMins`, `CreateOdsInstanceManagesMaxRetryAttempts`, + `DeleteOdsInstanceManagesSweepIntervalInMins`, `DeleteOdsInstanceManagesMaxRetryAttempts` + (breaking config-key rename — call out in release notes so deployers update + `appsettings.json`/environment overrides). + +### Explicitly out of scope (left unchanged) + +- `CreateInstanceJob` / `DeleteInstanceJob` (both v2's and v3's own copies) — generic names that + don't contain "DbInstance"/"DbDataStore". +- The table's own `OdsInstanceId`/`OdsInstanceName` columns (link a row to an actual ODS + instance) — unrelated to the FK-style `Id` field being renamed. +- No backward-compatible routes, no deprecated aliases, no config-key fallback shims. +- `NuGet.config` files (per repository convention — not touched unless explicitly requested). + +## Migration + +New artifact `00007-RenameDbInstancesToOdsInstanceManages.sql`, added in all four locations +(MsSql/PgSql × v2/v3 Artifacts trees), using an **in-place rename** to preserve existing data and +identity/serial sequences: + +- MSSQL: `sp_rename 'adminapi.DbInstances', 'OdsInstanceManages'`, plus `sp_rename` for the two + indexes (`IX_DbInstances_Name` → `IX_OdsInstanceManages_Name`, + `IX_DbInstances_OdsInstanceId` → `IX_OdsInstanceManages_OdsInstanceId`). +- PostgreSQL: `ALTER TABLE adminapi."DbInstances" RENAME TO "OdsInstanceManages"`, plus + `ALTER INDEX ... RENAME TO ...` for the same two indexes. +- Idempotent, following the existing style of prior scripts in this tree (guard with existence + checks so re-running the migration is a no-op). + +EF Core mapping updates in both `AdminApiDbContext` classes (v2 and v3): +`modelBuilder.Entity().ToTable("OdsInstanceManages")...`, `DbSet OdsInstanceManages`. + +## v2 changes + +**Folder move**: `Features\DbInstances\` → `Features\OdsInstances\Manage\` (new subfolder inside +the existing `OdsInstances` feature). Old `Features\DbInstances\` folder removed entirely. + +**File renames** (namespace `EdFi.Ods.AdminApi.Features.OdsInstances.Manage`): + +| Old | New | +|---|---| +| `AddDbInstance.cs` | `AddOdsInstanceManage.cs` | +| `ReadDbInstance.cs` | `ReadOdsInstanceManage.cs` | +| `DeleteDbInstance.cs` | `DeleteOdsInstanceManage.cs` | +| `DbInstanceModel.cs` | `OdsInstanceManageModel.cs` | +| `DbInstanceMapper.cs` | `OdsInstanceManageMapper.cs` | +| `DbInstanceDatabaseNameFormatter.cs` | `OdsInstanceManageDatabaseNameFormatter.cs` | + +**Routes**: `POST/GET/DELETE /dbInstances...` → `POST/GET/DELETE /odsInstances/manage...` +(served as `/v2/odsInstances/manage`, still `BuildForVersions(AdminApiVersions.V2)`). + +**Infrastructure layer** (stays in its existing `Infrastructure\Database\Queries` / +`Commands` / `Services\Jobs` locations — renamed in place, not moved, consistent with the +existing Features/Infrastructure architectural split): + +- `IGetDbInstancesQuery`/`GetDbInstancesQuery` → `IGetOdsInstanceManagesQuery`/`GetOdsInstanceManagesQuery` +- `IGetDbInstanceByIdQuery`/`GetDbInstanceByIdQuery` → `IGetOdsInstanceManageByIdQuery`/`GetOdsInstanceManageByIdQuery` +- `AddDbInstanceCommand`/`IAddDbInstanceModel` → `AddOdsInstanceManageCommand`/`IAddOdsInstanceManageModel` +- `IDeleteDbInstanceCommand`/`DeleteDbInstanceCommand` → `IDeleteOdsInstanceManageCommand`/`DeleteOdsInstanceManageCommand` +- `CreatePendingDbInstancesDispatcherJob`/`DeletePendingDbInstancesDispatcherJob` → + `CreatePendingOdsInstanceManagesDispatcherJob`/`DeletePendingOdsInstanceManagesDispatcherJob` + +**Existing v2 `OdsInstances` files touched (not moved)**: + +- `OdsInstanceWithEducationOrganizationsModel.cs`: `DbInstanceId` property → `OdsInstanceManageId`. +- `ReadEducationOrganizations.cs`: injects `IGetOdsInstanceManagesQuery` instead of + `IGetDbInstancesQuery`; `MergeDbInstanceData` → `MergeOdsInstanceManageData`; uses + `OdsInstanceManageStatus`. + +## v3 changes + +**Folder move**: `Features\DbDataStores\` → `Features\DataStores\Manage\`. Old +`Features\DbDataStores\` folder removed entirely. + +**File renames** (namespace `EdFi.Ods.AdminApi.V3.Features.DataStores.Manage`): + +| Old | New | +|---|---| +| `AddDbDataStore.cs` | `AddDataStoreManage.cs` | +| `ReadDbDataStore.cs` | `ReadDataStoreManage.cs` | +| `DeleteDbDataStore.cs` | `DeleteDataStoreManage.cs` | +| `DbDataStoreModel.cs` | `DataStoreManageModel.cs` | +| `DbDataStoreMapper.cs` | `DataStoreManageMapper.cs` | +| `DbDataStoreDatabaseNameFormatter.cs` | `DataStoreManageDatabaseNameFormatter.cs` | + +`DbDataStoreModel`'s existing `DataStoreId`/`DataStoreName` properties carry over unchanged into +`DataStoreManageModel` — only the `DbDataStore`-named parts change. + +**Routes**: `POST/GET/DELETE /dbDataStores...` → `POST/GET/DELETE /dataStores/manage...` +(served as `/v3/dataStores/manage`, still `BuildForVersions(AdminApiVersions.V3)`). + +**Infrastructure layer** (renamed in place, stays under v3's own +`Infrastructure\Database\Queries`/`Commands`/`Services\Jobs`): + +- `IGetDbDataStoresQuery`/`GetDbDataStoresQuery` → `IGetDataStoreManagesQuery`/`GetDataStoreManagesQuery` +- `IGetDbDataStoreByIdQuery`/`GetDbDataStoreByIdQuery` → `IGetDataStoreManageByIdQuery`/`GetDataStoreManageByIdQuery` +- `AddDbDataStoreCommand`/`IAddDbDataStoreModel` → `AddDataStoreManageCommand`/`IAddDataStoreManageModel` +- `IDeleteDbDataStoreCommand`/`DeleteDbDataStoreCommand` → `IDeleteDataStoreManageCommand`/`DeleteDataStoreManageCommand` +- `CreatePendingDbInstancesDispatcherJob`/`DeletePendingDbInstancesDispatcherJob` (v3's own + copies) → `CreatePendingDataStoreManagesDispatcherJob`/`DeletePendingDataStoreManagesDispatcherJob` + +**Existing v3 `DataStores` files touched (not moved)**: + +- `ReadEducationOrganizations.cs`: injects `IGetDataStoreManagesQuery` instead of + `IGetDbDataStoresQuery`; `MergeDbDataStoreData` → `MergeDataStoreManageData`; uses + `OdsInstanceManageStatus` (shared enum). +- `DataStoreWithEducationOrganizationsModel.cs`: add a new `DataStoreManageId` field, closing + the parity gap with v2's `OdsInstanceManageId`. + +## Tests + +**Unit tests** — renamed/moved in lockstep with production files, same mapping as above: + +- v2: `Features\DbInstances\AddDbInstanceTests.cs` → `Features\OdsInstances\Manage\AddOdsInstanceManageTests.cs` + (same pattern for `ReadDbInstanceTests.cs`/`DeleteDbInstanceTests.cs`), plus + `Infrastructure\Database\Commands\AddDbInstanceCommandTests.cs` → + `AddOdsInstanceManageCommandTests.cs` (and `Delete...`), `Infrastructure\Database\Queries\GetDbInstanceByIdQueryTests.cs`/ + `GetDbInstancesQueryTests.cs` → `GetOdsInstanceManageByIdQueryTests.cs`/`GetOdsInstanceManagesQueryTests.cs`, + `Infrastructure\Services\Jobs\CreatePendingDbInstancesDispatcherJobTests.cs`/ + `DeletePendingDbInstancesDispatcherJobTests.cs` → `CreatePendingOdsInstanceManagesDispatcherJobTests.cs`/ + `DeletePendingOdsInstanceManagesDispatcherJobTests.cs`. +- v3: same pattern with `DataStoreManage` naming, e.g. `Features\DbDataStores\AddDbDataStoreTests.cs` → + `Features\DataStores\Manage\AddDataStoreManageTests.cs`. +- **New v3 unit tests added** (closing a pre-existing coverage gap — v2 has query-level unit + tests that v3 lacked): `GetDataStoreManagesQueryTests.cs` and + `GetDataStoreManageByIdQueryTests.cs` in `EdFi.Ods.AdminApi.V3.UnitTests`, mirroring v2's + existing `GetOdsInstanceManagesQueryTests.cs`/`GetOdsInstanceManageByIdQueryTests.cs`. + +**DBTests** (this repo's integration-test project) — same rename pattern: +`Database\CommandTests\AddDbInstanceCommandTests.cs` → `AddOdsInstanceManageCommandTests.cs`, +`Database\QueryTests\GetDbInstancesQueryTests.cs` → `GetOdsInstanceManagesQueryTests.cs`, and v3 +equivalents (`DataStoreManage`-named). `GetTenantEdOrgsByInstancesTests.cs`/ +`GetTenantEdOrgsByDataStoresTests.cs` keep their file names (they don't contain the renamed term) +but have internal references updated. + +**Bruno E2E collections**: + +- v2: `E2E Tests\V2\...\v2\DbInstances\` → moved under `v2\OdsInstances\Manage\`; each `.bru` + file renamed (e.g. `POST - DbInstances.bru` → `POST - OdsInstances Manage.bru`) with request + URLs updated to `/v2/odsInstances/manage...`. +- v3: `E2E Tests\Bruno Admin API E2E 3.0\v3\DbDataStores\` → moved under + `v3\DataStores\Manage\`; `.bru` files renamed similarly, URLs updated to + `/v3/dataStores/manage...`. + +**`docs/http/dbinstances.http`**: renamed to `docs/http/odsinstances-manage.http`. All +`/v2/dbinstances` → `/v2/odsinstances/manage` and `/v3/dbDataStores` → `/v3/dataStores/manage`. +This file currently has uncommitted local edits (manual testing notes) — the rename is applied +on top of those edits rather than discarding them. + +## Risk / compatibility notes + +- **Breaking change for API consumers**: old routes (`/v2/dbInstances/*`, `/v3/dbDataStores/*`) + are removed with no aliasing. Any external client or automation hitting those paths must be + updated to the new `/v2/odsInstances/manage/*` / `/v3/dataStores/manage/*` paths. +- **Breaking change for deployers**: `AppSettings` config keys change names (see Naming scheme + section) — existing `appsettings.json`/environment-variable overrides referencing the old key + names will silently stop applying (fall back to code defaults) unless updated. Call this out + in release notes. +- **Data-preserving migration**: the table rename is in-place (`sp_rename`/`ALTER TABLE...RENAME`), + so existing `DbInstances` rows in deployed databases carry over as `OdsInstanceManages` rows + with no data loss and no identity/serial reseed. From 7ccad1d3aee76926daf92b677df7d6564f9d11d7 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 15:52:39 -0600 Subject: [PATCH 03/35] Add implementation plan for renaming DbInstances/DbDataStores to OdsInstanceManage Covers the in-place migration, shared entity/enum/config-key rename, v2's Features/DbInstances move to OdsInstances/Manage, v3's Features/DbDataStores move to DataStores/Manage, and all dependent unit/integration/E2E test updates. Co-Authored-By: Claude Sonnet 5 --- ...rename-dbinstances-to-odsinstancemanage.md | 3609 +++++++++++++++++ 1 file changed, 3609 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-27-rename-dbinstances-to-odsinstancemanage.md diff --git a/docs/superpowers/plans/2026-07-27-rename-dbinstances-to-odsinstancemanage.md b/docs/superpowers/plans/2026-07-27-rename-dbinstances-to-odsinstancemanage.md new file mode 100644 index 000000000..79f36567f --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-rename-dbinstances-to-odsinstancemanage.md @@ -0,0 +1,3609 @@ +# Rename DbInstances/DbDataStores to OdsInstanceManage/DataStoreManage Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rename the shared `DbInstance` entity/table to `OdsInstanceManage`, move v2's `Features\DbInstances` into `Features\OdsInstances\Manage` (`/v2/odsInstances/manage`), move v3's `Features\DbDataStores` into `Features\DataStores\Manage` (`/v3/dataStores/manage`), and update every dependent identifier, config key, test, and E2E artifact. + +**Architecture:** No new architecture — this is a mechanical rename+move refactor across two parallel, already-symmetric API version trees (v2 and v3) that share one entity/table in the `EdFi.Ods.AdminApi.Common` project. Each task renames one cohesive slice (shared layer, then v2, then v3, mirroring the same steps) and ends with a build+test checkpoint. + +**Tech Stack:** .NET / ASP.NET Core minimal APIs, EF Core, Quartz.NET, FluentValidation, NUnit + Shouldly, Bruno E2E collections, raw SQL migrations (MSSQL + PostgreSQL). + +## Global Constraints + +- Breaking change: old routes (`/v2/dbInstances/*`, `/v3/dbDataStores/*`) and old `AppSettings` config keys are removed outright — no back-compat aliases, no deprecation shims. +- Table rename is in-place (`sp_rename` / `ALTER TABLE ... RENAME TO`) to preserve existing data and identity/serial sequences — never drop-and-recreate. +- New migration artifact uses the next sequential number `00007-...sql`, added in all four locations: `Application\EdFi.Ods.AdminApi\Artifacts\{MsSql,PgSql}\Structure\Admin\` and `Application\EdFi.Ods.AdminApi.V3\Artifacts\{MsSql,PgSql}\Structure\Admin\` (only the v2 copy is actually consumed by `Install-AdminApiTables`/Docker init scripts; the v3 copy is a documentation-only duplicate kept in sync by convention). +- `CreateInstanceJob`/`DeleteInstanceJob` (both v2's and v3's own copies) are NOT renamed — generic names that don't contain "DbInstance"/"DbDataStore". +- v2's FK-style field is named `OdsInstanceManageId`; v3's is named `DataStoreManageId` — they are DTO-level fields on different response models, not the same property. +- New v3 unit tests are added for `GetDataStoreManagesQuery`/`GetDataStoreManageByIdQuery` to close a pre-existing coverage gap (v2 has these, v3 didn't). +- A new `DataStoreManageId` field is added to v3's `DataStoreWithEducationOrganizationsModel` for parity with v2's `OdsInstanceManageId`. +- `appsettings.json`/`appsettings.Development.json` config keys are renamed; `appsettings.Development.json` currently has uncommitted local edits (personal dev environment values) — preserve those values, only rename the keys. +- `docs/http/dbinstances.http` (renamed to `odsinstances-manage.http`) currently has uncommitted manual-testing edits — rebase the rename on top of those, don't discard them. +- Spec: `docs/superpowers/specs/2026-07-27-rename-dbinstances-to-odsinstancemanage-design.md`. +- v3's original `AddDbDataStore.cs`/`DeleteDbDataStore.cs` used parameter names that matched their type names in capitalization (e.g. `AddDbDataStoreCommand AddDbDataStoreCommand`, `IGetDbDataStoreByIdQuery GetDbDataStoreByIdQuery`) — an inconsistency with v2's lowerCamelCase convention. Tasks 11's rewritten code normalizes these to standard lowerCamelCase (`addDataStoreManageCommand`, `getDataStoreManageByIdQuery`) as an incidental, same-line cleanup, not a separate refactor. + +--- + +## Master Rename Table + +Reference this table from every task below instead of repeating it. Every occurrence of the "Old" token (as a whole identifier/word, not substring-inside-unrelated-word) becomes "New" in the files that task touches. + +### Shared (`EdFi.Ods.AdminApi.Common` project — affects both v2 and v3) + +| Old | New | +|---|---| +| `DbInstance` (entity class) | `OdsInstanceManage` | +| `DbInstances` (DbSet property / table name) | `OdsInstanceManages` | +| `DbInstanceStatus` (enum) | `OdsInstanceManageStatus` | +| `JobConstants.DbInstanceIdKey` | `JobConstants.OdsInstanceManageIdKey` | +| `JobConstants.CreatePendingDbInstancesDispatcherJobName` | `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName` | +| `JobConstants.DeletePendingDbInstancesDispatcherJobName` | `JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName` | +| `AppSettings.CreateDbInstancesSweepIntervalInMins` | `AppSettings.CreateOdsInstanceManagesSweepIntervalInMins` | +| `AppSettings.CreateDbInstancesMaxRetryAttempts` | `AppSettings.CreateOdsInstanceManagesMaxRetryAttempts` | +| `AppSettings.DeleteDbInstancesSweepIntervalInMins` | `AppSettings.DeleteOdsInstanceManagesSweepIntervalInMins` | +| `AppSettings.DeleteDbInstancesMaxRetryAttempts` | `AppSettings.DeleteOdsInstanceManagesMaxRetryAttempts` | +| Table `adminapi.DbInstances` | `adminapi.OdsInstanceManages` | +| Index `IX_DbInstances_Name` | `IX_OdsInstanceManages_Name` | +| Index `IX_DbInstances_OdsInstanceId` | `IX_OdsInstanceManages_OdsInstanceId` | +| Constraint `PK_DbInstances` | `PK_OdsInstanceManages` | + +### v2-specific (`EdFi.Ods.AdminApi` project) + +| Old | New | +|---|---| +| Namespace `EdFi.Ods.AdminApi.Features.DbInstances` | `EdFi.Ods.AdminApi.Features.OdsInstances.Manage` | +| `AddDbInstance` (class/file) | `AddOdsInstanceManage` | +| `ReadDbInstance` (class/file) | `ReadOdsInstanceManage` | +| `DeleteDbInstance` (class/file) | `DeleteOdsInstanceManage` | +| `DbInstanceModel` (class/file) | `OdsInstanceManageModel` | +| `DbInstanceMapper` (class/file) | `OdsInstanceManageMapper` | +| `DbInstanceDatabaseNameFormatter` (class/file) | `OdsInstanceManageDatabaseNameFormatter` | +| `AddDbInstanceRequest` | `AddOdsInstanceManageRequest` | +| `IAddDbInstanceModel` | `IAddOdsInstanceManageModel` | +| `AddDbInstanceCommand` | `AddOdsInstanceManageCommand` | +| `IDeleteDbInstanceCommand` / `DeleteDbInstanceCommand` | `IDeleteOdsInstanceManageCommand` / `DeleteOdsInstanceManageCommand` | +| `IGetDbInstancesQuery` / `GetDbInstancesQuery` | `IGetOdsInstanceManagesQuery` / `GetOdsInstanceManagesQuery` | +| `IGetDbInstanceByIdQuery` / `GetDbInstanceByIdQuery` | `IGetOdsInstanceManageByIdQuery` / `GetOdsInstanceManageByIdQuery` | +| `CreatePendingDbInstancesDispatcherJob` (v2 copy) | `CreatePendingOdsInstanceManagesDispatcherJob` | +| `DeletePendingDbInstancesDispatcherJob` (v2 copy) | `DeletePendingOdsInstanceManagesDispatcherJob` | +| `MaxDbInstanceNameLength` | `MaxOdsInstanceManageNameLength` | +| `_validDbInstanceNamePattern` | `_validOdsInstanceManageNamePattern` | +| Route `/dbInstances` | `/odsInstances/manage` | +| `OdsInstanceWithEducationOrganizationsModel.DbInstanceId` | `OdsInstanceManageId` | +| `MergeDbInstanceData` | `MergeOdsInstanceManageData` | + +### v3-specific (`EdFi.Ods.AdminApi.V3` project) + +| Old | New | +|---|---| +| Namespace `EdFi.Ods.AdminApi.V3.Features.DbDataStores` | `EdFi.Ods.AdminApi.V3.Features.DataStores.Manage` | +| `AddDbDataStore` (class/file) | `AddDataStoreManage` | +| `ReadDbDataStore` (class/file) | `ReadDataStoreManage` | +| `DeleteDbDataStore` (class/file) | `DeleteDataStoreManage` | +| `DbDataStoreModel` (class/file) | `DataStoreManageModel` | +| `DbDataStoreMapper` (class/file) | `DataStoreManageMapper` | +| `DbDataStoreDatabaseNameFormatter` (class/file) | `DataStoreManageDatabaseNameFormatter` | +| `AddDbDataStoreRequest` | `AddDataStoreManageRequest` | +| `IAddDbDataStoreModel` | `IAddDataStoreManageModel` | +| `AddDbDataStoreCommand` | `AddDataStoreManageCommand` | +| `IDeleteDbDataStoreCommand` / `DeleteDbDataStoreCommand` | `IDeleteDataStoreManageCommand` / `DeleteDataStoreManageCommand` | +| `IGetDbDataStoresQuery` / `GetDbDataStoresQuery` | `IGetDataStoreManagesQuery` / `GetDataStoreManagesQuery` | +| `IGetDbDataStoreByIdQuery` / `GetDbDataStoreByIdQuery` | `IGetDataStoreManageByIdQuery` / `GetDataStoreManageByIdQuery` | +| `CreatePendingDbInstancesDispatcherJob` (v3 copy) | `CreatePendingDataStoreManagesDispatcherJob` | +| `DeletePendingDbInstancesDispatcherJob` (v3 copy) | `DeletePendingDataStoreManagesDispatcherJob` | +| `MaxDbDataStoreNameLength` | `MaxDataStoreManageNameLength` | +| `_validDbDataStoreNamePattern` | `_validDataStoreManageNamePattern` | +| Route `/dbDataStores` | `/dataStores/manage` | +| `MergeDbDataStoreData` | `MergeDataStoreManageData` | +| *(new field, no old counterpart)* | `DataStoreWithEducationOrganizationsModel.DataStoreManageId` | + +Note: `DataStoreModel.DataStoreId`/`DataStoreModel.DataStoreType` (the existing `DataStore`'s own DTO fields, unrelated file) and `DbDataStoreModel.DataStoreId`/`DataStoreName` (already-renamed-at-DTO-level fields carried over unchanged into `DataStoreManageModel`) are **not** touched — only tokens literally containing `DbInstance`/`DbDataStore` change. + +--- + +### Task 1: Migration — rename `adminapi.DbInstances` to `adminapi.OdsInstanceManages` + +**Files:** +- Create: `Application\EdFi.Ods.AdminApi\Artifacts\MsSql\Structure\Admin\00007-RenameDbInstancesToOdsInstanceManages.sql` +- Create: `Application\EdFi.Ods.AdminApi\Artifacts\PgSql\Structure\Admin\00007-RenameDbInstancesToOdsInstanceManages.sql` +- Create: `Application\EdFi.Ods.AdminApi.V3\Artifacts\MsSql\Structure\Admin\00007-RenameDbInstancesToOdsInstanceManages.sql` +- Create: `Application\EdFi.Ods.AdminApi.V3\Artifacts\PgSql\Structure\Admin\00007-RenameDbInstancesToOdsInstanceManages.sql` + +**Interfaces:** +- Produces: table `adminapi.OdsInstanceManages` with the same columns as the old `adminapi.DbInstances` (`Id, Name, OdsInstanceId, OdsInstanceName, Status, DatabaseTemplate, DatabaseName, LastRefreshed, LastModifiedDate`), indexes `IX_OdsInstanceManages_Name` / `IX_OdsInstanceManages_OdsInstanceId`. Task 2's EF Core mapping depends on this table name existing. + +- [ ] **Step 1: Write the MSSQL migration script** + +```sql +-- 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. + +IF EXISTS (SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = 'adminapi' AND TABLE_NAME = 'DbInstances') + AND NOT EXISTS (SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = 'adminapi' AND TABLE_NAME = 'OdsInstanceManages') +BEGIN + EXEC sp_rename 'adminapi.DbInstances', 'OdsInstanceManages'; + EXEC sp_rename 'adminapi.PK_DbInstances', 'PK_OdsInstanceManages'; + EXEC sp_rename 'adminapi.OdsInstanceManages.IX_DbInstances_Name', 'IX_OdsInstanceManages_Name', 'INDEX'; + EXEC sp_rename 'adminapi.OdsInstanceManages.IX_DbInstances_OdsInstanceId', 'IX_OdsInstanceManages_OdsInstanceId', 'INDEX'; +END +``` + +Save this file to all two MSSQL locations (`Application\EdFi.Ods.AdminApi\Artifacts\MsSql\Structure\Admin\` and `Application\EdFi.Ods.AdminApi.V3\Artifacts\MsSql\Structure\Admin\`) — byte-identical, matching the existing convention where `00005-CreateDbInstances.sql` is duplicated across both trees. + +- [ ] **Step 2: Write the PostgreSQL migration script** + +```sql +-- 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. + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'adminapi' AND table_name = 'dbinstances') + AND NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'adminapi' AND table_name = 'odsinstancemanages') + THEN + ALTER TABLE adminapi.DbInstances RENAME TO OdsInstanceManages; + ALTER TABLE adminapi.OdsInstanceManages RENAME CONSTRAINT pk_dbinstances TO pk_odsinstancemanages; + ALTER INDEX adminapi.idx_dbinstances_name RENAME TO idx_odsinstancemanages_name; + ALTER INDEX adminapi.idx_dbinstances_odsinstanceid RENAME TO idx_odsinstancemanages_odsinstanceid; + END IF; +END $$; +``` + +Save this file to both PgSql locations (`Application\EdFi.Ods.AdminApi\Artifacts\PgSql\Structure\Admin\` and `Application\EdFi.Ods.AdminApi.V3\Artifacts\PgSql\Structure\Admin\`) — byte-identical. + +- [ ] **Step 3: Verify against a local database** + +Run: `./eng/run-dbup-migrations.ps1` (or the equivalent Docker init flow already used locally) against a database that has the old `00001`-`00006` scripts applied, then confirm: +- MSSQL: `SELECT name FROM sys.tables WHERE schema_id = SCHEMA_ID('adminapi');` shows `OdsInstanceManages`, not `DbInstances`. +- PostgreSQL: `\dt adminapi.*` shows `odsinstancemanages`, not `dbinstances`. +- Existing rows (if any were present before the rename) are unchanged in count and content. +- Re-running the script a second time is a no-op (idempotency guard prevents re-running `sp_rename`/`ALTER TABLE RENAME` once already renamed). + +- [ ] **Step 4: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql" \ + "Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql" \ + "Application/EdFi.Ods.AdminApi.V3/Artifacts/MsSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql" \ + "Application/EdFi.Ods.AdminApi.V3/Artifacts/PgSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql" +git commit -m "Add migration renaming adminapi.DbInstances to adminapi.OdsInstanceManages" +``` + +--- + +### Task 2: Rename shared entity, enum, JobConstants, AppSettings (Common project + both DbContexts) + +**Files:** +- Modify: `Application\EdFi.Ods.AdminApi.Common\Infrastructure\Models\DbInstance.cs` → rename file to `OdsInstanceManage.cs` +- Modify: `Application\EdFi.Ods.AdminApi.Common\Constants\Constants.cs` +- Modify: `Application\EdFi.Ods.AdminApi.Common\Infrastructure\Jobs\JobConstants.cs` +- Modify: `Application\EdFi.Ods.AdminApi.Common\Settings\AppSettings.cs` +- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\AdminApiDbContext.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\AdminApiDbContext.cs` +- Modify: `Application\EdFi.Ods.AdminApi\appsettings.json` +- Modify: `Application\EdFi.Ods.AdminApi\appsettings.Development.json` +- Modify: `Application\EdFi.Ods.AdminApi.V3\appsettings.json` + +**Interfaces:** +- Produces: `OdsInstanceManage` entity class (namespace `EdFi.Ods.AdminApi.Common.Infrastructure.Models`), `OdsInstanceManageStatus` enum (namespace `EdFi.Ods.AdminApi.Common.Constants`), `JobConstants.OdsInstanceManageIdKey`/`CreatePendingOdsInstanceManagesDispatcherJobName`/`DeletePendingOdsInstanceManagesDispatcherJobName`, `AppSettings.CreateOdsInstanceManagesSweepIntervalInMins`/`CreateOdsInstanceManagesMaxRetryAttempts`/`DeleteOdsInstanceManagesSweepIntervalInMins`/`DeleteOdsInstanceManagesMaxRetryAttempts`. Every later task in this plan consumes these exact names. + +- [ ] **Step 1: Rename the entity file and class** + +```bash +git mv "Application/EdFi.Ods.AdminApi.Common/Infrastructure/Models/DbInstance.cs" "Application/EdFi.Ods.AdminApi.Common/Infrastructure/Models/OdsInstanceManage.cs" +``` + +Edit the file content to: + +```csharp +// 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.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EdFi.Ods.AdminApi.Common.Infrastructure.Models; + +public class OdsInstanceManage +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [Required] + [StringLength(100)] + public string Name { get; set; } = string.Empty; + + public int? OdsInstanceId { get; set; } + + [StringLength(100)] + public string? OdsInstanceName { get; set; } + + [Required] + [StringLength(75)] + public string Status { get; set; } = string.Empty; + + [Required] + [StringLength(100)] + public string DatabaseTemplate { get; set; } = string.Empty; + + [StringLength(255)] + public string? DatabaseName { get; set; } + + [Required] + public DateTime LastRefreshed { get; set; } = DateTime.UtcNow; + + public DateTime? LastModifiedDate { get; set; } +} +``` + +- [ ] **Step 2: Rename the `DbInstanceStatus` enum in `Constants.cs`** + +In `Application\EdFi.Ods.AdminApi.Common\Constants\Constants.cs`, replace: + +```csharp +public enum DbInstanceStatus +``` + +with: + +```csharp +public enum OdsInstanceManageStatus +``` + +(the enum's member list — `PendingCreate, Created, CreateInProgress, CreateFailed, CreateError, PendingDelete, DeleteInProgress, Deleted, DeleteFailed, DeleteError` — is unchanged.) + +- [ ] **Step 3: Rename `JobConstants` members** + +In `Application\EdFi.Ods.AdminApi.Common\Infrastructure\Jobs\JobConstants.cs`, replace: + +```csharp + public const string DbInstanceIdKey = "DbInstanceId"; + public const string OdsInstanceIdKey = "OdsInstanceId"; + public const string CreateInstanceJobName = "CreateInstanceJob"; + public const string CreatePendingDbInstancesDispatcherJobName = "CreatePendingDbInstancesDispatcherJob"; + public const string DeleteInstanceJobName = "DeleteInstanceJob"; + public const string DeletePendingDbInstancesDispatcherJobName = "DeletePendingDbInstancesDispatcherJob"; +``` + +with: + +```csharp + public const string OdsInstanceManageIdKey = "OdsInstanceManageId"; + public const string OdsInstanceIdKey = "OdsInstanceId"; + public const string CreateInstanceJobName = "CreateInstanceJob"; + public const string CreatePendingOdsInstanceManagesDispatcherJobName = "CreatePendingOdsInstanceManagesDispatcherJob"; + public const string DeleteInstanceJobName = "DeleteInstanceJob"; + public const string DeletePendingOdsInstanceManagesDispatcherJobName = "DeletePendingOdsInstanceManagesDispatcherJob"; +``` + +(`OdsInstanceIdKey`, `CreateInstanceJobName`, `DeleteInstanceJobName`, `RefreshEducationOrganizationsJobName`, `RunIdKey`, `JobTypeKey`, `TenantNameKey` are unrelated and unchanged.) + +- [ ] **Step 4: Rename `AppSettings` properties** + +In `Application\EdFi.Ods.AdminApi.Common\Settings\AppSettings.cs`, replace: + +```csharp + public int CreateDbInstancesSweepIntervalInMins { get; set; } = 5; + public int CreateDbInstancesMaxRetryAttempts { get; set; } = 3; + public int DeleteDbInstancesSweepIntervalInMins { get; set; } = 5; + public int DeleteDbInstancesMaxRetryAttempts { get; set; } = 3; +``` + +with: + +```csharp + public int CreateOdsInstanceManagesSweepIntervalInMins { get; set; } = 5; + public int CreateOdsInstanceManagesMaxRetryAttempts { get; set; } = 3; + public int DeleteOdsInstanceManagesSweepIntervalInMins { get; set; } = 5; + public int DeleteOdsInstanceManagesMaxRetryAttempts { get; set; } = 3; +``` + +- [ ] **Step 5: Update both `AdminApiDbContext` classes** + +In `Application\EdFi.Ods.AdminApi\Infrastructure\AdminApiDbContext.cs` (and identically in `Application\EdFi.Ods.AdminApi.V3\Infrastructure\AdminApiDbContext.cs`), replace: + +```csharp + public DbSet DbInstances { get; set; } +``` + +with: + +```csharp + public DbSet OdsInstanceManages { get; set; } +``` + +and replace: + +```csharp + modelBuilder.Entity().ToTable("DbInstances").HasKey(t => t.Id); +``` + +with: + +```csharp + modelBuilder.Entity().ToTable("OdsInstanceManages").HasKey(t => t.Id); +``` + +- [ ] **Step 6: Update `appsettings.json` (v2) config keys** + +In `Application\EdFi.Ods.AdminApi\appsettings.json`, replace the four keys (preserving their existing values `120`/`3`/`120`/`3`): + +```json + "CreateDbInstancesSweepIntervalInMins": 120, + "CreateDbInstancesMaxRetryAttempts": 3, + "DeleteDbInstancesSweepIntervalInMins": 120, + "DeleteDbInstancesMaxRetryAttempts": 3, +``` + +with: + +```json + "CreateOdsInstanceManagesSweepIntervalInMins": 120, + "CreateOdsInstanceManagesMaxRetryAttempts": 3, + "DeleteOdsInstanceManagesSweepIntervalInMins": 120, + "DeleteOdsInstanceManagesMaxRetryAttempts": 3, +``` + +- [ ] **Step 7: Update `appsettings.Development.json` (v2) config keys** + +This file currently has uncommitted local edits. Read the file first to get its current exact values, then replace only the four key names (`CreateDbInstancesSweepIntervalInMins`, `CreateDbInstancesMaxRetryAttempts`, `DeleteDbInstancesSweepIntervalInMins`, `DeleteDbInstancesMaxRetryAttempts`) with their `OdsInstanceManages`-named equivalents, preserving whatever values are currently present (as of this plan's writing: `5`, `3`, `5`, `3`) and every other uncommitted edit in the file untouched. + +- [ ] **Step 8: Update `appsettings.json` (v3) config keys** + +Same four-key rename as Step 6, applied to `Application\EdFi.Ods.AdminApi.V3\appsettings.json` (existing values `120`/`3`/`120`/`3`). + +- [ ] **Step 9: Build to confirm no compile errors yet from callers** + +Run: `dotnet build Application/EdFi.Ods.AdminApi.sln` (or the solution file this repo uses — check for a `.sln` at the repo root or under `Application\`). +Expected: FAILS — callers in v2/v3 Features/Infrastructure/Jobs still reference the old `DbInstance`/`DbInstanceStatus`/old `JobConstants`/old `AppSettings` names. This is expected at this checkpoint; Tasks 3–15 fix each caller. Confirm the failures are all in files this plan's later tasks will touch (grep the build output for `DbInstance`/`DbDataStore` to sanity-check no unexpected file is affected). + +- [ ] **Step 10: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi.Common/Infrastructure/Models/OdsInstanceManage.cs" \ + "Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs" \ + "Application/EdFi.Ods.AdminApi.Common/Infrastructure/Jobs/JobConstants.cs" \ + "Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs" \ + "Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiDbContext.cs" \ + "Application/EdFi.Ods.AdminApi.V3/Infrastructure/AdminApiDbContext.cs" \ + "Application/EdFi.Ods.AdminApi/appsettings.json" \ + "Application/EdFi.Ods.AdminApi/appsettings.Development.json" \ + "Application/EdFi.Ods.AdminApi.V3/appsettings.json" +git commit -m "Rename shared DbInstance entity/enum/config keys to OdsInstanceManage" +``` + +Note: this commit intentionally leaves the solution non-building until Task 3 onward completes — that's expected for a rename this wide; each subsequent task is reviewed independently but the "build passes" checkpoint only becomes true again at the end of Task 9 (v2 side fully done) and again at the end of Task 15 (v3 side fully done). If your workflow requires green-build-per-commit, squash Tasks 2–9 (or 2–15) before merging instead of committing at each intermediate step — call this out to whoever reviews the branch. + +--- + +### Task 3: v2 Infrastructure layer rename (Queries, Commands, Jobs) + +**Files:** +- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Database\Queries\GetDbInstancesQuery.cs` → rename to `GetOdsInstanceManagesQuery.cs` +- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Database\Queries\GetDbInstanceByIdQuery.cs` → rename to `GetOdsInstanceManageByIdQuery.cs` +- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Database\Commands\AddDbInstanceCommand.cs` → rename to `AddOdsInstanceManageCommand.cs` +- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Database\Commands\DeleteDbInstanceCommand.cs` → rename to `DeleteOdsInstanceManageCommand.cs` +- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Jobs\CreatePendingDbInstancesDispatcherJob.cs` → rename to `CreatePendingOdsInstanceManagesDispatcherJob.cs` +- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Jobs\DeletePendingDbInstancesDispatcherJob.cs` → rename to `DeletePendingOdsInstanceManagesDispatcherJob.cs` +- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Jobs\CreateInstanceJob.cs` (renamed in place, filename unchanged) +- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Jobs\DeleteInstanceJob.cs` (renamed in place, filename unchanged) + +**Interfaces:** +- Consumes: `OdsInstanceManage` entity, `OdsInstanceManageStatus` enum, `JobConstants.OdsInstanceManageIdKey`, `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName`/`DeletePendingOdsInstanceManagesDispatcherJobName`, `AppSettings.CreateOdsInstanceManagesMaxRetryAttempts`/`DeleteOdsInstanceManagesMaxRetryAttempts` (all from Task 2), `AdminApiDbContext.OdsInstanceManages` (from Task 2). +- Produces: `IGetOdsInstanceManagesQuery`/`GetOdsInstanceManagesQuery`, `IGetOdsInstanceManageByIdQuery`/`GetOdsInstanceManageByIdQuery`, `AddOdsInstanceManageCommand`/`IAddOdsInstanceManageModel`, `IDeleteOdsInstanceManageCommand`/`DeleteOdsInstanceManageCommand`, `CreatePendingOdsInstanceManagesDispatcherJob`, `DeletePendingOdsInstanceManagesDispatcherJob` — consumed by Task 4 (feature files) and Task 6 (wiring). + +- [ ] **Step 1: Rename and update the Queries** + +```bash +git mv "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetDbInstancesQuery.cs" "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManagesQuery.cs" +git mv "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetDbInstanceByIdQuery.cs" "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQuery.cs" +``` + +`GetOdsInstanceManagesQuery.cs`: + +```csharp +// 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.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Extensions; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetOdsInstanceManagesQuery +{ + List Execute(CommonQueryParams commonQueryParams, int? id, string? name); +} + +public class GetOdsInstanceManagesQuery : IGetOdsInstanceManagesQuery +{ + private readonly AdminApiDbContext _context; + private readonly IOptions _options; + + public GetOdsInstanceManagesQuery(AdminApiDbContext context, IOptions options) + { + _context = context; + _options = options; + } + + public List Execute(CommonQueryParams commonQueryParams, int? id, string? name) + { + return _context.OdsInstanceManages + .Where(d => id == null || d.Id == id) + .Where(d => name == null || d.Name == name) + .OrderBy(d => d.Id) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + .ToList(); + } +} +``` + +`GetOdsInstanceManageByIdQuery.cs`: + +```csharp +// 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.Infrastructure.Models; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +public interface IGetOdsInstanceManageByIdQuery +{ + OdsInstanceManage? Execute(int id); +} + +public class GetOdsInstanceManageByIdQuery : IGetOdsInstanceManageByIdQuery +{ + private readonly AdminApiDbContext _context; + + public GetOdsInstanceManageByIdQuery(AdminApiDbContext context) + { + _context = context; + } + + public OdsInstanceManage? Execute(int id) + { + return _context.OdsInstanceManages.SingleOrDefault(d => d.Id == id); + } +} +``` + +- [ ] **Step 2: Rename and update the Commands** + +```bash +git mv "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddDbInstanceCommand.cs" "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceManageCommand.cs" +git mv "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteDbInstanceCommand.cs" "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommand.cs" +``` + +`AddOdsInstanceManageCommand.cs`: + +```csharp +// 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.Constants; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public class AddOdsInstanceManageCommand +{ + private readonly AdminApiDbContext _context; + + public AddOdsInstanceManageCommand(AdminApiDbContext context) + { + _context = context; + } + + public OdsInstanceManage Execute(IAddOdsInstanceManageModel model) + { + if (string.IsNullOrWhiteSpace(model.Name)) + throw new ArgumentException("Name is required.", nameof(model)); + if (string.IsNullOrWhiteSpace(model.DatabaseTemplate)) + throw new ArgumentException("DatabaseTemplate is required.", nameof(model)); + + var now = DateTime.UtcNow; + + var odsInstanceManage = new OdsInstanceManage + { + Name = model.Name.Trim(), + DatabaseTemplate = model.DatabaseTemplate.Trim(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), + OdsInstanceId = null, + OdsInstanceName = null, + DatabaseName = null, + LastRefreshed = now, + LastModifiedDate = now + }; + + _context.OdsInstanceManages.Add(odsInstanceManage); + _context.SaveChanges(); + return odsInstanceManage; + } +} + +public interface IAddOdsInstanceManageModel +{ + string? Name { get; } + string? DatabaseTemplate { get; } +} +``` + +`DeleteOdsInstanceManageCommand.cs`: + +```csharp +// 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.Constants; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IDeleteOdsInstanceManageCommand +{ + void Execute(int id); +} + +public class DeleteOdsInstanceManageCommand : IDeleteOdsInstanceManageCommand +{ + private readonly AdminApiDbContext _context; + + public DeleteOdsInstanceManageCommand(AdminApiDbContext context) + { + _context = context; + } + + public void Execute(int id) + { + var odsInstanceManage = + _context.OdsInstanceManages.Find(id) + ?? throw new NotFoundException("odsInstanceManage", id); + + if (odsInstanceManage.Status != OdsInstanceManageStatus.Created.ToString()) + throw new NotFoundException("odsInstanceManage", id); + + odsInstanceManage.Status = OdsInstanceManageStatus.PendingDelete.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + + _context.SaveChanges(); + } +} +``` + +- [ ] **Step 3: Rename and update the dispatcher Jobs** + +```bash +git mv "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJob.cs" "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJob.cs" +git mv "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJob.cs" "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJob.cs" +``` + +`CreatePendingOdsInstanceManagesDispatcherJob.cs` (apply the Master Rename Table to the existing content — class name, constructor param, local variable names `eligibleDbInstances`→`eligibleOdsInstanceManages`, `dbInstance`→`odsInstanceManage` loop variable, `JobConstants.DbInstanceIdKey`→`JobConstants.OdsInstanceManageIdKey`, `DbInstanceStatus`→`OdsInstanceManageStatus`, `_options.Value.CreateDbInstancesMaxRetryAttempts`→`_options.Value.CreateOdsInstanceManagesMaxRetryAttempts`, `adminApiDbContext.DbInstances`→`adminApiDbContext.OdsInstanceManages`): + +```csharp +// 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.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Constants; +using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Quartz; + +namespace EdFi.Ods.AdminApi.Infrastructure.Services.Jobs; + +[DisallowConcurrentExecution] +public class CreatePendingOdsInstanceManagesDispatcherJob( + ILogger logger, + IJobStatusService jobStatusService, + AdminApiDbContext dbContext, + ITenantSpecificDbContextProvider tenantSpecificDbContextProvider, + IOptions options) + : AdminApiQuartzJobBase(logger, jobStatusService) +{ + private const int DefaultMaxRetryAttempts = 3; + + private readonly AdminApiDbContext _dbContext = dbContext; + private readonly ITenantSpecificDbContextProvider _tenantSpecificDbContextProvider = tenantSpecificDbContextProvider; + private readonly IOptions _options = options; + + protected override async Task ExecuteJobAsync(IJobExecutionContext context) + { + var multiTenancyEnabled = _options.Value.MultiTenancy; + var tenantName = GetTenantName(context, multiTenancyEnabled); + AdminApiDbContext? tenantAdminApiDbContext = null; + var adminApiDbContext = _dbContext; + + try + { + if (multiTenancyEnabled) + { + tenantAdminApiDbContext = _tenantSpecificDbContextProvider.GetAdminApiDbContext(tenantName!); + adminApiDbContext = tenantAdminApiDbContext; + } + + var eligibleOdsInstanceManages = await adminApiDbContext.OdsInstanceManages + .Where(instance => instance.Status == OdsInstanceManageStatus.PendingCreate.ToString() || instance.Status == OdsInstanceManageStatus.CreateFailed.ToString()) + .OrderBy(instance => instance.Id) + .ToListAsync(); + + foreach (var odsInstanceManage in eligibleOdsInstanceManages) + { + if (string.Equals(odsInstanceManage.Status, OdsInstanceManageStatus.PendingCreate.ToString(), StringComparison.OrdinalIgnoreCase)) + { + await ScheduleCreateJobAsync(context, odsInstanceManage.Id, tenantName); + continue; + } + + if (!await IsRetryEligibleAsync(adminApiDbContext, odsInstanceManage, tenantName)) + { + odsInstanceManage.Status = OdsInstanceManageStatus.CreateError.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; + await adminApiDbContext.SaveChangesAsync(); + continue; + } + + odsInstanceManage.Status = OdsInstanceManageStatus.PendingCreate.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; + await adminApiDbContext.SaveChangesAsync(); + + await ScheduleCreateJobAsync(context, odsInstanceManage.Id, tenantName); + } + } + finally + { + if (tenantAdminApiDbContext is not null) + { + await tenantAdminApiDbContext.DisposeAsync(); + } + } + } + + private async Task IsRetryEligibleAsync(AdminApiDbContext adminApiDbContext, OdsInstanceManage odsInstanceManage, string? tenantName) + { + var maxRetryAttempts = _options.Value.CreateOdsInstanceManagesMaxRetryAttempts > 0 + ? _options.Value.CreateOdsInstanceManagesMaxRetryAttempts + : DefaultMaxRetryAttempts; + + var jobIdPrefix = $"{CreateInstanceJob.BuildJobIdentity(odsInstanceManage.Id, tenantName)}_"; + var errorCount = await adminApiDbContext.JobStatuses + .CountAsync(status => status.JobId.StartsWith(jobIdPrefix) && status.Status == QuartzJobStatus.Error.ToString()); + + return errorCount < maxRetryAttempts; + } + + private static async Task ScheduleCreateJobAsync(IJobExecutionContext context, int odsInstanceManageId, string? tenantName) + { + var jobData = new Dictionary + { + [JobConstants.OdsInstanceManageIdKey] = odsInstanceManageId + }; + + if (!string.IsNullOrWhiteSpace(tenantName)) + { + jobData[JobConstants.TenantNameKey] = tenantName; + } + + await QuartzJobScheduler.ScheduleJob( + context.Scheduler, + CreateInstanceJob.CreateJobKey(odsInstanceManageId, tenantName), + jobData, + startImmediately: true); + } + + private static string? GetTenantName(IJobExecutionContext context, bool multiTenancyEnabled) + { + if (!multiTenancyEnabled) + { + return null; + } + + var tenantName = context.MergedJobDataMap.ContainsKey(JobConstants.TenantNameKey) + ? context.MergedJobDataMap.GetString(JobConstants.TenantNameKey) + : null; + + if (string.IsNullOrWhiteSpace(tenantName)) + { + throw new InvalidOperationException( + $"{JobConstants.TenantNameKey} must be provided when multi-tenancy is enabled."); + } + + return tenantName; + } +} +``` + +`DeletePendingOdsInstanceManagesDispatcherJob.cs` — apply the identical transformation (class name, `eligibleOdsInstanceManages`, `odsInstanceManage` loop var, `DeleteOdsInstanceManagesMaxRetryAttempts`, `PendingDelete`/`DeleteFailed`/`DeleteError` status branches instead of Create's) mirroring `CreatePendingOdsInstanceManagesDispatcherJob.cs` above but keeping the Delete-specific status logic and calling `DeleteInstanceJob` (unchanged name) instead of `CreateInstanceJob`. + +- [ ] **Step 4: Update `CreateInstanceJob.cs` and `DeleteInstanceJob.cs` in place (filenames unchanged, class names unchanged — only internal references)** + +In `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Jobs\CreateInstanceJob.cs`: +- Replace `using EdFi.Ods.AdminApi.Features.DbInstances;` with `using EdFi.Ods.AdminApi.Features.OdsInstances.Manage;` (namespace of `OdsInstanceManageDatabaseNameFormatter`, produced by Task 4 — this file will not compile until Task 4 completes; that's expected, note it in the PR). +- Replace every `DbInstance? dbInstance` / `DbInstance dbInstance` type reference with `OdsInstanceManage? odsInstanceManage` / `OdsInstanceManage odsInstanceManage` (rename the local variable throughout the method body too, e.g. `dbInstance.Status`→`odsInstanceManage.Status`). +- Replace `adminApiDbContext.DbInstances` with `adminApiDbContext.OdsInstanceManages`. +- Replace `DbInstanceStatus` with `OdsInstanceManageStatus` (all switch/enum references). +- Replace `JobConstants.DbInstanceIdKey` with `JobConstants.OdsInstanceManageIdKey`. +- Replace `DbInstanceDatabaseNameFormatter` with `OdsInstanceManageDatabaseNameFormatter`. +- Replace `int dbInstanceId` parameter names with `int odsInstanceManageId` in `CreateJobKey`/`BuildJobIdentity`. +- Update the comment `// The CreatePendingDbInstancesDispatcherJob may have already scheduled...` (if present) and any other prose comment mentioning "DbInstance" to say "OdsInstanceManage" instead, and `CreatePendingDbInstancesDispatcherJob` to `CreatePendingOdsInstanceManagesDispatcherJob`. + +In `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Jobs\DeleteInstanceJob.cs`: apply the identical set of replacements (this file doesn't reference `DbInstanceDatabaseNameFormatter`, so skip that one). + +- [ ] **Step 5: Build to confirm the Infrastructure layer compiles in isolation** + +Run: `dotnet build Application/EdFi.Ods.AdminApi.sln` +Expected: still FAILS — `Features\DbInstances\*` (Task 4), `Features\OdsInstances\*` (Task 5), `Program.cs`/`WebApplicationBuilderExtensions.cs` (Task 6) haven't been updated yet. Confirm the remaining errors are now confined to those files only (no errors left in `Infrastructure\Database\*` or `Infrastructure\Services\Jobs\*`). + +- [ ] **Step 6: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManagesQuery.cs" \ + "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQuery.cs" \ + "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceManageCommand.cs" \ + "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommand.cs" \ + "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJob.cs" \ + "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJob.cs" \ + "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreateInstanceJob.cs" \ + "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeleteInstanceJob.cs" +git commit -m "Rename v2 Infrastructure Queries/Commands/Jobs to OdsInstanceManage naming" +``` + +--- + +### Task 4: v2 Feature folder move — `Features\DbInstances` → `Features\OdsInstances\Manage` + +**Files:** +- Create: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\Manage\AddOdsInstanceManage.cs` +- Create: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\Manage\ReadOdsInstanceManage.cs` +- Create: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\Manage\DeleteOdsInstanceManage.cs` +- Create: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\Manage\OdsInstanceManageModel.cs` +- Create: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\Manage\OdsInstanceManageMapper.cs` +- Create: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\Manage\OdsInstanceManageDatabaseNameFormatter.cs` +- Delete: `Application\EdFi.Ods.AdminApi\Features\DbInstances\` (entire folder, all 6 old files) + +**Interfaces:** +- Consumes: `AddOdsInstanceManageCommand`/`IAddOdsInstanceManageModel`, `IGetOdsInstanceManagesQuery`/`IGetOdsInstanceManageByIdQuery`, `IDeleteOdsInstanceManageCommand` (Task 3), `OdsInstanceManage`/`OdsInstanceManageStatus` (Task 2), `JobConstants.OdsInstanceManageIdKey` (Task 2), `CreateInstanceJob`/`DeleteInstanceJob` (Task 3, unchanged names). +- Produces: routes `POST/GET/DELETE /odsInstances/manage` under `EdFi.Ods.AdminApi.Features.OdsInstances.Manage`, consumed by Task 9 (Bruno E2E) and Task 17 (`.http` file). + +- [ ] **Step 1: Move the folder with git so history follows, then delete leftovers** + +```bash +git mv "Application/EdFi.Ods.AdminApi/Features/DbInstances/AddDbInstance.cs" "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/AddOdsInstanceManage.cs" +git mv "Application/EdFi.Ods.AdminApi/Features/DbInstances/ReadDbInstance.cs" "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/ReadOdsInstanceManage.cs" +git mv "Application/EdFi.Ods.AdminApi/Features/DbInstances/DeleteDbInstance.cs" "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/DeleteOdsInstanceManage.cs" +git mv "Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceModel.cs" "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageModel.cs" +git mv "Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceMapper.cs" "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageMapper.cs" +git mv "Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceDatabaseNameFormatter.cs" "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageDatabaseNameFormatter.cs" +``` + +Confirm `Application\EdFi.Ods.AdminApi\Features\DbInstances\` is now empty and remove the empty folder if git/your OS leaves it behind. + +- [ ] **Step 2: Replace `AddOdsInstanceManage.cs` content** + +```csharp +// 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.RegularExpressions; + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Context; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; +using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Common.Constants; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Services.Jobs; +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Quartz; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; + +public class AddOdsInstanceManage : IFeature +{ + private const int MaxSynchronizedNameLength = 100; + private const int MaxOdsInstanceManageNameLength = MaxSynchronizedNameLength; + private static readonly Regex _validOdsInstanceManageNamePattern = new( + "^[A-Za-z0-9 _]+$", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapPost(endpoints, "/odsInstances/manage", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(202)) + .BuildForVersions(AdminApiVersions.V2); + } + + public async static Task Handle( + Validator validator, + AddOdsInstanceManageCommand addOdsInstanceManageCommand, + [FromServices] ISchedulerFactory schedulerFactory, + [FromServices] IContextProvider tenantConfigurationProvider, + [FromServices] IOptions options, + AddOdsInstanceManageRequest request) + { + await validator.GuardAsync(request); + + var added = addOdsInstanceManageCommand.Execute(request); + + var tenantIdentifier = options.Value.MultiTenancy + ? tenantConfigurationProvider.Get()?.TenantIdentifier + : null; + + var jobBuilder = JobBuilder.Create() + .WithIdentity(CreateInstanceJob.CreateJobKey(added.Id, tenantIdentifier)) + .UsingJobData(JobConstants.OdsInstanceManageIdKey, added.Id); + + if (!string.IsNullOrWhiteSpace(tenantIdentifier)) + { + jobBuilder = jobBuilder.UsingJobData(JobConstants.TenantNameKey, tenantIdentifier); + } + + var trigger = TriggerBuilder.Create() + .StartNow() + .Build(); + + var scheduler = await schedulerFactory.GetScheduler(); + + try + { + await scheduler.ScheduleJob(jobBuilder.Build(), trigger); + } + catch (ObjectAlreadyExistsException) + { + // The CreatePendingOdsInstanceManagesDispatcherJob may have already scheduled this job + // (e.g. it fired between the DB insert and this ScheduleJob call). Treat duplicate + // scheduling as success — the job is already queued and will process the OdsInstanceManage. + } + + return Results.Accepted($"/odsinstances/manage/{added.Id}", null); + } + + [SwaggerSchema(Title = "AddOdsInstanceManageRequest")] + public class AddOdsInstanceManageRequest : IAddOdsInstanceManageModel + { + [SwaggerSchema(Description = "Name of the database instance", Nullable = false)] + public string? Name { get; set; } + + [SwaggerSchema(Description = "Database template to use for the instance", Nullable = false)] + public string? DatabaseTemplate { get; set; } + } + + public class Validator : AbstractValidator + { + private static readonly string[] _validDatabaseTemplates = Enum.GetNames(); + private readonly AdminApiDbContext _adminApiDbContext; + private readonly IUsersContext _usersContext; + + public Validator(AdminApiDbContext adminApiDbContext, IUsersContext usersContext) + { + _adminApiDbContext = adminApiDbContext; + _usersContext = usersContext; + + RuleFor(m => m.Name) + .NotEmpty() + .MaximumLength(MaxOdsInstanceManageNameLength) + .WithMessage($"'{{PropertyName}}' must be {MaxOdsInstanceManageNameLength} characters or fewer so the synchronized ODS instance name fits within {MaxSynchronizedNameLength} characters.") + .Matches(_validOdsInstanceManageNamePattern) + .WithMessage("'{PropertyName}' may only contain letters, numbers, spaces, and underscores."); + + RuleFor(m => m.DatabaseTemplate).NotEmpty().MaximumLength(100) + .Must(t => t != null && _validDatabaseTemplates.Contains(t)) + .WithMessage($"'{{PropertyValue}}' is not a valid database template. Allowed values are: {string.Join(", ", _validDatabaseTemplates)}."); + + RuleFor(m => m).CustomAsync(async (request, context, cancellationToken) => + { + if (string.IsNullOrWhiteSpace(request.Name) + || string.IsNullOrWhiteSpace(request.DatabaseTemplate) + || request.Name.Length > MaxOdsInstanceManageNameLength + || !_validOdsInstanceManageNamePattern.IsMatch(request.Name) + || !_validDatabaseTemplates.Contains(request.DatabaseTemplate)) + { + return; + } + + var normalizedName = request.Name.Trim(); + + if (await _adminApiDbContext.OdsInstanceManages.AnyAsync(instance => instance.Name == normalizedName && instance.Status != OdsInstanceManageStatus.Deleted.ToString(), cancellationToken)) + { + context.AddFailure( + nameof(AddOdsInstanceManageRequest.Name), + $"An OdsInstanceManage named '{normalizedName}' already exists."); + return; + } + + if (await _usersContext.OdsInstances.AnyAsync(instance => instance.Name == normalizedName, cancellationToken)) + { + context.AddFailure( + nameof(AddOdsInstanceManageRequest.Name), + $"An OdsInstance named '{normalizedName}' already exists."); + return; + } + + var databaseName = OdsInstanceManageDatabaseNameFormatter.Build(request.Name, request.DatabaseTemplate); + + if (databaseName.Length > OdsInstanceManageDatabaseNameFormatter.MaxPortableDatabaseNameLength) + { + context.AddFailure( + nameof(AddOdsInstanceManageRequest.Name), + $"The generated database name '{databaseName}' exceeds the portable limit of {OdsInstanceManageDatabaseNameFormatter.MaxPortableDatabaseNameLength} characters. Shorten Name or DatabaseTemplate."); + } + }); + } + } +} +``` + +- [ ] **Step 3: Replace `ReadOdsInstanceManage.cs` content** + +```csharp +// 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.ErrorHandling; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; + +namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; + +public class ReadOdsInstanceManage : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstances/manage", GetOdsInstanceManages) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + + AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstances/manage/{id}", GetOdsInstanceManage) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static Task GetOdsInstanceManages(IGetOdsInstanceManagesQuery query, + [AsParameters] CommonQueryParams commonQueryParams, int? id, string? name) + { + var list = OdsInstanceManageMapper.ToModelList(query.Execute(commonQueryParams, id, name)); + return Task.FromResult(Results.Ok(list)); + } + + public static Task GetOdsInstanceManage(IGetOdsInstanceManageByIdQuery query, int id) + { + var odsInstanceManage = query.Execute(id); + if (odsInstanceManage == null) + { + throw new NotFoundException("odsInstanceManage", id); + } + var model = OdsInstanceManageMapper.ToModel(odsInstanceManage); + return Task.FromResult(Results.Ok(model)); + } +} +``` + +- [ ] **Step 4: Replace `DeleteOdsInstanceManage.cs` content** + +```csharp +// 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.Constants; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Context; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; +using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.Infrastructure.Services.Jobs; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Quartz; + +namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; + +public class DeleteOdsInstanceManage : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapDelete(endpoints, "/odsInstances/manage/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(204)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task Handle( + IGetOdsInstanceManageByIdQuery getOdsInstanceManageByIdQuery, + IDeleteOdsInstanceManageCommand deleteOdsInstanceManageCommand, + [FromServices] ISchedulerFactory schedulerFactory, + [FromServices] IContextProvider tenantConfigurationProvider, + [FromServices] IOptions options, + int id + ) + { + var odsInstanceManage = getOdsInstanceManageByIdQuery.Execute(id); + if (odsInstanceManage is null) + throw new NotFoundException("odsInstanceManage", id); + + if (odsInstanceManage.Status == OdsInstanceManageStatus.Deleted.ToString()) + throw new NotFoundException("odsInstanceManage", id); + + var blockingMessage = GetBlockingStatusMessage(odsInstanceManage.Status); + if (blockingMessage is not null) + throw new ValidationException([new ValidationFailure(nameof(id), blockingMessage)]); + + deleteOdsInstanceManageCommand.Execute(id); + + var tenantName = options.Value.MultiTenancy + ? tenantConfigurationProvider.Get()?.TenantIdentifier + : null; + var jobData = new Dictionary + { + [JobConstants.OdsInstanceManageIdKey] = id + }; + + if (!string.IsNullOrWhiteSpace(tenantName)) + { + jobData[JobConstants.TenantNameKey] = tenantName; + } + + var scheduler = await schedulerFactory.GetScheduler(); + + try + { + await QuartzJobScheduler.ScheduleJob( + scheduler, + DeleteInstanceJob.CreateJobKey(id, tenantName), + jobData, + startImmediately: true); + } + catch (ObjectAlreadyExistsException) + { + // The DeletePendingOdsInstanceManagesDispatcherJob may have already scheduled this job. + // Treat duplicate scheduling as success — the job is already queued. + } + + return Results.NoContent(); + } + + private static string? GetBlockingStatusMessage(string status) + { + if (Enum.TryParse(status, ignoreCase: true, out var parsed)) + { + return parsed switch + { + OdsInstanceManageStatus.PendingCreate => "OdsInstanceManage is being provisioned. Wait for creation to complete.", + OdsInstanceManageStatus.CreateInProgress => "OdsInstanceManage is currently being provisioned. Wait for creation to complete.", + OdsInstanceManageStatus.CreateFailed => "OdsInstanceManage creation failed. It will be retried automatically by the background job.", + OdsInstanceManageStatus.CreateError => "OdsInstanceManage creation failed permanently. Manual database intervention required before deleting.", + OdsInstanceManageStatus.PendingDelete => "OdsInstanceManage is already queued for deletion.", + OdsInstanceManageStatus.DeleteInProgress => "OdsInstanceManage is currently being deleted.", + OdsInstanceManageStatus.DeleteFailed => "OdsInstanceManage deletion failed. It will be retried automatically by the background job.", + OdsInstanceManageStatus.DeleteError => "OdsInstanceManage deletion failed permanently. Manual database intervention required.", + _ => null, + }; + } + + return null; + } +} +``` + +- [ ] **Step 5: Replace `OdsInstanceManageModel.cs` content** + +```csharp +// 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 Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; + +[SwaggerSchema(Title = "OdsInstanceManage")] +public class OdsInstanceManageModel +{ + public int? Id { get; set; } + public string? Name { get; set; } + public int? OdsInstanceId { get; set; } + public string? OdsInstanceName { get; set; } + public string? Status { get; set; } + public string? DatabaseTemplate { get; set; } + public string? DatabaseName { get; set; } + public DateTime? LastRefreshed { get; set; } + public DateTime? LastModifiedDate { get; set; } +} +``` + +- [ ] **Step 6: Replace `OdsInstanceManageMapper.cs` content** + +```csharp +// 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.Infrastructure.Models; + +namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; + +public static class OdsInstanceManageMapper +{ + public static OdsInstanceManageModel ToModel(OdsInstanceManage source) + { + return new OdsInstanceManageModel + { + Id = source.Id, + Name = source.Name, + OdsInstanceId = source.OdsInstanceId, + OdsInstanceName = source.OdsInstanceName, + Status = source.Status, + DatabaseTemplate = source.DatabaseTemplate, + DatabaseName = source.DatabaseName, + LastRefreshed = source.LastRefreshed, + LastModifiedDate = source.LastModifiedDate, + }; + } + + public static List ToModelList(IEnumerable source) + { + return source.Select(ToModel).ToList(); + } +} +``` + +- [ ] **Step 7: Replace `OdsInstanceManageDatabaseNameFormatter.cs` content** + +```csharp +// 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.RegularExpressions; + +namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; + +internal static class OdsInstanceManageDatabaseNameFormatter +{ + private const string CanonicalPrefix = "EdFi_Ods"; + + // Use PostgreSQL's identifier limit as the portable ceiling so the persisted + // DatabaseName always matches the real provisioned database across engines. + internal const int MaxPortableDatabaseNameLength = 63; + + private static readonly Regex _leadingCanonicalPrefixPattern = new( + @"^(?:(?:edfi_+ods)(?:_+|$))+", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + internal static string Build(string instanceName, string databaseTemplate) + { + ArgumentException.ThrowIfNullOrWhiteSpace(instanceName); + ArgumentException.ThrowIfNullOrWhiteSpace(databaseTemplate); + + var normalizedName = NormalizeSegment(instanceName); + var normalizedDatabaseTemplate = NormalizeSegment(databaseTemplate); + var normalizedNameWithoutPrefix = _leadingCanonicalPrefixPattern.Replace(normalizedName, string.Empty).Trim('_'); + + return string.IsNullOrWhiteSpace(normalizedNameWithoutPrefix) + ? $"{CanonicalPrefix}_{normalizedDatabaseTemplate}" + : $"{CanonicalPrefix}_{normalizedNameWithoutPrefix}_{normalizedDatabaseTemplate}"; + } + + private static string NormalizeSegment(string value) + => value.Replace(' ', '_').Trim('_'); +} +``` + +- [ ] **Step 8: Build** + +Run: `dotnet build Application/EdFi.Ods.AdminApi.sln` +Expected: remaining errors confined to `Features\OdsInstances\OdsInstanceWithEducationOrganizationsModel.cs`/`ReadEducationOrganizations.cs` (Task 5) and `Program.cs`/`WebApplicationBuilderExtensions.cs` (Task 6). + +- [ ] **Step 9: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage" \ + "Application/EdFi.Ods.AdminApi/Features/DbInstances" +git commit -m "Move v2 DbInstances feature into OdsInstances/Manage, rename routes to /odsInstances/manage" +``` + +--- + +### Task 5: v2 existing `OdsInstances` files — update references to the renamed query/enum + +**Files:** +- Modify: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\OdsInstanceWithEducationOrganizationsModel.cs` +- Modify: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\ReadEducationOrganizations.cs` + +**Interfaces:** +- Consumes: `IGetOdsInstanceManagesQuery` (Task 3), `OdsInstanceManageStatus` (Task 2). +- Produces: `OdsInstanceWithEducationOrganizationsModel.OdsInstanceManageId` (renamed from `DbInstanceId`), consumed by nothing else in this plan (public API response shape only). + +- [ ] **Step 1: Rename the `DbInstanceId` property** + +In `OdsInstanceWithEducationOrganizationsModel.cs`, replace: + +```csharp + [SwaggerSchema(Description = "DbInstance identifier for this ODS instance")] + public int? DbInstanceId { get; set; } +``` + +with: + +```csharp + [SwaggerSchema(Description = "OdsInstanceManage identifier for this ODS instance")] + public int? OdsInstanceManageId { get; set; } +``` + +- [ ] **Step 2: Update `ReadEducationOrganizations.cs`** + +Replace the full file content with: + +```csharp +// 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.Constants; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using Microsoft.AspNetCore.Mvc; + +namespace EdFi.Ods.AdminApi.Features.OdsInstances; + +public class ReadEducationOrganizations : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapGet(endpoints, "/odsInstances/{instanceId}/edOrgs", GetEducationOrganizationsByInstance) + .WithSummaryAndDescription( + "Retrieves education organizations for a specific ODS instance", + "Returns all education organizations for the specified ODS instance in a nested structure" + ) + .WithRouteOptions(b => b.WithResponse>(200)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task GetEducationOrganizationsByInstance( + [FromServices] IGetEducationOrganizationsQuery getEducationOrganizationsQuery, + [FromServices] IGetOdsInstanceQuery getOdsInstanceQuery, + [FromServices] IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery, + [AsParameters] CommonQueryParams commonQueryParams, + int instanceId) + { + getOdsInstanceQuery.Execute(instanceId); + + var educationOrganizations = await getEducationOrganizationsQuery.ExecuteAsync( + commonQueryParams, + instanceId: instanceId); + + MergeOdsInstanceManageData(educationOrganizations, getOdsInstanceManagesQuery); + return Results.Ok(educationOrganizations); + } + + private static void MergeOdsInstanceManageData( + List instances, + IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery) + { + var allOdsInstanceManages = getOdsInstanceManagesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); + + var linkedById = allOdsInstanceManages + .Where(d => d.OdsInstanceId is not null) + .GroupBy(d => d.OdsInstanceId!.Value) + .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); + + foreach (var instance in instances) + { + if (instance.Id is int instanceId && linkedById.TryGetValue(instanceId, out var odsInstanceManage)) + { + instance.OdsInstanceManageId = odsInstanceManage.Id; + instance.Status = odsInstanceManage.Status; + instance.DatabaseTemplate = odsInstanceManage.DatabaseTemplate; + instance.DatabaseName = odsInstanceManage.DatabaseName; + } + else + { + instance.Status = OdsInstanceManageStatus.Created.ToString(); + } + } + } +} +``` + +- [ ] **Step 3: Build** + +Run: `dotnet build Application/EdFi.Ods.AdminApi.sln` +Expected: remaining errors confined to `Program.cs`/`WebApplicationBuilderExtensions.cs` (Task 6). + +- [ ] **Step 4: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi/Features/OdsInstances/OdsInstanceWithEducationOrganizationsModel.cs" \ + "Application/EdFi.Ods.AdminApi/Features/OdsInstances/ReadEducationOrganizations.cs" +git commit -m "Update v2 OdsInstances EdOrgs merge to use renamed OdsInstanceManage query" +``` + +--- + +### Task 6: v2 wiring — `Program.cs` and `WebApplicationBuilderExtensions.cs` + +**Files:** +- Modify: `Application\EdFi.Ods.AdminApi\Program.cs` +- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\WebApplicationBuilderExtensions.cs` + +**Interfaces:** +- Consumes: `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName`/`DeletePendingOdsInstanceManagesDispatcherJobName` (Task 2), `AppSettings.CreateOdsInstanceManagesSweepIntervalInMins`/`DeleteOdsInstanceManagesSweepIntervalInMins` (Task 2), `CreatePendingOdsInstanceManagesDispatcherJob`/`DeletePendingOdsInstanceManagesDispatcherJob` (Task 3). +- Produces: fully wired v2 Quartz scheduling — this is the last v2-side file needed for the solution to build; Task 7's build checkpoint depends on this completing cleanly. + +- [ ] **Step 1: Update config key lookups in `Program.cs`** + +Replace: + +```csharp +var createDbInstancesSweepIntervalInMins = app.Configuration.GetValue( + "AppSettings:CreateDbInstancesSweepIntervalInMins" +); +var deleteDbInstancesSweepIntervalInMins = app.Configuration.GetValue( + "AppSettings:DeleteDbInstancesSweepIntervalInMins" +); +``` + +with: + +```csharp +var createOdsInstanceManagesSweepIntervalInMins = app.Configuration.GetValue( + "AppSettings:CreateOdsInstanceManagesSweepIntervalInMins" +); +var deleteOdsInstanceManagesSweepIntervalInMins = app.Configuration.GetValue( + "AppSettings:DeleteOdsInstanceManagesSweepIntervalInMins" +); +``` + +- [ ] **Step 2: Rename every local variable derived from those two lines, in both the `AdminApiMode.V2` and `AdminApiMode.V3` branches** + +Throughout both branches, rename: +- `createDbInstancesSweepIntervalInMins` → `createOdsInstanceManagesSweepIntervalInMins` +- `deleteDbInstancesSweepIntervalInMins` → `deleteOdsInstanceManagesSweepIntervalInMins` +- `createDbInstancesSweepInterval` → `createOdsInstanceManagesSweepInterval` +- `deleteDbInstancesSweepInterval` → `deleteOdsInstanceManagesSweepInterval` + +(these are `double.TryParse(..., out var createDbInstancesSweepInterval)`-style declarations and their later `TimeSpan.FromMinutes(...)` usages — rename the declaration and every usage site.) + +- [ ] **Step 3: Update `JobKey`/job-type references in the `AdminApiMode.V2` branch** + +Replace every occurrence in the V2 branch of: + +```csharp +await QuartzJobScheduler.ScheduleJob( + scheduler, + jobKey: new JobKey($"{JobConstants.CreatePendingDbInstancesDispatcherJobName}_{tenantName}"), +``` + +and the non-multi-tenant equivalent + +```csharp +await QuartzJobScheduler.ScheduleJob( + scheduler, + jobKey: new JobKey(JobConstants.CreatePendingDbInstancesDispatcherJobName), +``` + +with the `CreatePendingOdsInstanceManagesDispatcherJob` class and `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName` constant (both multi-tenant and non-multi-tenant branches). Do the same for the `Delete...` pair (`DeletePendingDbInstancesDispatcherJob` → `DeletePendingOdsInstanceManagesDispatcherJob`, `JobConstants.DeletePendingDbInstancesDispatcherJobName` → `JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName`). + +Also update the two `_logger.Error(...)` messages: + +```csharp +_logger.Error("Invalid value for CreateDbInstancesSweepIntervalInMins. Please ensure it is a valid number."); +... +_logger.Error("Invalid value for DeleteDbInstancesSweepIntervalInMins. Please ensure it is a valid number."); +``` + +to: + +```csharp +_logger.Error("Invalid value for CreateOdsInstanceManagesSweepIntervalInMins. Please ensure it is a valid number."); +... +_logger.Error("Invalid value for DeleteOdsInstanceManagesSweepIntervalInMins. Please ensure it is a valid number."); +``` + +(these log messages appear once in the V2 branch and once in the V3 branch — update both; Task 13 covers the V3-branch job-type swap to `V3Jobs.CreatePendingDataStoreManagesDispatcherJob`/`DeletePendingDataStoreManagesDispatcherJob`, but the log message text and interval variable renames happen here since they're shared local variables computed once before the `if/else if` branches.) + +- [ ] **Step 4: Update `WebApplicationBuilderExtensions.cs` DI registrations for the v2 (`else`) branch** + +Find the `else` branch of `RegisterQuartzServices` (the non-V3 branch, currently registering `CreateInstanceJob`, `CreatePendingDbInstancesDispatcherJob`, `RefreshEducationOrganizationsJob`, etc. — grep for `webApplicationBuilder.Services.AddTransient();`) and replace: + +```csharp + webApplicationBuilder.Services.AddTransient(); +``` + +with: + +```csharp + webApplicationBuilder.Services.AddTransient(); +``` + +Also find and rename the corresponding `DeletePendingDbInstancesDispatcherJob` registration (grep the same file for it — it sits near the `Create...` registration in the same `else` block) to `DeletePendingOdsInstanceManagesDispatcherJob`. + +- [ ] **Step 5: Build the full solution** + +Run: `dotnet build Application/EdFi.Ods.AdminApi.sln` +Expected: v2 side (everything except v3's `EdFi.Ods.AdminApi.V3` project) now compiles cleanly. Remaining errors should be entirely inside `Application\EdFi.Ods.AdminApi.V3\` (fixed by Tasks 10–13) and its `.UnitTests`/`.DBTests` counterparts (Tasks 14–15) and `Application\EdFi.Ods.AdminApi.UnitTests`/`.DBTests` (Tasks 7–8, not yet done). + +- [ ] **Step 6: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi/Program.cs" \ + "Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs" +git commit -m "Update v2 Quartz wiring to use renamed OdsInstanceManage jobs and config keys" +``` + +--- + +### Task 7: v2 Unit tests rename (`EdFi.Ods.AdminApi.UnitTests`) + +**Files:** +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Features\DbInstances\AddDbInstanceTests.cs` → move+rename to `Application\EdFi.Ods.AdminApi.UnitTests\Features\OdsInstances\Manage\AddOdsInstanceManageTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Features\DbInstances\ReadDbInstanceTests.cs` → move+rename to `Application\EdFi.Ods.AdminApi.UnitTests\Features\OdsInstances\Manage\ReadOdsInstanceManageTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Features\DbInstances\DeleteDbInstanceTests.cs` → move+rename to `Application\EdFi.Ods.AdminApi.UnitTests\Features\OdsInstances\Manage\DeleteOdsInstanceManageTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Database\Commands\AddDbInstanceCommandTests.cs` → rename to `AddOdsInstanceManageCommandTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Database\Commands\DeleteDbInstanceCommandTests.cs` → rename to `DeleteOdsInstanceManageCommandTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Database\Queries\GetDbInstancesQueryTests.cs` → rename to `GetOdsInstanceManagesQueryTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Database\Queries\GetDbInstanceByIdQueryTests.cs` → rename to `GetOdsInstanceManageByIdQueryTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Jobs\CreatePendingDbInstancesDispatcherJobTests.cs` → rename to `CreatePendingOdsInstanceManagesDispatcherJobTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Jobs\DeletePendingDbInstancesDispatcherJobTests.cs` → rename to `DeletePendingOdsInstanceManagesDispatcherJobTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Jobs\CreateInstanceJobTests.cs` (renamed in place, filename unchanged) +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Jobs\DeleteInstanceJobTests.cs` (renamed in place, filename unchanged) + +**Interfaces:** +- Consumes: every production type from Tasks 2–6 (`OdsInstanceManage`, `OdsInstanceManageStatus`, `IGetOdsInstanceManagesQuery`, `AddOdsInstanceManageCommand`, `AddOdsInstanceManage`/`ReadOdsInstanceManage`/`DeleteOdsInstanceManage` features, `CreatePendingOdsInstanceManagesDispatcherJob`, etc.). + +- [ ] **Step 1: Move and rename the query test files (full content known — apply directly)** + +```bash +git mv "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetDbInstancesQueryTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManagesQueryTests.cs" +git mv "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetDbInstanceByIdQueryTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQueryTests.cs" +``` + +`GetOdsInstanceManagesQueryTests.cs`: + +```csharp +// 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. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Database.Queries; + +[TestFixture] +public class GetOdsInstanceManagesQueryTests +{ + private static AdminApiDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: $"GetOdsInstanceManagesQueryTests_{Guid.NewGuid()}") + .Options; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AppSettings:DatabaseEngine"] = "Postgres" + }) + .Build(); + + return new AdminApiDbContext(options, configuration); + } + + private static IOptions DefaultOptions() => + Options.Create(new AppSettings { DatabaseEngine = "Postgres", DefaultPageSizeLimit = 25 }); + + [Test] + public void Execute_WithoutFilters_ReturnsAllOdsInstanceManages() + { + using var context = CreateContext(); + context.OdsInstanceManages.AddRange( + new OdsInstanceManage { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, + new OdsInstanceManage { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); + context.SaveChanges(); + + var query = new GetOdsInstanceManagesQuery(context, DefaultOptions()); + + var result = query.Execute(new CommonQueryParams(0, 25), null, null); + + result.Count.ShouldBe(2); + result.Select(x => x.Name).ShouldBe(["Sandbox A", "Sandbox B"], ignoreOrder: true); + } + + [Test] + public void Execute_WithNameFilter_ReturnsMatchingOdsInstanceManage() + { + using var context = CreateContext(); + context.OdsInstanceManages.AddRange( + new OdsInstanceManage { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, + new OdsInstanceManage { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); + context.SaveChanges(); + + var query = new GetOdsInstanceManagesQuery(context, DefaultOptions()); + + var result = query.Execute(new CommonQueryParams(0, 25), null, "Sandbox B"); + + result.Count.ShouldBe(1); + result.Single().Name.ShouldBe("Sandbox B"); + } +} +``` + +`GetOdsInstanceManageByIdQueryTests.cs`: + +```csharp +// 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. + +#nullable enable + +using System; +using System.Collections.Generic; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; +using EdFi.Ods.AdminApi.Infrastructure; +using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Database.Queries; + +[TestFixture] +public class GetOdsInstanceManageByIdQueryTests +{ + private static AdminApiDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: $"GetOdsInstanceManageByIdQueryTests_{Guid.NewGuid()}") + .Options; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AppSettings:DatabaseEngine"] = "Postgres" + }) + .Build(); + + return new AdminApiDbContext(options, configuration); + } + + [Test] + public void Execute_WithExistingId_ReturnsOdsInstanceManage() + { + using var context = CreateContext(); + var odsInstanceManage = new OdsInstanceManage + { + Name = "Sandbox", + Status = "Healthy", + DatabaseTemplate = "Minimal" + }; + context.OdsInstanceManages.Add(odsInstanceManage); + context.SaveChanges(); + + var query = new GetOdsInstanceManageByIdQuery(context); + + var result = query.Execute(odsInstanceManage.Id); + + result.ShouldNotBeNull(); + result.Name.ShouldBe("Sandbox"); + } + + [Test] + public void Execute_WithUnknownId_ReturnsNull() + { + using var context = CreateContext(); + var query = new GetOdsInstanceManageByIdQuery(context); + + var result = query.Execute(999); + + result.ShouldBeNull(); + } +} +``` + +- [ ] **Step 2: Move and rename the remaining v2 unit test files, applying the Master Rename Table** + +```bash +git mv "Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/AddDbInstanceTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/AddOdsInstanceManageTests.cs" +git mv "Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/ReadDbInstanceTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/ReadOdsInstanceManageTests.cs" +git mv "Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/DeleteDbInstanceTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/DeleteOdsInstanceManageTests.cs" +git mv "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddDbInstanceCommandTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddOdsInstanceManageCommandTests.cs" +git mv "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteDbInstanceCommandTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommandTests.cs" +git mv "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJobTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJobTests.cs" +git mv "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJobTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJobTests.cs" +``` + +For each moved file and for the two in-place files (`CreateInstanceJobTests.cs`, `DeleteInstanceJobTests.cs`), open it and apply every substitution from the "v2-specific" and "Shared" Master Rename Table sections above (class name matching the new filename, `namespace ...UnitTests.Features.DbInstances` → `...UnitTests.Features.OdsInstances.Manage`, every `DbInstance`/`OdsInstance...`-adjacent identifier, string literal route fragments like `"/dbInstances"` → `"/odsInstances/manage"` if any test asserts on the route string, and any `[TestFixture] public class AddDbInstanceTests` → `AddOdsInstanceManageTests`). + +- [ ] **Step 3: Run the v2 unit test suite** + +Run: `dotnet test Application/EdFi.Ods.AdminApi.UnitTests/EdFi.Ods.AdminApi.UnitTests.csproj` +Expected: PASS, same test count as before the rename (compare `git stash`'s pre-change `dotnet test` output count if unsure, or just confirm no failures/errors and no tests silently vanished — count should match the pre-rename baseline exactly since this task renames tests, not delete/add them). + +- [ ] **Step 4: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage" \ + "Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances" \ + "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddOdsInstanceManageCommandTests.cs" \ + "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommandTests.cs" \ + "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManagesQueryTests.cs" \ + "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQueryTests.cs" \ + "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJobTests.cs" \ + "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJobTests.cs" \ + "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreateInstanceJobTests.cs" \ + "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeleteInstanceJobTests.cs" +git commit -m "Rename v2 unit tests to OdsInstanceManage naming" +``` + +--- + +### Task 8: v2 DBTests rename (`EdFi.Ods.AdminApi.DBTests`) + +**Files:** +- Modify: `Application\EdFi.Ods.AdminApi.DBTests\Database\CommandTests\AddDbInstanceCommandTests.cs` → rename to `AddOdsInstanceManageCommandTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.DBTests\Database\CommandTests\DeleteDbInstanceCommandTests.cs` → rename to `DeleteOdsInstanceManageCommandTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.DBTests\Database\QueryTests\GetDbInstanceByIdQueryTests.cs` → rename to `GetOdsInstanceManageByIdQueryTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.DBTests\Database\QueryTests\GetDbInstancesQueryTests.cs` → rename to `GetOdsInstanceManagesQueryTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.DBTests\...\GetTenantEdOrgsByInstancesTests.cs` (filename unchanged — update internal references only) + +**Interfaces:** +- Consumes: same production types as Task 7, against a real database (this project is this repo's integration-test layer). + +- [ ] **Step 1: Move and rename** + +```bash +git mv "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddDbInstanceCommandTests.cs" "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceManageCommandTests.cs" +git mv "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteDbInstanceCommandTests.cs" "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceManageCommandTests.cs" +git mv "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetDbInstanceByIdQueryTests.cs" "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManageByIdQueryTests.cs" +git mv "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetDbInstancesQueryTests.cs" "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManagesQueryTests.cs" +``` + +Apply the Master Rename Table to each moved file's content (class name, `AdminApiDbContext.DbInstances`→`.OdsInstanceManages`, `DbInstance`/`DbInstanceStatus`→`OdsInstanceManage`/`OdsInstanceManageStatus`, command/query type names). In `GetTenantEdOrgsByInstancesTests.cs` (filename unchanged, locate it first with a search since its exact path wasn't confirmed during research — search `Application\EdFi.Ods.AdminApi.DBTests` for `GetTenantEdOrgsByInstancesTests`), update only the internal references to renamed types (`IGetOdsInstanceManagesQuery`, `OdsInstanceManage`, etc.), not the filename or class name. + +- [ ] **Step 2: Run the v2 DB test suite** + +Run: `dotnet test Application/EdFi.Ods.AdminApi.DBTests/EdFi.Ods.AdminApi.DBTests.csproj` (requires a local test database per `docs/developer.md` DB migration instructions — apply Task 1's migration first). +Expected: PASS, same test count as the pre-rename baseline. + +- [ ] **Step 3: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceManageCommandTests.cs" \ + "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceManageCommandTests.cs" \ + "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManageByIdQueryTests.cs" \ + "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManagesQueryTests.cs" \ + "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddDbInstanceCommandTests.cs" \ + "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteDbInstanceCommandTests.cs" \ + "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetDbInstanceByIdQueryTests.cs" \ + "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetDbInstancesQueryTests.cs" +git commit -m "Rename v2 DBTests to OdsInstanceManage naming" +``` + +--- + +### Task 9: v2 Bruno E2E collection rename + +**Files:** +- Modify: `Application\EdFi.Ods.AdminApi\E2E Tests\V2\Bruno Admin API E2E 2.0 refactor\v2\DbInstances\` → move all files to `...\v2\OdsInstances\Manage\` + +**Interfaces:** +- Consumes: routes `/v2/odsInstances/manage*` (Task 4). + +- [ ] **Step 1: Move the folder and rename every `.bru` file** + +```bash +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/folder.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/folder.bru" +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage.bru" +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Sample Template.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Sample Template.bru" +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Invalid.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Invalid.bru" +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Invalid Database Template.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Invalid Database Template.bru" +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage.bru" +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances by ID.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage by ID.bru" +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Offset.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Offset.bru" +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Limit.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Limit.bru" +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Limit and Offset.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Limit and Offset.bru" +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Not Found.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Not Found.bru" +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Filter by Name.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Filter by Name.bru" +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/DELETE - DbInstance - Success.bru.disabled" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/DELETE - OdsInstance Manage - Success.bru.disabled" +git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/DELETE - DbInstance - Not Found.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/DELETE - OdsInstance Manage - Not Found.bru" +``` + +- [ ] **Step 2: Update `folder.bru`** + +``` +meta { + name: Manage + seq: 99 +} + +auth { + mode: inherit +} +``` + +- [ ] **Step 3: Update every moved `.bru` file's content** + +For each file: change `meta { name: DbInstances ... }` → `meta { name: OdsInstances Manage ... }` (or the per-file variant, e.g. `DbInstance` singular in the DELETE files → `OdsInstance Manage`), change the request URL from `{{API_URL}}/v2/dbinstances...` to `{{API_URL}}/v2/odsinstances/manage...` (preserve any `/{id}` suffix or query string), and rename any `bru.setVar("CreatedDbInstanceId", ...)` / `{{CreatedDbInstanceId}}` variable references to `CreatedOdsInstanceManageId`, and update `test("POST DbInstances: ...")`-style assertion description strings to say `OdsInstances Manage` instead of `DbInstances`. + +For example, `POST - OdsInstances Manage.bru`: + +``` +meta { + name: OdsInstances Manage + type: http + seq: 1 +} + +post { + url: {{API_URL}}/v2/odsinstances/manage + body: json + auth: inherit +} + +body:json { + { + "name": "Test DB Instance", + "databaseTemplate": "Minimal" + } +} + +script:post-response { + test("POST OdsInstances Manage: Status code is Accepted", function () { + expect(res.getStatus()).to.equal(202); + }); + + test("POST OdsInstances Manage: Response includes location in header", function () { + expect(res.getHeaders()).to.have.property("location"); + const id = res.getHeader("location").split("/")[2]; + if (id) { + bru.setVar("CreatedOdsInstanceManageId", id); + } + }); +} + +settings { + encodeUrl: true +} +``` + +Apply the same URL/variable/assertion-text substitution pattern to the remaining 11 files (`GET`, `DELETE`, invalid-input variants) based on each file's current content. + +- [ ] **Step 4: Run the v2 Bruno E2E suite** + +Run: `./eng/run-e2e-bruno.ps1 -ApiVersion 2 -TenantMode singletenant -TearDown` (adjust `-TenantMode` to match this repo's default if different — check `docs/developer.md` for the exact invocation). +Expected: PASS, all `OdsInstances Manage` requests succeed with the same assertions as before the rename. + +- [ ] **Step 5: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage" \ + "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances" +git commit -m "Rename v2 Bruno E2E DbInstances collection to OdsInstances/Manage" +``` + +--- + +### Task 10: v3 Infrastructure layer rename (Queries, Commands, Jobs) + +**Files:** +- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Database\Queries\GetDbDataStoresQuery.cs` → rename to `GetDataStoreManagesQuery.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Database\Queries\GetDbDataStoreByIdQuery.cs` → rename to `GetDataStoreManageByIdQuery.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Database\Commands\AddDbDataStoreCommand.cs` → rename to `AddDataStoreManageCommand.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Database\Commands\DeleteDbDataStoreCommand.cs` → rename to `DeleteDataStoreManageCommand.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Services\Jobs\CreatePendingDbInstancesDispatcherJob.cs` → rename to `CreatePendingDataStoreManagesDispatcherJob.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Services\Jobs\DeletePendingDbInstancesDispatcherJob.cs` → rename to `DeletePendingDataStoreManagesDispatcherJob.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Services\Jobs\CreateInstanceJob.cs` (renamed in place, filename unchanged) +- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Services\Jobs\DeleteInstanceJob.cs` (renamed in place, filename unchanged) + +**Interfaces:** +- Consumes: `OdsInstanceManage` entity, `OdsInstanceManageStatus` enum, `JobConstants.OdsInstanceManageIdKey` (Task 2 — shared, so v3 also uses the `OdsInstanceManage`-prefixed shared names even though its own feature-layer naming is `DataStoreManage`), `AppSettings.CreateOdsInstanceManagesMaxRetryAttempts`/`DeleteOdsInstanceManagesMaxRetryAttempts` (Task 2), `AdminApiDbContext.OdsInstanceManages` (Task 2, V3's own DbContext). +- Produces: `IGetDataStoreManagesQuery`/`GetDataStoreManagesQuery`, `IGetDataStoreManageByIdQuery`/`GetDataStoreManageByIdQuery`, `AddDataStoreManageCommand`/`IAddDataStoreManageModel`, `IDeleteDataStoreManageCommand`/`DeleteDataStoreManageCommand`, `CreatePendingDataStoreManagesDispatcherJob`, `DeletePendingDataStoreManagesDispatcherJob` — consumed by Task 11 (feature files) and Task 13 (wiring). + +- [ ] **Step 1: Rename and update the Queries** + +```bash +git mv "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDbDataStoresQuery.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManagesQuery.cs" +git mv "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDbDataStoreByIdQuery.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManageByIdQuery.cs" +``` + +`GetDataStoreManagesQuery.cs`: + +```csharp +// 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.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.V3.Infrastructure.Extensions; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; + +public interface IGetDataStoreManagesQuery +{ + List Execute(CommonQueryParams commonQueryParams, int? id, string? name); +} + +public class GetDataStoreManagesQuery : IGetDataStoreManagesQuery +{ + private readonly AdminApiDbContext _context; + private readonly IOptions _options; + + public GetDataStoreManagesQuery(AdminApiDbContext context, IOptions options) + { + _context = context; + _options = options; + } + + public List Execute(CommonQueryParams commonQueryParams, int? id, string? name) + { + return _context.OdsInstanceManages + .Where(d => id == null || d.Id == id) + .Where(d => name == null || d.Name == name) + .OrderBy(d => d.Id) + .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) + .ToList(); + } +} +``` + +`GetDataStoreManageByIdQuery.cs`: + +```csharp +// 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.Infrastructure.Models; + +namespace EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; + +public interface IGetDataStoreManageByIdQuery +{ + OdsInstanceManage? Execute(int id); +} + +public class GetDataStoreManageByIdQuery : IGetDataStoreManageByIdQuery +{ + private readonly AdminApiDbContext _context; + + public GetDataStoreManageByIdQuery(AdminApiDbContext context) + { + _context = context; + } + + public OdsInstanceManage? Execute(int id) + { + return _context.OdsInstanceManages.SingleOrDefault(d => d.Id == id); + } +} +``` + +- [ ] **Step 2: Rename and update the Commands** + +```bash +git mv "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/AddDbDataStoreCommand.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/AddDataStoreManageCommand.cs" +git mv "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/DeleteDbDataStoreCommand.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/DeleteDataStoreManageCommand.cs" +``` + +`AddDataStoreManageCommand.cs`: + +```csharp +// 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.Constants; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; + +namespace EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; + +public class AddDataStoreManageCommand +{ + private readonly AdminApiDbContext _context; + + public AddDataStoreManageCommand(AdminApiDbContext context) + { + _context = context; + } + + public OdsInstanceManage Execute(IAddDataStoreManageModel model) + { + if (string.IsNullOrWhiteSpace(model.Name)) + throw new ArgumentException("Name is required.", nameof(model)); + if (string.IsNullOrWhiteSpace(model.DatabaseTemplate)) + throw new ArgumentException("DatabaseTemplate is required.", nameof(model)); + + var now = DateTime.UtcNow; + + var odsInstanceManage = new OdsInstanceManage + { + Name = model.Name.Trim(), + DatabaseTemplate = model.DatabaseTemplate.Trim(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), + OdsInstanceId = null, + OdsInstanceName = null, + DatabaseName = null, + LastRefreshed = now, + LastModifiedDate = now + }; + + _context.OdsInstanceManages.Add(odsInstanceManage); + _context.SaveChanges(); + return odsInstanceManage; + } +} + +public interface IAddDataStoreManageModel +{ + string? Name { get; } + string? DatabaseTemplate { get; } +} +``` + +`DeleteDataStoreManageCommand.cs`: + +```csharp +// 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.Constants; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; + +public interface IDeleteDataStoreManageCommand +{ + void Execute(int id); +} + +public class DeleteDataStoreManageCommand : IDeleteDataStoreManageCommand +{ + private readonly AdminApiDbContext _context; + + public DeleteDataStoreManageCommand(AdminApiDbContext context) + { + _context = context; + } + + public void Execute(int id) + { + var odsInstanceManage = + _context.OdsInstanceManages.Find(id) + ?? throw new NotFoundException("dataStoreManage", id); + + if (odsInstanceManage.Status == OdsInstanceManageStatus.Deleted.ToString()) + throw new NotFoundException("dataStoreManage", id); + + odsInstanceManage.Status = OdsInstanceManageStatus.PendingDelete.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + + _context.SaveChanges(); + } +} +``` + +- [ ] **Step 3: Rename and update the dispatcher Jobs** + +```bash +git mv "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJob.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreatePendingDataStoreManagesDispatcherJob.cs" +git mv "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJob.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeletePendingDataStoreManagesDispatcherJob.cs" +``` + +`CreatePendingDataStoreManagesDispatcherJob.cs` (same transformation pattern as v2's `CreatePendingOdsInstanceManagesDispatcherJob.cs` from Task 3 — class renamed to `CreatePendingDataStoreManagesDispatcherJob`, everything else identical since v3's dispatcher logic was byte-identical to v2's): + +```csharp +// 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.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Constants; +using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.V3.Infrastructure.Services.Tenants; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using Quartz; + +namespace EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs; + +[DisallowConcurrentExecution] +public class CreatePendingDataStoreManagesDispatcherJob( + ILogger logger, + IJobStatusService jobStatusService, + AdminApiDbContext dbContext, + ITenantSpecificDbContextProvider tenantSpecificDbContextProvider, + IOptions options) + : AdminApiQuartzJobBase(logger, jobStatusService) +{ + private const int DefaultMaxRetryAttempts = 3; + + private readonly AdminApiDbContext _dbContext = dbContext; + private readonly ITenantSpecificDbContextProvider _tenantSpecificDbContextProvider = tenantSpecificDbContextProvider; + private readonly IOptions _options = options; + + protected override async Task ExecuteJobAsync(IJobExecutionContext context) + { + var multiTenancyEnabled = _options.Value.MultiTenancy; + var tenantName = GetTenantName(context, multiTenancyEnabled); + AdminApiDbContext? tenantAdminApiDbContext = null; + var adminApiDbContext = _dbContext; + + try + { + if (multiTenancyEnabled) + { + tenantAdminApiDbContext = _tenantSpecificDbContextProvider.GetAdminApiDbContext(tenantName!); + adminApiDbContext = tenantAdminApiDbContext; + } + + var eligibleOdsInstanceManages = await adminApiDbContext.OdsInstanceManages + .Where(instance => instance.Status == OdsInstanceManageStatus.PendingCreate.ToString() || instance.Status == OdsInstanceManageStatus.CreateFailed.ToString()) + .OrderBy(instance => instance.Id) + .ToListAsync(); + + foreach (var odsInstanceManage in eligibleOdsInstanceManages) + { + if (string.Equals(odsInstanceManage.Status, OdsInstanceManageStatus.PendingCreate.ToString(), StringComparison.OrdinalIgnoreCase)) + { + await ScheduleCreateJobAsync(context, odsInstanceManage.Id, tenantName); + continue; + } + + if (!await IsRetryEligibleAsync(adminApiDbContext, odsInstanceManage, tenantName)) + { + odsInstanceManage.Status = OdsInstanceManageStatus.CreateError.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; + await adminApiDbContext.SaveChangesAsync(); + continue; + } + + odsInstanceManage.Status = OdsInstanceManageStatus.PendingCreate.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; + await adminApiDbContext.SaveChangesAsync(); + + await ScheduleCreateJobAsync(context, odsInstanceManage.Id, tenantName); + } + } + finally + { + if (tenantAdminApiDbContext is not null) + { + await tenantAdminApiDbContext.DisposeAsync(); + } + } + } + + private async Task IsRetryEligibleAsync(AdminApiDbContext adminApiDbContext, OdsInstanceManage odsInstanceManage, string? tenantName) + { + var maxRetryAttempts = _options.Value.CreateOdsInstanceManagesMaxRetryAttempts > 0 + ? _options.Value.CreateOdsInstanceManagesMaxRetryAttempts + : DefaultMaxRetryAttempts; + + var jobIdPrefix = $"{CreateInstanceJob.BuildJobIdentity(odsInstanceManage.Id, tenantName)}_"; + var errorCount = await adminApiDbContext.JobStatuses + .CountAsync(status => status.JobId.StartsWith(jobIdPrefix) && status.Status == QuartzJobStatus.Error.ToString()); + + return errorCount < maxRetryAttempts; + } + + private static async Task ScheduleCreateJobAsync(IJobExecutionContext context, int odsInstanceManageId, string? tenantName) + { + var jobData = new Dictionary + { + [JobConstants.OdsInstanceManageIdKey] = odsInstanceManageId + }; + + if (!string.IsNullOrWhiteSpace(tenantName)) + { + jobData[JobConstants.TenantNameKey] = tenantName; + } + + await QuartzJobScheduler.ScheduleJob( + context.Scheduler, + CreateInstanceJob.CreateJobKey(odsInstanceManageId, tenantName), + jobData, + startImmediately: true); + } + + private static string? GetTenantName(IJobExecutionContext context, bool multiTenancyEnabled) + { + if (!multiTenancyEnabled) + { + return null; + } + + var tenantName = context.MergedJobDataMap.ContainsKey(JobConstants.TenantNameKey) + ? context.MergedJobDataMap.GetString(JobConstants.TenantNameKey) + : null; + + if (string.IsNullOrWhiteSpace(tenantName)) + { + throw new InvalidOperationException( + $"{JobConstants.TenantNameKey} must be provided when multi-tenancy is enabled."); + } + + return tenantName; + } +} +``` + +`DeletePendingDataStoreManagesDispatcherJob.cs` — apply the identical transformation (mirroring v2's `DeletePendingOdsInstanceManagesDispatcherJob.cs` from Task 3, renamed to `DeletePendingDataStoreManagesDispatcherJob`, using `DeleteOdsInstanceManagesMaxRetryAttempts` and calling `DeleteInstanceJob`). + +- [ ] **Step 4: Update `CreateInstanceJob.cs` and `DeleteInstanceJob.cs` in place (v3 copies — filenames and class names unchanged, only internal references)** + +Apply the same substitutions as Task 3 Step 4, but for the v3 files: replace `using EdFi.Ods.AdminApi.V3.Features.DbDataStores;` with `using EdFi.Ods.AdminApi.V3.Features.DataStores.Manage;` (namespace of `DataStoreManageDatabaseNameFormatter`, produced by Task 11), rename `DbInstance`/`dbInstance` → `OdsInstanceManage`/`odsInstanceManage`, `adminApiDbContext.DbInstances` → `.OdsInstanceManages`, `DbInstanceStatus` → `OdsInstanceManageStatus`, `JobConstants.DbInstanceIdKey` → `JobConstants.OdsInstanceManageIdKey`, and (in `CreateInstanceJob.cs` only) `DbDataStoreDatabaseNameFormatter` → `DataStoreManageDatabaseNameFormatter`. + +- [ ] **Step 5: Build to confirm the v3 Infrastructure layer compiles in isolation** + +Run: `dotnet build Application/EdFi.Ods.AdminApi.V3.sln` (or the appropriate project reference — confirm the v3 project's build command from `docs/developer.md`). +Expected: remaining errors confined to `Features\DbDataStores\*` (Task 11), `Features\DataStores\*` (Task 12), and `Program.cs`/`WebApplicationBuilderExtensions.cs` (Task 13, shared file already partially updated in Task 6). + +- [ ] **Step 6: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManagesQuery.cs" \ + "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManageByIdQuery.cs" \ + "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/AddDataStoreManageCommand.cs" \ + "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/DeleteDataStoreManageCommand.cs" \ + "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreatePendingDataStoreManagesDispatcherJob.cs" \ + "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeletePendingDataStoreManagesDispatcherJob.cs" \ + "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreateInstanceJob.cs" \ + "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeleteInstanceJob.cs" +git commit -m "Rename v3 Infrastructure Queries/Commands/Jobs to DataStoreManage naming" +``` + +--- + +### Task 11: v3 Feature folder move — `Features\DbDataStores` → `Features\DataStores\Manage` + +**Files:** +- Create: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\Manage\AddDataStoreManage.cs` +- Create: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\Manage\ReadDataStoreManage.cs` +- Create: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\Manage\DeleteDataStoreManage.cs` +- Create: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\Manage\DataStoreManageModel.cs` +- Create: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\Manage\DataStoreManageMapper.cs` +- Create: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\Manage\DataStoreManageDatabaseNameFormatter.cs` +- Delete: `Application\EdFi.Ods.AdminApi.V3\Features\DbDataStores\` (entire folder, all 6 old files) + +**Interfaces:** +- Consumes: `AddDataStoreManageCommand`/`IAddDataStoreManageModel`, `IGetDataStoreManagesQuery`/`IGetDataStoreManageByIdQuery`, `IDeleteDataStoreManageCommand` (Task 10), `OdsInstanceManage`/`OdsInstanceManageStatus` (Task 2), `JobConstants.OdsInstanceManageIdKey` (Task 2), `CreateInstanceJob`/`DeleteInstanceJob` (Task 10, unchanged names). +- Produces: routes `POST/GET/DELETE /dataStores/manage` under `EdFi.Ods.AdminApi.V3.Features.DataStores.Manage`, consumed by Task 16 (Bruno E2E) and Task 17 (`.http` file). + +- [ ] **Step 1: Move the folder with git** + +```bash +git mv "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/AddDbDataStore.cs" "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/AddDataStoreManage.cs" +git mv "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/ReadDbDataStore.cs" "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/ReadDataStoreManage.cs" +git mv "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DeleteDbDataStore.cs" "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DeleteDataStoreManage.cs" +git mv "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreModel.cs" "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageModel.cs" +git mv "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreMapper.cs" "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageMapper.cs" +git mv "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreDatabaseNameFormatter.cs" "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageDatabaseNameFormatter.cs" +``` + +- [ ] **Step 2: Replace `AddDataStoreManage.cs` content** + +```csharp +// 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.RegularExpressions; + +using EdFi.Admin.DataAccess.Contexts; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Context; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; +using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Common.Constants; +using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs; +using FluentValidation; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Quartz; +using Swashbuckle.AspNetCore.Annotations; +using EdFi.Ods.AdminApi.V3.Infrastructure; + +namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; + +public class AddDataStoreManage : IFeature +{ + private const int MaxSynchronizedNameLength = 100; + private const int MaxDataStoreManageNameLength = MaxSynchronizedNameLength; + private static readonly Regex _validDataStoreManageNamePattern = new( + "^[A-Za-z0-9 _]+$", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapPost(endpoints, "/dataStores/manage", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(202)) + .BuildForVersions(AdminApiVersions.V3); + } + + public async static Task Handle( + Validator validator, + AddDataStoreManageCommand addDataStoreManageCommand, + [FromServices] ISchedulerFactory schedulerFactory, + [FromServices] IContextProvider tenantConfigurationProvider, + [FromServices] IOptions options, + AddDataStoreManageRequest request, + HttpContext httpContext) + { + await validator.GuardAsync(request); + + var added = addDataStoreManageCommand.Execute(request); + + var tenantIdentifier = options.Value.MultiTenancy + ? tenantConfigurationProvider.Get()?.TenantIdentifier + : null; + + var jobBuilder = JobBuilder.Create() + .WithIdentity(CreateInstanceJob.CreateJobKey(added.Id, tenantIdentifier)) + .UsingJobData(JobConstants.OdsInstanceManageIdKey, added.Id); + + if (!string.IsNullOrWhiteSpace(tenantIdentifier)) + { + jobBuilder = jobBuilder.UsingJobData(JobConstants.TenantNameKey, tenantIdentifier); + } + + var trigger = TriggerBuilder.Create() + .StartNow() + .Build(); + + var scheduler = await schedulerFactory.GetScheduler(); + + try + { + await scheduler.ScheduleJob(jobBuilder.Build(), trigger); + } + catch (ObjectAlreadyExistsException) + { + // The CreatePendingDataStoreManagesDispatcherJob may have already scheduled this job + // (e.g. it fired between the DB insert and this ScheduleJob call). Treat duplicate + // scheduling as success — the job is already queued and will process the OdsInstanceManage. + } + + var absoluteLocation = ResourceUrlHelper.BuildAbsoluteResourceUrl(httpContext, AdminApiMode.V3, $"/dataStores/manage/{added.Id}"); + return Results.Accepted(absoluteLocation, null); + } + + [SwaggerSchema(Title = "AddDataStoreManageRequest")] + public class AddDataStoreManageRequest : IAddDataStoreManageModel + { + [SwaggerSchema(Description = "Name of the DataStore database", Nullable = false)] + public string? Name { get; set; } + + [SwaggerSchema(Description = "Database template to use for the DataStore database", Nullable = false)] + public string? DatabaseTemplate { get; set; } + } + + public class Validator : AbstractValidator + { + private static readonly string[] _validDatabaseTemplates = Enum.GetNames(); + private readonly AdminApiDbContext _adminApiDbContext; + private readonly IUsersContext _usersContext; + + public Validator(AdminApiDbContext adminApiDbContext, IUsersContext usersContext) + { + _adminApiDbContext = adminApiDbContext; + _usersContext = usersContext; + + RuleFor(m => m.Name) + .NotEmpty() + .MaximumLength(MaxDataStoreManageNameLength) + .WithMessage($"'{{PropertyName}}' must be {MaxDataStoreManageNameLength} characters or fewer so the synchronized DataStore name fits within {MaxSynchronizedNameLength} characters.") + .Matches(_validDataStoreManageNamePattern) + .WithMessage("'{PropertyName}' may only contain letters, numbers, spaces, and underscores."); + + RuleFor(m => m.DatabaseTemplate).NotEmpty().MaximumLength(100) + .Must(t => t != null && _validDatabaseTemplates.Contains(t)) + .WithMessage($"'{{PropertyValue}}' is not a valid database template. Allowed values are: {string.Join(", ", _validDatabaseTemplates)}."); + + RuleFor(m => m).CustomAsync(async (request, context, cancellationToken) => + { + if (string.IsNullOrWhiteSpace(request.Name) + || string.IsNullOrWhiteSpace(request.DatabaseTemplate) + || request.Name.Length > MaxDataStoreManageNameLength + || !_validDataStoreManageNamePattern.IsMatch(request.Name) + || !_validDatabaseTemplates.Contains(request.DatabaseTemplate)) + { + return; + } + + var normalizedName = request.Name.Trim(); + + if (await _adminApiDbContext.OdsInstanceManages.AnyAsync(instance => instance.Name == normalizedName && instance.Status != OdsInstanceManageStatus.Deleted.ToString(), cancellationToken)) + { + context.AddFailure( + nameof(AddDataStoreManageRequest.Name), + $"A DataStoreManage named '{normalizedName}' already exists."); + return; + } + + if (await _usersContext.OdsInstances.AnyAsync(instance => instance.Name == normalizedName, cancellationToken)) + { + context.AddFailure( + nameof(AddDataStoreManageRequest.Name), + $"A DataStore named '{normalizedName}' already exists."); + return; + } + + var databaseName = DataStoreManageDatabaseNameFormatter.Build(request.Name, request.DatabaseTemplate); + + if (databaseName.Length > DataStoreManageDatabaseNameFormatter.MaxPortableDatabaseNameLength) + { + context.AddFailure( + nameof(AddDataStoreManageRequest.Name), + $"The generated database name '{databaseName}' exceeds the portable limit of {DataStoreManageDatabaseNameFormatter.MaxPortableDatabaseNameLength} characters. Shorten Name or DatabaseTemplate."); + } + }); + } + } +} +``` + +- [ ] **Step 3: Replace `ReadDataStoreManage.cs` content** + +```csharp +// 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.ErrorHandling; +using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; + +namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; + +public class ReadDataStoreManage : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/dataStores/manage", GetDataStoreManages) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V3); + + AdminApiEndpointBuilder.MapGet(endpoints, "/dataStores/manage/{id}", GetDataStoreManage) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(AdminApiVersions.V3); + } + + public static Task GetDataStoreManages(IGetDataStoreManagesQuery query, + [AsParameters] CommonQueryParams commonQueryParams, int? id, string? name) + { + var list = DataStoreManageMapper.ToModelList(query.Execute(commonQueryParams, id, name)); + return Task.FromResult(Results.Ok(list)); + } + + public static Task GetDataStoreManage(IGetDataStoreManageByIdQuery query, int id) + { + var dataStoreManage = query.Execute(id); + if (dataStoreManage == null) + { + throw new NotFoundException("dataStoreManage", id); + } + var model = DataStoreManageMapper.ToModel(dataStoreManage); + return Task.FromResult(Results.Ok(model)); + } +} +``` + +- [ ] **Step 4: Replace `DeleteDataStoreManage.cs` content** + +```csharp +// 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.Constants; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Context; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; +using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; +using EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs; +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Quartz; + +namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; + +public class DeleteDataStoreManage : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapDelete(endpoints, "/dataStores/manage/{id}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(204)) + .BuildForVersions(AdminApiVersions.V3); + } + + public static async Task Handle( + IGetDataStoreManageByIdQuery getDataStoreManageByIdQuery, + IDeleteDataStoreManageCommand deleteDataStoreManageCommand, + [FromServices] ISchedulerFactory schedulerFactory, + [FromServices] IContextProvider tenantConfigurationProvider, + [FromServices] IOptions options, + int id + ) + { + var dataStoreManage = getDataStoreManageByIdQuery.Execute(id); + if (dataStoreManage is null) + throw new NotFoundException("dataStoreManage", id); + + if (dataStoreManage.Status == OdsInstanceManageStatus.Deleted.ToString()) + throw new NotFoundException("dataStoreManage", id); + + var blockingMessage = GetBlockingStatusMessage(dataStoreManage.Status); + if (blockingMessage is not null) + throw new ValidationException([new ValidationFailure(nameof(id), blockingMessage)]); + + deleteDataStoreManageCommand.Execute(id); + + var tenantName = options.Value.MultiTenancy + ? tenantConfigurationProvider.Get()?.TenantIdentifier + : null; + var jobData = new Dictionary + { + [JobConstants.OdsInstanceManageIdKey] = id + }; + + if (!string.IsNullOrWhiteSpace(tenantName)) + { + jobData[JobConstants.TenantNameKey] = tenantName; + } + + var scheduler = await schedulerFactory.GetScheduler(); + + try + { + await QuartzJobScheduler.ScheduleJob( + scheduler, + DeleteInstanceJob.CreateJobKey(id, tenantName), + jobData, + startImmediately: true); + } + catch (ObjectAlreadyExistsException) + { + // The DeletePendingDataStoreManagesDispatcherJob may have already scheduled this job. + // Treat duplicate scheduling as success — the job is already queued. + } + + return Results.NoContent(); + } + + private static string? GetBlockingStatusMessage(string status) + { + if (Enum.TryParse(status, out var parsed)) + { + return parsed switch + { + OdsInstanceManageStatus.PendingCreate => "OdsInstanceManage is being provisioned. Wait for creation to complete.", + OdsInstanceManageStatus.CreateInProgress => "OdsInstanceManage is currently being provisioned. Wait for creation to complete.", + OdsInstanceManageStatus.CreateFailed => "OdsInstanceManage creation failed. It will be retried automatically by the background job.", + OdsInstanceManageStatus.CreateError => "OdsInstanceManage creation failed permanently. Manual database intervention required before deleting.", + OdsInstanceManageStatus.PendingDelete => "OdsInstanceManage is already queued for deletion.", + OdsInstanceManageStatus.DeleteInProgress => "OdsInstanceManage is currently being deleted.", + OdsInstanceManageStatus.DeleteFailed => "OdsInstanceManage deletion failed. It will be retried automatically by the background job.", + OdsInstanceManageStatus.DeleteError => "OdsInstanceManage deletion failed permanently. Manual database intervention required.", + _ => null, + }; + } + + return null; + } +} +``` + +- [ ] **Step 5: Replace `DataStoreManageModel.cs` content** + +```csharp +// 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 Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; + +[SwaggerSchema(Title = "DataStoreManage")] +public class DataStoreManageModel +{ + public int? Id { get; set; } + public string? Name { get; set; } + public int? DataStoreId { get; set; } + public string? DataStoreName { get; set; } + public string? Status { get; set; } + public string? DatabaseTemplate { get; set; } + public string? DatabaseName { get; set; } + public DateTime? LastRefreshed { get; set; } + public DateTime? LastModifiedDate { get; set; } +} +``` + +- [ ] **Step 6: Replace `DataStoreManageMapper.cs` content** + +```csharp +// 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.Infrastructure.Models; + +namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; + +public static class DataStoreManageMapper +{ + public static DataStoreManageModel ToModel(OdsInstanceManage source) + { + return new DataStoreManageModel + { + Id = source.Id, + Name = source.Name, + DataStoreId = source.OdsInstanceId, + DataStoreName = source.OdsInstanceName, + Status = source.Status, + DatabaseTemplate = source.DatabaseTemplate, + DatabaseName = source.DatabaseName, + LastRefreshed = source.LastRefreshed, + LastModifiedDate = source.LastModifiedDate, + }; + } + + public static List ToModelList(IEnumerable source) + { + return source.Select(ToModel).ToList(); + } +} +``` + +- [ ] **Step 7: Replace `DataStoreManageDatabaseNameFormatter.cs` content** + +```csharp +// 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.RegularExpressions; + +namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; + +internal static class DataStoreManageDatabaseNameFormatter +{ + private const string CanonicalPrefix = "EdFi_Ods"; + + // Use PostgreSQL's identifier limit as the portable ceiling so the persisted + // DatabaseName always matches the real provisioned database across engines. + internal const int MaxPortableDatabaseNameLength = 63; + + private static readonly Regex _leadingCanonicalPrefixPattern = new( + @"^(?:(?:edfi_+ods)(?:_+|$))+", + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + internal static string Build(string dataStoreName, string databaseTemplate) + { + ArgumentException.ThrowIfNullOrWhiteSpace(dataStoreName); + ArgumentException.ThrowIfNullOrWhiteSpace(databaseTemplate); + + var normalizedName = NormalizeSegment(dataStoreName); + var normalizedDatabaseTemplate = NormalizeSegment(databaseTemplate); + var normalizedNameWithoutPrefix = _leadingCanonicalPrefixPattern.Replace(normalizedName, string.Empty).Trim('_'); + + return string.IsNullOrWhiteSpace(normalizedNameWithoutPrefix) + ? $"{CanonicalPrefix}_{normalizedDatabaseTemplate}" + : $"{CanonicalPrefix}_{normalizedNameWithoutPrefix}_{normalizedDatabaseTemplate}"; + } + + private static string NormalizeSegment(string value) + => value.Replace(' ', '_').Trim('_'); +} +``` + +- [ ] **Step 8: Build** + +Run: `dotnet build Application/EdFi.Ods.AdminApi.V3.sln` +Expected: remaining errors confined to `Features\DataStores\DataStoreWithEducationOrganizationsModel.cs`/`ReadEducationOrganizations.cs` (Task 12) and `Program.cs`/`WebApplicationBuilderExtensions.cs` V3 branches (Task 13). + +- [ ] **Step 9: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage" \ + "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores" +git commit -m "Move v3 DbDataStores feature into DataStores/Manage, rename routes to /dataStores/manage" +``` + +--- + +### Task 12: v3 existing `DataStores` files — update references and close the `DataStoreManageId` parity gap + +**Files:** +- Modify: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\DataStoreWithEducationOrganizationsModel.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\ReadEducationOrganizations.cs` + +**Interfaces:** +- Consumes: `IGetDataStoreManagesQuery` (Task 10), `OdsInstanceManageStatus` (Task 2). +- Produces: `DataStoreWithEducationOrganizationsModel.DataStoreManageId` (new field, parity with v2's `OdsInstanceManageId` from Task 5). + +- [ ] **Step 1: Add the `DataStoreManageId` field** + +In `DataStoreWithEducationOrganizationsModel.cs`, add a new property alongside `Id` (mirroring v2's `OdsInstanceWithEducationOrganizationsModel.OdsInstanceManageId` from Task 5): + +```csharp + [SwaggerSchema(Description = "Data store identifier")] + public int? Id { get; set; } + + [SwaggerSchema(Description = "DataStoreManage identifier for this data store")] + public int? DataStoreManageId { get; set; } +``` + +(insert the new property immediately after `Id`, before `Name`.) + +- [ ] **Step 2: Update `ReadEducationOrganizations.cs`** + +Replace the full file content with: + +```csharp +// 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.Constants; +using EdFi.Ods.AdminApi.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; +using Microsoft.AspNetCore.Mvc; + +namespace EdFi.Ods.AdminApi.V3.Features.DataStores; + +public class ReadEducationOrganizations : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapGet(endpoints, "/dataStores/{dataStoreId}/edOrgs", GetEducationOrganizationsByDataStore) + .WithSummaryAndDescription( + "Retrieves education organizations for a specific data store", + "Returns all education organizations for the specified data store in a nested structure" + ) + .WithRouteOptions(b => b.WithResponse>(200)) + .BuildForVersions(AdminApiVersions.V3); + } + + public static async Task GetEducationOrganizationsByDataStore( + [FromServices] IGetEducationOrganizationsQuery getEducationOrganizationsQuery, + [FromServices] IGetDataStoreQuery getDataStoreQuery, + [FromServices] IGetDataStoreManagesQuery getDataStoreManagesQuery, + [AsParameters] CommonQueryParams commonQueryParams, + int dataStoreId) + { + getDataStoreQuery.Execute(dataStoreId); + + var educationOrganizations = await getEducationOrganizationsQuery.ExecuteAsync( + commonQueryParams, + dataStoreId: dataStoreId); + + MergeDataStoreManageData(educationOrganizations, getDataStoreManagesQuery); + return Results.Ok(educationOrganizations); + } + + private static void MergeDataStoreManageData( + List instances, + IGetDataStoreManagesQuery getDataStoreManagesQuery) + { + var allDataStoreManages = getDataStoreManagesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); + + var linkedById = allDataStoreManages + .Where(d => d.OdsInstanceId is not null) + .GroupBy(d => d.OdsInstanceId!.Value) + .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); + + foreach (var instance in instances) + { + if (instance.Id is int dataStoreId && linkedById.TryGetValue(dataStoreId, out var dataStoreManage)) + { + instance.DataStoreManageId = dataStoreManage.Id; + instance.Status = dataStoreManage.Status; + instance.DatabaseTemplate = dataStoreManage.DatabaseTemplate; + instance.DatabaseName = dataStoreManage.DatabaseName; + } + else + { + instance.Status = OdsInstanceManageStatus.Created.ToString(); + } + } + } +} +``` + +(this adds `instance.DataStoreManageId = dataStoreManage.Id;` in the linked branch, which is the new parity behavior — v2's equivalent method already sets `instance.OdsInstanceManageId` the same way.) + +- [ ] **Step 3: Build** + +Run: `dotnet build Application/EdFi.Ods.AdminApi.V3.sln` +Expected: remaining errors confined to `Program.cs`/`WebApplicationBuilderExtensions.cs` V3 branches (Task 13). + +- [ ] **Step 4: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/DataStoreWithEducationOrganizationsModel.cs" \ + "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/ReadEducationOrganizations.cs" +git commit -m "Update v3 DataStores EdOrgs merge to use renamed DataStoreManage query, add DataStoreManageId for v2 parity" +``` + +--- + +### Task 13: v3 wiring — `Program.cs` V3 branch and `WebApplicationBuilderExtensions.cs` V3 branch + +**Files:** +- Modify: `Application\EdFi.Ods.AdminApi\Program.cs` (V3 branch only — the shared interval-variable renames were already done in Task 6) +- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\WebApplicationBuilderExtensions.cs` (V3 branch only) + +**Interfaces:** +- Consumes: `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName`/`DeletePendingOdsInstanceManagesDispatcherJobName` (Task 2 — same shared constants v2 uses, since `JobConstants` isn't version-specific), `CreatePendingDataStoreManagesDispatcherJob`/`DeletePendingDataStoreManagesDispatcherJob` (Task 10, the V3-namespaced classes aliased as `V3Jobs.*`). +- Produces: fully wired v3 Quartz scheduling — the last v3-side wiring file; Task 14's build checkpoint depends on this. + +- [ ] **Step 1: Update job-type references in the `AdminApiMode.V3` branch of `Program.cs`** + +Replace every occurrence in the V3 branch of: + +```csharp +await QuartzJobScheduler.ScheduleJob( + scheduler, + jobKey: new JobKey($"{JobConstants.CreatePendingDbInstancesDispatcherJobName}_{tenantName}"), +``` + +and the non-multi-tenant equivalent + +```csharp +await QuartzJobScheduler.ScheduleJob( + scheduler, + jobKey: new JobKey(JobConstants.CreatePendingDbInstancesDispatcherJobName), +``` + +with `V3Jobs.CreatePendingDataStoreManagesDispatcherJob` and `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName` (both branches). Do the same for `V3Jobs.DeletePendingDbInstancesDispatcherJob` → `V3Jobs.DeletePendingDataStoreManagesDispatcherJob` with `JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName`. + +Note: `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName`/`DeletePendingOdsInstanceManagesDispatcherJobName` are the same shared string constants v2 uses (from Task 2) — only the generic type parameter (`V3Jobs.CreatePendingDataStoreManagesDispatcherJob` vs. v2's bare `CreatePendingOdsInstanceManagesDispatcherJob`) differs between branches, matching the existing pre-rename pattern where both branches already used the same `JobConstants.CreatePendingDbInstancesDispatcherJobName` string. + +- [ ] **Step 2: Update `WebApplicationBuilderExtensions.cs` DI registrations for the V3 branch** + +Replace: + +```csharp + webApplicationBuilder.Services.AddTransient< + EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.CreatePendingDbInstancesDispatcherJob + >(); +``` + +with: + +```csharp + webApplicationBuilder.Services.AddTransient< + EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.CreatePendingDataStoreManagesDispatcherJob + >(); +``` + +and replace: + +```csharp + webApplicationBuilder.Services.AddTransient< + EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.DeletePendingDbInstancesDispatcherJob + >(); +``` + +with: + +```csharp + webApplicationBuilder.Services.AddTransient< + EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.DeletePendingDataStoreManagesDispatcherJob + >(); +``` + +(the surrounding `CreateInstanceJob`/`DeleteInstanceJob`/`JobStatusService` registrations in this same V3 block are unchanged — only the two dispatcher job type references change.) + +- [ ] **Step 3: Build the full solution** + +Run: `dotnet build` at the repository root (build everything — both v2 and v3 solutions/projects). +Expected: PASS with zero errors. Grep the full source tree for `DbInstance` and `DbDataStore` (`grep -rn "DbInstance\|DbDataStore" Application --include=*.cs`) — the only remaining matches at this point should be inside test projects (Tasks 7, 8, 14, 15 — not yet done) and E2E/`.http` artifacts (Tasks 9, 16, 17 — not yet done). No matches should remain in any non-test `.cs` file under `Application\EdFi.Ods.AdminApi\` or `Application\EdFi.Ods.AdminApi.V3\` (excluding their `E2E Tests` folders, which Tasks 9/16 handle). + +- [ ] **Step 4: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi/Program.cs" \ + "Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs" +git commit -m "Update v3 Quartz wiring to use renamed DataStoreManage dispatcher jobs" +``` + +--- + +### Task 14: v3 Unit tests rename + add missing query-test coverage (`EdFi.Ods.AdminApi.V3.UnitTests`) + +**Files:** +- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DbDataStores\AddDbDataStoreTests.cs` → move+rename to `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DataStores\Manage\AddDataStoreManageTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DbDataStores\ReadDbDataStoreTests.cs` → move+rename to `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DataStores\Manage\ReadDataStoreManageTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DbDataStores\DeleteDbDataStoreTests.cs` → move+rename to `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DataStores\Manage\DeleteDataStoreManageTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Database\Commands\AddDbDataStoreCommandTests.cs` → rename to `AddDataStoreManageCommandTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Database\Commands\DeleteDbDataStoreCommandTests.cs` → rename to `DeleteDataStoreManageCommandTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\CreatePendingDbInstancesDispatcherJobTests.cs` → rename to `CreatePendingDataStoreManagesDispatcherJobTests.cs` (search first to confirm exact path — the initial research report listed this file but it wasn't independently verified in this plan's file reads) +- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\DeletePendingDbInstancesDispatcherJobTests.cs` → rename to `DeletePendingDataStoreManagesDispatcherJobTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\CreateInstanceJobTests.cs` (filename unchanged — update `DbDataStore`/`DbInstance` references only) +- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\DeleteInstanceJobTests.cs` (filename unchanged — update references only) +- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DataStores\ReadEducationOrganizationsTests.cs` (filename unchanged — update `IGetDbDataStoresQuery`/`DbInstance` references) +- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Tenants\TenantServiceTests.cs` (filename unchanged — update references) +- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\Tenants\ReadTenantsTest.cs` (filename unchanged — update references) +- Create: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Database\Queries\GetDataStoreManagesQueryTests.cs` (new — closes the coverage gap) +- Create: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Database\Queries\GetDataStoreManageByIdQueryTests.cs` (new — closes the coverage gap) + +**Interfaces:** +- Consumes: every v3 production type from Tasks 10–13. + +- [ ] **Step 1: Move and rename the Command test files (full content known — apply directly)** + +```bash +git mv "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/AddDbDataStoreCommandTests.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/AddDataStoreManageCommandTests.cs" +git mv "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/DeleteDbDataStoreCommandTests.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/DeleteDataStoreManageCommandTests.cs" +``` + +`AddDataStoreManageCommandTests.cs`: + +```csharp +// 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.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.Common.Constants; +using EdFi.Ods.AdminApi.V3.Infrastructure; +using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using NUnit.Framework; +using Shouldly; + +#nullable enable + +namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Database.Commands; + +[TestFixture] +public class AddDataStoreManageCommandTests +{ + private static AdminApiDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: $"AddDataStoreManageCommand_{Guid.NewGuid()}") + .Options; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AppSettings:DatabaseEngine"] = "SqlServer" + }) + .Build(); + return new AdminApiDbContext(options, configuration); + } + + [Test] + public void Execute_WithValidModel_PersistsOdsInstanceManage() + { + using var context = CreateContext(); + var command = new AddDataStoreManageCommand(context); + var model = new AddDataStoreManageModelStub + { + Name = "Test Instance", + DatabaseTemplate = "Minimal" + }; + + var result = command.Execute(model); + + result.Id.ShouldBeGreaterThan(0); + context.OdsInstanceManages.Any(d => d.Id == result.Id).ShouldBeTrue(); + } + + [Test] + public void Execute_WithValidModel_SetsExpectedFieldValues() + { + using var context = CreateContext(); + var command = new AddDataStoreManageCommand(context); + var before = DateTime.UtcNow; + var model = new AddDataStoreManageModelStub + { + Name = " Test Instance ", + DatabaseTemplate = " Minimal " + }; + + var result = command.Execute(model); + + result.Name.ShouldBe("Test Instance"); + result.DatabaseTemplate.ShouldBe("Minimal"); + result.Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); + result.OdsInstanceId.ShouldBeNull(); + result.OdsInstanceName.ShouldBeNull(); + result.DatabaseName.ShouldBeNull(); + result.LastRefreshed.ShouldBeGreaterThanOrEqualTo(before); + result.LastModifiedDate.ShouldNotBeNull(); + result.LastModifiedDate!.Value.ShouldBeGreaterThanOrEqualTo(before); + } + + [Test] + public void Execute_WithSampleTemplate_PersistsWithCorrectTemplate() + { + using var context = CreateContext(); + var command = new AddDataStoreManageCommand(context); + var model = new AddDataStoreManageModelStub + { + Name = "Sample Instance", + DatabaseTemplate = "Sample" + }; + + var result = command.Execute(model); + + result.DatabaseTemplate.ShouldBe("Sample"); + context.OdsInstanceManages.Any(d => d.Id == result.Id && d.DatabaseTemplate == "Sample").ShouldBeTrue(); + } + + private sealed class AddDataStoreManageModelStub : IAddDataStoreManageModel + { + public string? Name { get; set; } + public string? DatabaseTemplate { get; set; } + } +} + +#nullable restore +``` + +`DeleteDataStoreManageCommandTests.cs`: + +```csharp +// 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.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.Common.Constants; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; +using EdFi.Ods.AdminApi.V3.Infrastructure; +using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using NUnit.Framework; +using Shouldly; + +#nullable enable + +namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Database.Commands; + +[TestFixture] +public class DeleteDataStoreManageCommandTests +{ + private static AdminApiDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: $"DeleteDataStoreManageCommand_{Guid.NewGuid()}") + .Options; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection( + new Dictionary { ["AppSettings:DatabaseEngine"] = "SqlServer" } + ) + .Build(); + return new AdminApiDbContext(options, configuration); + } + + [Test] + public void Execute_SetsStatusToPendingDelete() + { + using var context = CreateContext(); + var instance = new OdsInstanceManage + { + Name = "Test Instance", + Status = OdsInstanceManageStatus.PendingCreate.ToString(), + DatabaseTemplate = "Minimal", + LastRefreshed = DateTime.UtcNow, + }; + context.OdsInstanceManages.Add(instance); + context.SaveChanges(); + + var command = new DeleteDataStoreManageCommand(context); + command.Execute(instance.Id); + + var updated = context.OdsInstanceManages.Single(d => d.Id == instance.Id); + updated.Status.ShouldBe(OdsInstanceManageStatus.PendingDelete.ToString()); + } + + [Test] + public void Execute_UpdatesLastModifiedDate() + { + using var context = CreateContext(); + var before = DateTime.UtcNow; + var instance = new OdsInstanceManage + { + Name = "Test Instance", + Status = OdsInstanceManageStatus.PendingCreate.ToString(), + DatabaseTemplate = "Minimal", + LastRefreshed = DateTime.UtcNow, + }; + context.OdsInstanceManages.Add(instance); + context.SaveChanges(); + + var command = new DeleteDataStoreManageCommand(context); + command.Execute(instance.Id); + + var updated = context.OdsInstanceManages.Single(d => d.Id == instance.Id); + updated.LastModifiedDate.ShouldNotBeNull(); + updated.LastModifiedDate!.Value.ShouldBeGreaterThanOrEqualTo(before); + } + + [Test] + public void Execute_WithNonExistentId_ThrowsNotFoundException() + { + using var context = CreateContext(); + var command = new DeleteDataStoreManageCommand(context); + + Should.Throw>(() => command.Execute(9999)); + } + + [Test] + public void Execute_WhenStatusIsDeleted_ThrowsNotFoundException() + { + using var context = CreateContext(); + var instance = new OdsInstanceManage + { + Name = "Test Instance", + Status = OdsInstanceManageStatus.Deleted.ToString(), + DatabaseTemplate = "Minimal", + LastRefreshed = DateTime.UtcNow, + }; + context.OdsInstanceManages.Add(instance); + context.SaveChanges(); + + var command = new DeleteDataStoreManageCommand(context); + + Should.Throw>(() => command.Execute(instance.Id)); + } +} + +#nullable restore +``` + +- [ ] **Step 2: Write the new `GetDataStoreManagesQueryTests.cs` (closes the pre-existing v3 coverage gap)** + +```csharp +// 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. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.V3.Infrastructure; +using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Database.Queries; + +[TestFixture] +public class GetDataStoreManagesQueryTests +{ + private static AdminApiDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: $"GetDataStoreManagesQueryTests_{Guid.NewGuid()}") + .Options; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AppSettings:DatabaseEngine"] = "Postgres" + }) + .Build(); + + return new AdminApiDbContext(options, configuration); + } + + private static IOptions DefaultOptions() => + Options.Create(new AppSettings { DatabaseEngine = "Postgres", DefaultPageSizeLimit = 25 }); + + [Test] + public void Execute_WithoutFilters_ReturnsAllOdsInstanceManages() + { + using var context = CreateContext(); + context.OdsInstanceManages.AddRange( + new OdsInstanceManage { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, + new OdsInstanceManage { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); + context.SaveChanges(); + + var query = new GetDataStoreManagesQuery(context, DefaultOptions()); + + var result = query.Execute(new CommonQueryParams(0, 25), null, null); + + result.Count.ShouldBe(2); + result.Select(x => x.Name).ShouldBe(["Sandbox A", "Sandbox B"], ignoreOrder: true); + } + + [Test] + public void Execute_WithNameFilter_ReturnsMatchingOdsInstanceManage() + { + using var context = CreateContext(); + context.OdsInstanceManages.AddRange( + new OdsInstanceManage { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, + new OdsInstanceManage { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); + context.SaveChanges(); + + var query = new GetDataStoreManagesQuery(context, DefaultOptions()); + + var result = query.Execute(new CommonQueryParams(0, 25), null, "Sandbox B"); + + result.Count.ShouldBe(1); + result.Single().Name.ShouldBe("Sandbox B"); + } +} +``` + +- [ ] **Step 3: Write the new `GetDataStoreManageByIdQueryTests.cs` (closes the pre-existing v3 coverage gap)** + +```csharp +// 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. + +#nullable enable + +using System; +using System.Collections.Generic; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; +using EdFi.Ods.AdminApi.V3.Infrastructure; +using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Database.Queries; + +[TestFixture] +public class GetDataStoreManageByIdQueryTests +{ + private static AdminApiDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: $"GetDataStoreManageByIdQueryTests_{Guid.NewGuid()}") + .Options; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AppSettings:DatabaseEngine"] = "Postgres" + }) + .Build(); + + return new AdminApiDbContext(options, configuration); + } + + [Test] + public void Execute_WithExistingId_ReturnsOdsInstanceManage() + { + using var context = CreateContext(); + var odsInstanceManage = new OdsInstanceManage + { + Name = "Sandbox", + Status = "Healthy", + DatabaseTemplate = "Minimal" + }; + context.OdsInstanceManages.Add(odsInstanceManage); + context.SaveChanges(); + + var query = new GetDataStoreManageByIdQuery(context); + + var result = query.Execute(odsInstanceManage.Id); + + result.ShouldNotBeNull(); + result.Name.ShouldBe("Sandbox"); + } + + [Test] + public void Execute_WithUnknownId_ReturnsNull() + { + using var context = CreateContext(); + var query = new GetDataStoreManageByIdQuery(context); + + var result = query.Execute(999); + + result.ShouldBeNull(); + } +} +``` + +- [ ] **Step 4: Move and rename the remaining v3 unit test files, applying the Master Rename Table** + +```bash +git mv "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/AddDbDataStoreTests.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/AddDataStoreManageTests.cs" +git mv "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/ReadDbDataStoreTests.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/ReadDataStoreManageTests.cs" +git mv "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/DeleteDbDataStoreTests.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/DeleteDataStoreManageTests.cs" +``` + +Search for the exact dispatcher-job-test filenames first (`Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\`) — rename `CreatePendingDbInstancesDispatcherJobTests.cs`/`DeletePendingDbInstancesDispatcherJobTests.cs` to `CreatePendingDataStoreManagesDispatcherJobTests.cs`/`DeletePendingDataStoreManagesDispatcherJobTests.cs` if present. + +For each moved file, apply every substitution from the Master Rename Table (class name matching new filename, `namespace ...V3.UnitTests.Features.DbDataStores` → `...V3.UnitTests.Features.DataStores.Manage`, route string literals `"/dbDataStores"` → `"/dataStores/manage"` if asserted, mock setups referencing `IGetDbDataStoresQuery`/`AddDbDataStoreCommand`/etc. → their renamed equivalents). + +- [ ] **Step 5: Update the four non-renamed files that reference `DbDataStore`/`DbInstance` internally** + +Open each of the following and replace every `DbDataStore`/`DbInstance`-family token per the Master Rename Table (mock setups, fixture data, assertions) without changing the filename or test class name: +- `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\CreateInstanceJobTests.cs` +- `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\DeleteInstanceJobTests.cs` +- `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DataStores\ReadEducationOrganizationsTests.cs` +- `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Tenants\TenantServiceTests.cs` +- `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\Tenants\ReadTenantsTest.cs` + +- [ ] **Step 6: Run the v3 unit test suite** + +Run: `dotnet test Application/EdFi.Ods.AdminApi.V3.UnitTests/EdFi.Ods.AdminApi.V3.UnitTests.csproj` +Expected: PASS. Test count should be the pre-rename baseline **plus 4** (the two new `GetDataStoreManagesQueryTests` tests and two new `GetDataStoreManageByIdQueryTests` tests). + +- [ ] **Step 7: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage" \ + "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores" \ + "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/AddDataStoreManageCommandTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/DeleteDataStoreManageCommandTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Queries/GetDataStoreManagesQueryTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Queries/GetDataStoreManageByIdQueryTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs" \ + "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/ReadEducationOrganizationsTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/ReadTenantsTest.cs" +git commit -m "Rename v3 unit tests to DataStoreManage naming, add missing query test coverage" +``` + +--- + +### Task 15: v3 DBTests rename (`EdFi.Ods.AdminApi.V3.DBTests`) + +**Files:** +- Modify: `Application\EdFi.Ods.AdminApi.V3.DBTests\Database\CommandTests\AddDbDataStoreCommandTests.cs` → rename to `AddDataStoreManageCommandTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3.DBTests\Database\CommandTests\DeleteDbDataStoreCommandTests.cs` → rename to `DeleteDataStoreManageCommandTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3.DBTests\Database\QueryTests\GetDbDataStoreByIdQueryTests.cs` → rename to `GetDataStoreManageByIdQueryTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3.DBTests\Database\QueryTests\GetDbDataStoresQueryTests.cs` → rename to `GetDataStoreManagesQueryTests.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3.DBTests\Database\QueryTests\GetTenantEdOrgsByDataStoresTests.cs` (filename unchanged — update internal references only) + +**Interfaces:** +- Consumes: same v3 production types as Task 14, against a real database. + +- [ ] **Step 1: Move and rename** + +```bash +git mv "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDbDataStoreCommandTests.cs" "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDataStoreManageCommandTests.cs" +git mv "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDbDataStoreCommandTests.cs" "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDataStoreManageCommandTests.cs" +git mv "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoreByIdQueryTests.cs" "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManageByIdQueryTests.cs" +git mv "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoresQueryTests.cs" "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManagesQueryTests.cs" +``` + +Apply the Master Rename Table to each moved file's content (class name, `AdminApiDbContext.DbInstances`→`.OdsInstanceManages`, `DbInstance`/`DbInstanceStatus`→`OdsInstanceManage`/`OdsInstanceManageStatus`, command/query type names to their `DataStoreManage`-prefixed v3 equivalents). In `GetTenantEdOrgsByDataStoresTests.cs` (filename unchanged), update only the internal references to renamed types, not the filename or class name. + +- [ ] **Step 2: Run the v3 DB test suite** + +Run: `dotnet test Application/EdFi.Ods.AdminApi.V3.DBTests/EdFi.Ods.AdminApi.V3.DBTests.csproj` (requires the local test database with Task 1's migration applied). +Expected: PASS, same test count as the pre-rename baseline. + +- [ ] **Step 3: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDataStoreManageCommandTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDataStoreManageCommandTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManageByIdQueryTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManagesQueryTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetTenantEdOrgsByDataStoresTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDbDataStoreCommandTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDbDataStoreCommandTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoreByIdQueryTests.cs" \ + "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoresQueryTests.cs" +git commit -m "Rename v3 DBTests to DataStoreManage naming" +``` + +--- + +### Task 16: v3 Bruno E2E collection rename + +**Files:** +- Modify: `Application\EdFi.Ods.AdminApi.V3\E2E Tests\Bruno Admin API E2E 3.0\v3\DbDataStores\` → move all files to `...\v3\DataStores\Manage\` + +**Interfaces:** +- Consumes: routes `/v3/dataStores/manage*` (Task 11). + +- [ ] **Step 1: Move the folder and rename every `.bru` file** + +```bash +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/folder.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/folder.bru" +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage.bru" +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Sample Template.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Sample Template.bru" +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Invalid.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Invalid.bru" +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Invalid Database Template.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Invalid Database Template.bru" +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage.bru" +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores by ID.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage by ID.bru" +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Offset.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Offset.bru" +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Limit.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Limit.bru" +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Limit and Offset.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Limit and Offset.bru" +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Not Found.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Not Found.bru" +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Filter by Name.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Filter by Name.bru" +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/DELETE - DbDataStore - Success.bru.disabled" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/DELETE - DataStore Manage - Success.bru.disabled" +git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/DELETE - DbDataStore - Not Found.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/DELETE - DataStore Manage - Not Found.bru" +``` + +- [ ] **Step 2: Update `folder.bru`** + +``` +meta { + name: Manage + seq: 99 +} + +auth { + mode: inherit +} +``` + +- [ ] **Step 3: Update every moved `.bru` file's content** + +Same substitution pattern as Task 9 Step 3: `meta { name: DbDataStores ... }` → `meta { name: DataStores Manage ... }`, request URL `{{API_URL}}/v3/dbDataStores...` → `{{API_URL}}/v3/dataStores/manage...`, any `bru.setVar`/`{{...}}` variable named `CreatedDbDataStoreId`-style → `CreatedDataStoreManageId`, and `test("POST DbDataStores: ...")`-style description strings → `DataStores Manage`. + +For example, `POST - DataStores Manage.bru`: + +``` +meta { + name: DataStores Manage + type: http + seq: 1 +} + +post { + url: {{API_URL}}/v3/dataStores/manage + body: json + auth: inherit +} + +body:json { + { + "name": "Test DB Instance", + "databaseTemplate": "Minimal" + } +} + +script:post-response { + test("POST DataStores Manage: Status code is Accepted", function () { + expect(res.getStatus()).to.equal(202); + }); + + test("POST DataStores Manage: Response includes location in header", function () { + expect(res.getHeaders()).to.have.property("location"); + const id = res.getHeader("location").split("/").pop(); + if (id) { + bru.setVar("CreatedDataStoreManageId", id); + } + }); +} + +settings { + encodeUrl: true +} +``` + +Apply the same URL/variable/assertion-text substitution pattern to the remaining files based on each file's current content (note v3's POST uses an absolute `Location` header via `ResourceUrlHelper.BuildAbsoluteResourceUrl`, unlike v2's relative path — preserve whatever ID-extraction logic the original `.bru` file used). + +- [ ] **Step 4: Run the v3 Bruno E2E suite** + +Run: `./eng/run-e2e-bruno.ps1 -ApiVersion 3 -TenantMode singletenant -TearDown` (adjust `-TenantMode` per `docs/developer.md`). +Expected: PASS, all `DataStores Manage` requests succeed with the same assertions as before the rename. + +- [ ] **Step 5: Commit** + +```bash +git add "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage" \ + "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores" +git commit -m "Rename v3 Bruno E2E DbDataStores collection to DataStores/Manage" +``` + +--- + +### Task 17: Rename and update `docs/http/dbinstances.http` + +**Files:** +- Modify: `docs\http\dbinstances.http` → rename to `docs\http\odsinstances-manage.http` + +**Interfaces:** +- Consumes: routes `/v2/odsInstances/manage*` (Task 4) and `/v3/dataStores/manage*` (Task 11). + +- [ ] **Step 1: Read the file's current (uncommitted-edits-included) content first** + +Read `docs\http\dbinstances.http` fresh (don't rely on the diff seen earlier in this conversation — more local edits may have landed since) to get the exact current content, since this file has uncommitted personal edits that must be preserved. + +- [ ] **Step 2: Rename the file** + +```bash +git mv "docs/http/dbinstances.http" "docs/http/odsinstances-manage.http" +``` + +- [ ] **Step 3: Update every request URL in the file** + +Replace every occurrence of: +- `{{adminapi_url}}/v2/dbinstances` → `{{adminapi_url}}/v2/odsinstances/manage` +- `{{adminapi_url}}/v3/dbDataStores` → `{{adminapi_url}}/v3/dataStores/manage` + +preserving any path suffix (`/1`, `?offset=0&limit=10`, etc.), any `@name` block labels that reference `createDbInstance...` (rename to `createOdsInstanceManage...` for v2 blocks, `createDataStoreManage...` for v3 blocks), and every other line in the file exactly as currently written (commented-out `@adminapi_url` lines, tenant headers, existing `/v2/odsinstances` and `/v3/dataStores` blocks that were already correct, etc.). + +- [ ] **Step 4: Manually smoke-test a couple of requests** + +Using the VS Code REST Client (or whichever `.http` runner this repo's contributors use per `docs/developer.md`), run the renamed `POST`/`GET`/`DELETE` blocks against a locally running v2 and v3 instance and confirm they hit `/odsInstances/manage`/`/dataStores/manage` successfully. + +- [ ] **Step 5: Commit** + +```bash +git add "docs/http/odsinstances-manage.http" "docs/http/dbinstances.http" +git commit -m "Rename docs/http/dbinstances.http to odsinstances-manage.http, update routes" +``` + +--- + +### Task 18: Full solution verification + +**Files:** none (verification only). + +- [ ] **Step 1: Full clean build** + +Run: `dotnet build` at the repository root (or `./build.ps1 build` per this repo's build script convention). +Expected: zero errors, zero warnings introduced by this change. + +- [ ] **Step 2: Full unit test suite** + +Run: `./build.ps1 -Command UnitTest` +Expected: PASS. Total test count = pre-rename baseline + 4 (Task 14's new v3 query tests). + +- [ ] **Step 3: Full integration (DBTests) suite** + +Run the DBTests projects per `docs/developer.md`'s integration-test instructions (apply Task 1's migration to the test database first if not already applied). +Expected: PASS, same test count as pre-rename baseline. + +- [ ] **Step 4: Full-repo grep for any remaining stray references** + +Run: `grep -rn "DbInstance\|DbDataStore" --include=*.cs --include=*.sql --include=*.json --include=*.bru --include=*.http .` from the repository root. +Expected: zero matches, except: +- Historical migration scripts `00001`–`00006` (which correctly still say `DbInstances` — they describe the table's state at that point in history and must not be edited). +- This plan document and the design spec document themselves (`docs/superpowers/plans/...`, `docs/superpowers/specs/...`), which describe the rename and legitimately mention the old names. + +- [ ] **Step 5: Run both v2 and v3 Bruno E2E suites end-to-end** + +Run: `./eng/run-e2e-bruno.ps1 -ApiVersion 2 -TenantMode singletenant -TearDown` and `./eng/run-e2e-bruno.ps1 -ApiVersion 3 -TenantMode singletenant -TearDown` (and the multitenant variants if this repo's CI runs those too — check `docs/developer.md`). +Expected: PASS. + +- [ ] **Step 6: Final review pass** + +Re-read the Master Rename Table at the top of this plan against the grep output from Step 4 to confirm every listed identifier was actually renamed everywhere it appeared — no partial renames left (e.g. a class renamed but a lingering XML doc comment or log message still saying the old name). + +No commit for this task — it's a verification checkpoint. If Step 4 or 6 turns up a stray reference, fix it in place and amend the most relevant task's commit (or add a small follow-up commit), then re-run the affected verification step. + + + + From ea5ac6f8571b127ae837cb59303b3ee76345afc7 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 16:15:22 -0600 Subject: [PATCH 04/35] Add migration renaming adminapi.DbInstances to adminapi.OdsInstanceManages This adds the 00007 migration script across both AdminApi and AdminApi.V3 artifacts for both MSSQL and PostgreSQL engines. The scripts use idempotency guards to safely rename the table, primary key constraint, and indexes. Co-Authored-By: Claude Sonnet 5 --- ...007-RenameDbInstancesToOdsInstanceManages.sql | 13 +++++++++++++ ...007-RenameDbInstancesToOdsInstanceManages.sql | 16 ++++++++++++++++ ...007-RenameDbInstancesToOdsInstanceManages.sql | 13 +++++++++++++ ...007-RenameDbInstancesToOdsInstanceManages.sql | 16 ++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 Application/EdFi.Ods.AdminApi.V3/Artifacts/MsSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql create mode 100644 Application/EdFi.Ods.AdminApi.V3/Artifacts/PgSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql create mode 100644 Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql create mode 100644 Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql diff --git a/Application/EdFi.Ods.AdminApi.V3/Artifacts/MsSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql b/Application/EdFi.Ods.AdminApi.V3/Artifacts/MsSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql new file mode 100644 index 000000000..30f1e7e11 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/Artifacts/MsSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql @@ -0,0 +1,13 @@ +-- 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. + +IF EXISTS (SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = 'adminapi' AND TABLE_NAME = 'DbInstances') + AND NOT EXISTS (SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = 'adminapi' AND TABLE_NAME = 'OdsInstanceManages') +BEGIN + EXEC sp_rename 'adminapi.DbInstances', 'OdsInstanceManages'; + EXEC sp_rename 'adminapi.PK_DbInstances', 'PK_OdsInstanceManages'; + EXEC sp_rename 'adminapi.OdsInstanceManages.IX_DbInstances_Name', 'IX_OdsInstanceManages_Name', 'INDEX'; + EXEC sp_rename 'adminapi.OdsInstanceManages.IX_DbInstances_OdsInstanceId', 'IX_OdsInstanceManages_OdsInstanceId', 'INDEX'; +END diff --git a/Application/EdFi.Ods.AdminApi.V3/Artifacts/PgSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql b/Application/EdFi.Ods.AdminApi.V3/Artifacts/PgSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql new file mode 100644 index 000000000..e7438b728 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/Artifacts/PgSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql @@ -0,0 +1,16 @@ +-- 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. + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'adminapi' AND table_name = 'dbinstances') + AND NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'adminapi' AND table_name = 'odsinstancemanages') + THEN + ALTER TABLE adminapi.DbInstances RENAME TO OdsInstanceManages; + ALTER TABLE adminapi.OdsInstanceManages RENAME CONSTRAINT pk_dbinstances TO pk_odsinstancemanages; + ALTER INDEX adminapi.idx_dbinstances_name RENAME TO idx_odsinstancemanages_name; + ALTER INDEX adminapi.idx_dbinstances_odsinstanceid RENAME TO idx_odsinstancemanages_odsinstanceid; + END IF; +END $$; diff --git a/Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql b/Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql new file mode 100644 index 000000000..30f1e7e11 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql @@ -0,0 +1,13 @@ +-- 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. + +IF EXISTS (SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = 'adminapi' AND TABLE_NAME = 'DbInstances') + AND NOT EXISTS (SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = 'adminapi' AND TABLE_NAME = 'OdsInstanceManages') +BEGIN + EXEC sp_rename 'adminapi.DbInstances', 'OdsInstanceManages'; + EXEC sp_rename 'adminapi.PK_DbInstances', 'PK_OdsInstanceManages'; + EXEC sp_rename 'adminapi.OdsInstanceManages.IX_DbInstances_Name', 'IX_OdsInstanceManages_Name', 'INDEX'; + EXEC sp_rename 'adminapi.OdsInstanceManages.IX_DbInstances_OdsInstanceId', 'IX_OdsInstanceManages_OdsInstanceId', 'INDEX'; +END diff --git a/Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql b/Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql new file mode 100644 index 000000000..e7438b728 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql @@ -0,0 +1,16 @@ +-- 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. + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'adminapi' AND table_name = 'dbinstances') + AND NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'adminapi' AND table_name = 'odsinstancemanages') + THEN + ALTER TABLE adminapi.DbInstances RENAME TO OdsInstanceManages; + ALTER TABLE adminapi.OdsInstanceManages RENAME CONSTRAINT pk_dbinstances TO pk_odsinstancemanages; + ALTER INDEX adminapi.idx_dbinstances_name RENAME TO idx_odsinstancemanages_name; + ALTER INDEX adminapi.idx_dbinstances_odsinstanceid RENAME TO idx_odsinstancemanages_odsinstanceid; + END IF; +END $$; From 2db33ed252f63bb132adb6aa502d92ce4b8fc53a Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 20:12:01 -0600 Subject: [PATCH 05/35] Rename shared DbInstance entity/enum/config keys to OdsInstanceManage Renames the shared entity (DbInstance -> OdsInstanceManage), its status enum, JobConstants members, and AppSettings sweep/retry keys in the Common project, plus the matching DbSet/table mappings in both AdminApiDbContext classes and the four config keys in the v2/v3 appsettings files. Note: this intentionally leaves the solution non-building until Task 3 onward completes, since v2/v3 Features/Infrastructure/Jobs callers still reference the old names. --- .../EdFi.Ods.AdminApi.Common/Constants/Constants.cs | 2 +- .../Infrastructure/Jobs/JobConstants.cs | 6 +++--- .../Models/{DbInstance.cs => OdsInstanceManage.cs} | 2 +- .../EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs | 8 ++++---- .../Infrastructure/AdminApiDbContext.cs | 4 ++-- Application/EdFi.Ods.AdminApi.V3/appsettings.json | 8 ++++---- .../EdFi.Ods.AdminApi/Infrastructure/AdminApiDbContext.cs | 4 ++-- .../EdFi.Ods.AdminApi/appsettings.Development.json | 8 ++++---- Application/EdFi.Ods.AdminApi/appsettings.json | 8 ++++---- 9 files changed, 25 insertions(+), 25 deletions(-) rename Application/EdFi.Ods.AdminApi.Common/Infrastructure/Models/{DbInstance.cs => OdsInstanceManage.cs} (97%) diff --git a/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs b/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs index 9b70b343c..747302914 100644 --- a/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs @@ -66,7 +66,7 @@ public enum AdminApiMode Unversioned } -public enum DbInstanceStatus +public enum OdsInstanceManageStatus { PendingCreate, Created, diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Jobs/JobConstants.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Jobs/JobConstants.cs index 4251fb71a..fc1fbc97a 100644 --- a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Jobs/JobConstants.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Jobs/JobConstants.cs @@ -9,12 +9,12 @@ public class JobConstants { public const string JobTypeKey = "JobType"; public const string TenantNameKey = "TenantName"; - public const string DbInstanceIdKey = "DbInstanceId"; + public const string OdsInstanceManageIdKey = "OdsInstanceManageId"; public const string OdsInstanceIdKey = "OdsInstanceId"; public const string CreateInstanceJobName = "CreateInstanceJob"; - public const string CreatePendingDbInstancesDispatcherJobName = "CreatePendingDbInstancesDispatcherJob"; + public const string CreatePendingOdsInstanceManagesDispatcherJobName = "CreatePendingOdsInstanceManagesDispatcherJob"; public const string DeleteInstanceJobName = "DeleteInstanceJob"; - public const string DeletePendingDbInstancesDispatcherJobName = "DeletePendingDbInstancesDispatcherJob"; + public const string DeletePendingOdsInstanceManagesDispatcherJobName = "DeletePendingOdsInstanceManagesDispatcherJob"; public const string RefreshEducationOrganizationsJobName = "RefreshEducationOrganizationsJob"; public const string RunIdKey = "RunId"; } diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Models/DbInstance.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Models/OdsInstanceManage.cs similarity index 97% rename from Application/EdFi.Ods.AdminApi.Common/Infrastructure/Models/DbInstance.cs rename to Application/EdFi.Ods.AdminApi.Common/Infrastructure/Models/OdsInstanceManage.cs index bbb8a5ea2..858cf851e 100644 --- a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Models/DbInstance.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Models/OdsInstanceManage.cs @@ -8,7 +8,7 @@ namespace EdFi.Ods.AdminApi.Common.Infrastructure.Models; -public class DbInstance +public class OdsInstanceManage { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs index 5ee6650ec..a7b23b781 100644 --- a/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs @@ -24,10 +24,10 @@ public class AppSettings public bool PreventDuplicateApplications { get; set; } public bool EnableApplicationResetEndpoint { get; set; } public int EdOrgsRefreshIntervalInMins { get; set; } - public int CreateDbInstancesSweepIntervalInMins { get; set; } = 5; - public int CreateDbInstancesMaxRetryAttempts { get; set; } = 3; - public int DeleteDbInstancesSweepIntervalInMins { get; set; } = 5; - public int DeleteDbInstancesMaxRetryAttempts { get; set; } = 3; + public int CreateOdsInstanceManagesSweepIntervalInMins { get; set; } = 5; + public int CreateOdsInstanceManagesMaxRetryAttempts { get; set; } = 3; + public int DeleteOdsInstanceManagesSweepIntervalInMins { get; set; } = 5; + public int DeleteOdsInstanceManagesMaxRetryAttempts { get; set; } = 3; public int MaxDegreeOfParallelism { get; set; } = 10; public string? AdminApiMode { get; set; } } diff --git a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/AdminApiDbContext.cs b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/AdminApiDbContext.cs index f770b9e75..f212bdab8 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/AdminApiDbContext.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/AdminApiDbContext.cs @@ -20,7 +20,7 @@ public class AdminApiDbContext(DbContextOptions options, ICon public DbSet EducationOrganizations { get; set; } - public DbSet DbInstances { get; set; } + public DbSet OdsInstanceManages { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -33,7 +33,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().ToTable("Tokens").HasKey(t => t.Id); modelBuilder.Entity().ToTable("EducationOrganizations").HasKey(t => t.Id); modelBuilder.Entity().ToTable("JobStatuses").HasKey(t => t.Id); - modelBuilder.Entity().ToTable("DbInstances").HasKey(t => t.Id); + modelBuilder.Entity().ToTable("OdsInstanceManages").HasKey(t => t.Id); var engine = _configuration.Get("AppSettings:DatabaseEngine", "SqlServer"); modelBuilder.ApplyDatabaseServerSpecificConventions(engine); diff --git a/Application/EdFi.Ods.AdminApi.V3/appsettings.json b/Application/EdFi.Ods.AdminApi.V3/appsettings.json index 8ecaa2feb..76fb52999 100644 --- a/Application/EdFi.Ods.AdminApi.V3/appsettings.json +++ b/Application/EdFi.Ods.AdminApi.V3/appsettings.json @@ -9,10 +9,10 @@ "PreventDuplicateApplications": false, "EnableApplicationResetEndpoint": false, "EdOrgsRefreshIntervalInMins": 60, - "CreateDbInstancesSweepIntervalInMins": 120, - "CreateDbInstancesMaxRetryAttempts": 3, - "DeleteDbInstancesSweepIntervalInMins": 120, - "DeleteDbInstancesMaxRetryAttempts": 3, + "CreateOdsInstanceManagesSweepIntervalInMins": 120, + "CreateOdsInstanceManagesMaxRetryAttempts": 3, + "DeleteOdsInstanceManagesSweepIntervalInMins": 120, + "DeleteOdsInstanceManagesMaxRetryAttempts": 3, "MaxDegreeOfParallelism": 10, "adminApiMode": "v2", "SqlServerMinimalBakFile": "", diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiDbContext.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiDbContext.cs index 74fe71db6..8c7171387 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiDbContext.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiDbContext.cs @@ -20,7 +20,7 @@ public class AdminApiDbContext(DbContextOptions options, ICon public DbSet EducationOrganizations { get; set; } - public DbSet DbInstances { get; set; } + public DbSet OdsInstanceManages { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -33,7 +33,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().ToTable("Tokens").HasKey(t => t.Id); modelBuilder.Entity().ToTable("EducationOrganizations").HasKey(t => t.Id); modelBuilder.Entity().ToTable("JobStatuses").HasKey(t => t.Id); - modelBuilder.Entity().ToTable("DbInstances").HasKey(t => t.Id); + modelBuilder.Entity().ToTable("OdsInstanceManages").HasKey(t => t.Id); var engine = _configuration.Get("AppSettings:DatabaseEngine", "SqlServer"); modelBuilder.ApplyDatabaseServerSpecificConventions(engine); diff --git a/Application/EdFi.Ods.AdminApi/appsettings.Development.json b/Application/EdFi.Ods.AdminApi/appsettings.Development.json index a86e8c2ea..ca8ba7e8f 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.Development.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.Development.json @@ -2,10 +2,10 @@ "AppSettings": { "MultiTenancy": true, "DatabaseEngine": "SqlServer", - "CreateDbInstancesSweepIntervalInMins": 5, - "CreateDbInstancesMaxRetryAttempts": 3, - "DeleteDbInstancesSweepIntervalInMins": 5, - "DeleteDbInstancesMaxRetryAttempts": 3, + "CreateOdsInstanceManagesSweepIntervalInMins": 5, + "CreateOdsInstanceManagesMaxRetryAttempts": 3, + "DeleteOdsInstanceManagesSweepIntervalInMins": 5, + "DeleteOdsInstanceManagesMaxRetryAttempts": 3, "IgnoresCertificateErrors": true }, "AdminConsoleSettings": { diff --git a/Application/EdFi.Ods.AdminApi/appsettings.json b/Application/EdFi.Ods.AdminApi/appsettings.json index 969b87d34..0c2d5cc2f 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.json @@ -9,10 +9,10 @@ "PreventDuplicateApplications": false, "EnableApplicationResetEndpoint": false, "EdOrgsRefreshIntervalInMins": 60, - "CreateDbInstancesSweepIntervalInMins": 120, - "CreateDbInstancesMaxRetryAttempts": 3, - "DeleteDbInstancesSweepIntervalInMins": 120, - "DeleteDbInstancesMaxRetryAttempts": 3, + "CreateOdsInstanceManagesSweepIntervalInMins": 120, + "CreateOdsInstanceManagesMaxRetryAttempts": 3, + "DeleteOdsInstanceManagesSweepIntervalInMins": 120, + "DeleteOdsInstanceManagesMaxRetryAttempts": 3, "MaxDegreeOfParallelism": 10, "adminApiMode": "v3", "SqlServerMinimalBakFile": "", From 102fc922281a51ed5e582e3bf3d444704563f694 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 20:26:04 -0600 Subject: [PATCH 06/35] Rename v2 Infrastructure Queries/Commands/Jobs to OdsInstanceManage naming --- ...mand.cs => AddOdsInstanceManageCommand.cs} | 16 ++-- .../Commands/DeleteDbInstanceCommand.cs | 39 -------- .../DeleteOdsInstanceManageCommand.cs | 39 ++++++++ ...ry.cs => GetOdsInstanceManageByIdQuery.cs} | 12 +-- ...Query.cs => GetOdsInstanceManagesQuery.cs} | 12 +-- .../Services/Jobs/CreateInstanceJob.cs | 96 +++++++++---------- ...PendingOdsInstanceManagesDispatcherJob.cs} | 46 ++++----- .../Services/Jobs/DeleteInstanceJob.cs | 58 +++++------ ...PendingOdsInstanceManagesDispatcherJob.cs} | 46 ++++----- 9 files changed, 182 insertions(+), 182 deletions(-) rename Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/{AddDbInstanceCommand.cs => AddOdsInstanceManageCommand.cs} (73%) delete mode 100644 Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteDbInstanceCommand.cs create mode 100644 Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommand.cs rename Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/{GetDbInstanceByIdQuery.cs => GetOdsInstanceManageByIdQuery.cs} (58%) rename Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/{GetDbInstancesQuery.cs => GetOdsInstanceManagesQuery.cs} (68%) rename Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/{CreatePendingDbInstancesDispatcherJob.cs => CreatePendingOdsInstanceManagesDispatcherJob.cs} (68%) rename Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/{DeletePendingDbInstancesDispatcherJob.cs => DeletePendingOdsInstanceManagesDispatcherJob.cs} (68%) diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddDbInstanceCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceManageCommand.cs similarity index 73% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddDbInstanceCommand.cs rename to Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceManageCommand.cs index 39c98b1f8..dd8063c26 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddDbInstanceCommand.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceManageCommand.cs @@ -8,16 +8,16 @@ namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -public class AddDbInstanceCommand +public class AddOdsInstanceManageCommand { private readonly AdminApiDbContext _context; - public AddDbInstanceCommand(AdminApiDbContext context) + public AddOdsInstanceManageCommand(AdminApiDbContext context) { _context = context; } - public DbInstance Execute(IAddDbInstanceModel model) + public OdsInstanceManage Execute(IAddOdsInstanceManageModel model) { if (string.IsNullOrWhiteSpace(model.Name)) throw new ArgumentException("Name is required.", nameof(model)); @@ -26,11 +26,11 @@ public DbInstance Execute(IAddDbInstanceModel model) var now = DateTime.UtcNow; - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Name = model.Name.Trim(), DatabaseTemplate = model.DatabaseTemplate.Trim(), - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), OdsInstanceId = null, OdsInstanceName = null, DatabaseName = null, @@ -38,13 +38,13 @@ public DbInstance Execute(IAddDbInstanceModel model) LastModifiedDate = now }; - _context.DbInstances.Add(dbInstance); + _context.OdsInstanceManages.Add(odsInstanceManage); _context.SaveChanges(); - return dbInstance; + return odsInstanceManage; } } -public interface IAddDbInstanceModel +public interface IAddOdsInstanceManageModel { string? Name { get; } string? DatabaseTemplate { get; } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteDbInstanceCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteDbInstanceCommand.cs deleted file mode 100644 index 91956bd80..000000000 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteDbInstanceCommand.cs +++ /dev/null @@ -1,39 +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.Constants; -using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; - -namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; - -public interface IDeleteDbInstanceCommand -{ - void Execute(int id); -} - -public class DeleteDbInstanceCommand : IDeleteDbInstanceCommand -{ - private readonly AdminApiDbContext _context; - - public DeleteDbInstanceCommand(AdminApiDbContext context) - { - _context = context; - } - - public void Execute(int id) - { - var dbInstance = - _context.DbInstances.Find(id) - ?? throw new NotFoundException("dbInstance", id); - - if (dbInstance.Status != DbInstanceStatus.Created.ToString()) - throw new NotFoundException("dbInstance", id); - - dbInstance.Status = DbInstanceStatus.PendingDelete.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - - _context.SaveChanges(); - } -} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommand.cs new file mode 100644 index 000000000..70327f080 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommand.cs @@ -0,0 +1,39 @@ +// 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.Constants; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public interface IDeleteOdsInstanceManageCommand +{ + void Execute(int id); +} + +public class DeleteOdsInstanceManageCommand : IDeleteOdsInstanceManageCommand +{ + private readonly AdminApiDbContext _context; + + public DeleteOdsInstanceManageCommand(AdminApiDbContext context) + { + _context = context; + } + + public void Execute(int id) + { + var odsInstanceManage = + _context.OdsInstanceManages.Find(id) + ?? throw new NotFoundException("odsInstanceManage", id); + + if (odsInstanceManage.Status != OdsInstanceManageStatus.Created.ToString()) + throw new NotFoundException("odsInstanceManage", id); + + odsInstanceManage.Status = OdsInstanceManageStatus.PendingDelete.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + + _context.SaveChanges(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetDbInstanceByIdQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQuery.cs similarity index 58% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetDbInstanceByIdQuery.cs rename to Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQuery.cs index daa166358..69648114c 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetDbInstanceByIdQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQuery.cs @@ -7,22 +7,22 @@ namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -public interface IGetDbInstanceByIdQuery +public interface IGetOdsInstanceManageByIdQuery { - DbInstance? Execute(int id); + OdsInstanceManage? Execute(int id); } -public class GetDbInstanceByIdQuery : IGetDbInstanceByIdQuery +public class GetOdsInstanceManageByIdQuery : IGetOdsInstanceManageByIdQuery { private readonly AdminApiDbContext _context; - public GetDbInstanceByIdQuery(AdminApiDbContext context) + public GetOdsInstanceManageByIdQuery(AdminApiDbContext context) { _context = context; } - public DbInstance? Execute(int id) + public OdsInstanceManage? Execute(int id) { - return _context.DbInstances.SingleOrDefault(d => d.Id == id); + return _context.OdsInstanceManages.SingleOrDefault(d => d.Id == id); } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetDbInstancesQuery.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManagesQuery.cs similarity index 68% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetDbInstancesQuery.cs rename to Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManagesQuery.cs index 644885610..60abf50ad 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetDbInstancesQuery.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManagesQuery.cs @@ -11,25 +11,25 @@ namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -public interface IGetDbInstancesQuery +public interface IGetOdsInstanceManagesQuery { - List Execute(CommonQueryParams commonQueryParams, int? id, string? name); + List Execute(CommonQueryParams commonQueryParams, int? id, string? name); } -public class GetDbInstancesQuery : IGetDbInstancesQuery +public class GetOdsInstanceManagesQuery : IGetOdsInstanceManagesQuery { private readonly AdminApiDbContext _context; private readonly IOptions _options; - public GetDbInstancesQuery(AdminApiDbContext context, IOptions options) + public GetOdsInstanceManagesQuery(AdminApiDbContext context, IOptions options) { _context = context; _options = options; } - public List Execute(CommonQueryParams commonQueryParams, int? id, string? name) + public List Execute(CommonQueryParams commonQueryParams, int? id, string? name) { - return _context.DbInstances + return _context.OdsInstanceManages .Where(d => id == null || d.Id == id) .Where(d => name == null || d.Name == name) .OrderBy(d => d.Id) diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreateInstanceJob.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreateInstanceJob.cs index ae1cd80f0..af44d5937 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreateInstanceJob.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreateInstanceJob.cs @@ -6,7 +6,7 @@ using EdFi.Admin.DataAccess.Contexts; using EdFi.Admin.DataAccess.Models; using EdFi.Ods.AdminApi.Common.Constants; -using EdFi.Ods.AdminApi.Features.DbInstances; +using EdFi.Ods.AdminApi.Features.OdsInstances.Manage; using EdFi.Ods.AdminApi.Common.Infrastructure.Context; using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; @@ -52,22 +52,22 @@ public class CreateInstanceJob( private readonly IConfiguration _configuration = configuration; private readonly IDbConnectionStringBuilderAdapterFactory _connectionStringBuilderAdapterFactory = connectionStringBuilderAdapterFactory; - internal static JobKey CreateJobKey(int dbInstanceId, string? tenantName) - => new(BuildJobIdentity(dbInstanceId, tenantName)); + internal static JobKey CreateJobKey(int odsInstanceManageId, string? tenantName) + => new(BuildJobIdentity(odsInstanceManageId, tenantName)); - internal static string BuildJobIdentity(int dbInstanceId, string? tenantName) + internal static string BuildJobIdentity(int odsInstanceManageId, string? tenantName) => string.IsNullOrWhiteSpace(tenantName) - ? $"{JobConstants.CreateInstanceJobName}-{dbInstanceId}" - : $"{JobConstants.CreateInstanceJobName}-{tenantName}-{dbInstanceId}"; + ? $"{JobConstants.CreateInstanceJobName}-{odsInstanceManageId}" + : $"{JobConstants.CreateInstanceJobName}-{tenantName}-{odsInstanceManageId}"; protected override async Task ExecuteJobAsync(IJobExecutionContext context) { - if (!context.MergedJobDataMap.ContainsKey(JobConstants.DbInstanceIdKey)) + if (!context.MergedJobDataMap.ContainsKey(JobConstants.OdsInstanceManageIdKey)) { - throw new InvalidOperationException($"{JobConstants.DbInstanceIdKey} must be provided for {JobConstants.CreateInstanceJobName}."); + throw new InvalidOperationException($"{JobConstants.OdsInstanceManageIdKey} must be provided for {JobConstants.CreateInstanceJobName}."); } - var dbInstanceId = context.MergedJobDataMap.GetInt(JobConstants.DbInstanceIdKey); + var odsInstanceManageId = context.MergedJobDataMap.GetInt(JobConstants.OdsInstanceManageIdKey); var multiTenancyEnabled = _options.Value.MultiTenancy; var tenantName = GetTenantName(context, multiTenancyEnabled); @@ -78,7 +78,7 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) TenantConfiguration? tenantConfiguration = null; var adminApiDbContext = _dbContext; var resolvedUsersContext = _usersContext; - DbInstance? dbInstance = null; + OdsInstanceManage? odsInstanceManage = null; try { @@ -95,7 +95,7 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) // on IContextProvider (e.g. ConfigConnectionStringsProvider) resolve // the correct per-tenant connection strings (EdFi_Master, EdFi_Ods, etc.). // The tenant name is always known at this point because it was stored in the job data map - // when CreatePendingDbInstancesDispatcherJob scheduled this job. + // when CreatePendingOdsInstanceManagesDispatcherJob scheduled this job. _tenantConfigurationContextProvider.Set(tenantConfiguration); tenantAdminApiDbContext = _tenantSpecificDbContextProvider.GetAdminApiDbContext(tenantName!); tenantUsersContext = _tenantSpecificDbContextProvider.GetUsersContext(tenantName!); @@ -103,52 +103,52 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) resolvedUsersContext = tenantUsersContext; } - dbInstance = await adminApiDbContext.DbInstances - .FirstOrDefaultAsync(instance => instance.Id == dbInstanceId); + odsInstanceManage = await adminApiDbContext.OdsInstanceManages + .FirstOrDefaultAsync(instance => instance.Id == odsInstanceManageId); - if (dbInstance is null) + if (odsInstanceManage is null) { - throw new InvalidOperationException($"DbInstance '{dbInstanceId}' was not found."); + throw new InvalidOperationException($"OdsInstanceManage '{odsInstanceManageId}' was not found."); } - if (!IsEligibleForProcessing(dbInstance)) + if (!IsEligibleForProcessing(odsInstanceManage)) { return; } - ValidatePendingState(dbInstance); + ValidatePendingState(odsInstanceManage); - var finalName = dbInstance.Name; + var finalName = odsInstanceManage.Name; ValidateFinalName(finalName); var existingOdsInstance = await GetExistingOdsInstanceByNameAsync(resolvedUsersContext, finalName); var now = DateTime.UtcNow; - dbInstance.Status = DbInstanceStatus.CreateInProgress.ToString(); - if (string.IsNullOrWhiteSpace(dbInstance.DatabaseName)) + odsInstanceManage.Status = OdsInstanceManageStatus.CreateInProgress.ToString(); + if (string.IsNullOrWhiteSpace(odsInstanceManage.DatabaseName)) { - dbInstance.DatabaseName = DbInstanceDatabaseNameFormatter.Build( - dbInstance.Name, - dbInstance.DatabaseTemplate); + odsInstanceManage.DatabaseName = OdsInstanceManageDatabaseNameFormatter.Build( + odsInstanceManage.Name, + odsInstanceManage.DatabaseTemplate); } - dbInstance.LastModifiedDate = now; - dbInstance.LastRefreshed = now; + odsInstanceManage.LastModifiedDate = now; + odsInstanceManage.LastRefreshed = now; await adminApiDbContext.SaveChangesAsync(); await _sandboxProvisioner.AddSandboxAsync( - dbInstance.DatabaseName, - GetSandboxType(dbInstance.DatabaseTemplate)); + odsInstanceManage.DatabaseName, + GetSandboxType(odsInstanceManage.DatabaseTemplate)); - var encryptedConnectionString = BuildEncryptedConnectionString(dbInstance.DatabaseName, tenantName); + var encryptedConnectionString = BuildEncryptedConnectionString(odsInstanceManage.DatabaseName, tenantName); var odsInstance = existingOdsInstance ?? new OdsInstance { Name = finalName, - InstanceType = dbInstance.DatabaseTemplate, + InstanceType = odsInstanceManage.DatabaseTemplate, ConnectionString = encryptedConnectionString }; - odsInstance.InstanceType = dbInstance.DatabaseTemplate; + odsInstance.InstanceType = odsInstanceManage.DatabaseTemplate; odsInstance.ConnectionString = encryptedConnectionString; if (existingOdsInstance is null) @@ -158,21 +158,21 @@ await _sandboxProvisioner.AddSandboxAsync( await resolvedUsersContext.SaveChangesAsync(CancellationToken.None); - dbInstance.OdsInstanceId = odsInstance.OdsInstanceId; - dbInstance.OdsInstanceName = finalName; - dbInstance.Status = DbInstanceStatus.Created.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.OdsInstanceId = odsInstance.OdsInstanceId; + odsInstanceManage.OdsInstanceName = finalName; + odsInstanceManage.Status = OdsInstanceManageStatus.Created.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); } catch { - if (dbInstance is not null) + if (odsInstanceManage is not null) { - dbInstance.Status = DbInstanceStatus.CreateFailed.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.CreateFailed.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); } @@ -262,29 +262,29 @@ private static SandboxType GetSandboxType(string databaseTemplate) $"DatabaseTemplate '{databaseTemplate}' cannot be mapped to {nameof(SandboxType)}."); } - private static bool IsEligibleForProcessing(DbInstance dbInstance) + private static bool IsEligibleForProcessing(OdsInstanceManage odsInstanceManage) { - if (!Enum.TryParse(dbInstance.Status, ignoreCase: true, out var status)) + if (!Enum.TryParse(odsInstanceManage.Status, ignoreCase: true, out var status)) { throw new InvalidOperationException( - $"DbInstance '{dbInstance.Id}' has unsupported status '{dbInstance.Status}'."); + $"OdsInstanceManage '{odsInstanceManage.Id}' has unsupported status '{odsInstanceManage.Status}'."); } - return status == DbInstanceStatus.PendingCreate; + return status == OdsInstanceManageStatus.PendingCreate; } - private static void ValidatePendingState(DbInstance dbInstance) + private static void ValidatePendingState(OdsInstanceManage odsInstanceManage) { - if (dbInstance.OdsInstanceId.HasValue || !string.IsNullOrWhiteSpace(dbInstance.OdsInstanceName)) + if (odsInstanceManage.OdsInstanceId.HasValue || !string.IsNullOrWhiteSpace(odsInstanceManage.OdsInstanceName)) { throw new InvalidOperationException( - $"DbInstance '{dbInstance.Id}' is in an invalid pending state because ODS references already exist."); + $"OdsInstanceManage '{odsInstanceManage.Id}' is in an invalid pending state because ODS references already exist."); } - if (string.IsNullOrWhiteSpace(dbInstance.DatabaseTemplate)) + if (string.IsNullOrWhiteSpace(odsInstanceManage.DatabaseTemplate)) { throw new InvalidOperationException( - $"DbInstance '{dbInstance.Id}' is missing DatabaseTemplate."); + $"OdsInstanceManage '{odsInstanceManage.Id}' is missing DatabaseTemplate."); } } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJob.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJob.cs similarity index 68% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJob.cs rename to Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJob.cs index f29a0ce7b..1a8d133ab 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJob.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJob.cs @@ -16,8 +16,8 @@ namespace EdFi.Ods.AdminApi.Infrastructure.Services.Jobs; [DisallowConcurrentExecution] -public class CreatePendingDbInstancesDispatcherJob( - ILogger logger, +public class CreatePendingOdsInstanceManagesDispatcherJob( + ILogger logger, IJobStatusService jobStatusService, AdminApiDbContext dbContext, ITenantSpecificDbContextProvider tenantSpecificDbContextProvider, @@ -45,34 +45,34 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) adminApiDbContext = tenantAdminApiDbContext; } - var eligibleDbInstances = await adminApiDbContext.DbInstances - .Where(instance => instance.Status == DbInstanceStatus.PendingCreate.ToString() || instance.Status == DbInstanceStatus.CreateFailed.ToString()) + var eligibleOdsInstanceManages = await adminApiDbContext.OdsInstanceManages + .Where(instance => instance.Status == OdsInstanceManageStatus.PendingCreate.ToString() || instance.Status == OdsInstanceManageStatus.CreateFailed.ToString()) .OrderBy(instance => instance.Id) .ToListAsync(); - foreach (var dbInstance in eligibleDbInstances) + foreach (var odsInstanceManage in eligibleOdsInstanceManages) { - if (string.Equals(dbInstance.Status, DbInstanceStatus.PendingCreate.ToString(), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(odsInstanceManage.Status, OdsInstanceManageStatus.PendingCreate.ToString(), StringComparison.OrdinalIgnoreCase)) { - await ScheduleCreateJobAsync(context, dbInstance.Id, tenantName); + await ScheduleCreateJobAsync(context, odsInstanceManage.Id, tenantName); continue; } - if (!await IsRetryEligibleAsync(adminApiDbContext, dbInstance, tenantName)) + if (!await IsRetryEligibleAsync(adminApiDbContext, odsInstanceManage, tenantName)) { - dbInstance.Status = DbInstanceStatus.CreateError.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.CreateError.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); continue; } - dbInstance.Status = DbInstanceStatus.PendingCreate.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.PendingCreate.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); - await ScheduleCreateJobAsync(context, dbInstance.Id, tenantName); + await ScheduleCreateJobAsync(context, odsInstanceManage.Id, tenantName); } } finally @@ -84,24 +84,24 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) } } - private async Task IsRetryEligibleAsync(AdminApiDbContext adminApiDbContext, DbInstance dbInstance, string? tenantName) + private async Task IsRetryEligibleAsync(AdminApiDbContext adminApiDbContext, OdsInstanceManage odsInstanceManage, string? tenantName) { - var maxRetryAttempts = _options.Value.CreateDbInstancesMaxRetryAttempts > 0 - ? _options.Value.CreateDbInstancesMaxRetryAttempts + var maxRetryAttempts = _options.Value.CreateOdsInstanceManagesMaxRetryAttempts > 0 + ? _options.Value.CreateOdsInstanceManagesMaxRetryAttempts : DefaultMaxRetryAttempts; - var jobIdPrefix = $"{CreateInstanceJob.BuildJobIdentity(dbInstance.Id, tenantName)}_"; + var jobIdPrefix = $"{CreateInstanceJob.BuildJobIdentity(odsInstanceManage.Id, tenantName)}_"; var errorCount = await adminApiDbContext.JobStatuses .CountAsync(status => status.JobId.StartsWith(jobIdPrefix) && status.Status == QuartzJobStatus.Error.ToString()); return errorCount < maxRetryAttempts; } - private static async Task ScheduleCreateJobAsync(IJobExecutionContext context, int dbInstanceId, string? tenantName) + private static async Task ScheduleCreateJobAsync(IJobExecutionContext context, int odsInstanceManageId, string? tenantName) { var jobData = new Dictionary { - [JobConstants.DbInstanceIdKey] = dbInstanceId + [JobConstants.OdsInstanceManageIdKey] = odsInstanceManageId }; if (!string.IsNullOrWhiteSpace(tenantName)) @@ -111,7 +111,7 @@ private static async Task ScheduleCreateJobAsync(IJobExecutionContext context, i await QuartzJobScheduler.ScheduleJob( context.Scheduler, - CreateInstanceJob.CreateJobKey(dbInstanceId, tenantName), + CreateInstanceJob.CreateJobKey(odsInstanceManageId, tenantName), jobData, startImmediately: true); } @@ -135,4 +135,4 @@ await QuartzJobScheduler.ScheduleJob( return tenantName; } -} \ No newline at end of file +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeleteInstanceJob.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeleteInstanceJob.cs index 48b962ee4..1638fbec8 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeleteInstanceJob.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeleteInstanceJob.cs @@ -39,22 +39,22 @@ public class DeleteInstanceJob( private readonly ISandboxProvisioner _sandboxProvisioner = sandboxProvisioner; private readonly IOptions _options = options; - internal static JobKey CreateJobKey(int dbInstanceId, string? tenantName) - => new(BuildJobIdentity(dbInstanceId, tenantName)); + internal static JobKey CreateJobKey(int odsInstanceManageId, string? tenantName) + => new(BuildJobIdentity(odsInstanceManageId, tenantName)); - internal static string BuildJobIdentity(int dbInstanceId, string? tenantName) + internal static string BuildJobIdentity(int odsInstanceManageId, string? tenantName) => string.IsNullOrWhiteSpace(tenantName) - ? $"{JobConstants.DeleteInstanceJobName}-{dbInstanceId}" - : $"{JobConstants.DeleteInstanceJobName}-{tenantName}-{dbInstanceId}"; + ? $"{JobConstants.DeleteInstanceJobName}-{odsInstanceManageId}" + : $"{JobConstants.DeleteInstanceJobName}-{tenantName}-{odsInstanceManageId}"; protected override async Task ExecuteJobAsync(IJobExecutionContext context) { - if (!context.MergedJobDataMap.ContainsKey(JobConstants.DbInstanceIdKey)) + if (!context.MergedJobDataMap.ContainsKey(JobConstants.OdsInstanceManageIdKey)) { - throw new InvalidOperationException($"{JobConstants.DbInstanceIdKey} must be provided for {JobConstants.DeleteInstanceJobName}."); + throw new InvalidOperationException($"{JobConstants.OdsInstanceManageIdKey} must be provided for {JobConstants.DeleteInstanceJobName}."); } - var dbInstanceId = context.MergedJobDataMap.GetInt(JobConstants.DbInstanceIdKey); + var odsInstanceManageId = context.MergedJobDataMap.GetInt(JobConstants.OdsInstanceManageIdKey); var multiTenancyEnabled = _options.Value.MultiTenancy; var tenantName = GetTenantName(context, multiTenancyEnabled); @@ -62,7 +62,7 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) IUsersContext? tenantUsersContext = null; var adminApiDbContext = _dbContext; var resolvedUsersContext = _usersContext; - DbInstance? dbInstance = null; + OdsInstanceManage? odsInstanceManage = null; try { @@ -81,35 +81,35 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) resolvedUsersContext = tenantUsersContext; } - dbInstance = await adminApiDbContext.DbInstances - .FirstOrDefaultAsync(instance => instance.Id == dbInstanceId); + odsInstanceManage = await adminApiDbContext.OdsInstanceManages + .FirstOrDefaultAsync(instance => instance.Id == odsInstanceManageId); - if (dbInstance is null) + if (odsInstanceManage is null) { - throw new InvalidOperationException($"DbInstance '{dbInstanceId}' was not found."); + throw new InvalidOperationException($"OdsInstanceManage '{odsInstanceManageId}' was not found."); } // Guard against race conditions — only process PendingDelete rows. - if (!Enum.TryParse(dbInstance.Status, ignoreCase: true, out var status) - || status != DbInstanceStatus.PendingDelete) + if (!Enum.TryParse(odsInstanceManage.Status, ignoreCase: true, out var status) + || status != OdsInstanceManageStatus.PendingDelete) { return; } - dbInstance.Status = DbInstanceStatus.DeleteInProgress.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.DeleteInProgress.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); - if (!string.IsNullOrWhiteSpace(dbInstance.DatabaseName)) + if (!string.IsNullOrWhiteSpace(odsInstanceManage.DatabaseName)) { - await _sandboxProvisioner.DeleteSandboxesAsync(dbInstance.DatabaseName); + await _sandboxProvisioner.DeleteSandboxesAsync(odsInstanceManage.DatabaseName); } - if (dbInstance.OdsInstanceId.HasValue) + if (odsInstanceManage.OdsInstanceId.HasValue) { var odsInstance = await resolvedUsersContext.OdsInstances - .FindAsync(dbInstance.OdsInstanceId.Value); + .FindAsync(odsInstanceManage.OdsInstanceId.Value); if (odsInstance is not null) { @@ -118,18 +118,18 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) } } - dbInstance.Status = DbInstanceStatus.Deleted.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.Deleted.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); } catch { - if (dbInstance is not null) + if (odsInstanceManage is not null) { - dbInstance.Status = DbInstanceStatus.DeleteFailed.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.DeleteFailed.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); } diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJob.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJob.cs similarity index 68% rename from Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJob.cs rename to Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJob.cs index 14255d7e5..dc5d93c35 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJob.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJob.cs @@ -15,8 +15,8 @@ namespace EdFi.Ods.AdminApi.Infrastructure.Services.Jobs; [DisallowConcurrentExecution] -public class DeletePendingDbInstancesDispatcherJob( - ILogger logger, +public class DeletePendingOdsInstanceManagesDispatcherJob( + ILogger logger, IJobStatusService jobStatusService, AdminApiDbContext dbContext, ITenantSpecificDbContextProvider tenantSpecificDbContextProvider, @@ -44,35 +44,35 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) adminApiDbContext = tenantAdminApiDbContext; } - var eligibleDbInstances = await adminApiDbContext.DbInstances - .Where(instance => instance.Status == DbInstanceStatus.PendingDelete.ToString() - || instance.Status == DbInstanceStatus.DeleteFailed.ToString()) + var eligibleOdsInstanceManages = await adminApiDbContext.OdsInstanceManages + .Where(instance => instance.Status == OdsInstanceManageStatus.PendingDelete.ToString() + || instance.Status == OdsInstanceManageStatus.DeleteFailed.ToString()) .OrderBy(instance => instance.Id) .ToListAsync(); - foreach (var dbInstance in eligibleDbInstances) + foreach (var odsInstanceManage in eligibleOdsInstanceManages) { - if (string.Equals(dbInstance.Status, DbInstanceStatus.PendingDelete.ToString(), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(odsInstanceManage.Status, OdsInstanceManageStatus.PendingDelete.ToString(), StringComparison.OrdinalIgnoreCase)) { - await ScheduleDeleteJobAsync(context, dbInstance.Id, tenantName); + await ScheduleDeleteJobAsync(context, odsInstanceManage.Id, tenantName); continue; } - if (!await IsRetryEligibleAsync(adminApiDbContext, dbInstance, tenantName)) + if (!await IsRetryEligibleAsync(adminApiDbContext, odsInstanceManage, tenantName)) { - dbInstance.Status = DbInstanceStatus.DeleteError.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.DeleteError.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); continue; } - dbInstance.Status = DbInstanceStatus.PendingDelete.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.PendingDelete.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); - await ScheduleDeleteJobAsync(context, dbInstance.Id, tenantName); + await ScheduleDeleteJobAsync(context, odsInstanceManage.Id, tenantName); } } finally @@ -84,24 +84,24 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) } } - private async Task IsRetryEligibleAsync(AdminApiDbContext adminApiDbContext, DbInstance dbInstance, string? tenantName) + private async Task IsRetryEligibleAsync(AdminApiDbContext adminApiDbContext, OdsInstanceManage odsInstanceManage, string? tenantName) { - var maxRetryAttempts = _options.Value.DeleteDbInstancesMaxRetryAttempts > 0 - ? _options.Value.DeleteDbInstancesMaxRetryAttempts + var maxRetryAttempts = _options.Value.DeleteOdsInstanceManagesMaxRetryAttempts > 0 + ? _options.Value.DeleteOdsInstanceManagesMaxRetryAttempts : DefaultMaxRetryAttempts; - var jobIdPrefix = $"{DeleteInstanceJob.BuildJobIdentity(dbInstance.Id, tenantName)}_"; + var jobIdPrefix = $"{DeleteInstanceJob.BuildJobIdentity(odsInstanceManage.Id, tenantName)}_"; var errorCount = await adminApiDbContext.JobStatuses .CountAsync(status => status.JobId.StartsWith(jobIdPrefix) && status.Status == QuartzJobStatus.Error.ToString()); return errorCount < maxRetryAttempts; } - private static async Task ScheduleDeleteJobAsync(IJobExecutionContext context, int dbInstanceId, string? tenantName) + private static async Task ScheduleDeleteJobAsync(IJobExecutionContext context, int odsInstanceManageId, string? tenantName) { var jobData = new Dictionary { - [JobConstants.DbInstanceIdKey] = dbInstanceId + [JobConstants.OdsInstanceManageIdKey] = odsInstanceManageId }; if (!string.IsNullOrWhiteSpace(tenantName)) @@ -111,7 +111,7 @@ private static async Task ScheduleDeleteJobAsync(IJobExecutionContext context, i await QuartzJobScheduler.ScheduleJob( context.Scheduler, - DeleteInstanceJob.CreateJobKey(dbInstanceId, tenantName), + DeleteInstanceJob.CreateJobKey(odsInstanceManageId, tenantName), jobData, startImmediately: true); } From 99c6f633a808adad39e9d1ce3d4f12bf05a6d9f2 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 20:34:01 -0600 Subject: [PATCH 07/35] Amend plan: cover Features/Tenants + TenantService (missed in original research) Task 3's implementer found that Features/Tenants/* and Infrastructure/Services/Tenants/TenantService.cs (both v2 and v3) also consume the renamed DbInstance/DbDataStore query and entity types, but the original plan never listed them. Folds the fix into the existing Tasks 5, 7, 12, and 14 rather than adding new task numbers. Also fixes the plan's incorrect .sln filename references (Ed-Fi-ODS-AdminApi.sln is the actual solution file). --- ...rename-dbinstances-to-odsinstancemanage.md | 492 +++++++++++++++++- 1 file changed, 467 insertions(+), 25 deletions(-) diff --git a/docs/superpowers/plans/2026-07-27-rename-dbinstances-to-odsinstancemanage.md b/docs/superpowers/plans/2026-07-27-rename-dbinstances-to-odsinstancemanage.md index 79f36567f..16b47cf30 100644 --- a/docs/superpowers/plans/2026-07-27-rename-dbinstances-to-odsinstancemanage.md +++ b/docs/superpowers/plans/2026-07-27-rename-dbinstances-to-odsinstancemanage.md @@ -71,6 +71,8 @@ Reference this table from every task below instead of repeating it. Every occurr | Route `/dbInstances` | `/odsInstances/manage` | | `OdsInstanceWithEducationOrganizationsModel.DbInstanceId` | `OdsInstanceManageId` | | `MergeDbInstanceData` | `MergeOdsInstanceManageData` | +| `TenantOdsInstanceModel.DbInstanceId` (`Features\Tenants\TenantDetailModel.cs`) | `OdsInstanceManageId` | +| `TenantMapper.ToUnlinkedDbInstanceModel` | `TenantMapper.ToUnlinkedOdsInstanceManageModel` | ### v3-specific (`EdFi.Ods.AdminApi.V3` project) @@ -96,6 +98,7 @@ Reference this table from every task below instead of repeating it. Every occurr | Route `/dbDataStores` | `/dataStores/manage` | | `MergeDbDataStoreData` | `MergeDataStoreManageData` | | *(new field, no old counterpart)* | `DataStoreWithEducationOrganizationsModel.DataStoreManageId` | +| `TenantMapper.ToUnlinkedDbDataStoreModel` | `TenantMapper.ToUnlinkedDataStoreManageModel` | Note: `DataStoreModel.DataStoreId`/`DataStoreModel.DataStoreType` (the existing `DataStore`'s own DTO fields, unrelated file) and `DbDataStoreModel.DataStoreId`/`DataStoreName` (already-renamed-at-DTO-level fields carried over unchanged into `DataStoreManageModel`) are **not** touched — only tokens literally containing `DbInstance`/`DbDataStore` change. @@ -361,7 +364,7 @@ Same four-key rename as Step 6, applied to `Application\EdFi.Ods.AdminApi.V3\app - [ ] **Step 9: Build to confirm no compile errors yet from callers** -Run: `dotnet build Application/EdFi.Ods.AdminApi.sln` (or the solution file this repo uses — check for a `.sln` at the repo root or under `Application\`). +Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` (or the solution file this repo uses — check for a `.sln` at the repo root or under `Application\`). Expected: FAILS — callers in v2/v3 Features/Infrastructure/Jobs still reference the old `DbInstance`/`DbInstanceStatus`/old `JobConstants`/old `AppSettings` names. This is expected at this checkpoint; Tasks 3–15 fix each caller. Confirm the failures are all in files this plan's later tasks will touch (grep the build output for `DbInstance`/`DbDataStore` to sanity-check no unexpected file is affected). - [ ] **Step 10: Commit** @@ -758,7 +761,7 @@ In `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Jobs\DeleteInstanceJob - [ ] **Step 5: Build to confirm the Infrastructure layer compiles in isolation** -Run: `dotnet build Application/EdFi.Ods.AdminApi.sln` +Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` Expected: still FAILS — `Features\DbInstances\*` (Task 4), `Features\OdsInstances\*` (Task 5), `Program.cs`/`WebApplicationBuilderExtensions.cs` (Task 6) haven't been updated yet. Confirm the remaining errors are now confined to those files only (no errors left in `Infrastructure\Database\*` or `Infrastructure\Services\Jobs\*`). - [ ] **Step 6: Commit** @@ -1247,7 +1250,7 @@ internal static class OdsInstanceManageDatabaseNameFormatter - [ ] **Step 8: Build** -Run: `dotnet build Application/EdFi.Ods.AdminApi.sln` +Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` Expected: remaining errors confined to `Features\OdsInstances\OdsInstanceWithEducationOrganizationsModel.cs`/`ReadEducationOrganizations.cs` (Task 5) and `Program.cs`/`WebApplicationBuilderExtensions.cs` (Task 6). - [ ] **Step 9: Commit** @@ -1260,15 +1263,21 @@ git commit -m "Move v2 DbInstances feature into OdsInstances/Manage, rename rout --- -### Task 5: v2 existing `OdsInstances` files — update references to the renamed query/enum +### Task 5: v2 existing `OdsInstances` and `Tenants` files — update references to the renamed query/enum + +**Amendment (discovered during Task 3 implementation):** the original plan missed a whole consumer area — `Features\Tenants\*` and `Infrastructure\Services\Tenants\TenantService.cs` also inject `IGetDbInstancesQuery`/`DbInstance`/`DbInstanceStatus` to build the `/tenants/{tenantName}/odsInstances/edOrgs` response. This section folds that fix into Task 5 rather than adding a new task number. **Files:** - Modify: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\OdsInstanceWithEducationOrganizationsModel.cs` - Modify: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\ReadEducationOrganizations.cs` +- Modify: `Application\EdFi.Ods.AdminApi\Features\Tenants\TenantDetailModel.cs` +- Modify: `Application\EdFi.Ods.AdminApi\Features\Tenants\TenantMapper.cs` +- Modify: `Application\EdFi.Ods.AdminApi\Features\Tenants\ReadTenants.cs` +- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Tenants\TenantService.cs` **Interfaces:** - Consumes: `IGetOdsInstanceManagesQuery` (Task 3), `OdsInstanceManageStatus` (Task 2). -- Produces: `OdsInstanceWithEducationOrganizationsModel.OdsInstanceManageId` (renamed from `DbInstanceId`), consumed by nothing else in this plan (public API response shape only). +- Produces: `OdsInstanceWithEducationOrganizationsModel.OdsInstanceManageId` (renamed from `DbInstanceId`), `TenantOdsInstanceModel.OdsInstanceManageId` (renamed from `DbInstanceId`), `TenantMapper.ToUnlinkedOdsInstanceManageModel` (renamed from `ToUnlinkedDbInstanceModel`), `ITenantsService.GetTenantEdOrgsByInstancesAsync(..., IGetOdsInstanceManagesQuery, ...)` — all public API response shape / internal wiring only, consumed by nothing else in this plan. - [ ] **Step 1: Rename the `DbInstanceId` property** @@ -1364,17 +1373,236 @@ public class ReadEducationOrganizations : IFeature } ``` -- [ ] **Step 3: Build** +- [ ] **Step 3: Rename `TenantOdsInstanceModel.DbInstanceId` in `TenantDetailModel.cs`** -Run: `dotnet build Application/EdFi.Ods.AdminApi.sln` -Expected: remaining errors confined to `Program.cs`/`WebApplicationBuilderExtensions.cs` (Task 6). +Replace: -- [ ] **Step 4: Commit** +```csharp + [JsonPropertyName("id")] + public int? OdsInstanceId { get; set; } + public int? DbInstanceId { get; set; } +``` + +with: + +```csharp + [JsonPropertyName("id")] + public int? OdsInstanceId { get; set; } + public int? OdsInstanceManageId { get; set; } +``` + +- [ ] **Step 4: Update `TenantMapper.cs`** + +Replace the full file content with: + +```csharp +// 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.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; + +namespace EdFi.Ods.AdminApi.Features.Tenants; + +public static class TenantMapper +{ + public static TenantOdsInstanceModel ToOdsInstanceModel(OdsInstance source) + { + return new TenantOdsInstanceModel + { + OdsInstanceId = source.OdsInstanceId, + Name = source.Name, + InstanceType = source.InstanceType, + }; + } + + public static List ToOdsInstanceModelList(IEnumerable source) + { + return source.Select(ToOdsInstanceModel).ToList(); + } + + public static TenantOdsInstanceModel ToUnlinkedOdsInstanceManageModel(OdsInstanceManage source) + { + return new TenantOdsInstanceModel + { + OdsInstanceId = null, + OdsInstanceManageId = source.Id, + Name = source.Name, + Status = source.Status, + DatabaseTemplate = source.DatabaseTemplate, + DatabaseName = source.DatabaseName, + }; + } +} +``` + +- [ ] **Step 5: Update `ReadTenants.cs`** + +Replace: + +```csharp + IGetDbInstancesQuery getDbInstancesQuery, +``` + +with: + +```csharp + IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery, +``` + +and replace: + +```csharp + var tenant = await tenantsService.GetTenantEdOrgsByInstancesAsync( + getOdsInstancesQuery, getEducationOrganizationQuery, getDbInstancesQuery, tenantName); +``` + +with: + +```csharp + var tenant = await tenantsService.GetTenantEdOrgsByInstancesAsync( + getOdsInstancesQuery, getEducationOrganizationQuery, getOdsInstanceManagesQuery, tenantName); +``` + +Add `using EdFi.Ods.AdminApi.Infrastructure.Database.Queries;` if not already present (it already is — `IGetOdsInstanceManagesQuery` lives in the same namespace as `IGetOdsInstancesQuery`/`IGetEducationOrganizationQuery`, both already imported in this file). + +- [ ] **Step 6: Update `TenantService.cs`** + +Replace the interface line: + +```csharp + Task GetTenantEdOrgsByInstancesAsync(IGetOdsInstancesQuery getOdsInstancesQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, IGetDbInstancesQuery getDbInstancesQuery, string tenantName); +``` + +with: + +```csharp + Task GetTenantEdOrgsByInstancesAsync(IGetOdsInstancesQuery getOdsInstancesQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery, string tenantName); +``` + +Replace the method signature: + +```csharp + public async Task GetTenantEdOrgsByInstancesAsync( + IGetOdsInstancesQuery getOdsInstancesQuery, + IGetEducationOrganizationQuery getEducationOrganizationQuery, + IGetDbInstancesQuery getDbInstancesQuery, + string tenantName) +``` + +with: + +```csharp + public async Task GetTenantEdOrgsByInstancesAsync( + IGetOdsInstancesQuery getOdsInstancesQuery, + IGetEducationOrganizationQuery getEducationOrganizationQuery, + IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery, + string tenantName) +``` + +Replace the method body's use of the renamed query and entity: + +```csharp + var allDbInstances = getDbInstancesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); + + var linkedDbInstancesByOdsId = allDbInstances + .Where(d => d.OdsInstanceId is not null) + .GroupBy(d => d.OdsInstanceId!.Value) + .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); + + foreach (var odsInstance in tenantDetails.OdsInstances) + { + if (odsInstance.OdsInstanceId is int odsInstanceId && linkedDbInstancesByOdsId.TryGetValue(odsInstanceId, out var dbInstance)) + { + odsInstance.DbInstanceId = dbInstance.Id; + odsInstance.Status = dbInstance.Status; + odsInstance.DatabaseTemplate = dbInstance.DatabaseTemplate; + odsInstance.DatabaseName = dbInstance.DatabaseName; + } + else + { + odsInstance.Status = DbInstanceStatus.Created.ToString(); + } + } + + var existingOdsInstanceIds = tenantDetails.OdsInstances + .Where(i => i.OdsInstanceId is int) + .Select(i => i.OdsInstanceId!.Value) + .ToHashSet(); + + var unlinkedDbInstances = allDbInstances + .Where(d => d.OdsInstanceId is null) + .Concat(allDbInstances + .Where(d => d.OdsInstanceId is not null && !existingOdsInstanceIds.Contains(d.OdsInstanceId.Value)) + .GroupBy(d => d.OdsInstanceId!.Value) + .Select(g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First())) + .ToList(); + foreach (var dbInstance in unlinkedDbInstances) + { + tenantDetails.OdsInstances.Add(TenantMapper.ToUnlinkedDbInstanceModel(dbInstance)); + } +``` + +with: + +```csharp + var allOdsInstanceManages = getOdsInstanceManagesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); + + var linkedOdsInstanceManagesByOdsId = allOdsInstanceManages + .Where(d => d.OdsInstanceId is not null) + .GroupBy(d => d.OdsInstanceId!.Value) + .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); + + foreach (var odsInstance in tenantDetails.OdsInstances) + { + if (odsInstance.OdsInstanceId is int odsInstanceId && linkedOdsInstanceManagesByOdsId.TryGetValue(odsInstanceId, out var odsInstanceManage)) + { + odsInstance.OdsInstanceManageId = odsInstanceManage.Id; + odsInstance.Status = odsInstanceManage.Status; + odsInstance.DatabaseTemplate = odsInstanceManage.DatabaseTemplate; + odsInstance.DatabaseName = odsInstanceManage.DatabaseName; + } + else + { + odsInstance.Status = OdsInstanceManageStatus.Created.ToString(); + } + } + + var existingOdsInstanceIds = tenantDetails.OdsInstances + .Where(i => i.OdsInstanceId is int) + .Select(i => i.OdsInstanceId!.Value) + .ToHashSet(); + + var unlinkedOdsInstanceManages = allOdsInstanceManages + .Where(d => d.OdsInstanceId is null) + .Concat(allOdsInstanceManages + .Where(d => d.OdsInstanceId is not null && !existingOdsInstanceIds.Contains(d.OdsInstanceId.Value)) + .GroupBy(d => d.OdsInstanceId!.Value) + .Select(g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First())) + .ToList(); + foreach (var odsInstanceManage in unlinkedOdsInstanceManages) + { + tenantDetails.OdsInstances.Add(TenantMapper.ToUnlinkedOdsInstanceManageModel(odsInstanceManage)); + } +``` + +- [ ] **Step 7: Build** + +Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` +Expected: remaining errors confined to `Program.cs`/`WebApplicationBuilderExtensions.cs` (Task 6) and all of v3 (Tasks 10-13, not yet done). + +- [ ] **Step 8: Commit** ```bash git add "Application/EdFi.Ods.AdminApi/Features/OdsInstances/OdsInstanceWithEducationOrganizationsModel.cs" \ - "Application/EdFi.Ods.AdminApi/Features/OdsInstances/ReadEducationOrganizations.cs" -git commit -m "Update v2 OdsInstances EdOrgs merge to use renamed OdsInstanceManage query" + "Application/EdFi.Ods.AdminApi/Features/OdsInstances/ReadEducationOrganizations.cs" \ + "Application/EdFi.Ods.AdminApi/Features/Tenants/TenantDetailModel.cs" \ + "Application/EdFi.Ods.AdminApi/Features/Tenants/TenantMapper.cs" \ + "Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs" \ + "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Tenants/TenantService.cs" +git commit -m "Update v2 OdsInstances and Tenants EdOrgs merges to use renamed OdsInstanceManage query" ``` --- @@ -1479,7 +1707,7 @@ Also find and rename the corresponding `DeletePendingDbInstancesDispatcherJob` r - [ ] **Step 5: Build the full solution** -Run: `dotnet build Application/EdFi.Ods.AdminApi.sln` +Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` Expected: v2 side (everything except v3's `EdFi.Ods.AdminApi.V3` project) now compiles cleanly. Remaining errors should be entirely inside `Application\EdFi.Ods.AdminApi.V3\` (fixed by Tasks 10–13) and its `.UnitTests`/`.DBTests` counterparts (Tasks 14–15) and `Application\EdFi.Ods.AdminApi.UnitTests`/`.DBTests` (Tasks 7–8, not yet done). - [ ] **Step 6: Commit** @@ -1506,9 +1734,14 @@ git commit -m "Update v2 Quartz wiring to use renamed OdsInstanceManage jobs and - Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Jobs\DeletePendingDbInstancesDispatcherJobTests.cs` → rename to `DeletePendingOdsInstanceManagesDispatcherJobTests.cs` - Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Jobs\CreateInstanceJobTests.cs` (renamed in place, filename unchanged) - Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Jobs\DeleteInstanceJobTests.cs` (renamed in place, filename unchanged) +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Features\Tenants\ReadTenantsTest.cs` (filename unchanged — update references to Task 5's renamed Tenants types) +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Features\Tenants\TenantDetailModelTests.cs` (filename unchanged — update references) +- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Tenants\TenantServiceTests.cs` (filename unchanged — update references) + +**Amendment (same gap as Task 5):** the three Tenants test files above were missed in the original plan — they test `Features\Tenants\*`/`TenantService.cs`, which Task 5 (amended) now renames. Their production counterparts changed in Task 5, so these tests need matching updates or they won't compile. **Interfaces:** -- Consumes: every production type from Tasks 2–6 (`OdsInstanceManage`, `OdsInstanceManageStatus`, `IGetOdsInstanceManagesQuery`, `AddOdsInstanceManageCommand`, `AddOdsInstanceManage`/`ReadOdsInstanceManage`/`DeleteOdsInstanceManage` features, `CreatePendingOdsInstanceManagesDispatcherJob`, etc.). +- Consumes: every production type from Tasks 2–6 (`OdsInstanceManage`, `OdsInstanceManageStatus`, `IGetOdsInstanceManagesQuery`, `AddOdsInstanceManageCommand`, `AddOdsInstanceManage`/`ReadOdsInstanceManage`/`DeleteOdsInstanceManage` features, `CreatePendingOdsInstanceManagesDispatcherJob`, etc.), plus Task 5's renamed `TenantOdsInstanceModel.OdsInstanceManageId`, `TenantMapper.ToUnlinkedOdsInstanceManageModel`, `ITenantsService.GetTenantEdOrgsByInstancesAsync(..., IGetOdsInstanceManagesQuery, ...)`. - [ ] **Step 1: Move and rename the query test files (full content known — apply directly)** @@ -1688,12 +1921,16 @@ git mv "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/Del For each moved file and for the two in-place files (`CreateInstanceJobTests.cs`, `DeleteInstanceJobTests.cs`), open it and apply every substitution from the "v2-specific" and "Shared" Master Rename Table sections above (class name matching the new filename, `namespace ...UnitTests.Features.DbInstances` → `...UnitTests.Features.OdsInstances.Manage`, every `DbInstance`/`OdsInstance...`-adjacent identifier, string literal route fragments like `"/dbInstances"` → `"/odsInstances/manage"` if any test asserts on the route string, and any `[TestFixture] public class AddDbInstanceTests` → `AddOdsInstanceManageTests`). -- [ ] **Step 3: Run the v2 unit test suite** +- [ ] **Step 3: Update the three Tenants-area test files (filenames unchanged)** + +Open `Application\EdFi.Ods.AdminApi.UnitTests\Features\Tenants\ReadTenantsTest.cs`, `Application\EdFi.Ods.AdminApi.UnitTests\Features\Tenants\TenantDetailModelTests.cs`, and `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Tenants\TenantServiceTests.cs`. Update every reference to match Task 5's renames: `IGetDbInstancesQuery`/`getDbInstancesQuery` → `IGetOdsInstanceManagesQuery`/`getOdsInstanceManagesQuery`, `DbInstance`/`DbInstanceStatus` → `OdsInstanceManage`/`OdsInstanceManageStatus` (mock/fixture setups), `TenantOdsInstanceModel.DbInstanceId` → `.OdsInstanceManageId`, `TenantMapper.ToUnlinkedDbInstanceModel` → `.ToUnlinkedOdsInstanceManageModel`. Do not rename the files or their test class names — only internal references. + +- [ ] **Step 4: Run the v2 unit test suite** Run: `dotnet test Application/EdFi.Ods.AdminApi.UnitTests/EdFi.Ods.AdminApi.UnitTests.csproj` Expected: PASS, same test count as before the rename (compare `git stash`'s pre-change `dotnet test` output count if unsure, or just confirm no failures/errors and no tests silently vanished — count should match the pre-rename baseline exactly since this task renames tests, not delete/add them). -- [ ] **Step 4: Commit** +- [ ] **Step 5: Commit** ```bash git add "Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage" \ @@ -1705,7 +1942,10 @@ git add "Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage" \ "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJobTests.cs" \ "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJobTests.cs" \ "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreateInstanceJobTests.cs" \ - "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeleteInstanceJobTests.cs" + "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeleteInstanceJobTests.cs" \ + "Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/ReadTenantsTest.cs" \ + "Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/TenantDetailModelTests.cs" \ + "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs" git commit -m "Rename v2 unit tests to OdsInstanceManage naming" ``` @@ -2222,7 +2462,7 @@ Apply the same substitutions as Task 3 Step 4, but for the v3 files: replace `us - [ ] **Step 5: Build to confirm the v3 Infrastructure layer compiles in isolation** -Run: `dotnet build Application/EdFi.Ods.AdminApi.V3.sln` (or the appropriate project reference — confirm the v3 project's build command from `docs/developer.md`). +Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` (or the appropriate project reference — confirm the v3 project's build command from `docs/developer.md`). Expected: remaining errors confined to `Features\DbDataStores\*` (Task 11), `Features\DataStores\*` (Task 12), and `Program.cs`/`WebApplicationBuilderExtensions.cs` (Task 13, shared file already partially updated in Task 6). - [ ] **Step 6: Commit** @@ -2711,7 +2951,7 @@ internal static class DataStoreManageDatabaseNameFormatter - [ ] **Step 8: Build** -Run: `dotnet build Application/EdFi.Ods.AdminApi.V3.sln` +Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` Expected: remaining errors confined to `Features\DataStores\DataStoreWithEducationOrganizationsModel.cs`/`ReadEducationOrganizations.cs` (Task 12) and `Program.cs`/`WebApplicationBuilderExtensions.cs` V3 branches (Task 13). - [ ] **Step 9: Commit** @@ -2724,15 +2964,20 @@ git commit -m "Move v3 DbDataStores feature into DataStores/Manage, rename route --- -### Task 12: v3 existing `DataStores` files — update references and close the `DataStoreManageId` parity gap +### Task 12: v3 existing `DataStores` and `Tenants` files — update references and close the `DataStoreManageId` parity gap + +**Amendment (same gap as Task 5, mirrored on the v3 side):** `Features\Tenants\*` and `Infrastructure\Services\Tenants\TenantService.cs` inject `IGetDbDataStoresQuery`/`DbInstance`/`DbInstanceStatus` to build the `/tenants/{tenantName}/dataStores/edOrgs` response. v3's `TenantDetailModel.cs` does NOT reference these old names (its `TenantDataStoreModel` has no linking-ID field equivalent to v2's `DbInstanceId` — that's a pre-existing v2/v3 asymmetry, not something to fix here), so only `TenantMapper.cs`, `ReadTenants.cs`, and `TenantService.cs` need changes. **Files:** - Modify: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\DataStoreWithEducationOrganizationsModel.cs` - Modify: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\ReadEducationOrganizations.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3\Features\Tenants\TenantMapper.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3\Features\Tenants\ReadTenants.cs` +- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Services\Tenants\TenantService.cs` **Interfaces:** - Consumes: `IGetDataStoreManagesQuery` (Task 10), `OdsInstanceManageStatus` (Task 2). -- Produces: `DataStoreWithEducationOrganizationsModel.DataStoreManageId` (new field, parity with v2's `OdsInstanceManageId` from Task 5). +- Produces: `DataStoreWithEducationOrganizationsModel.DataStoreManageId` (new field, parity with v2's `OdsInstanceManageId` from Task 5), `TenantMapper.ToUnlinkedDataStoreManageModel` (renamed from `ToUnlinkedDbDataStoreModel`), `ITenantsService.GetTenantEdOrgsByInstancesAsync(..., IGetDataStoreManagesQuery, ...)`. - [ ] **Step 1: Add the `DataStoreManageId` field** @@ -2828,17 +3073,214 @@ public class ReadEducationOrganizations : IFeature (this adds `instance.DataStoreManageId = dataStoreManage.Id;` in the linked branch, which is the new parity behavior — v2's equivalent method already sets `instance.OdsInstanceManageId` the same way.) -- [ ] **Step 3: Build** +- [ ] **Step 3: Update `TenantMapper.cs`** + +Replace the full file content with: + +```csharp +// 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.Admin.DataAccess.Models; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; + +namespace EdFi.Ods.AdminApi.V3.Features.Tenants; + +public static class TenantMapper +{ + public static TenantDataStoreModel ToDataStoreModel(OdsInstance source) + { + return new TenantDataStoreModel + { + DataStoreId = source.OdsInstanceId, + Name = source.Name, + DataStoreType = source.InstanceType, + }; + } + + public static List ToDataStoreModelList(IEnumerable source) + { + return source.Select(ToDataStoreModel).ToList(); + } + + public static TenantDataStoreModel ToUnlinkedDataStoreManageModel(OdsInstanceManage source) + { + return new TenantDataStoreModel + { + DataStoreId = null, + Name = source.Name, + Status = source.Status, + DatabaseTemplate = source.DatabaseTemplate, + DatabaseName = source.DatabaseName, + }; + } +} +``` + +- [ ] **Step 4: Update `ReadTenants.cs`** + +Replace: + +```csharp + IGetDbDataStoresQuery getDbDataStoresQuery, +``` + +with: + +```csharp + IGetDataStoreManagesQuery getDataStoreManagesQuery, +``` + +and replace: + +```csharp + var tenant = await tenantsService.GetTenantEdOrgsByInstancesAsync( + getDataStoresQuery, getEducationOrganizationQuery, getDbDataStoresQuery, tenantName); +``` -Run: `dotnet build Application/EdFi.Ods.AdminApi.V3.sln` +with: + +```csharp + var tenant = await tenantsService.GetTenantEdOrgsByInstancesAsync( + getDataStoresQuery, getEducationOrganizationQuery, getDataStoreManagesQuery, tenantName); +``` + +(`IGetDataStoreManagesQuery` lives in the same `EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries` namespace already imported in this file — no new using needed.) + +- [ ] **Step 5: Update `TenantService.cs`** + +Replace the interface line: + +```csharp + Task GetTenantEdOrgsByInstancesAsync(IGetDataStoresQuery getDataStoresQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, IGetDbDataStoresQuery getDbDataStoresQuery, string tenantName); +``` + +with: + +```csharp + Task GetTenantEdOrgsByInstancesAsync(IGetDataStoresQuery getDataStoresQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, IGetDataStoreManagesQuery getDataStoreManagesQuery, string tenantName); +``` + +Replace the method signature: + +```csharp + public async Task GetTenantEdOrgsByInstancesAsync( + IGetDataStoresQuery getDataStoresQuery, + IGetEducationOrganizationQuery getEducationOrganizationQuery, + IGetDbDataStoresQuery getDbDataStoresQuery, + string tenantName) +``` + +with: + +```csharp + public async Task GetTenantEdOrgsByInstancesAsync( + IGetDataStoresQuery getDataStoresQuery, + IGetEducationOrganizationQuery getEducationOrganizationQuery, + IGetDataStoreManagesQuery getDataStoreManagesQuery, + string tenantName) +``` + +Replace the method body's use of the renamed query/entity: + +```csharp + var allDbDataStores = getDbDataStoresQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); + + var linkedDbDataStoresByDataStoreId = allDbDataStores + .Where(d => d.OdsInstanceId is not null) + .GroupBy(d => d.OdsInstanceId!.Value) + .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); + + foreach (var dataStore in tenantDetails.DataStores) + { + if (dataStore.DataStoreId is int dataStoreId && linkedDbDataStoresByDataStoreId.TryGetValue(dataStoreId, out var dbDataStore)) + { + dataStore.Status = dbDataStore.Status; + dataStore.DatabaseTemplate = dbDataStore.DatabaseTemplate; + dataStore.DatabaseName = dbDataStore.DatabaseName; + } + else + { + dataStore.Status = DbInstanceStatus.Created.ToString(); + } + } + + var existingDataStoreIds = tenantDetails.DataStores + .Where(i => i.DataStoreId is int) + .Select(i => i.DataStoreId!.Value) + .ToHashSet(); + + var unlinkedDbDataStores = allDbDataStores + .Where(d => d.OdsInstanceId is null) + .Concat(allDbDataStores + .Where(d => d.OdsInstanceId is not null && !existingDataStoreIds.Contains(d.OdsInstanceId.Value)) + .GroupBy(d => d.OdsInstanceId!.Value) + .Select(g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First())) + .ToList(); + foreach (var dbDataStore in unlinkedDbDataStores) + { + tenantDetails.DataStores.Add(TenantMapper.ToUnlinkedDbDataStoreModel(dbDataStore)); + } +``` + +with: + +```csharp + var allDataStoreManages = getDataStoreManagesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); + + var linkedDataStoreManagesByDataStoreId = allDataStoreManages + .Where(d => d.OdsInstanceId is not null) + .GroupBy(d => d.OdsInstanceId!.Value) + .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); + + foreach (var dataStore in tenantDetails.DataStores) + { + if (dataStore.DataStoreId is int dataStoreId && linkedDataStoreManagesByDataStoreId.TryGetValue(dataStoreId, out var dataStoreManage)) + { + dataStore.Status = dataStoreManage.Status; + dataStore.DatabaseTemplate = dataStoreManage.DatabaseTemplate; + dataStore.DatabaseName = dataStoreManage.DatabaseName; + } + else + { + dataStore.Status = OdsInstanceManageStatus.Created.ToString(); + } + } + + var existingDataStoreIds = tenantDetails.DataStores + .Where(i => i.DataStoreId is int) + .Select(i => i.DataStoreId!.Value) + .ToHashSet(); + + var unlinkedDataStoreManages = allDataStoreManages + .Where(d => d.OdsInstanceId is null) + .Concat(allDataStoreManages + .Where(d => d.OdsInstanceId is not null && !existingDataStoreIds.Contains(d.OdsInstanceId.Value)) + .GroupBy(d => d.OdsInstanceId!.Value) + .Select(g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First())) + .ToList(); + foreach (var dataStoreManage in unlinkedDataStoreManages) + { + tenantDetails.DataStores.Add(TenantMapper.ToUnlinkedDataStoreManageModel(dataStoreManage)); + } +``` + +- [ ] **Step 6: Build** + +Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` Expected: remaining errors confined to `Program.cs`/`WebApplicationBuilderExtensions.cs` V3 branches (Task 13). -- [ ] **Step 4: Commit** +- [ ] **Step 7: Commit** ```bash git add "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/DataStoreWithEducationOrganizationsModel.cs" \ - "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/ReadEducationOrganizations.cs" -git commit -m "Update v3 DataStores EdOrgs merge to use renamed DataStoreManage query, add DataStoreManageId for v2 parity" + "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/ReadEducationOrganizations.cs" \ + "Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs" \ + "Application/EdFi.Ods.AdminApi.V3/Features/Tenants/ReadTenants.cs" \ + "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs" +git commit -m "Update v3 DataStores and Tenants EdOrgs merges to use renamed DataStoreManage query, add DataStoreManageId for v2 parity" ``` --- From 04eff5856d29ab92b58832a34987cadc9f75eb67 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 20:47:00 -0600 Subject: [PATCH 08/35] Move v2 DbInstances feature into OdsInstances/Manage, rename routes to /odsInstances/manage Task 4 of the OdsInstanceManage rename plan: relocates the v2 Features folder from Features/DbInstances to Features/OdsInstances/Manage, renaming all types/files to the OdsInstanceManage naming scheme and routes from /dbInstances to /odsInstances/manage. --- .../Manage/AddOdsInstanceManage.cs} | 56 +++++++++---------- .../Manage/DeleteOdsInstanceManage.cs} | 47 ++++++++-------- ...OdsInstanceManageDatabaseNameFormatter.cs} | 6 +- .../Manage/OdsInstanceManageMapper.cs} | 12 ++-- .../Manage/OdsInstanceManageModel.cs} | 6 +- .../Manage/ReadOdsInstanceManage.cs} | 26 ++++----- 6 files changed, 76 insertions(+), 77 deletions(-) rename Application/EdFi.Ods.AdminApi/Features/{DbInstances/AddDbInstance.cs => OdsInstances/Manage/AddOdsInstanceManage.cs} (67%) rename Application/EdFi.Ods.AdminApi/Features/{DbInstances/DeleteDbInstance.cs => OdsInstances/Manage/DeleteOdsInstanceManage.cs} (55%) rename Application/EdFi.Ods.AdminApi/Features/{DbInstances/DbInstanceDatabaseNameFormatter.cs => OdsInstances/Manage/OdsInstanceManageDatabaseNameFormatter.cs} (93%) rename Application/EdFi.Ods.AdminApi/Features/{DbInstances/DbInstanceMapper.cs => OdsInstances/Manage/OdsInstanceManageMapper.cs} (72%) rename Application/EdFi.Ods.AdminApi/Features/{DbInstances/DbInstanceModel.cs => OdsInstances/Manage/OdsInstanceManageModel.cs} (83%) rename Application/EdFi.Ods.AdminApi/Features/{DbInstances/ReadDbInstance.cs => OdsInstances/Manage/ReadOdsInstanceManage.cs} (50%) diff --git a/Application/EdFi.Ods.AdminApi/Features/DbInstances/AddDbInstance.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/AddOdsInstanceManage.cs similarity index 67% rename from Application/EdFi.Ods.AdminApi/Features/DbInstances/AddDbInstance.cs rename to Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/AddOdsInstanceManage.cs index 7907da2a4..7abb2d0c0 100644 --- a/Application/EdFi.Ods.AdminApi/Features/DbInstances/AddDbInstance.cs +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/AddOdsInstanceManage.cs @@ -24,20 +24,20 @@ using Quartz; using Swashbuckle.AspNetCore.Annotations; -namespace EdFi.Ods.AdminApi.Features.DbInstances; +namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; -public class AddDbInstance : IFeature +public class AddOdsInstanceManage : IFeature { private const int MaxSynchronizedNameLength = 100; - private const int MaxDbInstanceNameLength = MaxSynchronizedNameLength; - private static readonly Regex _validDbInstanceNamePattern = new( + private const int MaxOdsInstanceManageNameLength = MaxSynchronizedNameLength; + private static readonly Regex _validOdsInstanceManageNamePattern = new( "^[A-Za-z0-9 _]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant); public void MapEndpoints(IEndpointRouteBuilder endpoints) { AdminApiEndpointBuilder - .MapPost(endpoints, "/dbInstances", Handle) + .MapPost(endpoints, "/odsInstances/manage", Handle) .WithDefaultSummaryAndDescription() .WithRouteOptions(b => b.WithResponseCode(202)) .BuildForVersions(AdminApiVersions.V2); @@ -45,15 +45,15 @@ public void MapEndpoints(IEndpointRouteBuilder endpoints) public async static Task Handle( Validator validator, - AddDbInstanceCommand addDbInstanceCommand, + AddOdsInstanceManageCommand addOdsInstanceManageCommand, [FromServices] ISchedulerFactory schedulerFactory, [FromServices] IContextProvider tenantConfigurationProvider, [FromServices] IOptions options, - AddDbInstanceRequest request) + AddOdsInstanceManageRequest request) { await validator.GuardAsync(request); - var added = addDbInstanceCommand.Execute(request); + var added = addOdsInstanceManageCommand.Execute(request); var tenantIdentifier = options.Value.MultiTenancy ? tenantConfigurationProvider.Get()?.TenantIdentifier @@ -61,7 +61,7 @@ public async static Task Handle( var jobBuilder = JobBuilder.Create() .WithIdentity(CreateInstanceJob.CreateJobKey(added.Id, tenantIdentifier)) - .UsingJobData(JobConstants.DbInstanceIdKey, added.Id); + .UsingJobData(JobConstants.OdsInstanceManageIdKey, added.Id); if (!string.IsNullOrWhiteSpace(tenantIdentifier)) { @@ -80,16 +80,16 @@ public async static Task Handle( } catch (ObjectAlreadyExistsException) { - // The CreatePendingDbInstancesDispatcherJob may have already scheduled this job + // The CreatePendingOdsInstanceManagesDispatcherJob may have already scheduled this job // (e.g. it fired between the DB insert and this ScheduleJob call). Treat duplicate - // scheduling as success — the job is already queued and will process the DbInstance. + // scheduling as success — the job is already queued and will process the OdsInstanceManage. } - return Results.Accepted($"/dbinstances/{added.Id}", null); + return Results.Accepted($"/odsinstances/manage/{added.Id}", null); } - [SwaggerSchema(Title = "AddDbInstanceRequest")] - public class AddDbInstanceRequest : IAddDbInstanceModel + [SwaggerSchema(Title = "AddOdsInstanceManageRequest")] + public class AddOdsInstanceManageRequest : IAddOdsInstanceManageModel { [SwaggerSchema(Description = "Name of the database instance", Nullable = false)] public string? Name { get; set; } @@ -98,7 +98,7 @@ public class AddDbInstanceRequest : IAddDbInstanceModel public string? DatabaseTemplate { get; set; } } - public class Validator : AbstractValidator + public class Validator : AbstractValidator { private static readonly string[] _validDatabaseTemplates = Enum.GetNames(); private readonly AdminApiDbContext _adminApiDbContext; @@ -111,9 +111,9 @@ public Validator(AdminApiDbContext adminApiDbContext, IUsersContext usersContext RuleFor(m => m.Name) .NotEmpty() - .MaximumLength(MaxDbInstanceNameLength) - .WithMessage($"'{{PropertyName}}' must be {MaxDbInstanceNameLength} characters or fewer so the synchronized ODS instance name fits within {MaxSynchronizedNameLength} characters.") - .Matches(_validDbInstanceNamePattern) + .MaximumLength(MaxOdsInstanceManageNameLength) + .WithMessage($"'{{PropertyName}}' must be {MaxOdsInstanceManageNameLength} characters or fewer so the synchronized ODS instance name fits within {MaxSynchronizedNameLength} characters.") + .Matches(_validOdsInstanceManageNamePattern) .WithMessage("'{PropertyName}' may only contain letters, numbers, spaces, and underscores."); RuleFor(m => m.DatabaseTemplate).NotEmpty().MaximumLength(100) @@ -124,8 +124,8 @@ public Validator(AdminApiDbContext adminApiDbContext, IUsersContext usersContext { if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.DatabaseTemplate) - || request.Name.Length > MaxDbInstanceNameLength - || !_validDbInstanceNamePattern.IsMatch(request.Name) + || request.Name.Length > MaxOdsInstanceManageNameLength + || !_validOdsInstanceManageNamePattern.IsMatch(request.Name) || !_validDatabaseTemplates.Contains(request.DatabaseTemplate)) { return; @@ -133,29 +133,29 @@ public Validator(AdminApiDbContext adminApiDbContext, IUsersContext usersContext var normalizedName = request.Name.Trim(); - if (await _adminApiDbContext.DbInstances.AnyAsync(instance => instance.Name == normalizedName && instance.Status != DbInstanceStatus.Deleted.ToString(), cancellationToken)) + if (await _adminApiDbContext.OdsInstanceManages.AnyAsync(instance => instance.Name == normalizedName && instance.Status != OdsInstanceManageStatus.Deleted.ToString(), cancellationToken)) { context.AddFailure( - nameof(AddDbInstanceRequest.Name), - $"A DbInstance named '{normalizedName}' already exists."); + nameof(AddOdsInstanceManageRequest.Name), + $"An OdsInstanceManage named '{normalizedName}' already exists."); return; } if (await _usersContext.OdsInstances.AnyAsync(instance => instance.Name == normalizedName, cancellationToken)) { context.AddFailure( - nameof(AddDbInstanceRequest.Name), + nameof(AddOdsInstanceManageRequest.Name), $"An OdsInstance named '{normalizedName}' already exists."); return; } - var databaseName = DbInstanceDatabaseNameFormatter.Build(request.Name, request.DatabaseTemplate); + var databaseName = OdsInstanceManageDatabaseNameFormatter.Build(request.Name, request.DatabaseTemplate); - if (databaseName.Length > DbInstanceDatabaseNameFormatter.MaxPortableDatabaseNameLength) + if (databaseName.Length > OdsInstanceManageDatabaseNameFormatter.MaxPortableDatabaseNameLength) { context.AddFailure( - nameof(AddDbInstanceRequest.Name), - $"The generated database name '{databaseName}' exceeds the portable limit of {DbInstanceDatabaseNameFormatter.MaxPortableDatabaseNameLength} characters. Shorten Name or DatabaseTemplate."); + nameof(AddOdsInstanceManageRequest.Name), + $"The generated database name '{databaseName}' exceeds the portable limit of {OdsInstanceManageDatabaseNameFormatter.MaxPortableDatabaseNameLength} characters. Shorten Name or DatabaseTemplate."); } }); } diff --git a/Application/EdFi.Ods.AdminApi/Features/DbInstances/DeleteDbInstance.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/DeleteOdsInstanceManage.cs similarity index 55% rename from Application/EdFi.Ods.AdminApi/Features/DbInstances/DeleteDbInstance.cs rename to Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/DeleteOdsInstanceManage.cs index bbd57834f..afef2ef2a 100644 --- a/Application/EdFi.Ods.AdminApi/Features/DbInstances/DeleteDbInstance.cs +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/DeleteOdsInstanceManage.cs @@ -20,47 +20,47 @@ using Microsoft.Extensions.Options; using Quartz; -namespace EdFi.Ods.AdminApi.Features.DbInstances; +namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; -public class DeleteDbInstance : IFeature +public class DeleteOdsInstanceManage : IFeature { public void MapEndpoints(IEndpointRouteBuilder endpoints) { AdminApiEndpointBuilder - .MapDelete(endpoints, "/dbInstances/{id}", Handle) + .MapDelete(endpoints, "/odsInstances/manage/{id}", Handle) .WithDefaultSummaryAndDescription() .WithRouteOptions(b => b.WithResponseCode(204)) .BuildForVersions(AdminApiVersions.V2); } public static async Task Handle( - IGetDbInstanceByIdQuery getDbInstanceByIdQuery, - IDeleteDbInstanceCommand deleteDbInstanceCommand, + IGetOdsInstanceManageByIdQuery getOdsInstanceManageByIdQuery, + IDeleteOdsInstanceManageCommand deleteOdsInstanceManageCommand, [FromServices] ISchedulerFactory schedulerFactory, [FromServices] IContextProvider tenantConfigurationProvider, [FromServices] IOptions options, int id ) { - var dbInstance = getDbInstanceByIdQuery.Execute(id); - if (dbInstance is null) - throw new NotFoundException("dbInstance", id); + var odsInstanceManage = getOdsInstanceManageByIdQuery.Execute(id); + if (odsInstanceManage is null) + throw new NotFoundException("odsInstanceManage", id); - if (dbInstance.Status == DbInstanceStatus.Deleted.ToString()) - throw new NotFoundException("dbInstance", id); + if (odsInstanceManage.Status == OdsInstanceManageStatus.Deleted.ToString()) + throw new NotFoundException("odsInstanceManage", id); - var blockingMessage = GetBlockingStatusMessage(dbInstance.Status); + var blockingMessage = GetBlockingStatusMessage(odsInstanceManage.Status); if (blockingMessage is not null) throw new ValidationException([new ValidationFailure(nameof(id), blockingMessage)]); - deleteDbInstanceCommand.Execute(id); + deleteOdsInstanceManageCommand.Execute(id); var tenantName = options.Value.MultiTenancy ? tenantConfigurationProvider.Get()?.TenantIdentifier : null; var jobData = new Dictionary { - [JobConstants.DbInstanceIdKey] = id + [JobConstants.OdsInstanceManageIdKey] = id }; if (!string.IsNullOrWhiteSpace(tenantName)) @@ -80,7 +80,7 @@ await QuartzJobScheduler.ScheduleJob( } catch (ObjectAlreadyExistsException) { - // The DeletePendingDbInstancesDispatcherJob may have already scheduled this job. + // The DeletePendingOdsInstanceManagesDispatcherJob may have already scheduled this job. // Treat duplicate scheduling as success — the job is already queued. } @@ -89,18 +89,18 @@ await QuartzJobScheduler.ScheduleJob( private static string? GetBlockingStatusMessage(string status) { - if (Enum.TryParse(status, ignoreCase: true, out var parsed)) + if (Enum.TryParse(status, ignoreCase: true, out var parsed)) { return parsed switch { - DbInstanceStatus.PendingCreate => "DbInstance is being provisioned. Wait for creation to complete.", - DbInstanceStatus.CreateInProgress => "DbInstance is currently being provisioned. Wait for creation to complete.", - DbInstanceStatus.CreateFailed => "DbInstance creation failed. It will be retried automatically by the background job.", - DbInstanceStatus.CreateError => "DbInstance creation failed permanently. Manual database intervention required before deleting.", - DbInstanceStatus.PendingDelete => "DbInstance is already queued for deletion.", - DbInstanceStatus.DeleteInProgress => "DbInstance is currently being deleted.", - DbInstanceStatus.DeleteFailed => "DbInstance deletion failed. It will be retried automatically by the background job.", - DbInstanceStatus.DeleteError => "DbInstance deletion failed permanently. Manual database intervention required.", + OdsInstanceManageStatus.PendingCreate => "OdsInstanceManage is being provisioned. Wait for creation to complete.", + OdsInstanceManageStatus.CreateInProgress => "OdsInstanceManage is currently being provisioned. Wait for creation to complete.", + OdsInstanceManageStatus.CreateFailed => "OdsInstanceManage creation failed. It will be retried automatically by the background job.", + OdsInstanceManageStatus.CreateError => "OdsInstanceManage creation failed permanently. Manual database intervention required before deleting.", + OdsInstanceManageStatus.PendingDelete => "OdsInstanceManage is already queued for deletion.", + OdsInstanceManageStatus.DeleteInProgress => "OdsInstanceManage is currently being deleted.", + OdsInstanceManageStatus.DeleteFailed => "OdsInstanceManage deletion failed. It will be retried automatically by the background job.", + OdsInstanceManageStatus.DeleteError => "OdsInstanceManage deletion failed permanently. Manual database intervention required.", _ => null, }; } @@ -108,4 +108,3 @@ await QuartzJobScheduler.ScheduleJob( return null; } } - diff --git a/Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceDatabaseNameFormatter.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageDatabaseNameFormatter.cs similarity index 93% rename from Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceDatabaseNameFormatter.cs rename to Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageDatabaseNameFormatter.cs index 1cfb3c430..3048116fa 100644 --- a/Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceDatabaseNameFormatter.cs +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageDatabaseNameFormatter.cs @@ -5,9 +5,9 @@ using System.Text.RegularExpressions; -namespace EdFi.Ods.AdminApi.Features.DbInstances; +namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; -internal static class DbInstanceDatabaseNameFormatter +internal static class OdsInstanceManageDatabaseNameFormatter { private const string CanonicalPrefix = "EdFi_Ods"; @@ -35,4 +35,4 @@ internal static string Build(string instanceName, string databaseTemplate) private static string NormalizeSegment(string value) => value.Replace(' ', '_').Trim('_'); -} \ No newline at end of file +} diff --git a/Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceMapper.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageMapper.cs similarity index 72% rename from Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceMapper.cs rename to Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageMapper.cs index 7821ba816..c2de67058 100644 --- a/Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceMapper.cs +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageMapper.cs @@ -5,13 +5,13 @@ using EdFi.Ods.AdminApi.Common.Infrastructure.Models; -namespace EdFi.Ods.AdminApi.Features.DbInstances; +namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; -public static class DbInstanceMapper +public static class OdsInstanceManageMapper { - public static DbInstanceModel ToModel(DbInstance source) + public static OdsInstanceManageModel ToModel(OdsInstanceManage source) { - return new DbInstanceModel + return new OdsInstanceManageModel { Id = source.Id, Name = source.Name, @@ -25,8 +25,8 @@ public static DbInstanceModel ToModel(DbInstance source) }; } - public static List ToModelList(IEnumerable source) + public static List ToModelList(IEnumerable source) { return source.Select(ToModel).ToList(); } -} \ No newline at end of file +} diff --git a/Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceModel.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageModel.cs similarity index 83% rename from Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceModel.cs rename to Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageModel.cs index b1cb5dcee..f38b6dd66 100644 --- a/Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceModel.cs +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageModel.cs @@ -5,10 +5,10 @@ using Swashbuckle.AspNetCore.Annotations; -namespace EdFi.Ods.AdminApi.Features.DbInstances; +namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; -[SwaggerSchema(Title = "DbInstance")] -public class DbInstanceModel +[SwaggerSchema(Title = "OdsInstanceManage")] +public class OdsInstanceManageModel { public int? Id { get; set; } public string? Name { get; set; } diff --git a/Application/EdFi.Ods.AdminApi/Features/DbInstances/ReadDbInstance.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/ReadOdsInstanceManage.cs similarity index 50% rename from Application/EdFi.Ods.AdminApi/Features/DbInstances/ReadDbInstance.cs rename to Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/ReadOdsInstanceManage.cs index 104e4c45d..c3a15e944 100644 --- a/Application/EdFi.Ods.AdminApi/Features/DbInstances/ReadDbInstance.cs +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/ReadOdsInstanceManage.cs @@ -8,38 +8,38 @@ using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -namespace EdFi.Ods.AdminApi.Features.DbInstances; +namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; -public class ReadDbInstance : IFeature +public class ReadOdsInstanceManage : IFeature { public void MapEndpoints(IEndpointRouteBuilder endpoints) { - AdminApiEndpointBuilder.MapGet(endpoints, "/dbInstances", GetDbInstances) + AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstances/manage", GetOdsInstanceManages) .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponse(200)) + .WithRouteOptions(b => b.WithResponse(200)) .BuildForVersions(AdminApiVersions.V2); - AdminApiEndpointBuilder.MapGet(endpoints, "/dbInstances/{id}", GetDbInstance) + AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstances/manage/{id}", GetOdsInstanceManage) .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponse(200)) + .WithRouteOptions(b => b.WithResponse(200)) .BuildForVersions(AdminApiVersions.V2); } - public static Task GetDbInstances(IGetDbInstancesQuery query, + public static Task GetOdsInstanceManages(IGetOdsInstanceManagesQuery query, [AsParameters] CommonQueryParams commonQueryParams, int? id, string? name) { - var list = DbInstanceMapper.ToModelList(query.Execute(commonQueryParams, id, name)); + var list = OdsInstanceManageMapper.ToModelList(query.Execute(commonQueryParams, id, name)); return Task.FromResult(Results.Ok(list)); } - public static Task GetDbInstance(IGetDbInstanceByIdQuery query, int id) + public static Task GetOdsInstanceManage(IGetOdsInstanceManageByIdQuery query, int id) { - var dbInstance = query.Execute(id); - if (dbInstance == null) + var odsInstanceManage = query.Execute(id); + if (odsInstanceManage == null) { - throw new NotFoundException("dbInstance", id); + throw new NotFoundException("odsInstanceManage", id); } - var model = DbInstanceMapper.ToModel(dbInstance); + var model = OdsInstanceManageMapper.ToModel(odsInstanceManage); return Task.FromResult(Results.Ok(model)); } } From c6b1808aa6f3c8008d31117ca9560ba3d7f28b12 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 20:54:32 -0600 Subject: [PATCH 09/35] Update v2 OdsInstances and Tenants EdOrgs merges to use renamed OdsInstanceManage query --- ...InstanceWithEducationOrganizationsModel.cs | 4 +-- .../ReadEducationOrganizations.cs | 24 ++++++++-------- .../Features/Tenants/ReadTenants.cs | 4 +-- .../Features/Tenants/TenantDetailModel.cs | 2 +- .../Features/Tenants/TenantMapper.cs | 6 ++-- .../Services/Tenants/TenantService.cs | 28 +++++++++---------- 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/OdsInstanceWithEducationOrganizationsModel.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/OdsInstanceWithEducationOrganizationsModel.cs index 383c39b61..fea22a515 100644 --- a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/OdsInstanceWithEducationOrganizationsModel.cs +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/OdsInstanceWithEducationOrganizationsModel.cs @@ -13,8 +13,8 @@ public class OdsInstanceWithEducationOrganizationsModel [SwaggerSchema(Description = "ODS instance identifier")] public int? Id { get; set; } - [SwaggerSchema(Description = "DbInstance identifier for this ODS instance")] - public int? DbInstanceId { get; set; } + [SwaggerSchema(Description = "OdsInstanceManage identifier for this ODS instance")] + public int? OdsInstanceManageId { get; set; } [SwaggerSchema(Description = "ODS instance name", Nullable = false)] public string Name { get; set; } = string.Empty; diff --git a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/ReadEducationOrganizations.cs b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/ReadEducationOrganizations.cs index 716d1396e..f32024235 100644 --- a/Application/EdFi.Ods.AdminApi/Features/OdsInstances/ReadEducationOrganizations.cs +++ b/Application/EdFi.Ods.AdminApi/Features/OdsInstances/ReadEducationOrganizations.cs @@ -28,7 +28,7 @@ public void MapEndpoints(IEndpointRouteBuilder endpoints) public static async Task GetEducationOrganizationsByInstance( [FromServices] IGetEducationOrganizationsQuery getEducationOrganizationsQuery, [FromServices] IGetOdsInstanceQuery getOdsInstanceQuery, - [FromServices] IGetDbInstancesQuery getDbInstancesQuery, + [FromServices] IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery, [AsParameters] CommonQueryParams commonQueryParams, int instanceId) { @@ -38,33 +38,33 @@ public static async Task GetEducationOrganizationsByInstance( commonQueryParams, instanceId: instanceId); - MergeDbInstanceData(educationOrganizations, getDbInstancesQuery); + MergeOdsInstanceManageData(educationOrganizations, getOdsInstanceManagesQuery); return Results.Ok(educationOrganizations); } - private static void MergeDbInstanceData( + private static void MergeOdsInstanceManageData( List instances, - IGetDbInstancesQuery getDbInstancesQuery) + IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery) { - var allDbInstances = getDbInstancesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); + var allOdsInstanceManages = getOdsInstanceManagesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); - var linkedById = allDbInstances + var linkedById = allOdsInstanceManages .Where(d => d.OdsInstanceId is not null) .GroupBy(d => d.OdsInstanceId!.Value) .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); foreach (var instance in instances) { - if (instance.Id is int instanceId && linkedById.TryGetValue(instanceId, out var dbInstance)) + if (instance.Id is int instanceId && linkedById.TryGetValue(instanceId, out var odsInstanceManage)) { - instance.DbInstanceId = dbInstance.Id; - instance.Status = dbInstance.Status; - instance.DatabaseTemplate = dbInstance.DatabaseTemplate; - instance.DatabaseName = dbInstance.DatabaseName; + instance.OdsInstanceManageId = odsInstanceManage.Id; + instance.Status = odsInstanceManage.Status; + instance.DatabaseTemplate = odsInstanceManage.DatabaseTemplate; + instance.DatabaseName = odsInstanceManage.DatabaseName; } else { - instance.Status = DbInstanceStatus.Created.ToString(); + instance.Status = OdsInstanceManageStatus.Created.ToString(); } } } diff --git a/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs b/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs index 4b86edf31..a3a9f474a 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs @@ -33,7 +33,7 @@ public static async Task GetTenantEdOrgsByInstancesAsync( [FromServices] ITenantsService tenantsService, IGetOdsInstancesQuery getOdsInstancesQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, - IGetDbInstancesQuery getDbInstancesQuery, + IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery, IMemoryCache memoryCache, IOptions options, IOptions _swaggerOptions, @@ -59,7 +59,7 @@ string tenantName } var tenant = await tenantsService.GetTenantEdOrgsByInstancesAsync( - getOdsInstancesQuery, getEducationOrganizationQuery, getDbInstancesQuery, tenantName); + getOdsInstancesQuery, getEducationOrganizationQuery, getOdsInstanceManagesQuery, tenantName); if (tenant is null) return Results.NotFound(); diff --git a/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantDetailModel.cs b/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantDetailModel.cs index 58280146d..905063e22 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantDetailModel.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantDetailModel.cs @@ -31,7 +31,7 @@ public class TenantOdsInstanceModel { [JsonPropertyName("id")] public int? OdsInstanceId { get; set; } - public int? DbInstanceId { get; set; } + public int? OdsInstanceManageId { get; set; } public string Name { get; set; } public string? InstanceType { get; set; } public string? Status { get; set; } diff --git a/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantMapper.cs b/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantMapper.cs index 619c146d8..7e5622d13 100644 --- a/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantMapper.cs +++ b/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantMapper.cs @@ -25,16 +25,16 @@ public static List ToOdsInstanceModelList(IEnumerable> GetTenantsAsync(bool fromCache = false); Task GetTenantByTenantIdAsync(string tenantName); - Task GetTenantEdOrgsByInstancesAsync(IGetOdsInstancesQuery getOdsInstancesQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, IGetDbInstancesQuery getDbInstancesQuery, string tenantName); + Task GetTenantEdOrgsByInstancesAsync(IGetOdsInstancesQuery getOdsInstancesQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery, string tenantName); } public class TenantService(IOptionsSnapshot options, @@ -113,7 +113,7 @@ public async Task> GetTenantsAsync(bool fromCache = false) public async Task GetTenantEdOrgsByInstancesAsync( IGetOdsInstancesQuery getOdsInstancesQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, - IGetDbInstancesQuery getDbInstancesQuery, + IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery, string tenantName) { var tenant = await GetTenantByTenantIdAsync(tenantName); @@ -142,25 +142,25 @@ public async Task> GetTenantsAsync(bool fromCache = false) } } - var allDbInstances = getDbInstancesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); + var allOdsInstanceManages = getOdsInstanceManagesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); - var linkedDbInstancesByOdsId = allDbInstances + var linkedOdsInstanceManagesByOdsId = allOdsInstanceManages .Where(d => d.OdsInstanceId is not null) .GroupBy(d => d.OdsInstanceId!.Value) .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); foreach (var odsInstance in tenantDetails.OdsInstances) { - if (odsInstance.OdsInstanceId is int odsInstanceId && linkedDbInstancesByOdsId.TryGetValue(odsInstanceId, out var dbInstance)) + if (odsInstance.OdsInstanceId is int odsInstanceId && linkedOdsInstanceManagesByOdsId.TryGetValue(odsInstanceId, out var odsInstanceManage)) { - odsInstance.DbInstanceId = dbInstance.Id; - odsInstance.Status = dbInstance.Status; - odsInstance.DatabaseTemplate = dbInstance.DatabaseTemplate; - odsInstance.DatabaseName = dbInstance.DatabaseName; + odsInstance.OdsInstanceManageId = odsInstanceManage.Id; + odsInstance.Status = odsInstanceManage.Status; + odsInstance.DatabaseTemplate = odsInstanceManage.DatabaseTemplate; + odsInstance.DatabaseName = odsInstanceManage.DatabaseName; } else { - odsInstance.Status = DbInstanceStatus.Created.ToString(); + odsInstance.Status = OdsInstanceManageStatus.Created.ToString(); } } @@ -169,16 +169,16 @@ public async Task> GetTenantsAsync(bool fromCache = false) .Select(i => i.OdsInstanceId!.Value) .ToHashSet(); - var unlinkedDbInstances = allDbInstances + var unlinkedOdsInstanceManages = allOdsInstanceManages .Where(d => d.OdsInstanceId is null) - .Concat(allDbInstances + .Concat(allOdsInstanceManages .Where(d => d.OdsInstanceId is not null && !existingOdsInstanceIds.Contains(d.OdsInstanceId.Value)) .GroupBy(d => d.OdsInstanceId!.Value) .Select(g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First())) .ToList(); - foreach (var dbInstance in unlinkedDbInstances) + foreach (var odsInstanceManage in unlinkedOdsInstanceManages) { - tenantDetails.OdsInstances.Add(TenantMapper.ToUnlinkedDbInstanceModel(dbInstance)); + tenantDetails.OdsInstances.Add(TenantMapper.ToUnlinkedOdsInstanceManageModel(odsInstanceManage)); } return tenantDetails; From 563e8014958abdc58c77f915822b971bcb0a66ca Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 21:07:19 -0600 Subject: [PATCH 10/35] Update v2 Quartz wiring to use renamed OdsInstanceManage jobs and config keys --- .../WebApplicationBuilderExtensions.cs | 2 +- Application/EdFi.Ods.AdminApi/Program.cs | 56 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs index 2fddccfb8..3097aba38 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs @@ -770,7 +770,7 @@ private static void RegisterQuartzServices(WebApplicationBuilder webApplicationB else { webApplicationBuilder.Services.AddTransient(); - webApplicationBuilder.Services.AddTransient(); + webApplicationBuilder.Services.AddTransient(); webApplicationBuilder.Services.AddTransient(); webApplicationBuilder.Services.AddTransient(); webApplicationBuilder.Services.AddTransient< diff --git a/Application/EdFi.Ods.AdminApi/Program.cs b/Application/EdFi.Ods.AdminApi/Program.cs index 6673cbb9f..84da279a3 100644 --- a/Application/EdFi.Ods.AdminApi/Program.cs +++ b/Application/EdFi.Ods.AdminApi/Program.cs @@ -105,11 +105,11 @@ var edOrgsRefreshIntervalInMins = app.Configuration.GetValue( "AppSettings:EdOrgsRefreshIntervalInMins" ); -var createDbInstancesSweepIntervalInMins = app.Configuration.GetValue( - "AppSettings:CreateDbInstancesSweepIntervalInMins" +var createOdsInstanceManagesSweepIntervalInMins = app.Configuration.GetValue( + "AppSettings:CreateOdsInstanceManagesSweepIntervalInMins" ); -var deleteDbInstancesSweepIntervalInMins = app.Configuration.GetValue( - "AppSettings:DeleteDbInstancesSweepIntervalInMins" +var deleteOdsInstanceManagesSweepIntervalInMins = app.Configuration.GetValue( + "AppSettings:DeleteOdsInstanceManagesSweepIntervalInMins" ); var isMultiTenancyEnabled = app.Configuration.GetValue( "AppSettings:MultiTenancy" @@ -117,8 +117,8 @@ if (adminApiMode == AdminApiMode.V2) { - var shouldScheduleDispatcher = double.TryParse(createDbInstancesSweepIntervalInMins, out var createDbInstancesSweepInterval); - var shouldScheduleDeleteDispatcher = double.TryParse(deleteDbInstancesSweepIntervalInMins, out var deleteDbInstancesSweepInterval); + var shouldScheduleDispatcher = double.TryParse(createOdsInstanceManagesSweepIntervalInMins, out var createOdsInstanceManagesSweepInterval); + var shouldScheduleDeleteDispatcher = double.TryParse(deleteOdsInstanceManagesSweepIntervalInMins, out var deleteOdsInstanceManagesSweepInterval); var shouldScheduleEdOrgsRefresh = double.TryParse(edOrgsRefreshIntervalInMins, out var refreshInterval); if (isMultiTenancyEnabled && (shouldScheduleDispatcher || shouldScheduleDeleteDispatcher || shouldScheduleEdOrgsRefresh)) @@ -180,32 +180,32 @@ await QuartzJobScheduler.ScheduleJob( foreach (var tenantName in tenants.Select(tenant => tenant.TenantName)) { - await QuartzJobScheduler.ScheduleJob( + await QuartzJobScheduler.ScheduleJob( scheduler, - jobKey: new JobKey($"{JobConstants.CreatePendingDbInstancesDispatcherJobName}_{tenantName}"), + jobKey: new JobKey($"{JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName}_{tenantName}"), jobData: new Dictionary { [JobConstants.TenantNameKey] = tenantName }, startImmediately: false, - interval: TimeSpan.FromMinutes(createDbInstancesSweepInterval) + interval: TimeSpan.FromMinutes(createOdsInstanceManagesSweepInterval) ); } } else { - await QuartzJobScheduler.ScheduleJob( + await QuartzJobScheduler.ScheduleJob( scheduler, - jobKey: new JobKey(JobConstants.CreatePendingDbInstancesDispatcherJobName), + jobKey: new JobKey(JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName), jobData: new Dictionary(), startImmediately: false, - interval: TimeSpan.FromMinutes(createDbInstancesSweepInterval) + interval: TimeSpan.FromMinutes(createOdsInstanceManagesSweepInterval) ); } } else { - _logger.Error("Invalid value for CreateDbInstancesSweepIntervalInMins. Please ensure it is a valid number."); + _logger.Error("Invalid value for CreateOdsInstanceManagesSweepIntervalInMins. Please ensure it is a valid number."); } if (shouldScheduleDeleteDispatcher) @@ -218,38 +218,38 @@ await QuartzJobScheduler.ScheduleJob( foreach (var tenantName in tenants.Select(tenant => tenant.TenantName)) { - await QuartzJobScheduler.ScheduleJob( + await QuartzJobScheduler.ScheduleJob( scheduler, - jobKey: new JobKey($"{JobConstants.DeletePendingDbInstancesDispatcherJobName}_{tenantName}"), + jobKey: new JobKey($"{JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName}_{tenantName}"), jobData: new Dictionary { [JobConstants.TenantNameKey] = tenantName }, startImmediately: false, - interval: TimeSpan.FromMinutes(deleteDbInstancesSweepInterval) + interval: TimeSpan.FromMinutes(deleteOdsInstanceManagesSweepInterval) ); } } else { - await QuartzJobScheduler.ScheduleJob( + await QuartzJobScheduler.ScheduleJob( scheduler, - jobKey: new JobKey(JobConstants.DeletePendingDbInstancesDispatcherJobName), + jobKey: new JobKey(JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName), jobData: new Dictionary(), startImmediately: false, - interval: TimeSpan.FromMinutes(deleteDbInstancesSweepInterval) + interval: TimeSpan.FromMinutes(deleteOdsInstanceManagesSweepInterval) ); } } else { - _logger.Error("Invalid value for DeleteDbInstancesSweepIntervalInMins. Please ensure it is a valid number."); + _logger.Error("Invalid value for DeleteOdsInstanceManagesSweepIntervalInMins. Please ensure it is a valid number."); } } else if (adminApiMode == AdminApiMode.V3) { - var shouldScheduleDispatcher = double.TryParse(createDbInstancesSweepIntervalInMins, out var createDbInstancesSweepInterval) && createDbInstancesSweepInterval > 0; - var shouldScheduleDeleteDispatcher = double.TryParse(deleteDbInstancesSweepIntervalInMins, out var deleteDbInstancesSweepInterval) && deleteDbInstancesSweepInterval > 0; + var shouldScheduleDispatcher = double.TryParse(createOdsInstanceManagesSweepIntervalInMins, out var createOdsInstanceManagesSweepInterval) && createOdsInstanceManagesSweepInterval > 0; + var shouldScheduleDeleteDispatcher = double.TryParse(deleteOdsInstanceManagesSweepIntervalInMins, out var deleteOdsInstanceManagesSweepInterval) && deleteOdsInstanceManagesSweepInterval > 0; var shouldScheduleEdOrgsRefresh = double.TryParse(edOrgsRefreshIntervalInMins, out var refreshInterval) && refreshInterval > 0; if (isMultiTenancyEnabled && (shouldScheduleDispatcher || shouldScheduleDeleteDispatcher || shouldScheduleEdOrgsRefresh)) @@ -318,7 +318,7 @@ await QuartzJobScheduler.ScheduleJob( [JobConstants.TenantNameKey] = tenantName }, startImmediately: false, - interval: TimeSpan.FromMinutes(createDbInstancesSweepInterval) + interval: TimeSpan.FromMinutes(createOdsInstanceManagesSweepInterval) ); } } @@ -329,13 +329,13 @@ await QuartzJobScheduler.ScheduleJob( jobKey: new JobKey(JobConstants.CreatePendingDbInstancesDispatcherJobName), jobData: new Dictionary(), startImmediately: false, - interval: TimeSpan.FromMinutes(createDbInstancesSweepInterval) + interval: TimeSpan.FromMinutes(createOdsInstanceManagesSweepInterval) ); } } else { - _logger.Error("Invalid value for CreateDbInstancesSweepIntervalInMins. Please ensure it is a valid number."); + _logger.Error("Invalid value for CreateOdsInstanceManagesSweepIntervalInMins. Please ensure it is a valid number."); } if (shouldScheduleDeleteDispatcher) @@ -356,7 +356,7 @@ await QuartzJobScheduler.ScheduleJob( [JobConstants.TenantNameKey] = tenantName }, startImmediately: false, - interval: TimeSpan.FromMinutes(deleteDbInstancesSweepInterval) + interval: TimeSpan.FromMinutes(deleteOdsInstanceManagesSweepInterval) ); } } @@ -367,13 +367,13 @@ await QuartzJobScheduler.ScheduleJob( jobKey: new JobKey(JobConstants.DeletePendingDbInstancesDispatcherJobName), jobData: new Dictionary(), startImmediately: false, - interval: TimeSpan.FromMinutes(deleteDbInstancesSweepInterval) + interval: TimeSpan.FromMinutes(deleteOdsInstanceManagesSweepInterval) ); } } else { - _logger.Error("Invalid value for DeleteDbInstancesSweepIntervalInMins. Please ensure it is a valid number."); + _logger.Error("Invalid value for DeleteOdsInstanceManagesSweepIntervalInMins. Please ensure it is a valid number."); } } From 23252b6e302bf91a61c0c7995958190209b615ae Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 21:15:20 -0600 Subject: [PATCH 11/35] [ADMINAPI-1477] Fix V3-branch JobConstants member references in Program.cs Task 2 already renamed JobConstants.CreatePendingDbInstancesDispatcherJobName and JobConstants.DeletePendingDbInstancesDispatcherJobName to their OdsInstanceManages equivalents. Program.cs's V3 branch still referenced the old member names on 4 lines, causing CS0117 errors when building EdFi.Ods.AdminApi.csproj standalone. JobConstants is shared (non-version- specific), so only the member names needed updating; the V3Jobs.* type references on the same lines correctly remain unchanged pending Task 13. Co-Authored-By: Claude Sonnet 5 --- Application/EdFi.Ods.AdminApi/Program.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Application/EdFi.Ods.AdminApi/Program.cs b/Application/EdFi.Ods.AdminApi/Program.cs index 84da279a3..866a26a7a 100644 --- a/Application/EdFi.Ods.AdminApi/Program.cs +++ b/Application/EdFi.Ods.AdminApi/Program.cs @@ -312,7 +312,7 @@ await QuartzJobScheduler.ScheduleJob( scheduler, - jobKey: new JobKey($"{JobConstants.CreatePendingDbInstancesDispatcherJobName}_{tenantName}"), + jobKey: new JobKey($"{JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName}_{tenantName}"), jobData: new Dictionary { [JobConstants.TenantNameKey] = tenantName @@ -326,7 +326,7 @@ await QuartzJobScheduler.ScheduleJob( scheduler, - jobKey: new JobKey(JobConstants.CreatePendingDbInstancesDispatcherJobName), + jobKey: new JobKey(JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName), jobData: new Dictionary(), startImmediately: false, interval: TimeSpan.FromMinutes(createOdsInstanceManagesSweepInterval) @@ -350,7 +350,7 @@ await QuartzJobScheduler.ScheduleJob( scheduler, - jobKey: new JobKey($"{JobConstants.DeletePendingDbInstancesDispatcherJobName}_{tenantName}"), + jobKey: new JobKey($"{JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName}_{tenantName}"), jobData: new Dictionary { [JobConstants.TenantNameKey] = tenantName @@ -364,7 +364,7 @@ await QuartzJobScheduler.ScheduleJob( scheduler, - jobKey: new JobKey(JobConstants.DeletePendingDbInstancesDispatcherJobName), + jobKey: new JobKey(JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName), jobData: new Dictionary(), startImmediately: false, interval: TimeSpan.FromMinutes(deleteOdsInstanceManagesSweepInterval) From f2a12adcc321ed0876f446f86c39e33bd09d3549 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 21:32:43 -0600 Subject: [PATCH 12/35] Rename v2 unit tests to OdsInstanceManage naming Moves and renames v2's DbInstances feature/command/query/dispatcher-job unit tests to match the OdsInstanceManage renames from Tasks 2-6, updates the in-place CreateInstanceJob/DeleteInstanceJob tests, and fixes references in the three Tenants-area test files (Task 5's amendment) plus ReadEducationOrganizationsTests.cs (an additional gap discovered while verifying the build, same class as the Tenants amendment). --- .../Manage/AddOdsInstanceManageTests.cs} | 156 +++++++++--------- .../Manage/DeleteOdsInstanceManageTests.cs} | 104 ++++++------ .../Manage/ReadOdsInstanceManageTests.cs} | 72 ++++---- .../ReadEducationOrganizationsTests.cs | 28 ++-- .../Features/Tenants/ReadTenantsTest.cs | 40 ++--- .../Tenants/TenantDetailModelTests.cs | 4 +- ...cs => AddOdsInstanceManageCommandTests.cs} | 26 +-- ...=> DeleteOdsInstanceManageCommandTests.cs} | 36 ++-- ... => GetOdsInstanceManageByIdQueryTests.cs} | 16 +- ....cs => GetOdsInstanceManagesQueryTests.cs} | 24 +-- .../Services/Jobs/CreateInstanceJobTests.cs | 116 ++++++------- ...ngOdsInstanceManagesDispatcherJobTests.cs} | 58 +++---- .../Services/Jobs/DeleteInstanceJobTests.cs | 68 ++++---- ...ngOdsInstanceManagesDispatcherJobTests.cs} | 62 +++---- .../Services/Tenants/TenantServiceTests.cs | 102 ++++++------ 15 files changed, 456 insertions(+), 456 deletions(-) rename Application/EdFi.Ods.AdminApi.UnitTests/Features/{DbInstances/AddDbInstanceTests.cs => OdsInstances/Manage/AddOdsInstanceManageTests.cs} (65%) rename Application/EdFi.Ods.AdminApi.UnitTests/Features/{DbInstances/DeleteDbInstanceTests.cs => OdsInstances/Manage/DeleteOdsInstanceManageTests.cs} (59%) rename Application/EdFi.Ods.AdminApi.UnitTests/Features/{DbInstances/ReadDbInstanceTests.cs => OdsInstances/Manage/ReadOdsInstanceManageTests.cs} (53%) rename Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/{AddDbInstanceCommandTests.cs => AddOdsInstanceManageCommandTests.cs} (74%) rename Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/{DeleteDbInstanceCommandTests.cs => DeleteOdsInstanceManageCommandTests.cs} (70%) rename Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/{GetDbInstanceByIdQueryTests.cs => GetOdsInstanceManageByIdQueryTests.cs} (76%) rename Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/{GetDbInstancesQueryTests.cs => GetOdsInstanceManagesQueryTests.cs} (67%) rename Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/{CreatePendingDbInstancesDispatcherJobTests.cs => CreatePendingOdsInstanceManagesDispatcherJobTests.cs} (77%) rename Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/{DeletePendingDbInstancesDispatcherJobTests.cs => DeletePendingOdsInstanceManagesDispatcherJobTests.cs} (78%) diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/AddDbInstanceTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/AddOdsInstanceManageTests.cs similarity index 65% rename from Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/AddDbInstanceTests.cs rename to Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/AddOdsInstanceManageTests.cs index d799770f3..3aaa36bcd 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/AddDbInstanceTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/AddOdsInstanceManageTests.cs @@ -14,7 +14,7 @@ using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Features.DbInstances; +using EdFi.Ods.AdminApi.Features.OdsInstances.Manage; using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; using EdFi.Ods.AdminApi.Infrastructure.Services.Jobs; @@ -30,15 +30,15 @@ #nullable enable -namespace EdFi.Ods.AdminApi.UnitTests.Features.DbInstances; +namespace EdFi.Ods.AdminApi.UnitTests.Features.OdsInstances.Manage; [TestFixture] -public class AddDbInstanceTests +public class AddOdsInstanceManageTests { private static AdminApiDbContext CreateContext() { var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"AddDbInstance_{Guid.NewGuid()}") + .UseInMemoryDatabase(databaseName: $"AddOdsInstanceManage_{Guid.NewGuid()}") .Options; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -55,7 +55,7 @@ private static IOptions CreateOptions(bool multiTenancy = false) private static SqlServerUsersContext CreateUsersContext() { var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"AddDbInstanceUsers_{Guid.NewGuid()}") + .UseInMemoryDatabase(databaseName: $"AddOdsInstanceManageUsers_{Guid.NewGuid()}") .Options; return new SqlServerUsersContext(options); @@ -92,41 +92,41 @@ public async Task Handle_WithValidRequest_ReturnsAccepted() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = "My DB Instance", DatabaseTemplate = "Minimal" }; - var result = await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request); + var result = await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request); result.ShouldBeOfType(); } [Test] - public async Task Handle_WithValidRequest_PersistsDbInstance() + public async Task Handle_WithValidRequest_PersistsOdsInstanceManage() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = "My DB Instance", DatabaseTemplate = "Sample" }; - await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request); + await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request); - context.DbInstances.Any(d => d.Name == "My DB Instance").ShouldBeTrue(); + context.OdsInstanceManages.Any(d => d.Name == "My DB Instance").ShouldBeTrue(); } [Test] @@ -134,8 +134,8 @@ public async Task Handle_WithValidRequest_SchedulesCreateInstanceJob() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out var scheduler); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); @@ -145,19 +145,19 @@ public async Task Handle_WithValidRequest_SchedulesCreateInstanceJob() .Invokes((IJobDetail job, ITrigger _, CancellationToken _) => scheduledJob = job) .Returns(Task.FromResult(DateTimeOffset.UtcNow)); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = "My DB Instance", DatabaseTemplate = "Minimal" }; - await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request); + await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request); - var dbInstance = context.DbInstances.Single(); + var odsInstanceManage = context.OdsInstanceManages.Single(); scheduledJob.ShouldNotBeNull(); - scheduledJob!.Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-{dbInstance.Id}"); - scheduledJob.JobDataMap.GetInt(JobConstants.DbInstanceIdKey).ShouldBe(dbInstance.Id); + scheduledJob!.Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-{odsInstanceManage.Id}"); + scheduledJob.JobDataMap.GetInt(JobConstants.OdsInstanceManageIdKey).ShouldBe(odsInstanceManage.Id); } [Test] @@ -165,8 +165,8 @@ public async Task Handle_WithMultiTenancyEnabled_SchedulesTenantAwareCreateInsta { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out var scheduler); var tenantProvider = CreateTenantConfigurationProvider("tenant1"); var options = CreateOptions(multiTenancy: true); @@ -176,18 +176,18 @@ public async Task Handle_WithMultiTenancyEnabled_SchedulesTenantAwareCreateInsta .Invokes((IJobDetail job, ITrigger _, CancellationToken _) => scheduledJob = job) .Returns(Task.FromResult(DateTimeOffset.UtcNow)); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = "My DB Instance", DatabaseTemplate = "Minimal" }; - await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request); + await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request); - var dbInstance = context.DbInstances.Single(); + var odsInstanceManage = context.OdsInstanceManages.Single(); scheduledJob.ShouldNotBeNull(); - scheduledJob!.Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-tenant1-{dbInstance.Id}"); + scheduledJob!.Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-tenant1-{odsInstanceManage.Id}"); scheduledJob.JobDataMap.GetString(JobConstants.TenantNameKey).ShouldBe("tenant1"); } @@ -196,18 +196,18 @@ public async Task Handle_WithEmptyName_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = string.Empty, DatabaseTemplate = "Minimal" }; - await Should.ThrowAsync(async () => await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); + await Should.ThrowAsync(async () => await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); } [Test] @@ -215,18 +215,18 @@ public async Task Handle_WithNullName_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = null, DatabaseTemplate = "Minimal" }; - await Should.ThrowAsync(async () => await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); + await Should.ThrowAsync(async () => await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); } [Test] @@ -234,18 +234,18 @@ public async Task Handle_WithNameExceedingMaxLength_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = new string('a', 101), DatabaseTemplate = "Minimal" }; - await Should.ThrowAsync(async () => await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); + await Should.ThrowAsync(async () => await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); } [Test] @@ -253,18 +253,18 @@ public async Task Handle_WithNameAtPortableDatabaseNameLimit_ReturnsAccepted() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = new string('a', 46), DatabaseTemplate = "Minimal" }; - var result = await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request); + var result = await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request); result.ShouldBeOfType(); } @@ -274,21 +274,21 @@ public async Task Handle_WithFormattedDatabaseNameExceedingPortableLimit_ThrowsV { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = new string('a', 47), DatabaseTemplate = "Minimal" }; - var exception = await Should.ThrowAsync(async () => await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); + var exception = await Should.ThrowAsync(async () => await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); exception.Errors.ShouldContain(error => - error.PropertyName == nameof(AddDbInstance.AddDbInstanceRequest.Name) + error.PropertyName == nameof(AddOdsInstanceManage.AddOdsInstanceManageRequest.Name) && error.ErrorMessage.Contains("portable limit of 63 characters")); } @@ -299,18 +299,18 @@ public async Task Handle_WithInvalidNameCharacters_ThrowsValidationException(str { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = name, DatabaseTemplate = "Minimal" }; - await Should.ThrowAsync(async () => await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); + await Should.ThrowAsync(async () => await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); } [Test] @@ -318,18 +318,18 @@ public async Task Handle_WithEmptyDatabaseTemplate_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = "My DB Instance", DatabaseTemplate = string.Empty }; - await Should.ThrowAsync(async () => await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); + await Should.ThrowAsync(async () => await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); } [Test] @@ -337,18 +337,18 @@ public async Task Handle_WithNullDatabaseTemplate_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = "My DB Instance", DatabaseTemplate = null }; - await Should.ThrowAsync(async () => await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); + await Should.ThrowAsync(async () => await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); } [Test] @@ -356,32 +356,32 @@ public async Task Handle_WithInvalidDatabaseTemplate_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = "My DB Instance", DatabaseTemplate = "InvalidTemplate" }; - await Should.ThrowAsync(async () => await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); + await Should.ThrowAsync(async () => await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); } [Test] - public async Task Handle_WithExistingDbInstanceName_ThrowsValidationException() + public async Task Handle_WithExistingOdsInstanceManageName_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); - context.DbInstances.Add(new Common.Infrastructure.Models.DbInstance + context.OdsInstanceManages.Add(new Common.Infrastructure.Models.OdsInstanceManage { Name = "Existing Instance", DatabaseTemplate = "Minimal", @@ -391,17 +391,17 @@ public async Task Handle_WithExistingDbInstanceName_ThrowsValidationException() }); await context.SaveChangesAsync(); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = "Existing Instance", DatabaseTemplate = "Minimal" }; - var exception = await Should.ThrowAsync(async () => await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); + var exception = await Should.ThrowAsync(async () => await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); exception.Errors.ShouldContain(error => - error.PropertyName == nameof(AddDbInstance.AddDbInstanceRequest.Name) - && error.ErrorMessage == "A DbInstance named 'Existing Instance' already exists."); + error.PropertyName == nameof(AddOdsInstanceManage.AddOdsInstanceManageRequest.Name) + && error.ErrorMessage == "An OdsInstanceManage named 'Existing Instance' already exists."); } [Test] @@ -409,8 +409,8 @@ public async Task Handle_WithExistingOdsInstanceName_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbInstance.Validator(context, usersContext); - var command = new AddDbInstanceCommand(context); + var validator = new AddOdsInstanceManage.Validator(context, usersContext); + var command = new AddOdsInstanceManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); @@ -423,16 +423,16 @@ public async Task Handle_WithExistingOdsInstanceName_ThrowsValidationException() }); await usersContext.SaveChangesAsync(); - var request = new AddDbInstance.AddDbInstanceRequest + var request = new AddOdsInstanceManage.AddOdsInstanceManageRequest { Name = "Existing Instance", DatabaseTemplate = "Minimal" }; - var exception = await Should.ThrowAsync(async () => await AddDbInstance.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); + var exception = await Should.ThrowAsync(async () => await AddOdsInstanceManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request)); exception.Errors.ShouldContain(error => - error.PropertyName == nameof(AddDbInstance.AddDbInstanceRequest.Name) + error.PropertyName == nameof(AddOdsInstanceManage.AddOdsInstanceManageRequest.Name) && error.ErrorMessage == "An OdsInstance named 'Existing Instance' already exists."); } } diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/DeleteDbInstanceTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/DeleteOdsInstanceManageTests.cs similarity index 59% rename from Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/DeleteDbInstanceTests.cs rename to Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/DeleteOdsInstanceManageTests.cs index 3b8a14eee..8be40c500 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/DeleteDbInstanceTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/DeleteOdsInstanceManageTests.cs @@ -12,7 +12,7 @@ using EdFi.Ods.AdminApi.Common.Infrastructure.Models; using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Features.DbInstances; +using EdFi.Ods.AdminApi.Features.OdsInstances.Manage; using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; using FakeItEasy; @@ -26,13 +26,13 @@ #nullable enable -namespace EdFi.Ods.AdminApi.UnitTests.Features.DbInstances; +namespace EdFi.Ods.AdminApi.UnitTests.Features.OdsInstances.Manage; [TestFixture] -public class DeleteDbInstanceTests +public class DeleteOdsInstanceManageTests { - private IGetDbInstanceByIdQuery _getDbInstanceByIdQuery = null!; - private IDeleteDbInstanceCommand _deleteDbInstanceCommand = null!; + private IGetOdsInstanceManageByIdQuery _getOdsInstanceManageByIdQuery = null!; + private IDeleteOdsInstanceManageCommand _deleteOdsInstanceManageCommand = null!; private ISchedulerFactory _schedulerFactory = null!; private IContextProvider _tenantConfigurationProvider = null!; private IOptions _options = null!; @@ -40,8 +40,8 @@ public class DeleteDbInstanceTests [SetUp] public void SetUp() { - _getDbInstanceByIdQuery = A.Fake(); - _deleteDbInstanceCommand = A.Fake(); + _getOdsInstanceManageByIdQuery = A.Fake(); + _deleteOdsInstanceManageCommand = A.Fake(); var scheduler = A.Fake(); A.CallTo(() => scheduler.ScheduleJob(A._, A._, A._)) @@ -58,18 +58,18 @@ public void SetUp() } private Task Handle(int id) - => DeleteDbInstance.Handle( - _getDbInstanceByIdQuery, - _deleteDbInstanceCommand, + => DeleteOdsInstanceManage.Handle( + _getOdsInstanceManageByIdQuery, + _deleteOdsInstanceManageCommand, _schedulerFactory, _tenantConfigurationProvider, _options, id); [Test] - public async Task Handle_WhenDbInstanceNotFound_ThrowsNotFoundException() + public async Task Handle_WhenOdsInstanceManageNotFound_ThrowsNotFoundException() { - A.CallTo(() => _getDbInstanceByIdQuery.Execute(99)).Returns(null); + A.CallTo(() => _getOdsInstanceManageByIdQuery.Execute(99)).Returns(null); await Should.ThrowAsync>(() => Handle(99)); } @@ -77,180 +77,180 @@ public async Task Handle_WhenDbInstanceNotFound_ThrowsNotFoundException() [Test] public async Task Handle_WhenStatusIsCreated_ExecutesCommandAndReturnsAccepted() { - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Id = 1, Name = "Test", - Status = DbInstanceStatus.Created.ToString(), + Status = OdsInstanceManageStatus.Created.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(1)).Returns(dbInstance); + A.CallTo(() => _getOdsInstanceManageByIdQuery.Execute(1)).Returns(odsInstanceManage); var result = await Handle(1); result.ShouldBeOfType(); - A.CallTo(() => _deleteDbInstanceCommand.Execute(1)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _deleteOdsInstanceManageCommand.Execute(1)).MustHaveHappenedOnceExactly(); } [Test] public async Task Handle_WhenStatusIsPendingCreate_ThrowsValidationException() { - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Id = 2, Name = "Test", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(2)).Returns(dbInstance); + A.CallTo(() => _getOdsInstanceManageByIdQuery.Execute(2)).Returns(odsInstanceManage); var ex = await Should.ThrowAsync(() => Handle(2)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("provisioned")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteOdsInstanceManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsCreateInProgress_ThrowsValidationException() { - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Id = 3, Name = "Test", - Status = DbInstanceStatus.CreateInProgress.ToString(), + Status = OdsInstanceManageStatus.CreateInProgress.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(3)).Returns(dbInstance); + A.CallTo(() => _getOdsInstanceManageByIdQuery.Execute(3)).Returns(odsInstanceManage); var ex = await Should.ThrowAsync(() => Handle(3)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("provisioned")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteOdsInstanceManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsCreateFailed_ThrowsValidationException() { - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Id = 4, Name = "Test", - Status = DbInstanceStatus.CreateFailed.ToString(), + Status = OdsInstanceManageStatus.CreateFailed.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(4)).Returns(dbInstance); + A.CallTo(() => _getOdsInstanceManageByIdQuery.Execute(4)).Returns(odsInstanceManage); var ex = await Should.ThrowAsync(() => Handle(4)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("creation failed")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteOdsInstanceManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsCreateError_ThrowsValidationException() { - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Id = 5, Name = "Test", - Status = DbInstanceStatus.CreateError.ToString(), + Status = OdsInstanceManageStatus.CreateError.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(5)).Returns(dbInstance); + A.CallTo(() => _getOdsInstanceManageByIdQuery.Execute(5)).Returns(odsInstanceManage); var ex = await Should.ThrowAsync(() => Handle(5)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("creation failed permanently")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteOdsInstanceManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsPendingDelete_ThrowsValidationException() { - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Id = 6, Name = "Test", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(6)).Returns(dbInstance); + A.CallTo(() => _getOdsInstanceManageByIdQuery.Execute(6)).Returns(odsInstanceManage); var ex = await Should.ThrowAsync(() => Handle(6)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("queued for deletion")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteOdsInstanceManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsDeleteInProgress_ThrowsValidationException() { - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Id = 7, Name = "Test", - Status = DbInstanceStatus.DeleteInProgress.ToString(), + Status = OdsInstanceManageStatus.DeleteInProgress.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(7)).Returns(dbInstance); + A.CallTo(() => _getOdsInstanceManageByIdQuery.Execute(7)).Returns(odsInstanceManage); var ex = await Should.ThrowAsync(() => Handle(7)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("currently being deleted")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteOdsInstanceManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsDeleteFailed_ThrowsValidationException() { - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Id = 8, Name = "Test", - Status = DbInstanceStatus.DeleteFailed.ToString(), + Status = OdsInstanceManageStatus.DeleteFailed.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(8)).Returns(dbInstance); + A.CallTo(() => _getOdsInstanceManageByIdQuery.Execute(8)).Returns(odsInstanceManage); var ex = await Should.ThrowAsync(() => Handle(8)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("retried automatically")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteOdsInstanceManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsDeleteError_ThrowsValidationException() { - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Id = 9, Name = "Test", - Status = DbInstanceStatus.DeleteError.ToString(), + Status = OdsInstanceManageStatus.DeleteError.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(9)).Returns(dbInstance); + A.CallTo(() => _getOdsInstanceManageByIdQuery.Execute(9)).Returns(odsInstanceManage); var ex = await Should.ThrowAsync(() => Handle(9)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("deletion failed permanently")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteOdsInstanceManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsDeleted_ThrowsNotFoundException() { - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Id = 10, Name = "Test", - Status = DbInstanceStatus.Deleted.ToString(), + Status = OdsInstanceManageStatus.Deleted.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(10)).Returns(dbInstance); + A.CallTo(() => _getOdsInstanceManageByIdQuery.Execute(10)).Returns(odsInstanceManage); await Should.ThrowAsync>(() => Handle(10)); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteOdsInstanceManageCommand.Execute(A._)).MustNotHaveHappened(); } } diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/ReadDbInstanceTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/ReadOdsInstanceManageTests.cs similarity index 53% rename from Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/ReadDbInstanceTests.cs rename to Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/ReadOdsInstanceManageTests.cs index 1c1509bd3..ba0455865 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/ReadDbInstanceTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/ReadOdsInstanceManageTests.cs @@ -8,33 +8,33 @@ using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; using EdFi.Ods.AdminApi.Common.Infrastructure.Models; -using EdFi.Ods.AdminApi.Features.DbInstances; +using EdFi.Ods.AdminApi.Features.OdsInstances.Manage; using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; using FakeItEasy; using NUnit.Framework; using Shouldly; -namespace EdFi.Ods.AdminApi.UnitTests.Features.DbInstances; +namespace EdFi.Ods.AdminApi.UnitTests.Features.OdsInstances.Manage; [TestFixture] -public class ReadDbInstanceTests +public class ReadOdsInstanceManageTests { [Test] - public async Task GetDbInstances_ReturnsOkWithMappedList() + public async Task GetOdsInstanceManages_ReturnsOkWithMappedList() { - var fakeQuery = A.Fake(); + var fakeQuery = A.Fake(); var queryParams = new CommonQueryParams(0, 10); - var queryResult = new List + var queryResult = new List { - new DbInstance { Id = 1, Name = "Instance A", Status = "Pending", DatabaseTemplate = "Minimal" } + new OdsInstanceManage { Id = 1, Name = "Instance A", Status = "Pending", DatabaseTemplate = "Minimal" } }; A.CallTo(() => fakeQuery.Execute(A._, null, null)).Returns(queryResult); - var result = await ReadDbInstance.GetDbInstances(fakeQuery, queryParams, null, null); + var result = await ReadOdsInstanceManage.GetOdsInstanceManages(fakeQuery, queryParams, null, null); - result.ShouldBeOfType>>(); - var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; + result.ShouldBeOfType>>(); + var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; okResult!.Value.ShouldNotBeNull(); okResult.Value.Count.ShouldBe(1); okResult.Value[0].Id.ShouldBe(1); @@ -44,17 +44,17 @@ public async Task GetDbInstances_ReturnsOkWithMappedList() } [Test] - public async Task GetDbInstance_ReturnsOkWithMappedModel() + public async Task GetOdsInstanceManage_ReturnsOkWithMappedModel() { - var fakeQuery = A.Fake(); - var queryResult = new DbInstance { Id = 5, Name = "Instance B", Status = "Completed", DatabaseTemplate = "Sample" }; + var fakeQuery = A.Fake(); + var queryResult = new OdsInstanceManage { Id = 5, Name = "Instance B", Status = "Completed", DatabaseTemplate = "Sample" }; A.CallTo(() => fakeQuery.Execute(5)).Returns(queryResult); - var result = await ReadDbInstance.GetDbInstance(fakeQuery, 5); + var result = await ReadOdsInstanceManage.GetOdsInstanceManage(fakeQuery, 5); - result.ShouldBeOfType>(); - var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok; + result.ShouldBeOfType>(); + var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok; okResult!.Value.ShouldNotBeNull(); okResult.Value.Id.ShouldBe(5); okResult.Value.Name.ShouldBe("Instance B"); @@ -63,67 +63,67 @@ public async Task GetDbInstance_ReturnsOkWithMappedModel() } [Test] - public void GetDbInstance_WhenNotFound_ThrowsNotFoundException() + public void GetOdsInstanceManage_WhenNotFound_ThrowsNotFoundException() { - var fakeQuery = A.Fake(); + var fakeQuery = A.Fake(); A.CallTo(() => fakeQuery.Execute(99)).Returns(null); Should.Throw>( - () => ReadDbInstance.GetDbInstance(fakeQuery, 99).GetAwaiter().GetResult()); + () => ReadOdsInstanceManage.GetOdsInstanceManage(fakeQuery, 99).GetAwaiter().GetResult()); } [Test] - public void GetDbInstances_WhenQueryThrows_ExceptionIsPropagated() + public void GetOdsInstanceManages_WhenQueryThrows_ExceptionIsPropagated() { - var fakeQuery = A.Fake(); + var fakeQuery = A.Fake(); A.CallTo(() => fakeQuery.Execute(A._, null, null)) .Throws(new System.Exception("Query failed")); Should.Throw(async () => - await ReadDbInstance.GetDbInstances(fakeQuery, new CommonQueryParams(0, 10), null, null)); + await ReadOdsInstanceManage.GetOdsInstanceManages(fakeQuery, new CommonQueryParams(0, 10), null, null)); } [Test] - public async Task GetDbInstances_ReturnsOkWithEmptyList() + public async Task GetOdsInstanceManages_ReturnsOkWithEmptyList() { - var fakeQuery = A.Fake(); + var fakeQuery = A.Fake(); var queryParams = new CommonQueryParams(0, 10); - A.CallTo(() => fakeQuery.Execute(A._, null, null)).Returns(new List()); + A.CallTo(() => fakeQuery.Execute(A._, null, null)).Returns(new List()); - var result = await ReadDbInstance.GetDbInstances(fakeQuery, queryParams, null, null); + var result = await ReadOdsInstanceManage.GetOdsInstanceManages(fakeQuery, queryParams, null, null); - result.ShouldBeOfType>>(); - var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; + result.ShouldBeOfType>>(); + var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; okResult!.Value.ShouldBeEmpty(); } [Test] - public async Task GetDbInstances_WithIdFilter_PassesIdToQuery() + public async Task GetOdsInstanceManages_WithIdFilter_PassesIdToQuery() { - var fakeQuery = A.Fake(); + var fakeQuery = A.Fake(); var queryParams = new CommonQueryParams(0, 10); - var queryResult = new List(); + var queryResult = new List(); A.CallTo(() => fakeQuery.Execute(A._, 42, null)).Returns(queryResult); - await ReadDbInstance.GetDbInstances(fakeQuery, queryParams, 42, null); + await ReadOdsInstanceManage.GetOdsInstanceManages(fakeQuery, queryParams, 42, null); A.CallTo(() => fakeQuery.Execute(A._, 42, null)).MustHaveHappenedOnceExactly(); } [Test] - public async Task GetDbInstances_WithNameFilter_PassesNameToQuery() + public async Task GetOdsInstanceManages_WithNameFilter_PassesNameToQuery() { - var fakeQuery = A.Fake(); + var fakeQuery = A.Fake(); var queryParams = new CommonQueryParams(0, 10); - var queryResult = new List(); + var queryResult = new List(); A.CallTo(() => fakeQuery.Execute(A._, null, "Instance A")).Returns(queryResult); - await ReadDbInstance.GetDbInstances(fakeQuery, queryParams, null, "Instance A"); + await ReadOdsInstanceManage.GetOdsInstanceManages(fakeQuery, queryParams, null, "Instance A"); A.CallTo(() => fakeQuery.Execute(A._, null, "Instance A")).MustHaveHappenedOnceExactly(); } diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/ReadEducationOrganizationsTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/ReadEducationOrganizationsTests.cs index 8ce74df37..ffe7f343f 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/ReadEducationOrganizationsTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/ReadEducationOrganizationsTests.cs @@ -21,7 +21,7 @@ namespace EdFi.Ods.AdminApi.UnitTests.Features.OdsInstances; public class ReadEducationOrganizationsTests { private IGetEducationOrganizationsQuery _getEdOrgsQuery = null!; - private IGetDbInstancesQuery _getDbInstancesQuery = null!; + private IGetOdsInstanceManagesQuery _getOdsInstanceManagesQuery = null!; private IGetOdsInstanceQuery _getOdsInstanceQuery = null!; private CommonQueryParams _queryParams; @@ -29,13 +29,13 @@ public class ReadEducationOrganizationsTests public void SetUp() { _getEdOrgsQuery = A.Fake(); - _getDbInstancesQuery = A.Fake(); + _getOdsInstanceManagesQuery = A.Fake(); _getOdsInstanceQuery = A.Fake(); _queryParams = new CommonQueryParams(0, 10); } [Test] - public async Task GetEducationOrganizationsByInstance_DoesNotAppendUnlinkedDbInstances() + public async Task GetEducationOrganizationsByInstance_DoesNotAppendUnlinkedOdsInstanceManages() { var instanceId = 3; A.CallTo(() => _getOdsInstanceQuery.Execute(instanceId)).Returns(new OdsInstance { OdsInstanceId = instanceId }); @@ -44,14 +44,14 @@ public async Task GetEducationOrganizationsByInstance_DoesNotAppendUnlinkedDbIns { new() { Id = instanceId, Name = "Instance3" } }); - A.CallTo(() => _getDbInstancesQuery.Execute(A._, null, null)) - .Returns(new List + A.CallTo(() => _getOdsInstanceManagesQuery.Execute(A._, null, null)) + .Returns(new List { - new DbInstance { Id = 1, Name = "Unlinked", OdsInstanceId = null, Status = "PendingCreate" } + new OdsInstanceManage { Id = 1, Name = "Unlinked", OdsInstanceId = null, Status = "PendingCreate" } }); var result = await ReadEducationOrganizations.GetEducationOrganizationsByInstance( - _getEdOrgsQuery, _getOdsInstanceQuery, _getDbInstancesQuery, _queryParams, instanceId); + _getEdOrgsQuery, _getOdsInstanceQuery, _getOdsInstanceManagesQuery, _queryParams, instanceId); var ok = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; ok.ShouldNotBeNull(); @@ -60,7 +60,7 @@ public async Task GetEducationOrganizationsByInstance_DoesNotAppendUnlinkedDbIns } [Test] - public async Task GetEducationOrganizationsByInstance_EnrichesLinkedDbInstanceFields() + public async Task GetEducationOrganizationsByInstance_EnrichesLinkedOdsInstanceManageFields() { var instanceId = 7; A.CallTo(() => _getOdsInstanceQuery.Execute(instanceId)).Returns(new OdsInstance { OdsInstanceId = instanceId }); @@ -69,18 +69,18 @@ public async Task GetEducationOrganizationsByInstance_EnrichesLinkedDbInstanceFi { new() { Id = instanceId, Name = "Instance7" } }); - A.CallTo(() => _getDbInstancesQuery.Execute(A._, null, null)) - .Returns(new List + A.CallTo(() => _getOdsInstanceManagesQuery.Execute(A._, null, null)) + .Returns(new List { - new DbInstance { Id = 5, OdsInstanceId = instanceId, Status = "Healthy", DatabaseTemplate = "Minimal", DatabaseName = "EdFi_Ods_7" } + new OdsInstanceManage { Id = 5, OdsInstanceId = instanceId, Status = "Healthy", DatabaseTemplate = "Minimal", DatabaseName = "EdFi_Ods_7" } }); var result = await ReadEducationOrganizations.GetEducationOrganizationsByInstance( - _getEdOrgsQuery, _getOdsInstanceQuery, _getDbInstancesQuery, _queryParams, instanceId); + _getEdOrgsQuery, _getOdsInstanceQuery, _getOdsInstanceManagesQuery, _queryParams, instanceId); var ok = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; ok.ShouldNotBeNull(); - ok.Value![0].DbInstanceId.ShouldBe(5); + ok.Value![0].OdsInstanceManageId.ShouldBe(5); ok.Value![0].Status.ShouldBe("Healthy"); ok.Value[0].DatabaseTemplate.ShouldBe("Minimal"); ok.Value[0].DatabaseName.ShouldBe("EdFi_Ods_7"); @@ -94,6 +94,6 @@ public void GetEducationOrganizationsByInstance_WhenOdsInstanceNotFound_ThrowsNo Should.Throw>(async () => await ReadEducationOrganizations.GetEducationOrganizationsByInstance( - _getEdOrgsQuery, _getOdsInstanceQuery, _getDbInstancesQuery, _queryParams, 99)); + _getEdOrgsQuery, _getOdsInstanceQuery, _getOdsInstanceManagesQuery, _queryParams, 99)); } } diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/ReadTenantsTest.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/ReadTenantsTest.cs index be0038be9..a48084969 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/ReadTenantsTest.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/ReadTenantsTest.cs @@ -26,15 +26,15 @@ public class ReadTenantsTest { private IGetOdsInstancesQuery _getOdsInstancesQuery = null!; private IGetEducationOrganizationQuery _getEducationOrganizationQuery = null!; - private IGetDbInstancesQuery _getDbInstancesQuery = null!; + private IGetOdsInstanceManagesQuery _getOdsInstanceManagesQuery = null!; [SetUp] public void SetUp() { _getOdsInstancesQuery = A.Fake(); _getEducationOrganizationQuery = A.Fake(); - _getDbInstancesQuery = A.Fake(); - A.CallTo(() => _getDbInstancesQuery.Execute(A._, A._, A.Ignored)) + _getOdsInstanceManagesQuery = A.Fake(); + A.CallTo(() => _getOdsInstanceManagesQuery.Execute(A._, A._, A.Ignored)) .Returns([]); } @@ -75,9 +75,9 @@ public async Task GetTenantEdOrgsByInstancesAsync_ReturnsOk_WhenTenantExists() A.CallTo(() => request.Path).Returns(new PathString("/tenants/tenant1/OdsInstances/edOrgs")); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, tenantName)).Returns(tenantDetailModel); - var result = await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, memoryCache, options, swaggerOptions, tenantName); + var result = await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, memoryCache, options, swaggerOptions, tenantName); result.ShouldNotBeNull(); } @@ -113,14 +113,14 @@ public async Task GetTenantEdOrgsByInstancesAsync_ReturnsNullId_WhenOdsInstanceI A.CallTo(() => request.Path).Returns(new PathString("/tenants/tenant1/OdsInstances/edOrgs")); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, tenantName)).Returns(tenantDetailModel); var result = await ReadTenants.GetTenantEdOrgsByInstancesAsync( request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, - _getDbInstancesQuery, + _getOdsInstanceManagesQuery, memoryCache, options, swaggerOptions, @@ -153,7 +153,7 @@ public void GetTenantEdOrgsByInstancesAsync_ThrowsValidationException_WhenTenant Should.ThrowAsync(async () => { - await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, memoryCache, options, swaggerOptions, tenantName); + await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, memoryCache, options, swaggerOptions, tenantName); }); } @@ -177,7 +177,7 @@ public void GetTenantEdOrgsByInstancesAsync_ThrowsValidationException_WhenTenant Should.ThrowAsync(async () => { - await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, memoryCache, options, swaggerOptions, tenantName); + await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, memoryCache, options, swaggerOptions, tenantName); }); } @@ -204,9 +204,9 @@ public async Task GetTenantEdOrgsByInstancesAsync_SkipsTenantHeaderValidation_Wh A.CallTo(() => request.Path).Returns(new PathString("/swagger/index.html")); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, tenantName)).Returns(tenantDetailModel); - var result = await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, memoryCache, options, swaggerOptions, tenantName); + var result = await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, memoryCache, options, swaggerOptions, tenantName); result.ShouldNotBeNull(); } @@ -234,9 +234,9 @@ public async Task GetTenantEdOrgsByInstancesAsync_SkipsTenantHeaderValidation_Wh A.CallTo(() => request.Path).Returns(new PathString("/tenants/tenant1/OdsInstances/edOrgs")); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, tenantName)).Returns(tenantDetailModel); - var result = await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, memoryCache, options, swaggerOptions, tenantName); + var result = await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, memoryCache, options, swaggerOptions, tenantName); result.ShouldNotBeNull(); } @@ -264,9 +264,9 @@ public async Task GetTenantEdOrgsByInstancesAsync_SkipsTenantHeaderValidation_Wh A.CallTo(() => request.Path).Returns(new PathString("/SWAGGER/index.html")); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, tenantName)).Returns(tenantDetailModel); - var result = await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, memoryCache, options, swaggerOptions, tenantName); + var result = await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, memoryCache, options, swaggerOptions, tenantName); result.ShouldNotBeNull(); } @@ -294,9 +294,9 @@ public async Task GetTenantEdOrgsByInstancesAsync_SkipsTenantHeaderValidation_Wh A.CallTo(() => request.Path).Returns(new PathString("/tenants/tenant1/OdsInstances/edOrgs")); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, tenantName)).Returns(tenantDetailModel); - var result = await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, memoryCache, options, swaggerOptions, tenantName); + var result = await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, memoryCache, options, swaggerOptions, tenantName); result.ShouldNotBeNull(); } @@ -321,7 +321,7 @@ public void GetTenantEdOrgsByInstancesAsync_EnforcesTenantHeaderValidation_WhenN Should.ThrowAsync(async () => { - await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, memoryCache, options, swaggerOptions, tenantName); + await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, memoryCache, options, swaggerOptions, tenantName); }); } @@ -348,9 +348,9 @@ public async Task GetTenantEdOrgsByInstancesAsync_SkipsTenantHeaderValidation_Wh A.CallTo(() => request.Path).Returns(new PathString()); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, tenantName)).Returns(tenantDetailModel); - var result = await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, memoryCache, options, swaggerOptions, tenantName); + var result = await ReadTenants.GetTenantEdOrgsByInstancesAsync(request, tenantsService, _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, memoryCache, options, swaggerOptions, tenantName); result.ShouldNotBeNull(); } diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/TenantDetailModelTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/TenantDetailModelTests.cs index fb7de8e3b..c9b761904 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/TenantDetailModelTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/TenantDetailModelTests.cs @@ -41,7 +41,7 @@ public void Properties_ShouldBeSettable() var odsInstance = new TenantOdsInstanceModel() { OdsInstanceId = 1, - DbInstanceId = 10, + OdsInstanceManageId = 10, EducationOrganizations = [educationOrganization] }; @@ -54,7 +54,7 @@ public void Properties_ShouldBeSettable() // Assert tenantDetailModel.TenantName.ShouldBe(tenantName); tenantDetailModel.OdsInstances.ShouldBe([odsInstance]); - tenantDetailModel.OdsInstances[0].DbInstanceId.ShouldBe(10); + tenantDetailModel.OdsInstances[0].OdsInstanceManageId.ShouldBe(10); tenantDetailModel.OdsInstances[0].EducationOrganizations.ShouldBe([educationOrganization]); } diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddDbInstanceCommandTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddOdsInstanceManageCommandTests.cs similarity index 74% rename from Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddDbInstanceCommandTests.cs rename to Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddOdsInstanceManageCommandTests.cs index 792f95346..81bab5606 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddDbInstanceCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddOdsInstanceManageCommandTests.cs @@ -19,12 +19,12 @@ namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Database.Commands; [TestFixture] -public class AddDbInstanceCommandTests +public class AddOdsInstanceManageCommandTests { private static AdminApiDbContext CreateContext() { var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"AddDbInstanceCommand_{Guid.NewGuid()}") + .UseInMemoryDatabase(databaseName: $"AddOdsInstanceManageCommand_{Guid.NewGuid()}") .Options; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -36,11 +36,11 @@ private static AdminApiDbContext CreateContext() } [Test] - public void Execute_WithValidModel_PersistsDbInstance() + public void Execute_WithValidModel_PersistsOdsInstanceManage() { using var context = CreateContext(); - var command = new AddDbInstanceCommand(context); - var model = new AddDbInstanceModelStub + var command = new AddOdsInstanceManageCommand(context); + var model = new AddOdsInstanceManageModelStub { Name = "Test Instance", DatabaseTemplate = "Minimal" @@ -49,16 +49,16 @@ public void Execute_WithValidModel_PersistsDbInstance() var result = command.Execute(model); result.Id.ShouldBeGreaterThan(0); - context.DbInstances.Any(d => d.Id == result.Id).ShouldBeTrue(); + context.OdsInstanceManages.Any(d => d.Id == result.Id).ShouldBeTrue(); } [Test] public void Execute_WithValidModel_SetsExpectedFieldValues() { using var context = CreateContext(); - var command = new AddDbInstanceCommand(context); + var command = new AddOdsInstanceManageCommand(context); var before = DateTime.UtcNow; - var model = new AddDbInstanceModelStub + var model = new AddOdsInstanceManageModelStub { Name = " Test Instance ", DatabaseTemplate = " Minimal " @@ -68,7 +68,7 @@ public void Execute_WithValidModel_SetsExpectedFieldValues() result.Name.ShouldBe("Test Instance"); result.DatabaseTemplate.ShouldBe("Minimal"); - result.Status.ShouldBe(DbInstanceStatus.PendingCreate.ToString()); + result.Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); result.OdsInstanceId.ShouldBeNull(); result.OdsInstanceName.ShouldBeNull(); result.DatabaseName.ShouldBeNull(); @@ -81,8 +81,8 @@ public void Execute_WithValidModel_SetsExpectedFieldValues() public void Execute_WithSampleTemplate_PersistsWithCorrectTemplate() { using var context = CreateContext(); - var command = new AddDbInstanceCommand(context); - var model = new AddDbInstanceModelStub + var command = new AddOdsInstanceManageCommand(context); + var model = new AddOdsInstanceManageModelStub { Name = "Sample Instance", DatabaseTemplate = "Sample" @@ -91,10 +91,10 @@ public void Execute_WithSampleTemplate_PersistsWithCorrectTemplate() var result = command.Execute(model); result.DatabaseTemplate.ShouldBe("Sample"); - context.DbInstances.Any(d => d.Id == result.Id && d.DatabaseTemplate == "Sample").ShouldBeTrue(); + context.OdsInstanceManages.Any(d => d.Id == result.Id && d.DatabaseTemplate == "Sample").ShouldBeTrue(); } - private sealed class AddDbInstanceModelStub : IAddDbInstanceModel + private sealed class AddOdsInstanceManageModelStub : IAddOdsInstanceManageModel { public string? Name { get; set; } public string? DatabaseTemplate { get; set; } diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteDbInstanceCommandTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommandTests.cs similarity index 70% rename from Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteDbInstanceCommandTests.cs rename to Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommandTests.cs index b8a06dcad..ff4ab584c 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteDbInstanceCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommandTests.cs @@ -21,12 +21,12 @@ namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Database.Commands; [TestFixture] -public class DeleteDbInstanceCommandTests +public class DeleteOdsInstanceManageCommandTests { private static AdminApiDbContext CreateContext() { var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"DeleteDbInstanceCommand_{Guid.NewGuid()}") + .UseInMemoryDatabase(databaseName: $"DeleteOdsInstanceManageCommand_{Guid.NewGuid()}") .Options; var configuration = new ConfigurationBuilder() .AddInMemoryCollection( @@ -40,21 +40,21 @@ private static AdminApiDbContext CreateContext() public void Execute_SetsStatusToPendingDelete() { using var context = CreateContext(); - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Test Instance", - Status = DbInstanceStatus.Created.ToString(), + Status = OdsInstanceManageStatus.Created.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow, }; - context.DbInstances.Add(instance); + context.OdsInstanceManages.Add(instance); context.SaveChanges(); - var command = new DeleteDbInstanceCommand(context); + var command = new DeleteOdsInstanceManageCommand(context); command.Execute(instance.Id); - var updated = context.DbInstances.Single(d => d.Id == instance.Id); - updated.Status.ShouldBe(DbInstanceStatus.PendingDelete.ToString()); + var updated = context.OdsInstanceManages.Single(d => d.Id == instance.Id); + updated.Status.ShouldBe(OdsInstanceManageStatus.PendingDelete.ToString()); } [Test] @@ -62,20 +62,20 @@ public void Execute_UpdatesLastModifiedDate() { using var context = CreateContext(); var before = DateTime.UtcNow; - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Test Instance", - Status = DbInstanceStatus.Created.ToString(), + Status = OdsInstanceManageStatus.Created.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow, }; - context.DbInstances.Add(instance); + context.OdsInstanceManages.Add(instance); context.SaveChanges(); - var command = new DeleteDbInstanceCommand(context); + var command = new DeleteOdsInstanceManageCommand(context); command.Execute(instance.Id); - var updated = context.DbInstances.Single(d => d.Id == instance.Id); + var updated = context.OdsInstanceManages.Single(d => d.Id == instance.Id); updated.LastModifiedDate.ShouldNotBeNull(); updated.LastModifiedDate!.Value.ShouldBeGreaterThanOrEqualTo(before); } @@ -84,7 +84,7 @@ public void Execute_UpdatesLastModifiedDate() public void Execute_WithNonExistentId_ThrowsNotFoundException() { using var context = CreateContext(); - var command = new DeleteDbInstanceCommand(context); + var command = new DeleteOdsInstanceManageCommand(context); Should.Throw>(() => command.Execute(9999)); } @@ -93,17 +93,17 @@ public void Execute_WithNonExistentId_ThrowsNotFoundException() public void Execute_WhenStatusIsDeleted_ThrowsNotFoundException() { using var context = CreateContext(); - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Test Instance", - Status = DbInstanceStatus.Deleted.ToString(), + Status = OdsInstanceManageStatus.Deleted.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow, }; - context.DbInstances.Add(instance); + context.OdsInstanceManages.Add(instance); context.SaveChanges(); - var command = new DeleteDbInstanceCommand(context); + var command = new DeleteOdsInstanceManageCommand(context); Should.Throw>(() => command.Execute(instance.Id)); } diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetDbInstanceByIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQueryTests.cs similarity index 76% rename from Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetDbInstanceByIdQueryTests.cs rename to Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQueryTests.cs index 5b4d9d3eb..89bb607b9 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetDbInstanceByIdQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQueryTests.cs @@ -18,12 +18,12 @@ namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Database.Queries; [TestFixture] -public class GetDbInstanceByIdQueryTests +public class GetOdsInstanceManageByIdQueryTests { private static AdminApiDbContext CreateContext() { var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"GetDbInstanceByIdQueryTests_{Guid.NewGuid()}") + .UseInMemoryDatabase(databaseName: $"GetOdsInstanceManageByIdQueryTests_{Guid.NewGuid()}") .Options; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -36,21 +36,21 @@ private static AdminApiDbContext CreateContext() } [Test] - public void Execute_WithExistingId_ReturnsDbInstance() + public void Execute_WithExistingId_ReturnsOdsInstanceManage() { using var context = CreateContext(); - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Name = "Sandbox", Status = "Healthy", DatabaseTemplate = "Minimal" }; - context.DbInstances.Add(dbInstance); + context.OdsInstanceManages.Add(odsInstanceManage); context.SaveChanges(); - var query = new GetDbInstanceByIdQuery(context); + var query = new GetOdsInstanceManageByIdQuery(context); - var result = query.Execute(dbInstance.Id); + var result = query.Execute(odsInstanceManage.Id); result.ShouldNotBeNull(); result.Name.ShouldBe("Sandbox"); @@ -60,7 +60,7 @@ public void Execute_WithExistingId_ReturnsDbInstance() public void Execute_WithUnknownId_ReturnsNull() { using var context = CreateContext(); - var query = new GetDbInstanceByIdQuery(context); + var query = new GetOdsInstanceManageByIdQuery(context); var result = query.Execute(999); diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetDbInstancesQueryTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManagesQueryTests.cs similarity index 67% rename from Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetDbInstancesQueryTests.cs rename to Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManagesQueryTests.cs index f633586d2..555acf0bc 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetDbInstancesQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManagesQueryTests.cs @@ -22,12 +22,12 @@ namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Database.Queries; [TestFixture] -public class GetDbInstancesQueryTests +public class GetOdsInstanceManagesQueryTests { private static AdminApiDbContext CreateContext() { var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"GetDbInstancesQueryTests_{Guid.NewGuid()}") + .UseInMemoryDatabase(databaseName: $"GetOdsInstanceManagesQueryTests_{Guid.NewGuid()}") .Options; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -43,15 +43,15 @@ private static IOptions DefaultOptions() => Options.Create(new AppSettings { DatabaseEngine = "Postgres", DefaultPageSizeLimit = 25 }); [Test] - public void Execute_WithoutFilters_ReturnsAllDbInstances() + public void Execute_WithoutFilters_ReturnsAllOdsInstanceManages() { using var context = CreateContext(); - context.DbInstances.AddRange( - new DbInstance { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, - new DbInstance { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); + context.OdsInstanceManages.AddRange( + new OdsInstanceManage { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, + new OdsInstanceManage { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); context.SaveChanges(); - var query = new GetDbInstancesQuery(context, DefaultOptions()); + var query = new GetOdsInstanceManagesQuery(context, DefaultOptions()); var result = query.Execute(new CommonQueryParams(0, 25), null, null); @@ -60,15 +60,15 @@ public void Execute_WithoutFilters_ReturnsAllDbInstances() } [Test] - public void Execute_WithNameFilter_ReturnsMatchingDbInstance() + public void Execute_WithNameFilter_ReturnsMatchingOdsInstanceManage() { using var context = CreateContext(); - context.DbInstances.AddRange( - new DbInstance { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, - new DbInstance { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); + context.OdsInstanceManages.AddRange( + new OdsInstanceManage { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, + new OdsInstanceManage { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); context.SaveChanges(); - var query = new GetDbInstancesQuery(context, DefaultOptions()); + var query = new GetOdsInstanceManagesQuery(context, DefaultOptions()); var result = query.Execute(new CommonQueryParams(0, 25), null, "Sandbox B"); diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreateInstanceJobTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreateInstanceJobTests.cs index 59a247ac3..382529dd4 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreateInstanceJobTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreateInstanceJobTests.cs @@ -15,7 +15,7 @@ using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; using EdFi.Ods.AdminApi.Common.Infrastructure.Providers.Interfaces; -using EdFi.Ods.AdminApi.Features.DbInstances; +using EdFi.Ods.AdminApi.Features.OdsInstances.Manage; using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; using EdFi.Ods.AdminApi.Common.Settings; using EdFi.Ods.AdminApi.Infrastructure; @@ -74,13 +74,13 @@ private static SqlServerUsersContext CreateUsersContext(string databaseName) return new NonDisposingSqlServerUsersContext(options); } - private static IJobExecutionContext CreateJobExecutionContext(int dbInstanceId, string tenantName = null) + private static IJobExecutionContext CreateJobExecutionContext(int odsInstanceManageId, string tenantName = null) { var jobExecutionContext = A.Fake(); var jobDetail = A.Fake(); var jobDataMap = new JobDataMap { - { JobConstants.DbInstanceIdKey, dbInstanceId } + { JobConstants.OdsInstanceManageIdKey, odsInstanceManageId } }; if (!string.IsNullOrWhiteSpace(tenantName)) @@ -152,11 +152,11 @@ public void CreateInstanceJob_ShouldPreventConcurrentExecution() [TestCase("EdFi Ods", "Minimal", "EdFi_Ods_Minimal")] public void BuildDatabaseName_UsesCanonicalFormat(string name, string databaseTemplate, string expectedDatabaseName) { - DbInstanceDatabaseNameFormatter.Build(name, databaseTemplate).ShouldBe(expectedDatabaseName); + OdsInstanceManageDatabaseNameFormatter.Build(name, databaseTemplate).ShouldBe(expectedDatabaseName); } [Test] - public async Task Execute_CreatesOdsInstance_AndCompletesDbInstance() + public async Task Execute_CreatesOdsInstance_AndCompletesOdsInstanceManage() { var configuration = CreateConfiguration(); using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}", configuration); @@ -173,16 +173,16 @@ public async Task Execute_CreatesOdsInstance_AndCompletesDbInstance() .Invokes((string connectionString, byte[] _) => plaintextConnectionString = connectionString) .ReturnsLazily((string connectionString, byte[] _) => $"encrypted::{connectionString}"); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -199,16 +199,16 @@ public async Task Execute_CreatesOdsInstance_AndCompletesDbInstance() configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - var persistedDbInstance = adminApiContext.DbInstances.Single(); + var persistedOdsInstanceManage = adminApiContext.OdsInstanceManages.Single(); var persistedOdsInstance = usersContext.OdsInstances.Single(); const string expectedDatabaseName = "EdFi_Ods_Sandbox_Minimal"; - persistedDbInstance.Status.ShouldBe(DbInstanceStatus.Created.ToString()); - persistedDbInstance.DatabaseName.ShouldBe(expectedDatabaseName); - persistedDbInstance.OdsInstanceId.ShouldNotBeNull(); - persistedDbInstance.OdsInstanceName.ShouldBe("Sandbox"); + persistedOdsInstanceManage.Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); + persistedOdsInstanceManage.DatabaseName.ShouldBe(expectedDatabaseName); + persistedOdsInstanceManage.OdsInstanceId.ShouldNotBeNull(); + persistedOdsInstanceManage.OdsInstanceName.ShouldBe("Sandbox"); persistedOdsInstance.Name.ShouldBe("Sandbox"); persistedOdsInstance.InstanceType.ShouldBe("Minimal"); A.CallTo(() => sandboxProvisioner.AddSandboxAsync(expectedDatabaseName, SandboxType.Minimal)) @@ -231,16 +231,16 @@ public async Task Execute_FormatsDatabaseName_WhenNameContainsSpaces() var encryptionProvider = A.Fake(); var sandboxProvisioner = A.Fake(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "My District", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -257,9 +257,9 @@ public async Task Execute_FormatsDatabaseName_WhenNameContainsSpaces() configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().DatabaseName.ShouldBe("EdFi_Ods_My_District_Minimal"); + adminApiContext.OdsInstanceManages.Single().DatabaseName.ShouldBe("EdFi_Ods_My_District_Minimal"); } [Test] @@ -281,17 +281,17 @@ public async Task Execute_ReusesExistingDatabaseName_WhenAlreadyAssigned() .Invokes((string connectionString, byte[] _) => plaintextConnectionString = connectionString) .ReturnsLazily((string connectionString, byte[] _) => $"encrypted::{connectionString}"); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", DatabaseName = existingDatabaseName, - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -308,16 +308,16 @@ public async Task Execute_ReusesExistingDatabaseName_WhenAlreadyAssigned() configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().DatabaseName.ShouldBe(existingDatabaseName); + adminApiContext.OdsInstanceManages.Single().DatabaseName.ShouldBe(existingDatabaseName); plaintextConnectionString.ShouldContain($"Initial Catalog={existingDatabaseName}"); A.CallTo(() => sandboxProvisioner.AddSandboxAsync(existingDatabaseName, SandboxType.Minimal)) .MustHaveHappenedOnceExactly(); } [Test] - public async Task Execute_DoesNothing_WhenDbInstanceIsNotPending() + public async Task Execute_DoesNothing_WhenOdsInstanceManageIsNotPending() { var configuration = CreateConfiguration(); using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}", configuration); @@ -329,16 +329,16 @@ public async Task Execute_DoesNothing_WhenDbInstanceIsNotPending() var encryptionProvider = A.Fake(); var sandboxProvisioner = A.Fake(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.Created.ToString(), + Status = OdsInstanceManageStatus.Created.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -355,16 +355,16 @@ public async Task Execute_DoesNothing_WhenDbInstanceIsNotPending() configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.Created.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); usersContext.OdsInstances.ShouldBeEmpty(); A.CallTo(() => sandboxProvisioner.AddSandboxAsync(A._, A._)).MustNotHaveHappened(); A.CallTo(() => encryptionProvider.Encrypt(A._, A._)).MustNotHaveHappened(); } [Test] - public async Task Execute_SetsDbInstanceToError_When_ProvisioningFails() + public async Task Execute_SetsOdsInstanceManageToError_When_ProvisioningFails() { var configuration = CreateConfiguration(); using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}", configuration); @@ -379,16 +379,16 @@ public async Task Execute_SetsDbInstanceToError_When_ProvisioningFails() A.CallTo(() => sandboxProvisioner.AddSandboxAsync(A._, A._)) .Throws(new InvalidOperationException("Provisioning failed.")); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -405,16 +405,16 @@ public async Task Execute_SetsDbInstanceToError_When_ProvisioningFails() configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.CreateFailed.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.CreateFailed.ToString()); usersContext.OdsInstances.ShouldBeEmpty(); A.CallTo(() => jobStatusService.SetStatusAsync(A._, QuartzJobStatus.Error, A._, A.That.Contains("Provisioning failed."))) .MustHaveHappenedOnceExactly(); } [Test] - public async Task Execute_SetsDbInstanceToError_WhenPendingStateAlreadyContainsOdsReferences() + public async Task Execute_SetsOdsInstanceManageToError_WhenPendingStateAlreadyContainsOdsReferences() { var configuration = CreateConfiguration(); using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}", configuration); @@ -426,18 +426,18 @@ public async Task Execute_SetsDbInstanceToError_WhenPendingStateAlreadyContainsO var encryptionProvider = A.Fake(); var sandboxProvisioner = A.Fake(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), OdsInstanceId = 42, OdsInstanceName = "Sandbox", LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -454,9 +454,9 @@ public async Task Execute_SetsDbInstanceToError_WhenPendingStateAlreadyContainsO configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.CreateFailed.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.CreateFailed.ToString()); usersContext.OdsInstances.ShouldBeEmpty(); A.CallTo(() => sandboxProvisioner.AddSandboxAsync(A._, A._)).MustNotHaveHappened(); A.CallTo(() => jobStatusService.SetStatusAsync(A._, QuartzJobStatus.Error, A._, A.That.Contains("invalid pending state"))) @@ -487,16 +487,16 @@ public async Task Execute_UsesTenantSpecificOdsConnectionString_WhenMultiTenancy .Invokes((string connectionString, byte[] _) => plaintextConnectionString = connectionString) .ReturnsLazily((string connectionString, byte[] _) => $"encrypted::{connectionString}"); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Sample", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - tenantAdminApiContext.DbInstances.Add(dbInstance); + tenantAdminApiContext.OdsInstanceManages.Add(odsInstanceManage); tenantAdminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -513,14 +513,14 @@ public async Task Execute_UsesTenantSpecificOdsConnectionString_WhenMultiTenancy configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id, "tenant1")); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id, "tenant1")); - var persistedDbInstance = tenantAdminApiContext.DbInstances.Single(); + var persistedOdsInstanceManage = tenantAdminApiContext.OdsInstanceManages.Single(); var persistedOdsInstance = tenantUsersContext.OdsInstances.Single(); const string expectedDatabaseName = "EdFi_Ods_Sandbox_Sample"; - persistedDbInstance.Status.ShouldBe(DbInstanceStatus.Created.ToString()); - persistedDbInstance.DatabaseName.ShouldBe(expectedDatabaseName); + persistedOdsInstanceManage.Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); + persistedOdsInstanceManage.DatabaseName.ShouldBe(expectedDatabaseName); persistedOdsInstance.InstanceType.ShouldBe("Sample"); plaintextConnectionString.ShouldNotBeNull(); plaintextConnectionString.ShouldContain($"Initial Catalog={expectedDatabaseName}"); @@ -551,16 +551,16 @@ public async Task Execute_ReusesExistingOdsInstance_WhenFinalNameAlreadyExists() A.CallTo(() => encryptionProvider.Encrypt(A._, A._)) .Returns(encryptedConnectionString); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); usersContext.OdsInstances.Add(new OdsInstance @@ -585,16 +585,16 @@ public async Task Execute_ReusesExistingOdsInstance_WhenFinalNameAlreadyExists() configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - var persistedDbInstance = adminApiContext.DbInstances.Single(); + var persistedOdsInstanceManage = adminApiContext.OdsInstanceManages.Single(); var persistedOdsInstance = usersContext.OdsInstances.Single(); const string expectedDatabaseName = "EdFi_Ods_Sandbox_Minimal"; - persistedDbInstance.Status.ShouldBe(DbInstanceStatus.Created.ToString()); - persistedDbInstance.DatabaseName.ShouldBe(expectedDatabaseName); - persistedDbInstance.OdsInstanceId.ShouldBe(persistedOdsInstance.OdsInstanceId); - persistedDbInstance.OdsInstanceName.ShouldBe("Sandbox"); + persistedOdsInstanceManage.Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); + persistedOdsInstanceManage.DatabaseName.ShouldBe(expectedDatabaseName); + persistedOdsInstanceManage.OdsInstanceId.ShouldBe(persistedOdsInstance.OdsInstanceId); + persistedOdsInstanceManage.OdsInstanceName.ShouldBe("Sandbox"); persistedOdsInstance.ConnectionString.ShouldBe(encryptedConnectionString); usersContext.OdsInstances.Count().ShouldBe(1); A.CallTo(() => sandboxProvisioner.AddSandboxAsync(expectedDatabaseName, SandboxType.Minimal)) diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJobTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJobTests.cs similarity index 77% rename from Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJobTests.cs rename to Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJobTests.cs index 6d731c649..4d76343a7 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJobTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJobTests.cs @@ -26,7 +26,7 @@ namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Services.Jobs; [TestFixture] -public class CreatePendingDbInstancesDispatcherJobTests +public class CreatePendingOdsInstanceManagesDispatcherJobTests { private sealed class NonDisposingAdminApiDbContext( DbContextOptions options, @@ -59,7 +59,7 @@ private static IOptions CreateOptions(bool multiTenancy = false, in { DatabaseEngine = "SqlServer", MultiTenancy = multiTenancy, - CreateDbInstancesMaxRetryAttempts = maxRetryAttempts + CreateOdsInstanceManagesMaxRetryAttempts = maxRetryAttempts }); private static IScheduler CreateScheduler(out List scheduledJobs) @@ -88,7 +88,7 @@ private static IJobExecutionContext CreateJobExecutionContext(IScheduler schedul jobDataMap.Put(JobConstants.TenantNameKey, tenantName); } - A.CallTo(() => jobDetail.Key).Returns(new JobKey(JobConstants.CreatePendingDbInstancesDispatcherJobName)); + A.CallTo(() => jobDetail.Key).Returns(new JobKey(JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName)); A.CallTo(() => jobExecutionContext.JobDetail).Returns(jobDetail); A.CallTo(() => jobExecutionContext.FireInstanceId).Returns(Guid.NewGuid().ToString()); A.CallTo(() => jobExecutionContext.MergedJobDataMap).Returns(jobDataMap); @@ -98,25 +98,25 @@ private static IJobExecutionContext CreateJobExecutionContext(IScheduler schedul } [Test] - public async Task Execute_SchedulesPendingDbInstance() + public async Task Execute_SchedulesPendingOdsInstanceManage() { using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}"); var tenantSpecificDbContextProvider = A.Fake(); var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out var scheduledJobs); - adminApiContext.DbInstances.Add(new Common.Infrastructure.Models.DbInstance + adminApiContext.OdsInstanceManages.Add(new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }); adminApiContext.SaveChanges(); - var job = new CreatePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new CreatePendingOdsInstanceManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, @@ -125,38 +125,38 @@ public async Task Execute_SchedulesPendingDbInstance() await job.Execute(CreateJobExecutionContext(scheduler)); scheduledJobs.Count.ShouldBe(1); - scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-{adminApiContext.DbInstances.Single().Id}"); + scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-{adminApiContext.OdsInstanceManages.Single().Id}"); } [Test] - public async Task Execute_RequeuesRetryableErrorDbInstance() + public async Task Execute_RequeuesRetryableErrorOdsInstanceManage() { using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}"); var tenantSpecificDbContextProvider = A.Fake(); var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out var scheduledJobs); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.CreateFailed.ToString(), + Status = OdsInstanceManageStatus.CreateFailed.ToString(), DatabaseName = "existingdb", LastRefreshed = DateTime.UtcNow.AddMinutes(-10), LastModifiedDate = DateTime.UtcNow.AddMinutes(-10) }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); adminApiContext.JobStatuses.Add(new JobStatus { - JobId = $"{CreateInstanceJob.BuildJobIdentity(dbInstance.Id, null)}_run-1", + JobId = $"{CreateInstanceJob.BuildJobIdentity(odsInstanceManage.Id, null)}_run-1", Status = QuartzJobStatus.Error.ToString() }); adminApiContext.SaveChanges(); - var job = new CreatePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new CreatePendingOdsInstanceManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, @@ -164,7 +164,7 @@ public async Task Execute_RequeuesRetryableErrorDbInstance() await job.Execute(CreateJobExecutionContext(scheduler)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.PendingCreate.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); scheduledJobs.Count.ShouldBe(1); } @@ -176,31 +176,31 @@ public async Task Execute_SetsCreateError_WhenRetryLimitIsReached() var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out var scheduledJobs); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.CreateFailed.ToString(), + Status = OdsInstanceManageStatus.CreateFailed.ToString(), LastRefreshed = DateTime.UtcNow.AddMinutes(-10), LastModifiedDate = DateTime.UtcNow.AddMinutes(-10) }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); for (var attempt = 1; attempt <= 3; attempt++) { adminApiContext.JobStatuses.Add(new JobStatus { - JobId = $"{CreateInstanceJob.BuildJobIdentity(dbInstance.Id, null)}_run-{attempt}", + JobId = $"{CreateInstanceJob.BuildJobIdentity(odsInstanceManage.Id, null)}_run-{attempt}", Status = QuartzJobStatus.Error.ToString() }); } adminApiContext.SaveChanges(); - var job = new CreatePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new CreatePendingOdsInstanceManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, @@ -208,7 +208,7 @@ public async Task Execute_SetsCreateError_WhenRetryLimitIsReached() await job.Execute(CreateJobExecutionContext(scheduler)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.CreateError.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.CreateError.ToString()); scheduledJobs.ShouldBeEmpty(); } @@ -224,18 +224,18 @@ public async Task Execute_UsesTenantSpecificContext_WhenMultiTenancyIsEnabled() A.CallTo(() => tenantSpecificDbContextProvider.GetAdminApiDbContext("tenant1")) .Returns(tenantAdminApiContext); - tenantAdminApiContext.DbInstances.Add(new Common.Infrastructure.Models.DbInstance + tenantAdminApiContext.OdsInstanceManages.Add(new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Sample", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }); tenantAdminApiContext.SaveChanges(); - var job = new CreatePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new CreatePendingOdsInstanceManagesDispatcherJob( + A.Fake>(), jobStatusService, defaultAdminApiContext, tenantSpecificDbContextProvider, @@ -244,7 +244,7 @@ public async Task Execute_UsesTenantSpecificContext_WhenMultiTenancyIsEnabled() await job.Execute(CreateJobExecutionContext(scheduler, "tenant1")); scheduledJobs.Count.ShouldBe(1); - scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-tenant1-{tenantAdminApiContext.DbInstances.Single().Id}"); + scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-tenant1-{tenantAdminApiContext.OdsInstanceManages.Single().Id}"); scheduledJobs[0].JobDataMap.GetString(JobConstants.TenantNameKey).ShouldBe("tenant1"); } } \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeleteInstanceJobTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeleteInstanceJobTests.cs index e130da148..5c657a82e 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeleteInstanceJobTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeleteInstanceJobTests.cs @@ -77,13 +77,13 @@ private static SqlServerUsersContext CreateUsersContext(string databaseName) return new NonDisposingSqlServerUsersContext(options); } - private static IJobExecutionContext CreateJobExecutionContext(int dbInstanceId, string tenantName = null) + private static IJobExecutionContext CreateJobExecutionContext(int odsInstanceManageId, string tenantName = null) { var jobExecutionContext = A.Fake(); var jobDetail = A.Fake(); var jobDataMap = new JobDataMap { - { JobConstants.DbInstanceIdKey, dbInstanceId } + { JobConstants.OdsInstanceManageIdKey, odsInstanceManageId } }; if (!string.IsNullOrWhiteSpace(tenantName)) @@ -149,18 +149,18 @@ public async Task Execute_DeletesDatabaseAndOdsInstance_AndSetsDeletedStatus() usersContext.OdsInstances.Add(odsInstance); usersContext.SaveChanges(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), DatabaseName = "EdFi_Ods_Sandbox_Minimal", OdsInstanceId = odsInstance.OdsInstanceId, LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new DeleteInstanceJob( @@ -174,9 +174,9 @@ public async Task Execute_DeletesDatabaseAndOdsInstance_AndSetsDeletedStatus() sandboxProvisioner, CreateOptions()); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.Deleted.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); usersContext.OdsInstances.ShouldBeEmpty(); A.CallTo(() => sandboxProvisioner.DeleteSandboxesAsync("EdFi_Ods_Sandbox_Minimal")) .MustHaveHappenedOnceExactly(); @@ -190,17 +190,17 @@ public async Task Execute_SkipsDatabaseDrop_WhenDatabaseNameIsNull() var jobStatusService = A.Fake(); var sandboxProvisioner = A.Fake(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), DatabaseName = null, LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new DeleteInstanceJob( @@ -214,9 +214,9 @@ public async Task Execute_SkipsDatabaseDrop_WhenDatabaseNameIsNull() sandboxProvisioner, CreateOptions()); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.Deleted.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); A.CallTo(() => sandboxProvisioner.DeleteSandboxesAsync(A._)).MustNotHaveHappened(); } @@ -228,18 +228,18 @@ public async Task Execute_SkipsOdsInstanceRemoval_WhenOdsInstanceIdIsNull() var jobStatusService = A.Fake(); var sandboxProvisioner = A.Fake(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), DatabaseName = "EdFi_Ods_Sandbox_Minimal", OdsInstanceId = null, LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new DeleteInstanceJob( @@ -253,31 +253,31 @@ public async Task Execute_SkipsOdsInstanceRemoval_WhenOdsInstanceIdIsNull() sandboxProvisioner, CreateOptions()); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.Deleted.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); usersContext.OdsInstances.ShouldBeEmpty(); } [Test] - public async Task Execute_DoesNothing_WhenDbInstanceIsNotPendingDelete() + public async Task Execute_DoesNothing_WhenOdsInstanceManageIsNotPendingDelete() { using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}"); using var usersContext = CreateUsersContext($"Users_{Guid.NewGuid()}"); var jobStatusService = A.Fake(); var sandboxProvisioner = A.Fake(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.Created.ToString(), + Status = OdsInstanceManageStatus.Created.ToString(), DatabaseName = "EdFi_Ods_Sandbox_Minimal", LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new DeleteInstanceJob( @@ -291,9 +291,9 @@ public async Task Execute_DoesNothing_WhenDbInstanceIsNotPendingDelete() sandboxProvisioner, CreateOptions()); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.Created.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); A.CallTo(() => sandboxProvisioner.DeleteSandboxesAsync(A._)).MustNotHaveHappened(); } @@ -308,17 +308,17 @@ public async Task Execute_SetsDeleteFailed_WhenProvisionerThrows() A.CallTo(() => sandboxProvisioner.DeleteSandboxesAsync(A._)) .Throws(new InvalidOperationException("Drop failed.")); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), DatabaseName = "EdFi_Ods_Sandbox_Minimal", LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new DeleteInstanceJob( @@ -332,9 +332,9 @@ public async Task Execute_SetsDeleteFailed_WhenProvisionerThrows() sandboxProvisioner, CreateOptions()); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.DeleteFailed.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.DeleteFailed.ToString()); } [Test] @@ -354,17 +354,17 @@ public async Task Execute_UsesTenantSpecificContext_WhenMultiTenancyIsEnabled() A.CallTo(() => tenantSpecificDbContextProvider.GetUsersContext("tenant1")) .Returns(tenantUsersContext); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), DatabaseName = "EdFi_Ods_Sandbox_Minimal", LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - tenantAdminApiContext.DbInstances.Add(dbInstance); + tenantAdminApiContext.OdsInstanceManages.Add(odsInstanceManage); tenantAdminApiContext.SaveChanges(); var job = new DeleteInstanceJob( @@ -378,9 +378,9 @@ public async Task Execute_UsesTenantSpecificContext_WhenMultiTenancyIsEnabled() sandboxProvisioner, CreateOptions(multiTenancy: true)); - await job.Execute(CreateJobExecutionContext(dbInstance.Id, "tenant1")); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id, "tenant1")); - tenantAdminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.Deleted.ToString()); - defaultAdminApiContext.DbInstances.ShouldBeEmpty(); + tenantAdminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); + defaultAdminApiContext.OdsInstanceManages.ShouldBeEmpty(); } } diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJobTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJobTests.cs similarity index 78% rename from Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJobTests.cs rename to Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJobTests.cs index 38946d635..e60c9c5c6 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJobTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJobTests.cs @@ -26,7 +26,7 @@ namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Services.Jobs; [TestFixture] -public class DeletePendingDbInstancesDispatcherJobTests +public class DeletePendingOdsInstanceManagesDispatcherJobTests { private sealed class NonDisposingAdminApiDbContext( DbContextOptions options, @@ -59,7 +59,7 @@ private static IOptions CreateOptions(bool multiTenancy = false, in { DatabaseEngine = "SqlServer", MultiTenancy = multiTenancy, - DeleteDbInstancesMaxRetryAttempts = maxRetryAttempts + DeleteOdsInstanceManagesMaxRetryAttempts = maxRetryAttempts }); private static IScheduler CreateScheduler(out List scheduledJobs) @@ -88,7 +88,7 @@ private static IJobExecutionContext CreateJobExecutionContext(IScheduler schedul jobDataMap.Put(JobConstants.TenantNameKey, tenantName); } - A.CallTo(() => jobDetail.Key).Returns(new JobKey(JobConstants.DeletePendingDbInstancesDispatcherJobName)); + A.CallTo(() => jobDetail.Key).Returns(new JobKey(JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName)); A.CallTo(() => jobExecutionContext.JobDetail).Returns(jobDetail); A.CallTo(() => jobExecutionContext.FireInstanceId).Returns(Guid.NewGuid().ToString()); A.CallTo(() => jobExecutionContext.MergedJobDataMap).Returns(jobDataMap); @@ -98,25 +98,25 @@ private static IJobExecutionContext CreateJobExecutionContext(IScheduler schedul } [Test] - public async Task Execute_SchedulesPendingDeleteDbInstance() + public async Task Execute_SchedulesPendingDeleteOdsInstanceManage() { using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}"); var tenantSpecificDbContextProvider = A.Fake(); var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out var scheduledJobs); - adminApiContext.DbInstances.Add(new Common.Infrastructure.Models.DbInstance + adminApiContext.OdsInstanceManages.Add(new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }); adminApiContext.SaveChanges(); - var job = new DeletePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new DeletePendingOdsInstanceManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, @@ -125,38 +125,38 @@ public async Task Execute_SchedulesPendingDeleteDbInstance() await job.Execute(CreateJobExecutionContext(scheduler)); scheduledJobs.Count.ShouldBe(1); - scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.DeleteInstanceJobName}-{adminApiContext.DbInstances.Single().Id}"); + scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.DeleteInstanceJobName}-{adminApiContext.OdsInstanceManages.Single().Id}"); } [Test] - public async Task Execute_RequeuesRetryableDeleteFailedDbInstance() + public async Task Execute_RequeuesRetryableDeleteFailedOdsInstanceManage() { using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}"); var tenantSpecificDbContextProvider = A.Fake(); var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out var scheduledJobs); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.DeleteFailed.ToString(), + Status = OdsInstanceManageStatus.DeleteFailed.ToString(), DatabaseName = "EdFi_Ods_Sandbox_Minimal", LastRefreshed = DateTime.UtcNow.AddMinutes(-10), LastModifiedDate = DateTime.UtcNow.AddMinutes(-10) }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); adminApiContext.JobStatuses.Add(new JobStatus { - JobId = $"{DeleteInstanceJob.BuildJobIdentity(dbInstance.Id, null)}_run-1", + JobId = $"{DeleteInstanceJob.BuildJobIdentity(odsInstanceManage.Id, null)}_run-1", Status = QuartzJobStatus.Error.ToString() }); adminApiContext.SaveChanges(); - var job = new DeletePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new DeletePendingOdsInstanceManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, @@ -164,7 +164,7 @@ public async Task Execute_RequeuesRetryableDeleteFailedDbInstance() await job.Execute(CreateJobExecutionContext(scheduler)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.PendingDelete.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.PendingDelete.ToString()); scheduledJobs.Count.ShouldBe(1); } @@ -176,31 +176,31 @@ public async Task Execute_SetsDeleteError_WhenRetryLimitIsReached() var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out var scheduledJobs); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.DeleteFailed.ToString(), + Status = OdsInstanceManageStatus.DeleteFailed.ToString(), LastRefreshed = DateTime.UtcNow.AddMinutes(-10), LastModifiedDate = DateTime.UtcNow.AddMinutes(-10) }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); for (var attempt = 1; attempt <= 3; attempt++) { adminApiContext.JobStatuses.Add(new JobStatus { - JobId = $"{DeleteInstanceJob.BuildJobIdentity(dbInstance.Id, null)}_run-{attempt}", + JobId = $"{DeleteInstanceJob.BuildJobIdentity(odsInstanceManage.Id, null)}_run-{attempt}", Status = QuartzJobStatus.Error.ToString() }); } adminApiContext.SaveChanges(); - var job = new DeletePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new DeletePendingOdsInstanceManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, @@ -208,7 +208,7 @@ public async Task Execute_SetsDeleteError_WhenRetryLimitIsReached() await job.Execute(CreateJobExecutionContext(scheduler)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.DeleteError.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.DeleteError.ToString()); scheduledJobs.ShouldBeEmpty(); } @@ -224,18 +224,18 @@ public async Task Execute_UsesTenantSpecificContext_WhenMultiTenancyIsEnabled() A.CallTo(() => tenantSpecificDbContextProvider.GetAdminApiDbContext("tenant1")) .Returns(tenantAdminApiContext); - tenantAdminApiContext.DbInstances.Add(new Common.Infrastructure.Models.DbInstance + tenantAdminApiContext.OdsInstanceManages.Add(new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }); tenantAdminApiContext.SaveChanges(); - var job = new DeletePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new DeletePendingOdsInstanceManagesDispatcherJob( + A.Fake>(), jobStatusService, defaultAdminApiContext, tenantSpecificDbContextProvider, @@ -244,7 +244,7 @@ public async Task Execute_UsesTenantSpecificContext_WhenMultiTenancyIsEnabled() await job.Execute(CreateJobExecutionContext(scheduler, "tenant1")); scheduledJobs.Count.ShouldBe(1); - scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.DeleteInstanceJobName}-tenant1-{tenantAdminApiContext.DbInstances.Single().Id}"); + scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.DeleteInstanceJobName}-tenant1-{tenantAdminApiContext.OdsInstanceManages.Single().Id}"); scheduledJobs[0].JobDataMap.GetString(JobConstants.TenantNameKey).ShouldBe("tenant1"); } @@ -256,8 +256,8 @@ public async Task Execute_Throws_WhenTenantNameMissingAndMultiTenancyEnabled() var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out _); - var job = new DeletePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new DeletePendingOdsInstanceManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs index 6daf9a94a..ddea39813 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs @@ -31,7 +31,7 @@ namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Services.Tenants; internal class TenantServiceTests { private static IEnumerable AllStatuses => - Enum.GetValues() + Enum.GetValues() .Select(status => status.ToString()); private IOptionsSnapshot _options = null!; @@ -39,7 +39,7 @@ internal class TenantServiceTests private AppSettingsFile _appSettings = null!; private IGetOdsInstancesQuery _getOdsInstancesQuery = null!; private IGetEducationOrganizationQuery _getEducationOrganizationQuery = null!; - private IGetDbInstancesQuery _getDbInstancesQuery = null!; + private IGetOdsInstanceManagesQuery _getOdsInstanceManagesQuery = null!; [SetUp] public void SetUp() @@ -48,8 +48,8 @@ public void SetUp() _memoryCache = new MemoryCache(new MemoryCacheOptions()); _getOdsInstancesQuery = A.Fake(); _getEducationOrganizationQuery = A.Fake(); - _getDbInstancesQuery = A.Fake(); - A.CallTo(() => _getDbInstancesQuery.Execute(A._, A._, A.Ignored)) + _getOdsInstanceManagesQuery = A.Fake(); + A.CallTo(() => _getOdsInstanceManagesQuery.Execute(A._, A._, A.Ignored)) .Returns([]); _appSettings = new AppSettingsFile { @@ -223,7 +223,7 @@ public async Task GetTenantEdOrgsByInstancesAsync_Should_Return_Correct_TenantDe A.CallTo(() => _getEducationOrganizationQuery.Execute(A.That.Matches(ids => ids.Length == 1 && ids[0] == 101))) .Returns([educationOrganization]); - var tenant = await service.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, tenantName); + var tenant = await service.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, tenantName); tenant.ShouldNotBeNull(); tenant!.TenantName.ShouldBe(tenantName); @@ -244,13 +244,13 @@ public async Task GetTenantEdOrgsByInstancesAsync_Should_Return_Null_If_Not_Foun { var service = new TenantService(_options, _memoryCache); - var tenant = await service.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, "notfound"); + var tenant = await service.GetTenantEdOrgsByInstancesAsync(_getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, "notfound"); tenant.ShouldBeNull(); } [Test] - public async Task GetTenantEdOrgsByInstancesAsync_SetsStatusCreated_WhenOdsInstanceHasNoLinkedDbInstance() + public async Task GetTenantEdOrgsByInstancesAsync_SetsStatusCreated_WhenOdsInstanceHasNoLinkedOdsInstanceManage() { _appSettings.AppSettings.MultiTenancy = false; var service = new TenantService(_options, _memoryCache); @@ -259,18 +259,18 @@ public async Task GetTenantEdOrgsByInstancesAsync_SetsStatusCreated_WhenOdsInsta A.CallTo(() => _getOdsInstancesQuery.Execute()).Returns([odsInstance]); var result = await service.GetTenantEdOrgsByInstancesAsync( - _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, Constants.DefaultTenantName); + _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, Constants.DefaultTenantName); result.ShouldNotBeNull(); result!.OdsInstances.Count.ShouldBe(1); - result.OdsInstances[0].Status.ShouldBe(DbInstanceStatus.Created.ToString()); - result.OdsInstances[0].DbInstanceId.ShouldBeNull(); + result.OdsInstances[0].Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); + result.OdsInstances[0].OdsInstanceManageId.ShouldBeNull(); result.OdsInstances[0].DatabaseTemplate.ShouldBeNull(); result.OdsInstances[0].DatabaseName.ShouldBeNull(); } [Test] - public async Task GetTenantEdOrgsByInstancesAsync_EnrichesOdsInstance_WithLinkedDbInstanceFields() + public async Task GetTenantEdOrgsByInstancesAsync_EnrichesOdsInstance_WithLinkedOdsInstanceManageFields() { _appSettings.AppSettings.MultiTenancy = false; var service = new TenantService(_options, _memoryCache); @@ -278,61 +278,61 @@ public async Task GetTenantEdOrgsByInstancesAsync_EnrichesOdsInstance_WithLinked var odsInstance = new OdsInstance { OdsInstanceId = 2, Name = "Instance2" }; A.CallTo(() => _getOdsInstancesQuery.Execute()).Returns([odsInstance]); - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Id = 10, Name = "DbInstance2", OdsInstanceId = 2, - Status = DbInstanceStatus.CreateInProgress.ToString(), + Status = OdsInstanceManageStatus.CreateInProgress.ToString(), DatabaseTemplate = "Minimal", DatabaseName = "EdFi_ODS_2", LastRefreshed = System.DateTime.UtcNow }; - A.CallTo(() => _getDbInstancesQuery.Execute(A._, A._, A.Ignored)) - .Returns([dbInstance]); + A.CallTo(() => _getOdsInstanceManagesQuery.Execute(A._, A._, A.Ignored)) + .Returns([odsInstanceManage]); var result = await service.GetTenantEdOrgsByInstancesAsync( - _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, Constants.DefaultTenantName); + _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, Constants.DefaultTenantName); result.ShouldNotBeNull(); result!.OdsInstances.Count.ShouldBe(1); var instance = result.OdsInstances[0]; - instance.DbInstanceId.ShouldBe(10); - instance.Status.ShouldBe(DbInstanceStatus.CreateInProgress.ToString()); + instance.OdsInstanceManageId.ShouldBe(10); + instance.Status.ShouldBe(OdsInstanceManageStatus.CreateInProgress.ToString()); instance.DatabaseTemplate.ShouldBe("Minimal"); instance.DatabaseName.ShouldBe("EdFi_ODS_2"); } [Test] - public async Task GetTenantEdOrgsByInstancesAsync_AddsUnlinkedDbInstances_WithNullIds() + public async Task GetTenantEdOrgsByInstancesAsync_AddsUnlinkedOdsInstanceManages_WithNullIds() { _appSettings.AppSettings.MultiTenancy = false; var service = new TenantService(_options, _memoryCache); A.CallTo(() => _getOdsInstancesQuery.Execute()).Returns([]); - var unlinked1 = new DbInstance + var unlinked1 = new OdsInstanceManage { Id = 20, Name = "Unlinked-A", OdsInstanceId = null, - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), DatabaseTemplate = "Sample", LastRefreshed = System.DateTime.UtcNow }; - var unlinked2 = new DbInstance + var unlinked2 = new OdsInstanceManage { Id = 21, Name = "Unlinked-B", OdsInstanceId = null, - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = System.DateTime.UtcNow }; - A.CallTo(() => _getDbInstancesQuery.Execute(A._, A._, A.Ignored)) + A.CallTo(() => _getOdsInstanceManagesQuery.Execute(A._, A._, A.Ignored)) .Returns([unlinked1, unlinked2]); var result = await service.GetTenantEdOrgsByInstancesAsync( - _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, Constants.DefaultTenantName); + _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, Constants.DefaultTenantName); result.ShouldNotBeNull(); result!.OdsInstances.Count.ShouldBe(2); - result.OdsInstances.ShouldContain(i => i.OdsInstanceId == null && i.Name == "Unlinked-A" && i.DbInstanceId == 20); - result.OdsInstances.ShouldContain(i => i.OdsInstanceId == null && i.Name == "Unlinked-B" && i.DbInstanceId == 21); + result.OdsInstances.ShouldContain(i => i.OdsInstanceId == null && i.Name == "Unlinked-A" && i.OdsInstanceManageId == 20); + result.OdsInstances.ShouldContain(i => i.OdsInstanceId == null && i.Name == "Unlinked-B" && i.OdsInstanceManageId == 21); } [Test] @@ -344,51 +344,51 @@ public async Task GetTenantEdOrgsByInstancesAsync_MixedScenario_LinkedAndUnlinke var odsInstance = new OdsInstance { OdsInstanceId = 5, Name = "Instance5" }; A.CallTo(() => _getOdsInstancesQuery.Execute()).Returns([odsInstance]); - var linked = new DbInstance + var linked = new OdsInstanceManage { Id = 30, Name = "Linked-5", OdsInstanceId = 5, - Status = DbInstanceStatus.Created.ToString(), + Status = OdsInstanceManageStatus.Created.ToString(), DatabaseTemplate = "Minimal", DatabaseName = "EdFi_ODS_5", LastRefreshed = System.DateTime.UtcNow }; - var unlinked = new DbInstance + var unlinked = new OdsInstanceManage { Id = 31, Name = "Unlinked-C", OdsInstanceId = null, - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), DatabaseTemplate = "Sample", LastRefreshed = System.DateTime.UtcNow }; - A.CallTo(() => _getDbInstancesQuery.Execute(A._, A._, A.Ignored)) + A.CallTo(() => _getOdsInstanceManagesQuery.Execute(A._, A._, A.Ignored)) .Returns([linked, unlinked]); var result = await service.GetTenantEdOrgsByInstancesAsync( - _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, Constants.DefaultTenantName); + _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, Constants.DefaultTenantName); result.ShouldNotBeNull(); result!.OdsInstances.Count.ShouldBe(2); var linkedInstance = result.OdsInstances.Single(i => i.OdsInstanceId == 5); - linkedInstance.DbInstanceId.ShouldBe(30); - linkedInstance.Status.ShouldBe(DbInstanceStatus.Created.ToString()); + linkedInstance.OdsInstanceManageId.ShouldBe(30); + linkedInstance.Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); linkedInstance.DatabaseTemplate.ShouldBe("Minimal"); linkedInstance.DatabaseName.ShouldBe("EdFi_ODS_5"); var unlinkedInstance = result.OdsInstances.Single(i => i.Name == "Unlinked-C"); - unlinkedInstance.DbInstanceId.ShouldBe(31); + unlinkedInstance.OdsInstanceManageId.ShouldBe(31); unlinkedInstance.Name.ShouldBe("Unlinked-C"); - unlinkedInstance.Status.ShouldBe(DbInstanceStatus.PendingCreate.ToString()); + unlinkedInstance.Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); unlinkedInstance.OdsInstanceId.ShouldBeNull(); } [Test] [TestCaseSource(nameof(AllStatuses))] - public async Task GetTenantEdOrgsByInstancesAsync_AddsDbInstance_WhenLinkedToMissingOdsInstance_ForAllStatuses(string status) + public async Task GetTenantEdOrgsByInstancesAsync_AddsOdsInstanceManage_WhenLinkedToMissingOdsInstance_ForAllStatuses(string status) { _appSettings.AppSettings.MultiTenancy = false; var service = new TenantService(_options, _memoryCache); A.CallTo(() => _getOdsInstancesQuery.Execute()).Returns([]); - var orphan = new DbInstance + var orphan = new OdsInstanceManage { Id = 42, Name = $"Orphan-{status}", @@ -398,59 +398,59 @@ public async Task GetTenantEdOrgsByInstancesAsync_AddsDbInstance_WhenLinkedToMis DatabaseName = "EdFi_ODS_9002", LastRefreshed = System.DateTime.UtcNow }; - A.CallTo(() => _getDbInstancesQuery.Execute(A._, A._, A.Ignored)) + A.CallTo(() => _getOdsInstanceManagesQuery.Execute(A._, A._, A.Ignored)) .Returns([orphan]); var result = await service.GetTenantEdOrgsByInstancesAsync( - _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, Constants.DefaultTenantName); + _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, Constants.DefaultTenantName); result.ShouldNotBeNull(); result!.OdsInstances.Count.ShouldBe(1); result.OdsInstances[0].OdsInstanceId.ShouldBeNull(); - result.OdsInstances[0].DbInstanceId.ShouldBe(42); + result.OdsInstances[0].OdsInstanceManageId.ShouldBe(42); result.OdsInstances[0].Status.ShouldBe(status); } [Test] - public async Task GetTenantEdOrgsByInstancesAsync_AppendsLatestDbInstancePerMissingOdsInstanceId() + public async Task GetTenantEdOrgsByInstancesAsync_AppendsLatestOdsInstanceManagePerMissingOdsInstanceId() { _appSettings.AppSettings.MultiTenancy = false; var service = new TenantService(_options, _memoryCache); A.CallTo(() => _getOdsInstancesQuery.Execute()).Returns([]); - var older = new DbInstance + var older = new OdsInstanceManage { Id = 50, Name = "Orphan-Older", OdsInstanceId = 9003, - Status = DbInstanceStatus.CreateFailed.ToString(), + Status = OdsInstanceManageStatus.CreateFailed.ToString(), DatabaseTemplate = "Minimal", DatabaseName = "EdFi_ODS_9003_old", LastRefreshed = System.DateTime.UtcNow.AddMinutes(-10) }; - var newer = new DbInstance + var newer = new OdsInstanceManage { Id = 51, Name = "Orphan-Newer", OdsInstanceId = 9003, - Status = DbInstanceStatus.Deleted.ToString(), + Status = OdsInstanceManageStatus.Deleted.ToString(), DatabaseTemplate = "Minimal", DatabaseName = "EdFi_ODS_9003_new", LastModifiedDate = System.DateTime.UtcNow }; - A.CallTo(() => _getDbInstancesQuery.Execute(A._, A._, A.Ignored)) + A.CallTo(() => _getOdsInstanceManagesQuery.Execute(A._, A._, A.Ignored)) .Returns([older, newer]); var result = await service.GetTenantEdOrgsByInstancesAsync( - _getOdsInstancesQuery, _getEducationOrganizationQuery, _getDbInstancesQuery, Constants.DefaultTenantName); + _getOdsInstancesQuery, _getEducationOrganizationQuery, _getOdsInstanceManagesQuery, Constants.DefaultTenantName); result.ShouldNotBeNull(); result!.OdsInstances.Count.ShouldBe(1); - result.OdsInstances[0].DbInstanceId.ShouldBe(51); - result.OdsInstances[0].Status.ShouldBe(DbInstanceStatus.Deleted.ToString()); + result.OdsInstances[0].OdsInstanceManageId.ShouldBe(51); + result.OdsInstances[0].Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); result.OdsInstances[0].Name.ShouldBe("Orphan-Newer"); } } From 70cf5c87afc83106b96805160142e15ed461d7b7 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 21:42:36 -0600 Subject: [PATCH 13/35] Rename v2 DBTests to OdsInstanceManage naming Renames the integration-test files for AddDbInstance/DeleteDbInstance commands and GetDbInstance(s) queries to match the OdsInstanceManage production rename, and updates GetTenantEdOrgsByInstancesTests.cs in place to reference the renamed types. --- ...nstanceCommandTests.cs => AddOdsInstanceManageCommandTests.cs} | 0 ...anceCommandTests.cs => DeleteOdsInstanceManageCommandTests.cs} | 0 ...nceByIdQueryTests.cs => GetOdsInstanceManageByIdQueryTests.cs} | 0 ...bInstancesQueryTests.cs => GetOdsInstanceManagesQueryTests.cs} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/{AddDbInstanceCommandTests.cs => AddOdsInstanceManageCommandTests.cs} (100%) rename Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/{DeleteDbInstanceCommandTests.cs => DeleteOdsInstanceManageCommandTests.cs} (100%) rename Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/{GetDbInstanceByIdQueryTests.cs => GetOdsInstanceManageByIdQueryTests.cs} (100%) rename Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/{GetDbInstancesQueryTests.cs => GetOdsInstanceManagesQueryTests.cs} (100%) diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddDbInstanceCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceManageCommandTests.cs similarity index 100% rename from Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddDbInstanceCommandTests.cs rename to Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceManageCommandTests.cs diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteDbInstanceCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceManageCommandTests.cs similarity index 100% rename from Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteDbInstanceCommandTests.cs rename to Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceManageCommandTests.cs diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetDbInstanceByIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManageByIdQueryTests.cs similarity index 100% rename from Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetDbInstanceByIdQueryTests.cs rename to Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManageByIdQueryTests.cs diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetDbInstancesQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManagesQueryTests.cs similarity index 100% rename from Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetDbInstancesQueryTests.cs rename to Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManagesQueryTests.cs From e0baf87d6d8d004c9eda005563f684e9ea8dbd1b Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 21:43:48 -0600 Subject: [PATCH 14/35] Fix v2 DBTests rename: apply actual renamed content The prior commit's git-add step aborted on stale pathspecs for the already-git-mv'd old filenames, leaving the index at the pre-rewrite snapshot. This commit stages the real working-tree content (renamed class/type names, OdsInstanceManage references) and includes GetTenantEdOrgsByInstancesTests.cs, which was missed entirely by the previous commit. --- .../AddOdsInstanceManageCommandTests.cs | 32 +++++------ .../DeleteOdsInstanceManageCommandTests.cs | 28 +++++----- .../GetOdsInstanceManageByIdQueryTests.cs | 14 ++--- .../GetOdsInstanceManagesQueryTests.cs | 56 +++++++++---------- .../GetTenantEdOrgsByInstancesTests.cs | 38 ++++++------- 5 files changed, 84 insertions(+), 84 deletions(-) diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceManageCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceManageCommandTests.cs index dfce4d8c9..c3bbc183a 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceManageCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceManageCommandTests.cs @@ -14,29 +14,29 @@ namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; [TestFixture] -public class AddDbInstanceCommandTests : AdminApiDbContextTestBase +public class AddOdsInstanceManageCommandTests : AdminApiDbContextTestBase { [Test] - public void ShouldAddDbInstance() + public void ShouldAddOdsInstanceManage() { - var model = new Mock(); + var model = new Mock(); model.Setup(x => x.Name).Returns("Test Instance"); model.Setup(x => x.DatabaseTemplate).Returns("Minimal"); var id = 0; Transaction(context => { - var command = new AddDbInstanceCommand(context); + var command = new AddOdsInstanceManageCommand(context); id = command.Execute(model.Object).Id; id.ShouldBeGreaterThan(0); }); Transaction(context => { - var instance = context.DbInstances.Single(d => d.Id == id); + var instance = context.OdsInstanceManages.Single(d => d.Id == id); instance.Name.ShouldBe("Test Instance"); instance.DatabaseTemplate.ShouldBe("Minimal"); - instance.Status.ShouldBe(DbInstanceStatus.PendingCreate.ToString()); + instance.Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); instance.OdsInstanceId.ShouldBeNull(); instance.OdsInstanceName.ShouldBeNull(); instance.DatabaseName.ShouldBeNull(); @@ -44,23 +44,23 @@ public void ShouldAddDbInstance() } [Test] - public void ShouldAddDbInstanceWithSampleTemplate() + public void ShouldAddOdsInstanceManageWithSampleTemplate() { - var model = new Mock(); + var model = new Mock(); model.Setup(x => x.Name).Returns("Sample Instance"); model.Setup(x => x.DatabaseTemplate).Returns("Sample"); var id = 0; Transaction(context => { - var command = new AddDbInstanceCommand(context); + var command = new AddOdsInstanceManageCommand(context); id = command.Execute(model.Object).Id; id.ShouldBeGreaterThan(0); }); Transaction(context => { - var instance = context.DbInstances.Single(d => d.Id == id); + var instance = context.OdsInstanceManages.Single(d => d.Id == id); instance.DatabaseTemplate.ShouldBe("Sample"); }); } @@ -68,20 +68,20 @@ public void ShouldAddDbInstanceWithSampleTemplate() [Test] public void ShouldTrimNameAndDatabaseTemplate() { - var model = new Mock(); + var model = new Mock(); model.Setup(x => x.Name).Returns(" Trimmed Instance "); model.Setup(x => x.DatabaseTemplate).Returns(" Minimal "); var id = 0; Transaction(context => { - var command = new AddDbInstanceCommand(context); + var command = new AddOdsInstanceManageCommand(context); id = command.Execute(model.Object).Id; }); Transaction(context => { - var instance = context.DbInstances.Single(d => d.Id == id); + var instance = context.OdsInstanceManages.Single(d => d.Id == id); instance.Name.ShouldBe("Trimmed Instance"); instance.DatabaseTemplate.ShouldBe("Minimal"); }); @@ -90,7 +90,7 @@ public void ShouldTrimNameAndDatabaseTemplate() [Test] public void ShouldSetLastRefreshedAndLastModifiedDate() { - var model = new Mock(); + var model = new Mock(); model.Setup(x => x.Name).Returns("Timestamp Instance"); model.Setup(x => x.DatabaseTemplate).Returns("Minimal"); @@ -98,13 +98,13 @@ public void ShouldSetLastRefreshedAndLastModifiedDate() var id = 0; Transaction(context => { - var command = new AddDbInstanceCommand(context); + var command = new AddOdsInstanceManageCommand(context); id = command.Execute(model.Object).Id; }); Transaction(context => { - var instance = context.DbInstances.Single(d => d.Id == id); + var instance = context.OdsInstanceManages.Single(d => d.Id == id); instance.LastRefreshed.ShouldBeGreaterThanOrEqualTo(before); instance.LastModifiedDate.ShouldNotBeNull(); instance.LastModifiedDate!.Value.ShouldBeGreaterThanOrEqualTo(before); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceManageCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceManageCommandTests.cs index edf6723c9..9a6b9ce49 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceManageCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceManageCommandTests.cs @@ -14,15 +14,15 @@ namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; [TestFixture] -public class DeleteDbInstanceCommandTests : AdminApiDbContextTestBase +public class DeleteOdsInstanceManageCommandTests : AdminApiDbContextTestBase { [Test] public void ShouldSetStatusToPendingDelete() { - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Delete Test Instance", - Status = DbInstanceStatus.Created.ToString(), + Status = OdsInstanceManageStatus.Created.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }; @@ -30,14 +30,14 @@ public void ShouldSetStatusToPendingDelete() Transaction(context => { - var command = new DeleteDbInstanceCommand(context); + var command = new DeleteOdsInstanceManageCommand(context); command.Execute(instance.Id); }); Transaction(context => { - var updated = context.DbInstances.Single(d => d.Id == instance.Id); - updated.Status.ShouldBe(DbInstanceStatus.PendingDelete.ToString()); + var updated = context.OdsInstanceManages.Single(d => d.Id == instance.Id); + updated.Status.ShouldBe(OdsInstanceManageStatus.PendingDelete.ToString()); }); } @@ -45,10 +45,10 @@ public void ShouldSetStatusToPendingDelete() public void ShouldUpdateLastModifiedDate() { var before = DateTime.UtcNow; - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Timestamp Test Instance", - Status = DbInstanceStatus.Created.ToString(), + Status = OdsInstanceManageStatus.Created.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }; @@ -56,13 +56,13 @@ public void ShouldUpdateLastModifiedDate() Transaction(context => { - var command = new DeleteDbInstanceCommand(context); + var command = new DeleteOdsInstanceManageCommand(context); command.Execute(instance.Id); }); Transaction(context => { - var updated = context.DbInstances.Single(d => d.Id == instance.Id); + var updated = context.OdsInstanceManages.Single(d => d.Id == instance.Id); updated.LastModifiedDate.ShouldNotBeNull(); updated.LastModifiedDate!.Value.ShouldBeGreaterThanOrEqualTo(before); }); @@ -71,10 +71,10 @@ public void ShouldUpdateLastModifiedDate() [Test] public void ShouldNotHardDeleteRecord() { - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "No Hard Delete Instance", - Status = DbInstanceStatus.Created.ToString(), + Status = OdsInstanceManageStatus.Created.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }; @@ -82,13 +82,13 @@ public void ShouldNotHardDeleteRecord() Transaction(context => { - var command = new DeleteDbInstanceCommand(context); + var command = new DeleteOdsInstanceManageCommand(context); command.Execute(instance.Id); }); Transaction(context => { - var stillExists = context.DbInstances.Any(d => d.Id == instance.Id); + var stillExists = context.OdsInstanceManages.Any(d => d.Id == instance.Id); stillExists.ShouldBeTrue(); }); } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManageByIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManageByIdQueryTests.cs index 073ee99d7..46597d0df 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManageByIdQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManageByIdQueryTests.cs @@ -12,23 +12,23 @@ namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; [TestFixture] -public class GetDbInstanceByIdQueryTests : AdminApiDbContextTestBase +public class GetOdsInstanceManageByIdQueryTests : AdminApiDbContextTestBase { [Test] public void ShouldReturnNullForNonExistentId() { Transaction(context => { - var query = new GetDbInstanceByIdQuery(context); + var query = new GetOdsInstanceManageByIdQuery(context); var result = query.Execute(0); result.ShouldBeNull(); }); } [Test] - public void ShouldGetDbInstanceById() + public void ShouldGetOdsInstanceManageById() { - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Test Instance", Status = "Pending", @@ -39,7 +39,7 @@ public void ShouldGetDbInstanceById() Transaction(context => { - var query = new GetDbInstanceByIdQuery(context); + var query = new GetOdsInstanceManageByIdQuery(context); var result = query.Execute(instance.Id); result.ShouldNotBeNull(); result!.Id.ShouldBe(instance.Id); @@ -52,7 +52,7 @@ public void ShouldGetDbInstanceById() [Test] public void ShouldReturnNullWhenIdDoesNotMatch() { - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Another Instance", Status = "Pending", @@ -63,7 +63,7 @@ public void ShouldReturnNullWhenIdDoesNotMatch() Transaction(context => { - var query = new GetDbInstanceByIdQuery(context); + var query = new GetOdsInstanceManageByIdQuery(context); var result = query.Execute(instance.Id + 9999); result.ShouldBeNull(); }); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManagesQueryTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManagesQueryTests.cs index 00ff76613..ab84663b7 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManagesQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManagesQueryTests.cs @@ -16,15 +16,15 @@ namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; [TestFixture] -public class GetDbInstancesQueryTests : AdminApiDbContextTestBase +public class GetOdsInstanceManagesQueryTests : AdminApiDbContextTestBase { private static IEnumerable AllStatuses => - Enum.GetValues().Select(status => status.ToString()); + Enum.GetValues().Select(status => status.ToString()); [Test] - public void ShouldRetrieveDbInstances() + public void ShouldRetrieveOdsInstanceManages() { - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Test Instance", Status = "Pending", @@ -35,7 +35,7 @@ public void ShouldRetrieveDbInstances() Transaction(context => { - var query = new GetDbInstancesQuery(context, Testing.GetAppSettings()); + var query = new GetOdsInstanceManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(), null, null); results.ShouldNotBeEmpty(); results.ShouldContain(d => d.Id == instance.Id && d.Name == "Test Instance"); @@ -43,11 +43,11 @@ public void ShouldRetrieveDbInstances() } [Test] - public void ShouldReturnEmptyListWhenNoDbInstances() + public void ShouldReturnEmptyListWhenNoOdsInstanceManages() { Transaction(context => { - var query = new GetDbInstancesQuery(context, Testing.GetAppSettings()); + var query = new GetOdsInstanceManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(), null, null); results.ShouldBeEmpty(); }); @@ -57,13 +57,13 @@ public void ShouldReturnEmptyListWhenNoDbInstances() public void ShouldFilterByName() { Save( - new DbInstance { Name = "Instance Alpha", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "Instance Beta", Status = "Pending", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow } + new OdsInstanceManage { Name = "Instance Alpha", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "Instance Beta", Status = "Pending", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow } ); Transaction(context => { - var query = new GetDbInstancesQuery(context, Testing.GetAppSettings()); + var query = new GetOdsInstanceManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(), null, "Instance Alpha"); results.Count.ShouldBe(1); results.Single().Name.ShouldBe("Instance Alpha"); @@ -73,13 +73,13 @@ public void ShouldFilterByName() [Test] public void ShouldFilterById() { - var instance1 = new DbInstance { Name = "Instance 1", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }; - var instance2 = new DbInstance { Name = "Instance 2", Status = "Pending", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow }; + var instance1 = new OdsInstanceManage { Name = "Instance 1", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }; + var instance2 = new OdsInstanceManage { Name = "Instance 2", Status = "Pending", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow }; Save(instance1, instance2); Transaction(context => { - var query = new GetDbInstancesQuery(context, Testing.GetAppSettings()); + var query = new GetOdsInstanceManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(), instance1.Id, null); results.Count.ShouldBe(1); results.Single().Id.ShouldBe(instance1.Id); @@ -88,11 +88,11 @@ public void ShouldFilterById() } [Test] - public void ShouldRetrieveDbInstancesWithOffsetAndLimit() + public void ShouldRetrieveOdsInstanceManagesWithOffsetAndLimit() { for (var i = 1; i <= 5; i++) { - Save(new DbInstance + Save(new OdsInstanceManage { Name = $"Instance {i:D2}", Status = "Pending", @@ -103,7 +103,7 @@ public void ShouldRetrieveDbInstancesWithOffsetAndLimit() Transaction(context => { - var query = new GetDbInstancesQuery(context, Testing.GetAppSettings()); + var query = new GetOdsInstanceManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, 2), null, null); results.Count.ShouldBe(2); @@ -117,11 +117,11 @@ public void ShouldRetrieveDbInstancesWithOffsetAndLimit() } [Test] - public void ShouldRetrieveAllDbInstancesWithoutLimit() + public void ShouldRetrieveAllOdsInstanceManagesWithoutLimit() { for (var i = 1; i <= 5; i++) { - Save(new DbInstance + Save(new OdsInstanceManage { Name = $"Instance {i:D2}", Status = "Pending", @@ -132,24 +132,24 @@ public void ShouldRetrieveAllDbInstancesWithoutLimit() Transaction(context => { - var query = new GetDbInstancesQuery(context, Testing.GetAppSettings()); + var query = new GetOdsInstanceManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, null), null, null); results.Count.ShouldBe(5); }); } [Test] - public void ShouldRetrieveDbInstancesOrderedById() + public void ShouldRetrieveOdsInstanceManagesOrderedById() { Save( - new DbInstance { Name = "Instance C", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "Instance A", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "Instance B", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow } + new OdsInstanceManage { Name = "Instance C", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "Instance A", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "Instance B", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow } ); Transaction(context => { - var query = new GetDbInstancesQuery(context, Testing.GetAppSettings()); + var query = new GetOdsInstanceManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(), null, null); var ids = results.Select(r => r.Id).ToList(); ids.ShouldBe(ids.OrderBy(x => x).ToList()); @@ -158,16 +158,16 @@ public void ShouldRetrieveDbInstancesOrderedById() [Test] [TestCaseSource(nameof(AllStatuses))] - public void ShouldIncludeDbInstances_ForAllStatuses_WhenNoFiltersApplied(string status) + public void ShouldIncludeOdsInstanceManages_ForAllStatuses_WhenNoFiltersApplied(string status) { Save( - new DbInstance { Name = $"Status-{status}", OdsInstanceId = 111, Status = status, DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "Active Instance", OdsInstanceId = 222, Status = "Created", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow } + new OdsInstanceManage { Name = $"Status-{status}", OdsInstanceId = 111, Status = status, DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "Active Instance", OdsInstanceId = 222, Status = "Created", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow } ); Transaction(context => { - var query = new GetDbInstancesQuery(context, Testing.GetAppSettings()); + var query = new GetOdsInstanceManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, null), null, null); results.Count.ShouldBe(2); results.ShouldContain(r => r.Name == $"Status-{status}" && r.Status == status); diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetTenantEdOrgsByInstancesTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetTenantEdOrgsByInstancesTests.cs index 706d9e11c..ff0e6f0e3 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetTenantEdOrgsByInstancesTests.cs +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetTenantEdOrgsByInstancesTests.cs @@ -17,9 +17,9 @@ namespace EdFi.Ods.AdminApi.DBTests.Database.QueryTests; public class GetTenantEdOrgsByInstancesTests : AdminApiDbContextTestBase { [Test] - public void ShouldDistinguishLinkedAndUnlinkedDbInstances() + public void ShouldDistinguishLinkedAndUnlinkedOdsInstanceManages() { - var linked = new DbInstance + var linked = new OdsInstanceManage { Name = "Linked-Instance", OdsInstanceId = 999, @@ -27,7 +27,7 @@ public void ShouldDistinguishLinkedAndUnlinkedDbInstances() DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }; - var unlinked = new DbInstance + var unlinked = new OdsInstanceManage { Name = "Unlinked-Instance", OdsInstanceId = null, @@ -39,7 +39,7 @@ public void ShouldDistinguishLinkedAndUnlinkedDbInstances() Transaction(context => { - var query = new GetDbInstancesQuery(context, Testing.GetAppSettings()); + var query = new GetOdsInstanceManagesQuery(context, Testing.GetAppSettings()); var allResults = query.Execute(new CommonQueryParams(0, null), null, null); var unlinkedResults = allResults.Where(d => d.OdsInstanceId == null).ToList(); @@ -56,26 +56,26 @@ public void ShouldDistinguishLinkedAndUnlinkedDbInstances() } [Test] - public void ShouldReturnAllDbInstances_WhenNoFiltersApplied() + public void ShouldReturnAllOdsInstanceManages_WhenNoFiltersApplied() { Save( - new DbInstance { Name = "A", OdsInstanceId = 1, Status = "Created", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "B", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "C", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow } + new OdsInstanceManage { Name = "A", OdsInstanceId = 1, Status = "Created", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "B", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "C", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow } ); Transaction(context => { - var query = new GetDbInstancesQuery(context, Testing.GetAppSettings()); + var query = new GetOdsInstanceManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, null), null, null); results.Count.ShouldBe(3); }); } [Test] - public void ShouldReturnAllDbInstanceFields_ForLinkedInstance() + public void ShouldReturnAllOdsInstanceManageFields_ForLinkedInstance() { - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Name = "Fully-Linked", OdsInstanceId = 42, @@ -84,16 +84,16 @@ public void ShouldReturnAllDbInstanceFields_ForLinkedInstance() DatabaseName = "EdFi_ODS_42", LastRefreshed = DateTime.UtcNow }; - Save(dbInstance); + Save(odsInstanceManage); Transaction(context => { - var query = new GetDbInstancesQuery(context, Testing.GetAppSettings()); + var query = new GetOdsInstanceManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, null), null, null); results.Count.ShouldBe(1); var result = results[0]; - result.Id.ShouldBe(dbInstance.Id); + result.Id.ShouldBe(odsInstanceManage.Id); result.OdsInstanceId.ShouldBe(42); result.Status.ShouldBe("Created"); result.DatabaseTemplate.ShouldBe("Minimal"); @@ -102,11 +102,11 @@ public void ShouldReturnAllDbInstanceFields_ForLinkedInstance() } [Test] - public void ShouldReturnEmptyList_WhenNoDbInstancesExist() + public void ShouldReturnEmptyList_WhenNoOdsInstanceManagesExist() { Transaction(context => { - var query = new GetDbInstancesQuery(context, Testing.GetAppSettings()); + var query = new GetOdsInstanceManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, null), null, null); results.ShouldBeEmpty(); }); @@ -116,13 +116,13 @@ public void ShouldReturnEmptyList_WhenNoDbInstancesExist() public void ShouldReturnMultipleUnlinkedInstances_InIdOrder() { Save( - new DbInstance { Name = "Z-Unlinked", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "A-Unlinked", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow } + new OdsInstanceManage { Name = "Z-Unlinked", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "A-Unlinked", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow } ); Transaction(context => { - var query = new GetDbInstancesQuery(context, Testing.GetAppSettings()); + var query = new GetOdsInstanceManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, null), null, null); var ids = results.Select(r => r.Id).ToList(); ids.ShouldBe(ids.OrderBy(x => x).ToList()); From b3d7f3679ae387b2567d604bfabc301a7b72fbe3 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 21:54:36 -0600 Subject: [PATCH 15/35] Rename v2 Bruno E2E DbInstances collection to OdsInstances/Manage --- .../DELETE - DbInstance - Not Found.bru | 32 ---------- .../GET - DbInstances - Not Found.bru | 32 ---------- ...DbInstances - Without Limit and Offset.bru | 25 -------- .../GET - DbInstances - Without Limit.bru | 29 --------- .../GET - DbInstances - Without Offset.bru | 29 --------- .../POST - DbInstances - Sample Template.bru | 52 ---------------- ...ELETE - OdsInstance Manage - Not Found.bru | 32 ++++++++++ ...OdsInstance Manage - Success.bru.disabled} | 60 +++++++++---------- ... OdsInstances Manage - Filter by Name.bru} | 10 ++-- .../GET - OdsInstances Manage - Not Found.bru | 32 ++++++++++ ...nces Manage - Without Limit and Offset.bru | 25 ++++++++ ... - OdsInstances Manage - Without Limit.bru | 29 +++++++++ ...- OdsInstances Manage - Without Offset.bru | 29 +++++++++ .../GET - OdsInstances Manage by ID.bru} | 14 ++--- .../Manage/GET - OdsInstances Manage.bru} | 12 ++-- ...es Manage - Invalid Database Template.bru} | 12 ++-- .../POST - OdsInstances Manage - Invalid.bru} | 12 ++-- ... OdsInstances Manage - Sample Template.bru | 52 ++++++++++++++++ .../Manage/POST - OdsInstances Manage.bru} | 10 ++-- .../Manage}/folder.bru | 2 +- 20 files changed, 265 insertions(+), 265 deletions(-) delete mode 100644 Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/DELETE - DbInstance - Not Found.bru delete mode 100644 Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Not Found.bru delete mode 100644 Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Limit and Offset.bru delete mode 100644 Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Limit.bru delete mode 100644 Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Offset.bru delete mode 100644 Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Sample Template.bru create mode 100644 Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/DELETE - OdsInstance Manage - Not Found.bru rename Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/{DbInstances/DELETE - DbInstance - Success.bru.disabled => OdsInstances/Manage/DELETE - OdsInstance Manage - Success.bru.disabled} (55%) rename Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/{DbInstances/GET - DbInstances - Filter by Name.bru => OdsInstances/Manage/GET - OdsInstances Manage - Filter by Name.bru} (52%) create mode 100644 Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Not Found.bru create mode 100644 Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Limit and Offset.bru create mode 100644 Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Limit.bru create mode 100644 Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Offset.bru rename Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/{DbInstances/GET - DbInstances by ID.bru => OdsInstances/Manage/GET - OdsInstances Manage by ID.bru} (67%) rename Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/{DbInstances/GET - DbInstances.bru => OdsInstances/Manage/GET - OdsInstances Manage.bru} (72%) rename Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/{DbInstances/POST - DbInstances - Invalid Database Template.bru => OdsInstances/Manage/POST - OdsInstances Manage - Invalid Database Template.bru} (55%) rename Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/{DbInstances/POST - DbInstances - Invalid.bru => OdsInstances/Manage/POST - OdsInstances Manage - Invalid.bru} (57%) create mode 100644 Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Sample Template.bru rename Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/{DbInstances/POST - DbInstances.bru => OdsInstances/Manage/POST - OdsInstances Manage.bru} (59%) rename Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/{DbInstances => OdsInstances/Manage}/folder.bru (69%) diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/DELETE - DbInstance - Not Found.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/DELETE - DbInstance - Not Found.bru deleted file mode 100644 index 13876fcec..000000000 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/DELETE - DbInstance - Not Found.bru +++ /dev/null @@ -1,32 +0,0 @@ -meta { - name: DbInstances - Delete - Not Found - type: http - seq: 12 -} - -delete { - url: {{API_URL}}/v2/dbinstances/0 - body: none - auth: inherit -} - -script:post-response { - test("DELETE DbInstance Not Found: Status code is Not Found", function () { - expect(res.getStatus()).to.equal(404); - }); - - const response = res.getBody(); - - test("DELETE DbInstance Not Found: Response matches error format", function () { - expect(response).to.have.property("title"); - }); - - test("DELETE DbInstance Not Found: Response title is helpful and accurate", function () { - expect(response.title.toLowerCase()).to.contain("not found"); - }); -} - -settings { - encodeUrl: true - timeout: 0 -} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Not Found.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Not Found.bru deleted file mode 100644 index 3255a752b..000000000 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Not Found.bru +++ /dev/null @@ -1,32 +0,0 @@ -meta { - name: DbInstances - Not Found - type: http - seq: 7 -} - -get { - url: {{API_URL}}/v2/dbinstances/0 - body: none - auth: inherit -} - -script:post-response { - test("GET DbInstance Not Found: Status code is Not Found", function () { - expect(res.getStatus()).to.equal(404); - }); - - const response = res.getBody(); - - test("GET DbInstance Not Found: Response matches error format", function () { - expect(response).to.have.property("title"); - }); - - test("GET DbInstance Not Found: Response title is helpful and accurate", function () { - expect(response.title).to.contain("Not found"); - expect(response.title.toLowerCase()).to.contain("dbinstance"); - }); -} - -settings { - encodeUrl: true -} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Limit and Offset.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Limit and Offset.bru deleted file mode 100644 index bc74ec432..000000000 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Limit and Offset.bru +++ /dev/null @@ -1,25 +0,0 @@ -meta { - name: DbInstances - Without Limit and Offset - type: http - seq: 9 -} - -get { - url: {{API_URL}}/v2/dbinstances - body: none - auth: inherit -} - -script:post-response { - test("GET DbInstances Without Limit and Offset: Status code is OK", function () { - expect(res.getStatus()).to.equal(200); - }); - - test("GET DbInstances Without Limit and Offset: Response is an array", function () { - expect(res.getBody()).to.be.an("array"); - }); -} - -settings { - encodeUrl: true -} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Limit.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Limit.bru deleted file mode 100644 index d98b4fff6..000000000 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Limit.bru +++ /dev/null @@ -1,29 +0,0 @@ -meta { - name: DbInstances - Without Limit - type: http - seq: 10 -} - -get { - url: {{API_URL}}/v2/dbinstances?offset={{offset}} - body: none - auth: inherit -} - -params:query { - offset: {{offset}} -} - -script:post-response { - test("GET DbInstances Without Limit: Status code is OK", function () { - expect(res.getStatus()).to.equal(200); - }); - - test("GET DbInstances Without Limit: Response is an array", function () { - expect(res.getBody()).to.be.an("array"); - }); -} - -settings { - encodeUrl: true -} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Offset.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Offset.bru deleted file mode 100644 index fe280e990..000000000 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Offset.bru +++ /dev/null @@ -1,29 +0,0 @@ -meta { - name: DbInstances - Without Offset - type: http - seq: 11 -} - -get { - url: {{API_URL}}/v2/dbinstances?limit={{limit}} - body: none - auth: inherit -} - -params:query { - limit: {{limit}} -} - -script:post-response { - test("GET DbInstances Without Offset: Status code is OK", function () { - expect(res.getStatus()).to.equal(200); - }); - - test("GET DbInstances Without Offset: Response is an array", function () { - expect(res.getBody()).to.be.an("array"); - }); -} - -settings { - encodeUrl: true -} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Sample Template.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Sample Template.bru deleted file mode 100644 index 03a21b92b..000000000 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Sample Template.bru +++ /dev/null @@ -1,52 +0,0 @@ -meta { - name: DbInstances - Sample Template - type: http - seq: 4 -} - -script:pre-request { - const sampleDbInstanceName = `Test DB Instance Sample ${Date.now()}`; - bru.setVar("SampleDbInstanceName", sampleDbInstanceName); - console.log(`Using unique Sample DbInstance name: ${sampleDbInstanceName}`); -} - -post { - url: {{API_URL}}/v2/dbinstances - body: json - auth: inherit -} - -body:json { - { - "name": "{{SampleDbInstanceName}}", - "databaseTemplate": "Sample" - } -} - -script:post-response { - const expectedStatus = 202; - const responseStatus = res.getStatus(); - - if (responseStatus !== expectedStatus) { - console.log("❌ POST DbInstances Sample request failed"); - console.log("Status:", responseStatus); - console.log("Response body:", JSON.stringify(res.getBody(), null, 2)); - } - - test("POST DbInstances Sample: Status code is Accepted", function () { - expect(responseStatus).to.equal(expectedStatus); - }); - - test("POST DbInstances Sample: Response includes location in header", function () { - expect(res.getHeaders()).to.have.property("location"); - const id = res.getHeader("location").split("/").pop(); - if (id) { - bru.setVar("CreatedDbInstanceIdSample", id); - } - }); -} - -settings { - encodeUrl: true - timeout: 0 -} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/DELETE - OdsInstance Manage - Not Found.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/DELETE - OdsInstance Manage - Not Found.bru new file mode 100644 index 000000000..1b1bd5d35 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/DELETE - OdsInstance Manage - Not Found.bru @@ -0,0 +1,32 @@ +meta { + name: OdsInstances Manage - Delete - Not Found + type: http + seq: 12 +} + +delete { + url: {{API_URL}}/v2/odsinstances/manage/0 + body: none + auth: inherit +} + +script:post-response { + test("DELETE OdsInstance Manage Not Found: Status code is Not Found", function () { + expect(res.getStatus()).to.equal(404); + }); + + const response = res.getBody(); + + test("DELETE OdsInstance Manage Not Found: Response matches error format", function () { + expect(response).to.have.property("title"); + }); + + test("DELETE OdsInstance Manage Not Found: Response title is helpful and accurate", function () { + expect(response.title.toLowerCase()).to.contain("not found"); + }); +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/DELETE - DbInstance - Success.bru.disabled b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/DELETE - OdsInstance Manage - Success.bru.disabled similarity index 55% rename from Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/DELETE - DbInstance - Success.bru.disabled rename to Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/DELETE - OdsInstance Manage - Success.bru.disabled index 28b24c6f2..3a5acaa0d 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/DELETE - DbInstance - Success.bru.disabled +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/DELETE - OdsInstance Manage - Success.bru.disabled @@ -1,5 +1,5 @@ meta { - name: DbInstances - Delete - Success + name: OdsInstances Manage - Delete - Success type: http seq: 13 skip: true @@ -8,7 +8,7 @@ meta { script:pre-request { // DISABLED: The CI pipeline does not seed the Minimal/Sample ODS template databases // (EdFi_Ods_Minimal_Template / EdFi_Ods_Populated_Template) before running the E2E suite. - // Without those source databases the provisioner cannot copy them and the DbInstance + // Without those source databases the provisioner cannot copy them and the OdsInstance Manage // transitions to Error status, causing this pre-request to throw and block the whole run. // To re-enable: remove this early-return block and remove 'skip: true' from meta. // See docs/design/DBINSTANCE-PROVISIONING-JOBS.md § Pending work. @@ -27,7 +27,7 @@ script:pre-request { const axios = require('axios'); const API_URL = bru.getEnvVar("API_URL"); const TOKEN = bru.getEnvVar("TOKEN"); - const deleteDbInstanceName = `Delete Test DB Instance ${Date.now()}`; + const deleteOdsInstanceManageName = `Delete Test DB Instance ${Date.now()}`; const requestTimeoutMs = 30000; const provisioningTimeoutMs = 180000; const pollingIntervalMs = 5000; @@ -56,66 +56,66 @@ script:pre-request { const sleep = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)); try { - console.log(`Creating delete test DbInstance: ${deleteDbInstanceName}`); + console.log(`Creating delete test OdsInstance Manage: ${deleteOdsInstanceManageName}`); - const createResponse = await axios.post(`${API_URL}/v2/dbinstances`, { - name: deleteDbInstanceName, + const createResponse = await axios.post(`${API_URL}/v2/odsinstances/manage`, { + name: deleteOdsInstanceManageName, databaseTemplate: "Minimal" }, axiosConfig); if (createResponse.status !== 202) { - logAxiosFailure("Failed to create delete test DbInstance", createResponse); - throw new Error(`Failed to create delete test DbInstance - Status: ${createResponse.status}`); + logAxiosFailure("Failed to create delete test OdsInstance Manage", createResponse); + throw new Error(`Failed to create delete test OdsInstance Manage - Status: ${createResponse.status}`); } const locationHeader = createResponse.headers.location; const locationValue = Array.isArray(locationHeader) ? locationHeader[0] : locationHeader; if (!locationValue) { - console.log("❌ Create delete test DbInstance response did not include a location header"); + console.log("❌ Create delete test OdsInstance Manage response did not include a location header"); console.log("Headers:", JSON.stringify(createResponse.headers, null, 2)); - throw new Error("Create delete test DbInstance response did not include a location header."); + throw new Error("Create delete test OdsInstance Manage response did not include a location header."); } - const freshPendingDbInstanceId = locationValue.split("/").pop(); + const freshPendingOdsInstanceManageId = locationValue.split("/").pop(); - if (!freshPendingDbInstanceId) { - throw new Error(`Could not parse DbInstance id from location header '${locationValue}'.`); + if (!freshPendingOdsInstanceManageId) { + throw new Error(`Could not parse OdsInstance Manage id from location header '${locationValue}'.`); } - bru.setVar("FreshPendingDbInstanceId", freshPendingDbInstanceId); - bru.setVar("DeleteDbInstanceName", deleteDbInstanceName); + bru.setVar("FreshPendingOdsInstanceManageId", freshPendingOdsInstanceManageId); + bru.setVar("DeleteOdsInstanceManageName", deleteOdsInstanceManageName); const provisioningDeadline = Date.now() + provisioningTimeoutMs; let isReadyToDelete = false; while (Date.now() < provisioningDeadline) { - const statusResponse = await axios.get(`${API_URL}/v2/dbinstances/${freshPendingDbInstanceId}`, axiosConfig); + const statusResponse = await axios.get(`${API_URL}/v2/odsinstances/manage/${freshPendingOdsInstanceManageId}`, axiosConfig); if (statusResponse.status !== 200) { - logAxiosFailure("Failed to poll delete test DbInstance", statusResponse); - throw new Error(`Failed to poll delete test DbInstance - Status: ${statusResponse.status}`); + logAxiosFailure("Failed to poll delete test OdsInstance Manage", statusResponse); + throw new Error(`Failed to poll delete test OdsInstance Manage - Status: ${statusResponse.status}`); } const currentStatus = statusResponse.data?.status; - console.log(`Delete test DbInstance ${freshPendingDbInstanceId} status: ${currentStatus}`); + console.log(`Delete test OdsInstance Manage ${freshPendingOdsInstanceManageId} status: ${currentStatus}`); if (currentStatus === "Completed" || currentStatus === "DeleteFailed") { - console.log(`✅ Delete test DbInstance ${freshPendingDbInstanceId} is ready to be deleted`); + console.log(`✅ Delete test OdsInstance Manage ${freshPendingOdsInstanceManageId} is ready to be deleted`); isReadyToDelete = true; break; } if (currentStatus === "Error") { - logAxiosFailure("Delete test DbInstance provisioning ended in Error", statusResponse); - throw new Error(`Delete test DbInstance provisioning ended in Error for id ${freshPendingDbInstanceId}.`); + logAxiosFailure("Delete test OdsInstance Manage provisioning ended in Error", statusResponse); + throw new Error(`Delete test OdsInstance Manage provisioning ended in Error for id ${freshPendingOdsInstanceManageId}.`); } await sleep(pollingIntervalMs); } if (!isReadyToDelete) { - throw new Error(`Timed out waiting for delete test DbInstance ${freshPendingDbInstanceId} to become deletable.`); + throw new Error(`Timed out waiting for delete test OdsInstance Manage ${freshPendingOdsInstanceManageId} to become deletable.`); } } catch (error) { console.log("Error in pre-request setup:", error.message); @@ -124,7 +124,7 @@ script:pre-request { } delete { - url: {{API_URL}}/v2/dbinstances/{{FreshPendingDbInstanceId}} + url: {{API_URL}}/v2/odsinstances/manage/{{FreshPendingOdsInstanceManageId}} body: none auth: inherit } @@ -137,19 +137,19 @@ script:post-response { const responseStatus = res.getStatus(); if (responseStatus !== expectedStatus) { - console.log("❌ DELETE DbInstance Success request failed"); - console.log("DbInstanceId:", bru.getVar("FreshPendingDbInstanceId")); - console.log("DbInstanceName:", bru.getVar("DeleteDbInstanceName")); + console.log("❌ DELETE OdsInstance Manage Success request failed"); + console.log("OdsInstanceManageId:", bru.getVar("FreshPendingOdsInstanceManageId")); + console.log("OdsInstanceManageName:", bru.getVar("DeleteOdsInstanceManageName")); console.log("Status:", responseStatus); console.log("Response body:", JSON.stringify(res.getBody(), null, 2)); } - test("DELETE DbInstance Success: Status code is No Content", function () { + test("DELETE OdsInstance Manage Success: Status code is No Content", function () { expect(responseStatus).to.equal(expectedStatus); }); - bru.deleteVar("FreshPendingDbInstanceId"); - bru.deleteVar("DeleteDbInstanceName"); + bru.deleteVar("FreshPendingOdsInstanceManageId"); + bru.deleteVar("DeleteOdsInstanceManageName"); } settings { diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Filter by Name.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Filter by Name.bru similarity index 52% rename from Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Filter by Name.bru rename to Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Filter by Name.bru index 0fcb1f857..d20eae99f 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Filter by Name.bru +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Filter by Name.bru @@ -1,11 +1,11 @@ meta { - name: DbInstances - Filter by Name + name: OdsInstances Manage - Filter by Name type: http seq: 8 } get { - url: {{API_URL}}/v2/dbinstances?name=Test DB Instance + url: {{API_URL}}/v2/odsinstances/manage?name=Test DB Instance body: none auth: inherit } @@ -15,15 +15,15 @@ params:query { } script:post-response { - test("GET DbInstances Filter by Name: Status code is OK", function () { + test("GET OdsInstances Manage Filter by Name: Status code is OK", function () { expect(res.getStatus()).to.equal(200); }); - test("GET DbInstances Filter by Name: Response is an array", function () { + test("GET OdsInstances Manage Filter by Name: Response is an array", function () { expect(res.getBody()).to.be.an("array"); }); - test("GET DbInstances Filter by Name: All results match the name filter", function () { + test("GET OdsInstances Manage Filter by Name: All results match the name filter", function () { const results = res.getBody(); results.forEach(function (item) { expect(item.name).to.equal("Test DB Instance"); diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Not Found.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Not Found.bru new file mode 100644 index 000000000..6b18e7570 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Not Found.bru @@ -0,0 +1,32 @@ +meta { + name: OdsInstances Manage - Not Found + type: http + seq: 7 +} + +get { + url: {{API_URL}}/v2/odsinstances/manage/0 + body: none + auth: inherit +} + +script:post-response { + test("GET OdsInstance Manage Not Found: Status code is Not Found", function () { + expect(res.getStatus()).to.equal(404); + }); + + const response = res.getBody(); + + test("GET OdsInstance Manage Not Found: Response matches error format", function () { + expect(response).to.have.property("title"); + }); + + test("GET OdsInstance Manage Not Found: Response title is helpful and accurate", function () { + expect(response.title).to.contain("Not found"); + expect(response.title.toLowerCase()).to.contain("odsinstancemanage"); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Limit and Offset.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Limit and Offset.bru new file mode 100644 index 000000000..2bbff4f05 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Limit and Offset.bru @@ -0,0 +1,25 @@ +meta { + name: OdsInstances Manage - Without Limit and Offset + type: http + seq: 9 +} + +get { + url: {{API_URL}}/v2/odsinstances/manage + body: none + auth: inherit +} + +script:post-response { + test("GET OdsInstances Manage Without Limit and Offset: Status code is OK", function () { + expect(res.getStatus()).to.equal(200); + }); + + test("GET OdsInstances Manage Without Limit and Offset: Response is an array", function () { + expect(res.getBody()).to.be.an("array"); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Limit.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Limit.bru new file mode 100644 index 000000000..03b4ae03a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Limit.bru @@ -0,0 +1,29 @@ +meta { + name: OdsInstances Manage - Without Limit + type: http + seq: 10 +} + +get { + url: {{API_URL}}/v2/odsinstances/manage?offset={{offset}} + body: none + auth: inherit +} + +params:query { + offset: {{offset}} +} + +script:post-response { + test("GET OdsInstances Manage Without Limit: Status code is OK", function () { + expect(res.getStatus()).to.equal(200); + }); + + test("GET OdsInstances Manage Without Limit: Response is an array", function () { + expect(res.getBody()).to.be.an("array"); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Offset.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Offset.bru new file mode 100644 index 000000000..d98e0542b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Offset.bru @@ -0,0 +1,29 @@ +meta { + name: OdsInstances Manage - Without Offset + type: http + seq: 11 +} + +get { + url: {{API_URL}}/v2/odsinstances/manage?limit={{limit}} + body: none + auth: inherit +} + +params:query { + limit: {{limit}} +} + +script:post-response { + test("GET OdsInstances Manage Without Offset: Status code is OK", function () { + expect(res.getStatus()).to.equal(200); + }); + + test("GET OdsInstances Manage Without Offset: Response is an array", function () { + expect(res.getBody()).to.be.an("array"); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances by ID.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage by ID.bru similarity index 67% rename from Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances by ID.bru rename to Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage by ID.bru index 7d89ca336..0be7e6d1d 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances by ID.bru +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage by ID.bru @@ -1,27 +1,27 @@ meta { - name: DbInstances by ID + name: OdsInstances Manage by ID type: http seq: 6 } get { - url: {{API_URL}}/v2/dbinstances/{{CreatedDbInstanceId}} + url: {{API_URL}}/v2/odsinstances/manage/{{CreatedOdsInstanceManageId}} body: none auth: inherit } script:post-response { - test("GET DbInstance by ID: Status code is OK", function () { + test("GET OdsInstance Manage by ID: Status code is OK", function () { expect(res.getStatus()).to.equal(200); }); const result = res.getBody(); - test("GET DbInstance by ID: Response id matches", function () { - expect(result.id).to.equal(parseInt(bru.getVar("CreatedDbInstanceId"))); + test("GET OdsInstance Manage by ID: Response id matches", function () { + expect(result.id).to.equal(parseInt(bru.getVar("CreatedOdsInstanceManageId"))); }); - test("GET DbInstance by ID: Response contains expected fields", function () { + test("GET OdsInstance Manage by ID: Response contains expected fields", function () { expect(result).to.have.property("name"); expect(result).to.have.property("status"); expect(result).to.have.property("databaseTemplate"); @@ -46,7 +46,7 @@ script:post-response { required: ["id", "name", "status", "databaseTemplate"] }; - test("GET DbInstance by ID: Response matches schema", function () { + test("GET OdsInstance Manage by ID: Response matches schema", function () { const valid = ajv.validate(schema, result); expect(valid).to.be.true; }); diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage.bru similarity index 72% rename from Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances.bru rename to Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage.bru index 18e87af70..8b12daf65 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances.bru +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage.bru @@ -1,11 +1,11 @@ meta { - name: DbInstances - Get All + name: OdsInstances Manage - Get All type: http seq: 5 } get { - url: {{API_URL}}/v2/dbinstances?offset={{offset}}&limit={{limit}} + url: {{API_URL}}/v2/odsinstances/manage?offset={{offset}}&limit={{limit}} body: none auth: inherit } @@ -16,15 +16,15 @@ params:query { } script:post-response { - test("GET DbInstances: Status code is OK", function () { + test("GET OdsInstances Manage: Status code is OK", function () { expect(res.getStatus()).to.equal(200); }); - test("GET DbInstances: Response is an array", function () { + test("GET OdsInstances Manage: Response is an array", function () { expect(res.getBody()).to.be.an("array"); }); - test("GET DbInstances: Response includes created instance", function () { + test("GET OdsInstances Manage: Response includes created instance", function () { expect(res.getBody().length).to.be.greaterThan(0); }); @@ -50,7 +50,7 @@ script:post-response { } }; - test("GET DbInstances: Response matches schema", function () { + test("GET OdsInstances Manage: Response matches schema", function () { const valid = ajv.validate(schema, res.getBody()); expect(valid).to.be.true; }); diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Invalid Database Template.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Invalid Database Template.bru similarity index 55% rename from Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Invalid Database Template.bru rename to Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Invalid Database Template.bru index ec07a06a5..72339dda7 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Invalid Database Template.bru +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Invalid Database Template.bru @@ -1,11 +1,11 @@ meta { - name: DbInstances - Invalid Database Template + name: OdsInstances Manage - Invalid Database Template type: http seq: 3 } post { - url: {{API_URL}}/v2/dbinstances + url: {{API_URL}}/v2/odsinstances/manage body: json auth: inherit } @@ -18,22 +18,22 @@ body:json { } script:post-response { - test("POST DbInstances Invalid Template: Status code is Bad Request", function () { + test("POST OdsInstances Manage Invalid Template: Status code is Bad Request", function () { expect(res.getStatus()).to.equal(400); }); const response = res.getBody(); - test("POST DbInstances Invalid Template: Response matches error format", function () { + test("POST OdsInstances Manage Invalid Template: Response matches error format", function () { expect(response).to.have.property("title"); expect(response).to.have.property("errors"); }); - test("POST DbInstances Invalid Template: Response errors include message for DatabaseTemplate", function () { + test("POST OdsInstances Manage Invalid Template: Response errors include message for DatabaseTemplate", function () { expect(response.errors["DatabaseTemplate"].length).to.be.greaterThan(0); }); - test("POST DbInstances Invalid Template: Error message mentions allowed values", function () { + test("POST OdsInstances Manage Invalid Template: Error message mentions allowed values", function () { const errorMessage = response.errors["DatabaseTemplate"][0].toLowerCase(); expect(errorMessage).to.match(/minimal|sample/); }); diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Invalid.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Invalid.bru similarity index 57% rename from Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Invalid.bru rename to Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Invalid.bru index 83ef05377..0b8e82d2e 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Invalid.bru +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Invalid.bru @@ -1,11 +1,11 @@ meta { - name: DbInstances - Invalid + name: OdsInstances Manage - Invalid type: http seq: 2 } post { - url: {{API_URL}}/v2/dbinstances + url: {{API_URL}}/v2/odsinstances/manage body: json auth: inherit } @@ -18,22 +18,22 @@ body:json { } script:post-response { - test("POST DbInstances Invalid: Status code is Bad Request", function () { + test("POST OdsInstances Manage Invalid: Status code is Bad Request", function () { expect(res.getStatus()).to.equal(400); }); const response = res.getBody(); - test("POST DbInstances Invalid: Response matches error format", function () { + test("POST OdsInstances Manage Invalid: Response matches error format", function () { expect(response).to.have.property("title"); expect(response).to.have.property("errors"); }); - test("POST DbInstances Invalid: Response title is helpful and accurate", function () { + test("POST OdsInstances Manage Invalid: Response title is helpful and accurate", function () { expect(response.title.toLowerCase()).to.contain("validation"); }); - test("POST DbInstances Invalid: Response errors include messages by property", function () { + test("POST OdsInstances Manage Invalid: Response errors include messages by property", function () { expect(response.errors["Name"].length).to.be.greaterThan(0); expect(response.errors["DatabaseTemplate"].length).to.be.greaterThan(0); }); diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Sample Template.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Sample Template.bru new file mode 100644 index 000000000..33eb80552 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Sample Template.bru @@ -0,0 +1,52 @@ +meta { + name: OdsInstances Manage - Sample Template + type: http + seq: 4 +} + +script:pre-request { + const sampleOdsInstanceManageName = `Test DB Instance Sample ${Date.now()}`; + bru.setVar("SampleOdsInstanceManageName", sampleOdsInstanceManageName); + console.log(`Using unique Sample OdsInstance Manage name: ${sampleOdsInstanceManageName}`); +} + +post { + url: {{API_URL}}/v2/odsinstances/manage + body: json + auth: inherit +} + +body:json { + { + "name": "{{SampleOdsInstanceManageName}}", + "databaseTemplate": "Sample" + } +} + +script:post-response { + const expectedStatus = 202; + const responseStatus = res.getStatus(); + + if (responseStatus !== expectedStatus) { + console.log("❌ POST OdsInstances Manage Sample request failed"); + console.log("Status:", responseStatus); + console.log("Response body:", JSON.stringify(res.getBody(), null, 2)); + } + + test("POST OdsInstances Manage Sample: Status code is Accepted", function () { + expect(responseStatus).to.equal(expectedStatus); + }); + + test("POST OdsInstances Manage Sample: Response includes location in header", function () { + expect(res.getHeaders()).to.have.property("location"); + const id = res.getHeader("location").split("/").pop(); + if (id) { + bru.setVar("CreatedOdsInstanceManageIdSample", id); + } + }); +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage.bru similarity index 59% rename from Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances.bru rename to Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage.bru index ec87c729d..89f3b35f1 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances.bru +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage.bru @@ -1,11 +1,11 @@ meta { - name: DbInstances + name: OdsInstances Manage type: http seq: 1 } post { - url: {{API_URL}}/v2/dbinstances + url: {{API_URL}}/v2/odsinstances/manage body: json auth: inherit } @@ -18,15 +18,15 @@ body:json { } script:post-response { - test("POST DbInstances: Status code is Accepted", function () { + test("POST OdsInstances Manage: Status code is Accepted", function () { expect(res.getStatus()).to.equal(202); }); - test("POST DbInstances: Response includes location in header", function () { + test("POST OdsInstances Manage: Response includes location in header", function () { expect(res.getHeaders()).to.have.property("location"); const id = res.getHeader("location").split("/")[2]; if (id) { - bru.setVar("CreatedDbInstanceId", id); + bru.setVar("CreatedOdsInstanceManageId", id); } }); } diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/folder.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/folder.bru similarity index 69% rename from Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/folder.bru rename to Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/folder.bru index f9ebbdd37..cffed6757 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/folder.bru +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/folder.bru @@ -1,5 +1,5 @@ meta { - name: DbInstances + name: Manage seq: 99 } From 76cc11a4d51f2b5e0f00f750b7154644431b6fd2 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 22:08:44 -0600 Subject: [PATCH 16/35] Rename v3 Infrastructure Queries/Commands/Jobs to DataStoreManage naming Renames v3's own Infrastructure layer (Queries, Commands, dispatcher Jobs) from DbDataStore-flavored names to DataStoreManage-flavored names, mirroring Task 3's v2 work. v3 uses DataStoreManage as its feature-layer naming convention while consuming the same shared OdsInstanceManage entity from Task 2. Co-Authored-By: Claude Sonnet 5 --- ...ommand.cs => AddDataStoreManageCommand.cs} | 19 ++-- ...and.cs => DeleteDataStoreManageCommand.cs} | 23 ++--- ...uery.cs => GetDataStoreManageByIdQuery.cs} | 15 ++- ...esQuery.cs => GetDataStoreManagesQuery.cs} | 15 ++- .../Services/Jobs/CreateInstanceJob.cs | 96 +++++++++---------- ...tePendingDataStoreManagesDispatcherJob.cs} | 44 ++++----- .../Services/Jobs/DeleteInstanceJob.cs | 58 +++++------ ...tePendingDataStoreManagesDispatcherJob.cs} | 46 ++++----- 8 files changed, 152 insertions(+), 164 deletions(-) rename Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/{AddDbDataStoreCommand.cs => AddDataStoreManageCommand.cs} (74%) rename Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/{DeleteDbDataStoreCommand.cs => DeleteDataStoreManageCommand.cs} (50%) rename Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/{GetDbDataStoreByIdQuery.cs => GetDataStoreManageByIdQuery.cs} (59%) rename Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/{GetDbDataStoresQuery.cs => GetDataStoreManagesQuery.cs} (69%) rename Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/{CreatePendingDbInstancesDispatcherJob.cs => CreatePendingDataStoreManagesDispatcherJob.cs} (69%) rename Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/{DeletePendingDbInstancesDispatcherJob.cs => DeletePendingDataStoreManagesDispatcherJob.cs} (68%) diff --git a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/AddDbDataStoreCommand.cs b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/AddDataStoreManageCommand.cs similarity index 74% rename from Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/AddDbDataStoreCommand.cs rename to Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/AddDataStoreManageCommand.cs index f7775d8ec..f6d31d74d 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/AddDbDataStoreCommand.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/AddDataStoreManageCommand.cs @@ -8,16 +8,16 @@ namespace EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; -public class AddDbDataStoreCommand +public class AddDataStoreManageCommand { private readonly AdminApiDbContext _context; - public AddDbDataStoreCommand(AdminApiDbContext context) + public AddDataStoreManageCommand(AdminApiDbContext context) { _context = context; } - public DbInstance Execute(IAddDbDataStoreModel model) + public OdsInstanceManage Execute(IAddDataStoreManageModel model) { if (string.IsNullOrWhiteSpace(model.Name)) throw new ArgumentException("Name is required.", nameof(model)); @@ -26,11 +26,11 @@ public DbInstance Execute(IAddDbDataStoreModel model) var now = DateTime.UtcNow; - var dbInstance = new DbInstance + var odsInstanceManage = new OdsInstanceManage { Name = model.Name.Trim(), DatabaseTemplate = model.DatabaseTemplate.Trim(), - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), OdsInstanceId = null, OdsInstanceName = null, DatabaseName = null, @@ -38,17 +38,14 @@ public DbInstance Execute(IAddDbDataStoreModel model) LastModifiedDate = now }; - _context.DbInstances.Add(dbInstance); + _context.OdsInstanceManages.Add(odsInstanceManage); _context.SaveChanges(); - return dbInstance; + return odsInstanceManage; } } -public interface IAddDbDataStoreModel +public interface IAddDataStoreManageModel { string? Name { get; } string? DatabaseTemplate { get; } } - - - diff --git a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/DeleteDbDataStoreCommand.cs b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/DeleteDataStoreManageCommand.cs similarity index 50% rename from Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/DeleteDbDataStoreCommand.cs rename to Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/DeleteDataStoreManageCommand.cs index 11c20b67b..ca467b7fa 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/DeleteDbDataStoreCommand.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/DeleteDataStoreManageCommand.cs @@ -8,35 +8,32 @@ namespace EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; -public interface IDeleteDbDataStoreCommand +public interface IDeleteDataStoreManageCommand { void Execute(int id); } -public class DeleteDbDataStoreCommand : IDeleteDbDataStoreCommand +public class DeleteDataStoreManageCommand : IDeleteDataStoreManageCommand { private readonly AdminApiDbContext _context; - public DeleteDbDataStoreCommand(AdminApiDbContext context) + public DeleteDataStoreManageCommand(AdminApiDbContext context) { _context = context; } public void Execute(int id) { - var dbInstance = - _context.DbInstances.Find(id) - ?? throw new NotFoundException("dbInstance", id); + var odsInstanceManage = + _context.OdsInstanceManages.Find(id) + ?? throw new NotFoundException("dataStoreManage", id); - if (dbInstance.Status == DbInstanceStatus.Deleted.ToString()) - throw new NotFoundException("dbInstance", id); + if (odsInstanceManage.Status == OdsInstanceManageStatus.Deleted.ToString()) + throw new NotFoundException("dataStoreManage", id); - dbInstance.Status = DbInstanceStatus.PendingDelete.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.PendingDelete.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; _context.SaveChanges(); } } - - - diff --git a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDbDataStoreByIdQuery.cs b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManageByIdQuery.cs similarity index 59% rename from Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDbDataStoreByIdQuery.cs rename to Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManageByIdQuery.cs index bf6c19347..b79991b89 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDbDataStoreByIdQuery.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManageByIdQuery.cs @@ -7,25 +7,22 @@ namespace EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; -public interface IGetDbDataStoreByIdQuery +public interface IGetDataStoreManageByIdQuery { - DbInstance? Execute(int id); + OdsInstanceManage? Execute(int id); } -public class GetDbDataStoreByIdQuery : IGetDbDataStoreByIdQuery +public class GetDataStoreManageByIdQuery : IGetDataStoreManageByIdQuery { private readonly AdminApiDbContext _context; - public GetDbDataStoreByIdQuery(AdminApiDbContext context) + public GetDataStoreManageByIdQuery(AdminApiDbContext context) { _context = context; } - public DbInstance? Execute(int id) + public OdsInstanceManage? Execute(int id) { - return _context.DbInstances.SingleOrDefault(d => d.Id == id); + return _context.OdsInstanceManages.SingleOrDefault(d => d.Id == id); } } - - - diff --git a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDbDataStoresQuery.cs b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManagesQuery.cs similarity index 69% rename from Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDbDataStoresQuery.cs rename to Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManagesQuery.cs index 14d16bda7..c5af51bc2 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDbDataStoresQuery.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManagesQuery.cs @@ -11,25 +11,25 @@ namespace EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; -public interface IGetDbDataStoresQuery +public interface IGetDataStoreManagesQuery { - List Execute(CommonQueryParams commonQueryParams, int? id, string? name); + List Execute(CommonQueryParams commonQueryParams, int? id, string? name); } -public class GetDbDataStoresQuery : IGetDbDataStoresQuery +public class GetDataStoreManagesQuery : IGetDataStoreManagesQuery { private readonly AdminApiDbContext _context; private readonly IOptions _options; - public GetDbDataStoresQuery(AdminApiDbContext context, IOptions options) + public GetDataStoreManagesQuery(AdminApiDbContext context, IOptions options) { _context = context; _options = options; } - public List Execute(CommonQueryParams commonQueryParams, int? id, string? name) + public List Execute(CommonQueryParams commonQueryParams, int? id, string? name) { - return _context.DbInstances + return _context.OdsInstanceManages .Where(d => id == null || d.Id == id) .Where(d => name == null || d.Name == name) .OrderBy(d => d.Id) @@ -37,6 +37,3 @@ public List Execute(CommonQueryParams commonQueryParams, int? id, st .ToList(); } } - - - diff --git a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreateInstanceJob.cs b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreateInstanceJob.cs index 34ad63e9d..98b42fa77 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreateInstanceJob.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreateInstanceJob.cs @@ -6,7 +6,7 @@ using EdFi.Admin.DataAccess.Contexts; using EdFi.Admin.DataAccess.Models; using EdFi.Ods.AdminApi.Common.Constants; -using EdFi.Ods.AdminApi.V3.Features.DbDataStores; +using EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; using EdFi.Ods.AdminApi.Common.Infrastructure.Context; using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; @@ -52,22 +52,22 @@ public class CreateInstanceJob( private readonly IConfiguration _configuration = configuration; private readonly IDbConnectionStringBuilderAdapterFactory _connectionStringBuilderAdapterFactory = connectionStringBuilderAdapterFactory; - internal static JobKey CreateJobKey(int dbInstanceId, string? tenantName) - => new(BuildJobIdentity(dbInstanceId, tenantName)); + internal static JobKey CreateJobKey(int odsInstanceManageId, string? tenantName) + => new(BuildJobIdentity(odsInstanceManageId, tenantName)); - internal static string BuildJobIdentity(int dbInstanceId, string? tenantName) + internal static string BuildJobIdentity(int odsInstanceManageId, string? tenantName) => string.IsNullOrWhiteSpace(tenantName) - ? $"{JobConstants.CreateInstanceJobName}-{dbInstanceId}" - : $"{JobConstants.CreateInstanceJobName}-{tenantName}-{dbInstanceId}"; + ? $"{JobConstants.CreateInstanceJobName}-{odsInstanceManageId}" + : $"{JobConstants.CreateInstanceJobName}-{tenantName}-{odsInstanceManageId}"; protected override async Task ExecuteJobAsync(IJobExecutionContext context) { - if (!context.MergedJobDataMap.ContainsKey(JobConstants.DbInstanceIdKey)) + if (!context.MergedJobDataMap.ContainsKey(JobConstants.OdsInstanceManageIdKey)) { - throw new InvalidOperationException($"{JobConstants.DbInstanceIdKey} must be provided for {JobConstants.CreateInstanceJobName}."); + throw new InvalidOperationException($"{JobConstants.OdsInstanceManageIdKey} must be provided for {JobConstants.CreateInstanceJobName}."); } - var dbInstanceId = context.MergedJobDataMap.GetInt(JobConstants.DbInstanceIdKey); + var odsInstanceManageId = context.MergedJobDataMap.GetInt(JobConstants.OdsInstanceManageIdKey); var multiTenancyEnabled = _options.Value.MultiTenancy; var tenantName = GetTenantName(context, multiTenancyEnabled); @@ -78,7 +78,7 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) TenantConfiguration? tenantConfiguration = null; var adminApiDbContext = _dbContext; var resolvedUsersContext = _usersContext; - DbInstance? dbInstance = null; + OdsInstanceManage? odsInstanceManage = null; try { @@ -95,7 +95,7 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) // on IContextProvider (e.g. ConfigConnectionStringsProvider) resolve // the correct per-tenant connection strings (EdFi_Master, EdFi_Ods, etc.). // The tenant name is always known at this point because it was stored in the job data map - // when CreatePendingDbInstancesDispatcherJob scheduled this job. + // when CreatePendingDataStoreManagesDispatcherJob scheduled this job. _tenantConfigurationContextProvider.Set(tenantConfiguration); tenantAdminApiDbContext = _tenantSpecificDbContextProvider.GetAdminApiDbContext(tenantName!); tenantUsersContext = _tenantSpecificDbContextProvider.GetUsersContext(tenantName!); @@ -103,52 +103,52 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) resolvedUsersContext = tenantUsersContext; } - dbInstance = await adminApiDbContext.DbInstances - .FirstOrDefaultAsync(instance => instance.Id == dbInstanceId); + odsInstanceManage = await adminApiDbContext.OdsInstanceManages + .FirstOrDefaultAsync(instance => instance.Id == odsInstanceManageId); - if (dbInstance is null) + if (odsInstanceManage is null) { - throw new InvalidOperationException($"DbInstance '{dbInstanceId}' was not found."); + throw new InvalidOperationException($"OdsInstanceManage '{odsInstanceManageId}' was not found."); } - if (!IsEligibleForProcessing(dbInstance)) + if (!IsEligibleForProcessing(odsInstanceManage)) { return; } - ValidatePendingState(dbInstance); + ValidatePendingState(odsInstanceManage); - var finalName = dbInstance.Name; + var finalName = odsInstanceManage.Name; ValidateFinalName(finalName); var existingDataStore = await GetExistingDataStoreByNameAsync(resolvedUsersContext, finalName); var now = DateTime.UtcNow; - dbInstance.Status = DbInstanceStatus.CreateInProgress.ToString(); - if (string.IsNullOrWhiteSpace(dbInstance.DatabaseName)) + odsInstanceManage.Status = OdsInstanceManageStatus.CreateInProgress.ToString(); + if (string.IsNullOrWhiteSpace(odsInstanceManage.DatabaseName)) { - dbInstance.DatabaseName = DbDataStoreDatabaseNameFormatter.Build( - dbInstance.Name, - dbInstance.DatabaseTemplate); + odsInstanceManage.DatabaseName = DataStoreManageDatabaseNameFormatter.Build( + odsInstanceManage.Name, + odsInstanceManage.DatabaseTemplate); } - dbInstance.LastModifiedDate = now; - dbInstance.LastRefreshed = now; + odsInstanceManage.LastModifiedDate = now; + odsInstanceManage.LastRefreshed = now; await adminApiDbContext.SaveChangesAsync(); await _sandboxProvisioner.AddSandboxAsync( - dbInstance.DatabaseName, - GetSandboxType(dbInstance.DatabaseTemplate)); + odsInstanceManage.DatabaseName, + GetSandboxType(odsInstanceManage.DatabaseTemplate)); - var encryptedConnectionString = BuildEncryptedConnectionString(dbInstance.DatabaseName, tenantName); + var encryptedConnectionString = BuildEncryptedConnectionString(odsInstanceManage.DatabaseName, tenantName); var dataStore = existingDataStore ?? new OdsInstance { Name = finalName, - InstanceType = dbInstance.DatabaseTemplate, + InstanceType = odsInstanceManage.DatabaseTemplate, ConnectionString = encryptedConnectionString }; - dataStore.InstanceType = dbInstance.DatabaseTemplate; + dataStore.InstanceType = odsInstanceManage.DatabaseTemplate; dataStore.ConnectionString = encryptedConnectionString; if (existingDataStore is null) @@ -158,21 +158,21 @@ await _sandboxProvisioner.AddSandboxAsync( await resolvedUsersContext.SaveChangesAsync(CancellationToken.None); - dbInstance.OdsInstanceId = dataStore.OdsInstanceId; - dbInstance.OdsInstanceName = finalName; - dbInstance.Status = DbInstanceStatus.Created.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.OdsInstanceId = dataStore.OdsInstanceId; + odsInstanceManage.OdsInstanceName = finalName; + odsInstanceManage.Status = OdsInstanceManageStatus.Created.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); } catch { - if (dbInstance is not null) + if (odsInstanceManage is not null) { - dbInstance.Status = DbInstanceStatus.CreateFailed.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.CreateFailed.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); } @@ -260,29 +260,29 @@ private static SandboxType GetSandboxType(string databaseTemplate) $"DatabaseTemplate '{databaseTemplate}' cannot be mapped to {nameof(SandboxType)}."); } - private static bool IsEligibleForProcessing(DbInstance dbInstance) + private static bool IsEligibleForProcessing(OdsInstanceManage odsInstanceManage) { - if (!Enum.TryParse(dbInstance.Status, ignoreCase: true, out var status)) + if (!Enum.TryParse(odsInstanceManage.Status, ignoreCase: true, out var status)) { throw new InvalidOperationException( - $"DbInstance '{dbInstance.Id}' has unsupported status '{dbInstance.Status}'."); + $"OdsInstanceManage '{odsInstanceManage.Id}' has unsupported status '{odsInstanceManage.Status}'."); } - return status == DbInstanceStatus.PendingCreate; + return status == OdsInstanceManageStatus.PendingCreate; } - private static void ValidatePendingState(DbInstance dbInstance) + private static void ValidatePendingState(OdsInstanceManage odsInstanceManage) { - if (dbInstance.OdsInstanceId.HasValue || !string.IsNullOrWhiteSpace(dbInstance.OdsInstanceName)) + if (odsInstanceManage.OdsInstanceId.HasValue || !string.IsNullOrWhiteSpace(odsInstanceManage.OdsInstanceName)) { throw new InvalidOperationException( - $"DbInstance '{dbInstance.Id}' is in an invalid pending state because ODS references already exist."); + $"OdsInstanceManage '{odsInstanceManage.Id}' is in an invalid pending state because ODS references already exist."); } - if (string.IsNullOrWhiteSpace(dbInstance.DatabaseTemplate)) + if (string.IsNullOrWhiteSpace(odsInstanceManage.DatabaseTemplate)) { throw new InvalidOperationException( - $"DbInstance '{dbInstance.Id}' is missing DatabaseTemplate."); + $"OdsInstanceManage '{odsInstanceManage.Id}' is missing DatabaseTemplate."); } } diff --git a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJob.cs b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreatePendingDataStoreManagesDispatcherJob.cs similarity index 69% rename from Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJob.cs rename to Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreatePendingDataStoreManagesDispatcherJob.cs index 60ea2f8c2..8ef31ebe8 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJob.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreatePendingDataStoreManagesDispatcherJob.cs @@ -16,8 +16,8 @@ namespace EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs; [DisallowConcurrentExecution] -public class CreatePendingDbInstancesDispatcherJob( - ILogger logger, +public class CreatePendingDataStoreManagesDispatcherJob( + ILogger logger, IJobStatusService jobStatusService, AdminApiDbContext dbContext, ITenantSpecificDbContextProvider tenantSpecificDbContextProvider, @@ -45,34 +45,34 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) adminApiDbContext = tenantAdminApiDbContext; } - var eligibleDbInstances = await adminApiDbContext.DbInstances - .Where(instance => instance.Status == DbInstanceStatus.PendingCreate.ToString() || instance.Status == DbInstanceStatus.CreateFailed.ToString()) + var eligibleOdsInstanceManages = await adminApiDbContext.OdsInstanceManages + .Where(instance => instance.Status == OdsInstanceManageStatus.PendingCreate.ToString() || instance.Status == OdsInstanceManageStatus.CreateFailed.ToString()) .OrderBy(instance => instance.Id) .ToListAsync(); - foreach (var dbInstance in eligibleDbInstances) + foreach (var odsInstanceManage in eligibleOdsInstanceManages) { - if (string.Equals(dbInstance.Status, DbInstanceStatus.PendingCreate.ToString(), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(odsInstanceManage.Status, OdsInstanceManageStatus.PendingCreate.ToString(), StringComparison.OrdinalIgnoreCase)) { - await ScheduleCreateJobAsync(context, dbInstance.Id, tenantName); + await ScheduleCreateJobAsync(context, odsInstanceManage.Id, tenantName); continue; } - if (!await IsRetryEligibleAsync(adminApiDbContext, dbInstance, tenantName)) + if (!await IsRetryEligibleAsync(adminApiDbContext, odsInstanceManage, tenantName)) { - dbInstance.Status = DbInstanceStatus.CreateError.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.CreateError.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); continue; } - dbInstance.Status = DbInstanceStatus.PendingCreate.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.PendingCreate.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); - await ScheduleCreateJobAsync(context, dbInstance.Id, tenantName); + await ScheduleCreateJobAsync(context, odsInstanceManage.Id, tenantName); } } finally @@ -84,24 +84,24 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) } } - private async Task IsRetryEligibleAsync(AdminApiDbContext adminApiDbContext, DbInstance dbInstance, string? tenantName) + private async Task IsRetryEligibleAsync(AdminApiDbContext adminApiDbContext, OdsInstanceManage odsInstanceManage, string? tenantName) { - var maxRetryAttempts = _options.Value.CreateDbInstancesMaxRetryAttempts > 0 - ? _options.Value.CreateDbInstancesMaxRetryAttempts + var maxRetryAttempts = _options.Value.CreateOdsInstanceManagesMaxRetryAttempts > 0 + ? _options.Value.CreateOdsInstanceManagesMaxRetryAttempts : DefaultMaxRetryAttempts; - var jobIdPrefix = $"{CreateInstanceJob.BuildJobIdentity(dbInstance.Id, tenantName)}_"; + var jobIdPrefix = $"{CreateInstanceJob.BuildJobIdentity(odsInstanceManage.Id, tenantName)}_"; var errorCount = await adminApiDbContext.JobStatuses .CountAsync(status => status.JobId.StartsWith(jobIdPrefix) && status.Status == QuartzJobStatus.Error.ToString()); return errorCount < maxRetryAttempts; } - private static async Task ScheduleCreateJobAsync(IJobExecutionContext context, int dbInstanceId, string? tenantName) + private static async Task ScheduleCreateJobAsync(IJobExecutionContext context, int odsInstanceManageId, string? tenantName) { var jobData = new Dictionary { - [JobConstants.DbInstanceIdKey] = dbInstanceId + [JobConstants.OdsInstanceManageIdKey] = odsInstanceManageId }; if (!string.IsNullOrWhiteSpace(tenantName)) @@ -111,7 +111,7 @@ private static async Task ScheduleCreateJobAsync(IJobExecutionContext context, i await QuartzJobScheduler.ScheduleJob( context.Scheduler, - CreateInstanceJob.CreateJobKey(dbInstanceId, tenantName), + CreateInstanceJob.CreateJobKey(odsInstanceManageId, tenantName), jobData, startImmediately: true); } diff --git a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeleteInstanceJob.cs b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeleteInstanceJob.cs index f53bbeba4..5110945c5 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeleteInstanceJob.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeleteInstanceJob.cs @@ -39,22 +39,22 @@ public class DeleteInstanceJob( private readonly ISandboxProvisioner _sandboxProvisioner = sandboxProvisioner; private readonly IOptions _options = options; - internal static JobKey CreateJobKey(int dbInstanceId, string? tenantName) - => new(BuildJobIdentity(dbInstanceId, tenantName)); + internal static JobKey CreateJobKey(int odsInstanceManageId, string? tenantName) + => new(BuildJobIdentity(odsInstanceManageId, tenantName)); - internal static string BuildJobIdentity(int dbInstanceId, string? tenantName) + internal static string BuildJobIdentity(int odsInstanceManageId, string? tenantName) => string.IsNullOrWhiteSpace(tenantName) - ? $"{JobConstants.DeleteInstanceJobName}-{dbInstanceId}" - : $"{JobConstants.DeleteInstanceJobName}-{tenantName}-{dbInstanceId}"; + ? $"{JobConstants.DeleteInstanceJobName}-{odsInstanceManageId}" + : $"{JobConstants.DeleteInstanceJobName}-{tenantName}-{odsInstanceManageId}"; protected override async Task ExecuteJobAsync(IJobExecutionContext context) { - if (!context.MergedJobDataMap.ContainsKey(JobConstants.DbInstanceIdKey)) + if (!context.MergedJobDataMap.ContainsKey(JobConstants.OdsInstanceManageIdKey)) { - throw new InvalidOperationException($"{JobConstants.DbInstanceIdKey} must be provided for {JobConstants.DeleteInstanceJobName}."); + throw new InvalidOperationException($"{JobConstants.OdsInstanceManageIdKey} must be provided for {JobConstants.DeleteInstanceJobName}."); } - var dbInstanceId = context.MergedJobDataMap.GetInt(JobConstants.DbInstanceIdKey); + var odsInstanceManageId = context.MergedJobDataMap.GetInt(JobConstants.OdsInstanceManageIdKey); var multiTenancyEnabled = _options.Value.MultiTenancy; var tenantName = GetTenantName(context, multiTenancyEnabled); @@ -62,7 +62,7 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) IUsersContext? tenantUsersContext = null; var adminApiDbContext = _dbContext; var resolvedUsersContext = _usersContext; - DbInstance? dbInstance = null; + OdsInstanceManage? odsInstanceManage = null; try { @@ -81,35 +81,35 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) resolvedUsersContext = tenantUsersContext; } - dbInstance = await adminApiDbContext.DbInstances - .FirstOrDefaultAsync(instance => instance.Id == dbInstanceId); + odsInstanceManage = await adminApiDbContext.OdsInstanceManages + .FirstOrDefaultAsync(instance => instance.Id == odsInstanceManageId); - if (dbInstance is null) + if (odsInstanceManage is null) { - throw new InvalidOperationException($"DbInstance '{dbInstanceId}' was not found."); + throw new InvalidOperationException($"OdsInstanceManage '{odsInstanceManageId}' was not found."); } // Guard against race conditions — only process PendingDelete rows. - if (!Enum.TryParse(dbInstance.Status, ignoreCase: true, out var status) - || status != DbInstanceStatus.PendingDelete) + if (!Enum.TryParse(odsInstanceManage.Status, ignoreCase: true, out var status) + || status != OdsInstanceManageStatus.PendingDelete) { return; } - dbInstance.Status = DbInstanceStatus.DeleteInProgress.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.DeleteInProgress.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); - if (!string.IsNullOrWhiteSpace(dbInstance.DatabaseName)) + if (!string.IsNullOrWhiteSpace(odsInstanceManage.DatabaseName)) { - await _sandboxProvisioner.DeleteSandboxesAsync(dbInstance.DatabaseName); + await _sandboxProvisioner.DeleteSandboxesAsync(odsInstanceManage.DatabaseName); } - if (dbInstance.OdsInstanceId.HasValue) + if (odsInstanceManage.OdsInstanceId.HasValue) { var dataStore = await resolvedUsersContext.OdsInstances - .FindAsync(dbInstance.OdsInstanceId.Value); + .FindAsync(odsInstanceManage.OdsInstanceId.Value); if (dataStore is not null) { @@ -118,18 +118,18 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) } } - dbInstance.Status = DbInstanceStatus.Deleted.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.Deleted.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); } catch { - if (dbInstance is not null) + if (odsInstanceManage is not null) { - dbInstance.Status = DbInstanceStatus.DeleteFailed.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.DeleteFailed.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); } diff --git a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJob.cs b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeletePendingDataStoreManagesDispatcherJob.cs similarity index 68% rename from Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJob.cs rename to Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeletePendingDataStoreManagesDispatcherJob.cs index 9c986d861..066288e56 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJob.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeletePendingDataStoreManagesDispatcherJob.cs @@ -16,8 +16,8 @@ namespace EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs; [DisallowConcurrentExecution] -public class DeletePendingDbInstancesDispatcherJob( - ILogger logger, +public class DeletePendingDataStoreManagesDispatcherJob( + ILogger logger, IJobStatusService jobStatusService, AdminApiDbContext dbContext, ITenantSpecificDbContextProvider tenantSpecificDbContextProvider, @@ -45,35 +45,35 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) adminApiDbContext = tenantAdminApiDbContext; } - var eligibleDbInstances = await adminApiDbContext.DbInstances - .Where(instance => instance.Status == DbInstanceStatus.PendingDelete.ToString() - || instance.Status == DbInstanceStatus.DeleteFailed.ToString()) + var eligibleOdsInstanceManages = await adminApiDbContext.OdsInstanceManages + .Where(instance => instance.Status == OdsInstanceManageStatus.PendingDelete.ToString() + || instance.Status == OdsInstanceManageStatus.DeleteFailed.ToString()) .OrderBy(instance => instance.Id) .ToListAsync(); - foreach (var dbInstance in eligibleDbInstances) + foreach (var odsInstanceManage in eligibleOdsInstanceManages) { - if (string.Equals(dbInstance.Status, DbInstanceStatus.PendingDelete.ToString(), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(odsInstanceManage.Status, OdsInstanceManageStatus.PendingDelete.ToString(), StringComparison.OrdinalIgnoreCase)) { - await ScheduleDeleteJobAsync(context, dbInstance.Id, tenantName); + await ScheduleDeleteJobAsync(context, odsInstanceManage.Id, tenantName); continue; } - if (!await IsRetryEligibleAsync(adminApiDbContext, dbInstance, tenantName)) + if (!await IsRetryEligibleAsync(adminApiDbContext, odsInstanceManage, tenantName)) { - dbInstance.Status = DbInstanceStatus.DeleteError.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.DeleteError.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); continue; } - dbInstance.Status = DbInstanceStatus.PendingDelete.ToString(); - dbInstance.LastModifiedDate = DateTime.UtcNow; - dbInstance.LastRefreshed = DateTime.UtcNow; + odsInstanceManage.Status = OdsInstanceManageStatus.PendingDelete.ToString(); + odsInstanceManage.LastModifiedDate = DateTime.UtcNow; + odsInstanceManage.LastRefreshed = DateTime.UtcNow; await adminApiDbContext.SaveChangesAsync(); - await ScheduleDeleteJobAsync(context, dbInstance.Id, tenantName); + await ScheduleDeleteJobAsync(context, odsInstanceManage.Id, tenantName); } } finally @@ -85,24 +85,24 @@ protected override async Task ExecuteJobAsync(IJobExecutionContext context) } } - private async Task IsRetryEligibleAsync(AdminApiDbContext adminApiDbContext, DbInstance dbInstance, string? tenantName) + private async Task IsRetryEligibleAsync(AdminApiDbContext adminApiDbContext, OdsInstanceManage odsInstanceManage, string? tenantName) { - var maxRetryAttempts = _options.Value.DeleteDbInstancesMaxRetryAttempts > 0 - ? _options.Value.DeleteDbInstancesMaxRetryAttempts + var maxRetryAttempts = _options.Value.DeleteOdsInstanceManagesMaxRetryAttempts > 0 + ? _options.Value.DeleteOdsInstanceManagesMaxRetryAttempts : DefaultMaxRetryAttempts; - var jobIdPrefix = $"{DeleteInstanceJob.BuildJobIdentity(dbInstance.Id, tenantName)}_"; + var jobIdPrefix = $"{DeleteInstanceJob.BuildJobIdentity(odsInstanceManage.Id, tenantName)}_"; var errorCount = await adminApiDbContext.JobStatuses .CountAsync(status => status.JobId.StartsWith(jobIdPrefix) && status.Status == QuartzJobStatus.Error.ToString()); return errorCount < maxRetryAttempts; } - private static async Task ScheduleDeleteJobAsync(IJobExecutionContext context, int dbInstanceId, string? tenantName) + private static async Task ScheduleDeleteJobAsync(IJobExecutionContext context, int odsInstanceManageId, string? tenantName) { var jobData = new Dictionary { - [JobConstants.DbInstanceIdKey] = dbInstanceId + [JobConstants.OdsInstanceManageIdKey] = odsInstanceManageId }; if (!string.IsNullOrWhiteSpace(tenantName)) @@ -112,7 +112,7 @@ private static async Task ScheduleDeleteJobAsync(IJobExecutionContext context, i await QuartzJobScheduler.ScheduleJob( context.Scheduler, - DeleteInstanceJob.CreateJobKey(dbInstanceId, tenantName), + DeleteInstanceJob.CreateJobKey(odsInstanceManageId, tenantName), jobData, startImmediately: true); } From 13e655344fe7a2d5890a6261879ee24f78555852 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 22:17:43 -0600 Subject: [PATCH 17/35] Move v3 DbDataStores feature into DataStores/Manage, rename routes to /dataStores/manage Renames Features/DbDataStores (6 files) into a new Features/DataStores/Manage subfolder, updating class/interface names, namespace, and route paths from /dbDataStores to /dataStores/manage. Wires up to the OdsInstanceManage model and Task 10's Add/Get/Delete command and query interfaces. --- .../Manage/AddDataStoreManage.cs} | 56 +++++++++---------- .../DataStoreManageDatabaseNameFormatter.cs} | 4 +- .../Manage/DataStoreManageMapper.cs} | 12 ++-- .../Manage/DataStoreManageModel.cs} | 7 +-- .../Manage/DeleteDataStoreManage.cs} | 46 +++++++-------- .../Manage/ReadDataStoreManage.cs} | 29 +++++----- 6 files changed, 74 insertions(+), 80 deletions(-) rename Application/EdFi.Ods.AdminApi.V3/Features/{DbDataStores/AddDbDataStore.cs => DataStores/Manage/AddDataStoreManage.cs} (69%) rename Application/EdFi.Ods.AdminApi.V3/Features/{DbDataStores/DbDataStoreDatabaseNameFormatter.cs => DataStores/Manage/DataStoreManageDatabaseNameFormatter.cs} (93%) rename Application/EdFi.Ods.AdminApi.V3/Features/{DbDataStores/DbDataStoreMapper.cs => DataStores/Manage/DataStoreManageMapper.cs} (72%) rename Application/EdFi.Ods.AdminApi.V3/Features/{DbDataStores/DbDataStoreModel.cs => DataStores/Manage/DataStoreManageModel.cs} (84%) rename Application/EdFi.Ods.AdminApi.V3/Features/{DbDataStores/DeleteDbDataStore.cs => DataStores/Manage/DeleteDataStoreManage.cs} (56%) rename Application/EdFi.Ods.AdminApi.V3/Features/{DbDataStores/ReadDbDataStore.cs => DataStores/Manage/ReadDataStoreManage.cs} (50%) diff --git a/Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/AddDbDataStore.cs b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/AddDataStoreManage.cs similarity index 69% rename from Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/AddDbDataStore.cs rename to Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/AddDataStoreManage.cs index eb7b96171..4b3ebc8f6 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/AddDbDataStore.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/AddDataStoreManage.cs @@ -24,20 +24,20 @@ using Swashbuckle.AspNetCore.Annotations; using EdFi.Ods.AdminApi.V3.Infrastructure; -namespace EdFi.Ods.AdminApi.V3.Features.DbDataStores; +namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; -public class AddDbDataStore : IFeature +public class AddDataStoreManage : IFeature { private const int MaxSynchronizedNameLength = 100; - private const int MaxDbDataStoreNameLength = MaxSynchronizedNameLength; - private static readonly Regex _validDbDataStoreNamePattern = new( + private const int MaxDataStoreManageNameLength = MaxSynchronizedNameLength; + private static readonly Regex _validDataStoreManageNamePattern = new( "^[A-Za-z0-9 _]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant); public void MapEndpoints(IEndpointRouteBuilder endpoints) { AdminApiEndpointBuilder - .MapPost(endpoints, "/dbDataStores", Handle) + .MapPost(endpoints, "/dataStores/manage", Handle) .WithDefaultSummaryAndDescription() .WithRouteOptions(b => b.WithResponseCode(202)) .BuildForVersions(AdminApiVersions.V3); @@ -45,16 +45,16 @@ public void MapEndpoints(IEndpointRouteBuilder endpoints) public async static Task Handle( Validator validator, - AddDbDataStoreCommand AddDbDataStoreCommand, + AddDataStoreManageCommand addDataStoreManageCommand, [FromServices] ISchedulerFactory schedulerFactory, [FromServices] IContextProvider tenantConfigurationProvider, [FromServices] IOptions options, - AddDbDataStoreRequest request, + AddDataStoreManageRequest request, HttpContext httpContext) { await validator.GuardAsync(request); - var added = AddDbDataStoreCommand.Execute(request); + var added = addDataStoreManageCommand.Execute(request); var tenantIdentifier = options.Value.MultiTenancy ? tenantConfigurationProvider.Get()?.TenantIdentifier @@ -62,7 +62,7 @@ public async static Task Handle( var jobBuilder = JobBuilder.Create() .WithIdentity(CreateInstanceJob.CreateJobKey(added.Id, tenantIdentifier)) - .UsingJobData(JobConstants.DbInstanceIdKey, added.Id); + .UsingJobData(JobConstants.OdsInstanceManageIdKey, added.Id); if (!string.IsNullOrWhiteSpace(tenantIdentifier)) { @@ -81,17 +81,17 @@ public async static Task Handle( } catch (ObjectAlreadyExistsException) { - // The CreatePendingDbInstancesDispatcherJob may have already scheduled this job + // The CreatePendingDataStoreManagesDispatcherJob may have already scheduled this job // (e.g. it fired between the DB insert and this ScheduleJob call). Treat duplicate - // scheduling as success — the job is already queued and will process the DbInstance. + // scheduling as success — the job is already queued and will process the OdsInstanceManage. } - var absoluteLocation = ResourceUrlHelper.BuildAbsoluteResourceUrl(httpContext, AdminApiMode.V3, $"/dbDataStores/{added.Id}"); + var absoluteLocation = ResourceUrlHelper.BuildAbsoluteResourceUrl(httpContext, AdminApiMode.V3, $"/dataStores/manage/{added.Id}"); return Results.Accepted(absoluteLocation, null); } - [SwaggerSchema(Title = "AddDbDataStoreRequest")] - public class AddDbDataStoreRequest : IAddDbDataStoreModel + [SwaggerSchema(Title = "AddDataStoreManageRequest")] + public class AddDataStoreManageRequest : IAddDataStoreManageModel { [SwaggerSchema(Description = "Name of the DataStore database", Nullable = false)] public string? Name { get; set; } @@ -100,7 +100,7 @@ public class AddDbDataStoreRequest : IAddDbDataStoreModel public string? DatabaseTemplate { get; set; } } - public class Validator : AbstractValidator + public class Validator : AbstractValidator { private static readonly string[] _validDatabaseTemplates = Enum.GetNames(); private readonly AdminApiDbContext _adminApiDbContext; @@ -113,9 +113,9 @@ public Validator(AdminApiDbContext adminApiDbContext, IUsersContext usersContext RuleFor(m => m.Name) .NotEmpty() - .MaximumLength(MaxDbDataStoreNameLength) - .WithMessage($"'{{PropertyName}}' must be {MaxDbDataStoreNameLength} characters or fewer so the synchronized DataStore name fits within {MaxSynchronizedNameLength} characters.") - .Matches(_validDbDataStoreNamePattern) + .MaximumLength(MaxDataStoreManageNameLength) + .WithMessage($"'{{PropertyName}}' must be {MaxDataStoreManageNameLength} characters or fewer so the synchronized DataStore name fits within {MaxSynchronizedNameLength} characters.") + .Matches(_validDataStoreManageNamePattern) .WithMessage("'{PropertyName}' may only contain letters, numbers, spaces, and underscores."); RuleFor(m => m.DatabaseTemplate).NotEmpty().MaximumLength(100) @@ -126,8 +126,8 @@ public Validator(AdminApiDbContext adminApiDbContext, IUsersContext usersContext { if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.DatabaseTemplate) - || request.Name.Length > MaxDbDataStoreNameLength - || !_validDbDataStoreNamePattern.IsMatch(request.Name) + || request.Name.Length > MaxDataStoreManageNameLength + || !_validDataStoreManageNamePattern.IsMatch(request.Name) || !_validDatabaseTemplates.Contains(request.DatabaseTemplate)) { return; @@ -135,29 +135,29 @@ public Validator(AdminApiDbContext adminApiDbContext, IUsersContext usersContext var normalizedName = request.Name.Trim(); - if (await _adminApiDbContext.DbInstances.AnyAsync(instance => instance.Name == normalizedName && instance.Status != DbInstanceStatus.Deleted.ToString(), cancellationToken)) + if (await _adminApiDbContext.OdsInstanceManages.AnyAsync(instance => instance.Name == normalizedName && instance.Status != OdsInstanceManageStatus.Deleted.ToString(), cancellationToken)) { context.AddFailure( - nameof(AddDbDataStoreRequest.Name), - $"A DbDataStore named '{normalizedName}' already exists."); + nameof(AddDataStoreManageRequest.Name), + $"A DataStoreManage named '{normalizedName}' already exists."); return; } if (await _usersContext.OdsInstances.AnyAsync(instance => instance.Name == normalizedName, cancellationToken)) { context.AddFailure( - nameof(AddDbDataStoreRequest.Name), + nameof(AddDataStoreManageRequest.Name), $"A DataStore named '{normalizedName}' already exists."); return; } - var databaseName = DbDataStoreDatabaseNameFormatter.Build(request.Name, request.DatabaseTemplate); + var databaseName = DataStoreManageDatabaseNameFormatter.Build(request.Name, request.DatabaseTemplate); - if (databaseName.Length > DbDataStoreDatabaseNameFormatter.MaxPortableDatabaseNameLength) + if (databaseName.Length > DataStoreManageDatabaseNameFormatter.MaxPortableDatabaseNameLength) { context.AddFailure( - nameof(AddDbDataStoreRequest.Name), - $"The generated database name '{databaseName}' exceeds the portable limit of {DbDataStoreDatabaseNameFormatter.MaxPortableDatabaseNameLength} characters. Shorten Name or DatabaseTemplate."); + nameof(AddDataStoreManageRequest.Name), + $"The generated database name '{databaseName}' exceeds the portable limit of {DataStoreManageDatabaseNameFormatter.MaxPortableDatabaseNameLength} characters. Shorten Name or DatabaseTemplate."); } }); } diff --git a/Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreDatabaseNameFormatter.cs b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageDatabaseNameFormatter.cs similarity index 93% rename from Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreDatabaseNameFormatter.cs rename to Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageDatabaseNameFormatter.cs index e00f8f2b2..2040a3cbc 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreDatabaseNameFormatter.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageDatabaseNameFormatter.cs @@ -5,9 +5,9 @@ using System.Text.RegularExpressions; -namespace EdFi.Ods.AdminApi.V3.Features.DbDataStores; +namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; -internal static class DbDataStoreDatabaseNameFormatter +internal static class DataStoreManageDatabaseNameFormatter { private const string CanonicalPrefix = "EdFi_Ods"; diff --git a/Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreMapper.cs b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageMapper.cs similarity index 72% rename from Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreMapper.cs rename to Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageMapper.cs index 0512f1979..5fdcb49eb 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreMapper.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageMapper.cs @@ -5,13 +5,13 @@ using EdFi.Ods.AdminApi.Common.Infrastructure.Models; -namespace EdFi.Ods.AdminApi.V3.Features.DbDataStores; +namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; -public static class DbDataStoreMapper +public static class DataStoreManageMapper { - public static DbDataStoreModel ToModel(DbInstance source) + public static DataStoreManageModel ToModel(OdsInstanceManage source) { - return new DbDataStoreModel + return new DataStoreManageModel { Id = source.Id, Name = source.Name, @@ -25,10 +25,8 @@ public static DbDataStoreModel ToModel(DbInstance source) }; } - public static List ToModelList(IEnumerable source) + public static List ToModelList(IEnumerable source) { return source.Select(ToModel).ToList(); } } - - diff --git a/Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreModel.cs b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageModel.cs similarity index 84% rename from Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreModel.cs rename to Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageModel.cs index 661a9b370..54dacbef9 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreModel.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageModel.cs @@ -5,10 +5,10 @@ using Swashbuckle.AspNetCore.Annotations; -namespace EdFi.Ods.AdminApi.V3.Features.DbDataStores; +namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; -[SwaggerSchema(Title = "DbDataStore")] -public class DbDataStoreModel +[SwaggerSchema(Title = "DataStoreManage")] +public class DataStoreManageModel { public int? Id { get; set; } public string? Name { get; set; } @@ -20,4 +20,3 @@ public class DbDataStoreModel public DateTime? LastRefreshed { get; set; } public DateTime? LastModifiedDate { get; set; } } - diff --git a/Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DeleteDbDataStore.cs b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DeleteDataStoreManage.cs similarity index 56% rename from Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DeleteDbDataStore.cs rename to Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DeleteDataStoreManage.cs index 1b7c6243d..0ab37be91 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DeleteDbDataStore.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DeleteDataStoreManage.cs @@ -20,47 +20,47 @@ using Microsoft.Extensions.Options; using Quartz; -namespace EdFi.Ods.AdminApi.V3.Features.DbDataStores; +namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; -public class DeleteDbDataStore : IFeature +public class DeleteDataStoreManage : IFeature { public void MapEndpoints(IEndpointRouteBuilder endpoints) { AdminApiEndpointBuilder - .MapDelete(endpoints, "/dbDataStores/{id}", Handle) + .MapDelete(endpoints, "/dataStores/manage/{id}", Handle) .WithDefaultSummaryAndDescription() .WithRouteOptions(b => b.WithResponseCode(204)) .BuildForVersions(AdminApiVersions.V3); } public static async Task Handle( - IGetDbDataStoreByIdQuery GetDbDataStoreByIdQuery, - IDeleteDbDataStoreCommand DeleteDbDataStoreCommand, + IGetDataStoreManageByIdQuery getDataStoreManageByIdQuery, + IDeleteDataStoreManageCommand deleteDataStoreManageCommand, [FromServices] ISchedulerFactory schedulerFactory, [FromServices] IContextProvider tenantConfigurationProvider, [FromServices] IOptions options, int id ) { - var dbDataStore = GetDbDataStoreByIdQuery.Execute(id); - if (dbDataStore is null) - throw new NotFoundException("dbDataStore", id); + var dataStoreManage = getDataStoreManageByIdQuery.Execute(id); + if (dataStoreManage is null) + throw new NotFoundException("dataStoreManage", id); - if (dbDataStore.Status == DbInstanceStatus.Deleted.ToString()) - throw new NotFoundException("dbDataStore", id); + if (dataStoreManage.Status == OdsInstanceManageStatus.Deleted.ToString()) + throw new NotFoundException("dataStoreManage", id); - var blockingMessage = GetBlockingStatusMessage(dbDataStore.Status); + var blockingMessage = GetBlockingStatusMessage(dataStoreManage.Status); if (blockingMessage is not null) throw new ValidationException([new ValidationFailure(nameof(id), blockingMessage)]); - DeleteDbDataStoreCommand.Execute(id); + deleteDataStoreManageCommand.Execute(id); var tenantName = options.Value.MultiTenancy ? tenantConfigurationProvider.Get()?.TenantIdentifier : null; var jobData = new Dictionary { - [JobConstants.DbInstanceIdKey] = id + [JobConstants.OdsInstanceManageIdKey] = id }; if (!string.IsNullOrWhiteSpace(tenantName)) @@ -80,7 +80,7 @@ await QuartzJobScheduler.ScheduleJob( } catch (ObjectAlreadyExistsException) { - // The DeletePendingDbInstancesDispatcherJob may have already scheduled this job. + // The DeletePendingDataStoreManagesDispatcherJob may have already scheduled this job. // Treat duplicate scheduling as success — the job is already queued. } @@ -89,18 +89,18 @@ await QuartzJobScheduler.ScheduleJob( private static string? GetBlockingStatusMessage(string status) { - if (Enum.TryParse(status, out var parsed)) + if (Enum.TryParse(status, out var parsed)) { return parsed switch { - DbInstanceStatus.PendingCreate => "DbInstance is being provisioned. Wait for creation to complete.", - DbInstanceStatus.CreateInProgress => "DbInstance is currently being provisioned. Wait for creation to complete.", - DbInstanceStatus.CreateFailed => "DbInstance creation failed. It will be retried automatically by the background job.", - DbInstanceStatus.CreateError => "DbInstance creation failed permanently. Manual database intervention required before deleting.", - DbInstanceStatus.PendingDelete => "DbInstance is already queued for deletion.", - DbInstanceStatus.DeleteInProgress => "DbInstance is currently being deleted.", - DbInstanceStatus.DeleteFailed => "DbInstance deletion failed. It will be retried automatically by the background job.", - DbInstanceStatus.DeleteError => "DbInstance deletion failed permanently. Manual database intervention required.", + OdsInstanceManageStatus.PendingCreate => "OdsInstanceManage is being provisioned. Wait for creation to complete.", + OdsInstanceManageStatus.CreateInProgress => "OdsInstanceManage is currently being provisioned. Wait for creation to complete.", + OdsInstanceManageStatus.CreateFailed => "OdsInstanceManage creation failed. It will be retried automatically by the background job.", + OdsInstanceManageStatus.CreateError => "OdsInstanceManage creation failed permanently. Manual database intervention required before deleting.", + OdsInstanceManageStatus.PendingDelete => "OdsInstanceManage is already queued for deletion.", + OdsInstanceManageStatus.DeleteInProgress => "OdsInstanceManage is currently being deleted.", + OdsInstanceManageStatus.DeleteFailed => "OdsInstanceManage deletion failed. It will be retried automatically by the background job.", + OdsInstanceManageStatus.DeleteError => "OdsInstanceManage deletion failed permanently. Manual database intervention required.", _ => null, }; } diff --git a/Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/ReadDbDataStore.cs b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/ReadDataStoreManage.cs similarity index 50% rename from Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/ReadDbDataStore.cs rename to Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/ReadDataStoreManage.cs index 1c0640c7e..96e38b783 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/ReadDbDataStore.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/ReadDataStoreManage.cs @@ -8,41 +8,38 @@ using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; -namespace EdFi.Ods.AdminApi.V3.Features.DbDataStores; +namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; -public class ReadDbDataStore : IFeature +public class ReadDataStoreManage : IFeature { public void MapEndpoints(IEndpointRouteBuilder endpoints) { - AdminApiEndpointBuilder.MapGet(endpoints, "/dbDataStores", GetDbDataStores) + AdminApiEndpointBuilder.MapGet(endpoints, "/dataStores/manage", GetDataStoreManages) .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponse(200)) + .WithRouteOptions(b => b.WithResponse(200)) .BuildForVersions(AdminApiVersions.V3); - AdminApiEndpointBuilder.MapGet(endpoints, "/dbDataStores/{id}", GetDbDataStore) + AdminApiEndpointBuilder.MapGet(endpoints, "/dataStores/manage/{id}", GetDataStoreManage) .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponse(200)) + .WithRouteOptions(b => b.WithResponse(200)) .BuildForVersions(AdminApiVersions.V3); } - public static Task GetDbDataStores(IGetDbDataStoresQuery query, + public static Task GetDataStoreManages(IGetDataStoreManagesQuery query, [AsParameters] CommonQueryParams commonQueryParams, int? id, string? name) { - var list = DbDataStoreMapper.ToModelList(query.Execute(commonQueryParams, id, name)); + var list = DataStoreManageMapper.ToModelList(query.Execute(commonQueryParams, id, name)); return Task.FromResult(Results.Ok(list)); } - public static Task GetDbDataStore(IGetDbDataStoreByIdQuery query, int id) + public static Task GetDataStoreManage(IGetDataStoreManageByIdQuery query, int id) { - var dbInstance = query.Execute(id); - if (dbInstance == null) + var dataStoreManage = query.Execute(id); + if (dataStoreManage == null) { - throw new NotFoundException("dbDataStore", id); + throw new NotFoundException("dataStoreManage", id); } - var model = DbDataStoreMapper.ToModel(dbInstance); + var model = DataStoreManageMapper.ToModel(dataStoreManage); return Task.FromResult(Results.Ok(model)); } } - - - From 6c74122d266c7d86b7761189827ae4ccd2e41019 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 22:26:08 -0600 Subject: [PATCH 18/35] Update v3 DataStores and Tenants EdOrgs merges to use renamed DataStoreManage query, add DataStoreManageId for v2 parity --- ...ataStoreWithEducationOrganizationsModel.cs | 3 +++ .../DataStores/ReadEducationOrganizations.cs | 23 ++++++++-------- .../Features/Tenants/ReadTenants.cs | 4 +-- .../Features/Tenants/TenantMapper.cs | 2 +- .../Services/Tenants/TenantService.cs | 26 +++++++++---------- 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/DataStoreWithEducationOrganizationsModel.cs b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/DataStoreWithEducationOrganizationsModel.cs index 96dcb83ca..2ae2ecdb5 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/DataStoreWithEducationOrganizationsModel.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/DataStoreWithEducationOrganizationsModel.cs @@ -13,6 +13,9 @@ public class DataStoreWithEducationOrganizationsModel [SwaggerSchema(Description = "Data store identifier")] public int? Id { get; set; } + [SwaggerSchema(Description = "DataStoreManage identifier for this data store")] + public int? DataStoreManageId { get; set; } + [SwaggerSchema(Description = "Data store name", Nullable = false)] public string Name { get; set; } = string.Empty; diff --git a/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/ReadEducationOrganizations.cs b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/ReadEducationOrganizations.cs index b37f7790a..71de2cdd0 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/ReadEducationOrganizations.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Features/DataStores/ReadEducationOrganizations.cs @@ -28,7 +28,7 @@ public void MapEndpoints(IEndpointRouteBuilder endpoints) public static async Task GetEducationOrganizationsByDataStore( [FromServices] IGetEducationOrganizationsQuery getEducationOrganizationsQuery, [FromServices] IGetDataStoreQuery getDataStoreQuery, - [FromServices] IGetDbDataStoresQuery getDbDataStoresQuery, + [FromServices] IGetDataStoreManagesQuery getDataStoreManagesQuery, [AsParameters] CommonQueryParams commonQueryParams, int dataStoreId) { @@ -38,32 +38,33 @@ public static async Task GetEducationOrganizationsByDataStore( commonQueryParams, dataStoreId: dataStoreId); - MergeDbDataStoreData(educationOrganizations, getDbDataStoresQuery); + MergeDataStoreManageData(educationOrganizations, getDataStoreManagesQuery); return Results.Ok(educationOrganizations); } - private static void MergeDbDataStoreData( + private static void MergeDataStoreManageData( List instances, - IGetDbDataStoresQuery getDbDataStoresQuery) + IGetDataStoreManagesQuery getDataStoreManagesQuery) { - var allDbDataStores = getDbDataStoresQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); + var allDataStoreManages = getDataStoreManagesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); - var linkedById = allDbDataStores + var linkedById = allDataStoreManages .Where(d => d.OdsInstanceId is not null) .GroupBy(d => d.OdsInstanceId!.Value) .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); foreach (var instance in instances) { - if (instance.Id is int dataStoreId && linkedById.TryGetValue(dataStoreId, out var dbDataStore)) + if (instance.Id is int dataStoreId && linkedById.TryGetValue(dataStoreId, out var dataStoreManage)) { - instance.Status = dbDataStore.Status; - instance.DatabaseTemplate = dbDataStore.DatabaseTemplate; - instance.DatabaseName = dbDataStore.DatabaseName; + instance.DataStoreManageId = dataStoreManage.Id; + instance.Status = dataStoreManage.Status; + instance.DatabaseTemplate = dataStoreManage.DatabaseTemplate; + instance.DatabaseName = dataStoreManage.DatabaseName; } else { - instance.Status = DbInstanceStatus.Created.ToString(); + instance.Status = OdsInstanceManageStatus.Created.ToString(); } } } diff --git a/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/ReadTenants.cs b/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/ReadTenants.cs index 494694792..006f20bc2 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/ReadTenants.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/ReadTenants.cs @@ -34,7 +34,7 @@ public static async Task GetTenantEdOrgsByDataStoresAsync( [FromServices] ITenantsService tenantsService, IGetDataStoresQuery getDataStoresQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, - IGetDbDataStoresQuery getDbDataStoresQuery, + IGetDataStoreManagesQuery getDataStoreManagesQuery, IMemoryCache memoryCache, IOptions options, IOptions _swaggerOptions, @@ -60,7 +60,7 @@ string tenantName } var tenant = await tenantsService.GetTenantEdOrgsByInstancesAsync( - getDataStoresQuery, getEducationOrganizationQuery, getDbDataStoresQuery, tenantName); + getDataStoresQuery, getEducationOrganizationQuery, getDataStoreManagesQuery, tenantName); if (tenant is null) return Results.NotFound(); diff --git a/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs b/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs index 2377bcdd5..689f85ee1 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs @@ -25,7 +25,7 @@ public static List ToDataStoreModelList(IEnumerable> GetTenantsAsync(bool fromCache = false); Task GetTenantByTenantIdAsync(string tenantName); - Task GetTenantEdOrgsByInstancesAsync(IGetDataStoresQuery getDataStoresQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, IGetDbDataStoresQuery getDbDataStoresQuery, string tenantName); + Task GetTenantEdOrgsByInstancesAsync(IGetDataStoresQuery getDataStoresQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, IGetDataStoreManagesQuery getDataStoreManagesQuery, string tenantName); } public class TenantService(IOptionsSnapshot options, @@ -113,7 +113,7 @@ public async Task> GetTenantsAsync(bool fromCache = false) public async Task GetTenantEdOrgsByInstancesAsync( IGetDataStoresQuery getDataStoresQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, - IGetDbDataStoresQuery getDbDataStoresQuery, + IGetDataStoreManagesQuery getDataStoreManagesQuery, string tenantName) { var tenant = await GetTenantByTenantIdAsync(tenantName); @@ -142,24 +142,24 @@ public async Task> GetTenantsAsync(bool fromCache = false) } } - var allDbDataStores = getDbDataStoresQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); + var allDataStoreManages = getDataStoreManagesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); - var linkedDbDataStoresByDataStoreId = allDbDataStores + var linkedDataStoreManagesByDataStoreId = allDataStoreManages .Where(d => d.OdsInstanceId is not null) .GroupBy(d => d.OdsInstanceId!.Value) .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); foreach (var dataStore in tenantDetails.DataStores) { - if (dataStore.DataStoreId is int dataStoreId && linkedDbDataStoresByDataStoreId.TryGetValue(dataStoreId, out var dbDataStore)) + if (dataStore.DataStoreId is int dataStoreId && linkedDataStoreManagesByDataStoreId.TryGetValue(dataStoreId, out var dataStoreManage)) { - dataStore.Status = dbDataStore.Status; - dataStore.DatabaseTemplate = dbDataStore.DatabaseTemplate; - dataStore.DatabaseName = dbDataStore.DatabaseName; + dataStore.Status = dataStoreManage.Status; + dataStore.DatabaseTemplate = dataStoreManage.DatabaseTemplate; + dataStore.DatabaseName = dataStoreManage.DatabaseName; } else { - dataStore.Status = DbInstanceStatus.Created.ToString(); + dataStore.Status = OdsInstanceManageStatus.Created.ToString(); } } @@ -168,16 +168,16 @@ public async Task> GetTenantsAsync(bool fromCache = false) .Select(i => i.DataStoreId!.Value) .ToHashSet(); - var unlinkedDbDataStores = allDbDataStores + var unlinkedDataStoreManages = allDataStoreManages .Where(d => d.OdsInstanceId is null) - .Concat(allDbDataStores + .Concat(allDataStoreManages .Where(d => d.OdsInstanceId is not null && !existingDataStoreIds.Contains(d.OdsInstanceId.Value)) .GroupBy(d => d.OdsInstanceId!.Value) .Select(g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First())) .ToList(); - foreach (var dbDataStore in unlinkedDbDataStores) + foreach (var dataStoreManage in unlinkedDataStoreManages) { - tenantDetails.DataStores.Add(TenantMapper.ToUnlinkedDbDataStoreModel(dbDataStore)); + tenantDetails.DataStores.Add(TenantMapper.ToUnlinkedDataStoreManageModel(dataStoreManage)); } return tenantDetails; From b02bb282083b6756f8c7c34883c02c8f04460cb5 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 22:34:35 -0600 Subject: [PATCH 19/35] Update v3 Quartz wiring to use renamed DataStoreManage dispatcher jobs Rename V3Jobs.CreatePendingDbInstancesDispatcherJob/DeletePendingDbInstancesDispatcherJob type references in Program.cs and the corresponding DI registrations in WebApplicationBuilderExtensions.cs to CreatePendingDataStoreManagesDispatcherJob/ DeletePendingDataStoreManagesDispatcherJob, completing the v3-side Quartz wiring rename. --- .../Infrastructure/WebApplicationBuilderExtensions.cs | 4 ++-- Application/EdFi.Ods.AdminApi/Program.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs index 3097aba38..dc6f75acf 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs @@ -750,13 +750,13 @@ private static void RegisterQuartzServices(WebApplicationBuilder webApplicationB EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.CreateInstanceJob >(); webApplicationBuilder.Services.AddTransient< - EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.CreatePendingDbInstancesDispatcherJob + EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.CreatePendingDataStoreManagesDispatcherJob >(); webApplicationBuilder.Services.AddTransient< EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.DeleteInstanceJob >(); webApplicationBuilder.Services.AddTransient< - EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.DeletePendingDbInstancesDispatcherJob + EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.DeletePendingDataStoreManagesDispatcherJob >(); webApplicationBuilder.Services.AddTransient< IJobStatusService, diff --git a/Application/EdFi.Ods.AdminApi/Program.cs b/Application/EdFi.Ods.AdminApi/Program.cs index 866a26a7a..6e0f919aa 100644 --- a/Application/EdFi.Ods.AdminApi/Program.cs +++ b/Application/EdFi.Ods.AdminApi/Program.cs @@ -310,7 +310,7 @@ await QuartzJobScheduler.ScheduleJob tenant.TenantName)) { - await QuartzJobScheduler.ScheduleJob( + await QuartzJobScheduler.ScheduleJob( scheduler, jobKey: new JobKey($"{JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName}_{tenantName}"), jobData: new Dictionary @@ -324,7 +324,7 @@ await QuartzJobScheduler.ScheduleJob( + await QuartzJobScheduler.ScheduleJob( scheduler, jobKey: new JobKey(JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName), jobData: new Dictionary(), @@ -348,7 +348,7 @@ await QuartzJobScheduler.ScheduleJob tenant.TenantName)) { - await QuartzJobScheduler.ScheduleJob( + await QuartzJobScheduler.ScheduleJob( scheduler, jobKey: new JobKey($"{JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName}_{tenantName}"), jobData: new Dictionary @@ -362,7 +362,7 @@ await QuartzJobScheduler.ScheduleJob( + await QuartzJobScheduler.ScheduleJob( scheduler, jobKey: new JobKey(JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName), jobData: new Dictionary(), From cc309befb1c3561ca1e3ac04e25690d9fabeae76 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Mon, 27 Jul 2026 22:54:30 -0600 Subject: [PATCH 20/35] Rename v3 unit tests to DataStoreManage naming, add missing query test coverage Task 14: renames DbDataStore-era v3 unit test files to match the DataStoreManage production types from Tasks 10-13, and adds GetDataStoreManagesQueryTests / GetDataStoreManageByIdQueryTests to close a pre-existing coverage gap (v3 never had query-level unit tests for this area). --- .../Manage/AddDataStoreManageTests.cs} | 158 +++++++++--------- .../Manage/DeleteDataStoreManageTests.cs} | 105 ++++++------ .../Manage/ReadDataStoreManageTests.cs} | 75 ++++----- .../ReadEducationOrganizationsTests.cs | 26 +-- .../Features/Tenants/ReadTenantsTest.cs | 40 ++--- ...s.cs => AddDataStoreManageCommandTests.cs} | 29 ++-- ...s => DeleteDataStoreManageCommandTests.cs} | 39 ++--- .../GetDataStoreManageByIdQueryTests.cs | 69 ++++++++ .../Queries/GetDataStoreManagesQueryTests.cs | 78 +++++++++ .../Services/Jobs/CreateInstanceJobTests.cs | 116 ++++++------- ...dingDataStoreManagesDispatcherJobTests.cs} | 58 +++---- .../Services/Jobs/DeleteInstanceJobTests.cs | 68 ++++---- ...dingDataStoreManagesDispatcherJobTests.cs} | 62 +++---- .../Services/Tenants/TenantServiceTests.cs | 88 +++++----- 14 files changed, 573 insertions(+), 438 deletions(-) rename Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/{DbDataStores/AddDbDataStoreTests.cs => DataStores/Manage/AddDataStoreManageTests.cs} (68%) rename Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/{DbDataStores/DeleteDbDataStoreTests.cs => DataStores/Manage/DeleteDataStoreManageTests.cs} (60%) rename Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/{DbDataStores/ReadDbDataStoreTests.cs => DataStores/Manage/ReadDataStoreManageTests.cs} (54%) rename Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/{AddDbDataStoreCommandTests.cs => AddDataStoreManageCommandTests.cs} (74%) rename Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/{DeleteDbDataStoreCommandTests.cs => DeleteDataStoreManageCommandTests.cs} (70%) create mode 100644 Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Queries/GetDataStoreManageByIdQueryTests.cs create mode 100644 Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Queries/GetDataStoreManagesQueryTests.cs rename Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/{CreatePendingDbInstancesDispatcherJobTests.cs => CreatePendingDataStoreManagesDispatcherJobTests.cs} (78%) rename Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/{DeletePendingDbInstancesDispatcherJobTests.cs => DeletePendingDataStoreManagesDispatcherJobTests.cs} (78%) diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/AddDbDataStoreTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/AddDataStoreManageTests.cs similarity index 68% rename from Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/AddDbDataStoreTests.cs rename to Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/AddDataStoreManageTests.cs index 843f04d02..0db4999fb 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/AddDbDataStoreTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/AddDataStoreManageTests.cs @@ -14,7 +14,7 @@ using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.V3.Features.DbDataStores; +using EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; using EdFi.Ods.AdminApi.V3.Infrastructure; using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; using FakeItEasy; @@ -30,10 +30,10 @@ #nullable enable -namespace EdFi.Ods.AdminApi.V3.UnitTests.Features.DbDataStores; +namespace EdFi.Ods.AdminApi.V3.UnitTests.Features.DataStores.Manage; [TestFixture] -public class AddDbDataStoreTests +public class AddDataStoreManageTests { private static HttpContext CreateHttpContext() { @@ -46,7 +46,7 @@ private static HttpContext CreateHttpContext() private static AdminApiDbContext CreateContext() { var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"AddDbInstance_{Guid.NewGuid()}") + .UseInMemoryDatabase(databaseName: $"AddDataStoreManage_{Guid.NewGuid()}") .Options; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -63,7 +63,7 @@ private static IOptions CreateOptions(bool multiTenancy = false) private static SqlServerUsersContext CreateUsersContext() { var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"AddDbInstanceUsers_{Guid.NewGuid()}") + .UseInMemoryDatabase(databaseName: $"AddDataStoreManageUsers_{Guid.NewGuid()}") .Options; return new SqlServerUsersContext(options); @@ -100,43 +100,43 @@ public async Task Handle_WithValidRequest_ReturnsAccepted() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); var httpContext = CreateHttpContext(); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = "My DB Instance", DatabaseTemplate = "Minimal" }; - var result = await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext); + var result = await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext); result.ShouldBeOfType(); } [Test] - public async Task Handle_WithValidRequest_PersistsDbInstance() + public async Task Handle_WithValidRequest_PersistsOdsInstanceManage() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); var httpContext = CreateHttpContext(); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = "My DB Instance", DatabaseTemplate = "Sample" }; - await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext); + await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext); - context.DbInstances.Any(d => d.Name == "My DB Instance").ShouldBeTrue(); + context.OdsInstanceManages.Any(d => d.Name == "My DB Instance").ShouldBeTrue(); } [Test] @@ -144,8 +144,8 @@ public async Task Handle_WithValidRequest_SchedulesCreateInstanceJob() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out var scheduler); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); @@ -156,19 +156,19 @@ public async Task Handle_WithValidRequest_SchedulesCreateInstanceJob() .Invokes((IJobDetail job, ITrigger _, CancellationToken _) => scheduledJob = job) .Returns(Task.FromResult(DateTimeOffset.UtcNow)); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = "My DB Instance", DatabaseTemplate = "Minimal" }; - await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext); + await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext); - var dbInstance = context.DbInstances.Single(); + var odsInstanceManage = context.OdsInstanceManages.Single(); scheduledJob.ShouldNotBeNull(); - scheduledJob!.Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-{dbInstance.Id}"); - scheduledJob.JobDataMap.GetInt(JobConstants.DbInstanceIdKey).ShouldBe(dbInstance.Id); + scheduledJob!.Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-{odsInstanceManage.Id}"); + scheduledJob.JobDataMap.GetInt(JobConstants.OdsInstanceManageIdKey).ShouldBe(odsInstanceManage.Id); } [Test] @@ -176,8 +176,8 @@ public async Task Handle_WithMultiTenancyEnabled_SchedulesTenantAwareCreateInsta { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out var scheduler); var tenantProvider = CreateTenantConfigurationProvider("tenant1"); var options = CreateOptions(multiTenancy: true); @@ -188,18 +188,18 @@ public async Task Handle_WithMultiTenancyEnabled_SchedulesTenantAwareCreateInsta .Invokes((IJobDetail job, ITrigger _, CancellationToken _) => scheduledJob = job) .Returns(Task.FromResult(DateTimeOffset.UtcNow)); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = "My DB Instance", DatabaseTemplate = "Minimal" }; - await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext); + await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext); - var dbInstance = context.DbInstances.Single(); + var odsInstanceManage = context.OdsInstanceManages.Single(); scheduledJob.ShouldNotBeNull(); - scheduledJob!.Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-tenant1-{dbInstance.Id}"); + scheduledJob!.Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-tenant1-{odsInstanceManage.Id}"); scheduledJob.JobDataMap.GetString(JobConstants.TenantNameKey).ShouldBe("tenant1"); } @@ -208,19 +208,19 @@ public async Task Handle_WithEmptyName_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); var httpContext = CreateHttpContext(); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = string.Empty, DatabaseTemplate = "Minimal" }; - await Should.ThrowAsync(async () => await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); + await Should.ThrowAsync(async () => await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); } [Test] @@ -228,19 +228,19 @@ public async Task Handle_WithNullName_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); var httpContext = CreateHttpContext(); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = null, DatabaseTemplate = "Minimal" }; - await Should.ThrowAsync(async () => await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); + await Should.ThrowAsync(async () => await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); } [Test] @@ -248,19 +248,19 @@ public async Task Handle_WithNameExceedingMaxLength_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); var httpContext = CreateHttpContext(); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = new string('a', 101), DatabaseTemplate = "Minimal" }; - await Should.ThrowAsync(async () => await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); + await Should.ThrowAsync(async () => await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); } [Test] @@ -268,19 +268,19 @@ public async Task Handle_WithNameAtPortableDatabaseNameLimit_ReturnsAccepted() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); var httpContext = CreateHttpContext(); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = new string('a', 46), DatabaseTemplate = "Minimal" }; - var result = await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext); + var result = await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext); result.ShouldBeOfType(); } @@ -290,22 +290,22 @@ public async Task Handle_WithFormattedDatabaseNameExceedingPortableLimit_ThrowsV { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); var httpContext = CreateHttpContext(); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = new string('a', 47), DatabaseTemplate = "Minimal" }; - var exception = await Should.ThrowAsync(async () => await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); + var exception = await Should.ThrowAsync(async () => await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); exception.Errors.ShouldContain(error => - error.PropertyName == nameof(AddDbDataStore.AddDbDataStoreRequest.Name) + error.PropertyName == nameof(AddDataStoreManage.AddDataStoreManageRequest.Name) && error.ErrorMessage.Contains("portable limit of 63 characters")); } @@ -316,19 +316,19 @@ public async Task Handle_WithInvalidNameCharacters_ThrowsValidationException(str { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); var httpContext = CreateHttpContext(); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = name, DatabaseTemplate = "Minimal" }; - await Should.ThrowAsync(async () => await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); + await Should.ThrowAsync(async () => await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); } [Test] @@ -336,19 +336,19 @@ public async Task Handle_WithEmptyDatabaseTemplate_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); var httpContext = CreateHttpContext(); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = "My DB Instance", DatabaseTemplate = string.Empty }; - await Should.ThrowAsync(async () => await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); + await Should.ThrowAsync(async () => await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); } [Test] @@ -356,19 +356,19 @@ public async Task Handle_WithNullDatabaseTemplate_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); var httpContext = CreateHttpContext(); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = "My DB Instance", DatabaseTemplate = null }; - await Should.ThrowAsync(async () => await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); + await Should.ThrowAsync(async () => await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); } [Test] @@ -376,34 +376,34 @@ public async Task Handle_WithInvalidDatabaseTemplate_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); var httpContext = CreateHttpContext(); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = "My DB Instance", DatabaseTemplate = "InvalidTemplate" }; - await Should.ThrowAsync(async () => await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); + await Should.ThrowAsync(async () => await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); } [Test] - public async Task Handle_WithExistingDbInstanceName_ThrowsValidationException() + public async Task Handle_WithExistingOdsInstanceManageName_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); var httpContext = CreateHttpContext(); - context.DbInstances.Add(new Common.Infrastructure.Models.DbInstance + context.OdsInstanceManages.Add(new Common.Infrastructure.Models.OdsInstanceManage { Name = "Existing Instance", DatabaseTemplate = "Minimal", @@ -413,17 +413,17 @@ public async Task Handle_WithExistingDbInstanceName_ThrowsValidationException() }); await context.SaveChangesAsync(); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = "Existing Instance", DatabaseTemplate = "Minimal" }; - var exception = await Should.ThrowAsync(async () => await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); + var exception = await Should.ThrowAsync(async () => await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); exception.Errors.ShouldContain(error => - error.PropertyName == nameof(AddDbDataStore.AddDbDataStoreRequest.Name) - && error.ErrorMessage == "A DbDataStore named 'Existing Instance' already exists."); + error.PropertyName == nameof(AddDataStoreManage.AddDataStoreManageRequest.Name) + && error.ErrorMessage == "A DataStoreManage named 'Existing Instance' already exists."); } [Test] @@ -431,8 +431,8 @@ public async Task Handle_WithExistingOdsInstanceName_ThrowsValidationException() { using var context = CreateContext(); using var usersContext = CreateUsersContext(); - var validator = new AddDbDataStore.Validator(context, usersContext); - var command = new AddDbDataStoreCommand(context); + var validator = new AddDataStoreManage.Validator(context, usersContext); + var command = new AddDataStoreManageCommand(context); var schedulerFactory = CreateSchedulerFactory(out _); var tenantProvider = CreateTenantConfigurationProvider(); var options = CreateOptions(); @@ -446,20 +446,18 @@ public async Task Handle_WithExistingOdsInstanceName_ThrowsValidationException() }); await usersContext.SaveChangesAsync(); - var request = new AddDbDataStore.AddDbDataStoreRequest + var request = new AddDataStoreManage.AddDataStoreManageRequest { Name = "Existing Instance", DatabaseTemplate = "Minimal" }; - var exception = await Should.ThrowAsync(async () => await AddDbDataStore.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); + var exception = await Should.ThrowAsync(async () => await AddDataStoreManage.Handle(validator, command, schedulerFactory, tenantProvider, options, request, httpContext)); exception.Errors.ShouldContain(error => - error.PropertyName == nameof(AddDbDataStore.AddDbDataStoreRequest.Name) + error.PropertyName == nameof(AddDataStoreManage.AddDataStoreManageRequest.Name) && error.ErrorMessage == "A DataStore named 'Existing Instance' already exists."); } } #nullable restore - - diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/DeleteDbDataStoreTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/DeleteDataStoreManageTests.cs similarity index 60% rename from Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/DeleteDbDataStoreTests.cs rename to Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/DeleteDataStoreManageTests.cs index 827d0277d..bfb551f96 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/DeleteDbDataStoreTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/DeleteDataStoreManageTests.cs @@ -12,7 +12,7 @@ using EdFi.Ods.AdminApi.Common.Infrastructure.Models; using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.V3.Features.DbDataStores; +using EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; using FakeItEasy; @@ -26,13 +26,13 @@ #nullable enable -namespace EdFi.Ods.AdminApi.V3.UnitTests.Features.DbDataStores; +namespace EdFi.Ods.AdminApi.V3.UnitTests.Features.DataStores.Manage; [TestFixture] -public class DeleteDbDataStoreTests +public class DeleteDataStoreManageTests { - private IGetDbDataStoreByIdQuery _getDbInstanceByIdQuery = null!; - private IDeleteDbDataStoreCommand _deleteDbInstanceCommand = null!; + private IGetDataStoreManageByIdQuery _getDataStoreManageByIdQuery = null!; + private IDeleteDataStoreManageCommand _deleteDataStoreManageCommand = null!; private ISchedulerFactory _schedulerFactory = null!; private IContextProvider _tenantConfigurationProvider = null!; private IOptions _options = null!; @@ -40,8 +40,8 @@ public class DeleteDbDataStoreTests [SetUp] public void SetUp() { - _getDbInstanceByIdQuery = A.Fake(); - _deleteDbInstanceCommand = A.Fake(); + _getDataStoreManageByIdQuery = A.Fake(); + _deleteDataStoreManageCommand = A.Fake(); var scheduler = A.Fake(); A.CallTo(() => scheduler.ScheduleJob(A._, A._, A._)) @@ -58,18 +58,18 @@ public void SetUp() } private Task Handle(int id) - => DeleteDbDataStore.Handle( - _getDbInstanceByIdQuery, - _deleteDbInstanceCommand, + => DeleteDataStoreManage.Handle( + _getDataStoreManageByIdQuery, + _deleteDataStoreManageCommand, _schedulerFactory, _tenantConfigurationProvider, _options, id); [Test] - public async Task Handle_WhenDbInstanceNotFound_ThrowsNotFoundException() + public async Task Handle_WhenDataStoreManageNotFound_ThrowsNotFoundException() { - A.CallTo(() => _getDbInstanceByIdQuery.Execute(99)).Returns(null); + A.CallTo(() => _getDataStoreManageByIdQuery.Execute(99)).Returns(null); await Should.ThrowAsync>(() => Handle(99)); } @@ -77,182 +77,181 @@ public async Task Handle_WhenDbInstanceNotFound_ThrowsNotFoundException() [Test] public async Task Handle_WhenStatusIsCreated_ExecutesCommandAndReturnsAccepted() { - var dbInstance = new DbInstance + var dataStoreManage = new OdsInstanceManage { Id = 1, Name = "Test", - Status = DbInstanceStatus.Created.ToString(), + Status = OdsInstanceManageStatus.Created.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(1)).Returns(dbInstance); + A.CallTo(() => _getDataStoreManageByIdQuery.Execute(1)).Returns(dataStoreManage); var result = await Handle(1); result.ShouldBeOfType(); - A.CallTo(() => _deleteDbInstanceCommand.Execute(1)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _deleteDataStoreManageCommand.Execute(1)).MustHaveHappenedOnceExactly(); } [Test] public async Task Handle_WhenStatusIsPendingCreate_ThrowsValidationException() { - var dbInstance = new DbInstance + var dataStoreManage = new OdsInstanceManage { Id = 2, Name = "Test", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(2)).Returns(dbInstance); + A.CallTo(() => _getDataStoreManageByIdQuery.Execute(2)).Returns(dataStoreManage); var ex = await Should.ThrowAsync(() => Handle(2)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("provisioned")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteDataStoreManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsCreateInProgress_ThrowsValidationException() { - var dbInstance = new DbInstance + var dataStoreManage = new OdsInstanceManage { Id = 3, Name = "Test", - Status = DbInstanceStatus.CreateInProgress.ToString(), + Status = OdsInstanceManageStatus.CreateInProgress.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(3)).Returns(dbInstance); + A.CallTo(() => _getDataStoreManageByIdQuery.Execute(3)).Returns(dataStoreManage); var ex = await Should.ThrowAsync(() => Handle(3)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("provisioned")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteDataStoreManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsCreateFailed_ThrowsValidationException() { - var dbInstance = new DbInstance + var dataStoreManage = new OdsInstanceManage { Id = 4, Name = "Test", - Status = DbInstanceStatus.CreateFailed.ToString(), + Status = OdsInstanceManageStatus.CreateFailed.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(4)).Returns(dbInstance); + A.CallTo(() => _getDataStoreManageByIdQuery.Execute(4)).Returns(dataStoreManage); var ex = await Should.ThrowAsync(() => Handle(4)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("creation failed")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteDataStoreManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsCreateError_ThrowsValidationException() { - var dbInstance = new DbInstance + var dataStoreManage = new OdsInstanceManage { Id = 5, Name = "Test", - Status = DbInstanceStatus.CreateError.ToString(), + Status = OdsInstanceManageStatus.CreateError.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(5)).Returns(dbInstance); + A.CallTo(() => _getDataStoreManageByIdQuery.Execute(5)).Returns(dataStoreManage); var ex = await Should.ThrowAsync(() => Handle(5)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("creation failed permanently")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteDataStoreManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsPendingDelete_ThrowsValidationException() { - var dbInstance = new DbInstance + var dataStoreManage = new OdsInstanceManage { Id = 6, Name = "Test", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(6)).Returns(dbInstance); + A.CallTo(() => _getDataStoreManageByIdQuery.Execute(6)).Returns(dataStoreManage); var ex = await Should.ThrowAsync(() => Handle(6)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("queued for deletion")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteDataStoreManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsDeleteInProgress_ThrowsValidationException() { - var dbInstance = new DbInstance + var dataStoreManage = new OdsInstanceManage { Id = 7, Name = "Test", - Status = DbInstanceStatus.DeleteInProgress.ToString(), + Status = OdsInstanceManageStatus.DeleteInProgress.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(7)).Returns(dbInstance); + A.CallTo(() => _getDataStoreManageByIdQuery.Execute(7)).Returns(dataStoreManage); var ex = await Should.ThrowAsync(() => Handle(7)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("currently being deleted")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteDataStoreManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsDeleteFailed_ThrowsValidationException() { - var dbInstance = new DbInstance + var dataStoreManage = new OdsInstanceManage { Id = 8, Name = "Test", - Status = DbInstanceStatus.DeleteFailed.ToString(), + Status = OdsInstanceManageStatus.DeleteFailed.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(8)).Returns(dbInstance); + A.CallTo(() => _getDataStoreManageByIdQuery.Execute(8)).Returns(dataStoreManage); var ex = await Should.ThrowAsync(() => Handle(8)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("retried automatically")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteDataStoreManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsDeleteError_ThrowsValidationException() { - var dbInstance = new DbInstance + var dataStoreManage = new OdsInstanceManage { Id = 9, Name = "Test", - Status = DbInstanceStatus.DeleteError.ToString(), + Status = OdsInstanceManageStatus.DeleteError.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(9)).Returns(dbInstance); + A.CallTo(() => _getDataStoreManageByIdQuery.Execute(9)).Returns(dataStoreManage); var ex = await Should.ThrowAsync(() => Handle(9)); ex.Errors.ShouldContain(e => e.ErrorMessage.Contains("deletion failed permanently")); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteDataStoreManageCommand.Execute(A._)).MustNotHaveHappened(); } [Test] public async Task Handle_WhenStatusIsDeleted_ThrowsNotFoundException() { - var dbInstance = new DbInstance + var dataStoreManage = new OdsInstanceManage { Id = 10, Name = "Test", - Status = DbInstanceStatus.Deleted.ToString(), + Status = OdsInstanceManageStatus.Deleted.ToString(), DatabaseTemplate = "Minimal", }; - A.CallTo(() => _getDbInstanceByIdQuery.Execute(10)).Returns(dbInstance); + A.CallTo(() => _getDataStoreManageByIdQuery.Execute(10)).Returns(dataStoreManage); await Should.ThrowAsync>(() => Handle(10)); - A.CallTo(() => _deleteDbInstanceCommand.Execute(A._)).MustNotHaveHappened(); + A.CallTo(() => _deleteDataStoreManageCommand.Execute(A._)).MustNotHaveHappened(); } } #nullable restore - diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/ReadDbDataStoreTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/ReadDataStoreManageTests.cs similarity index 54% rename from Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/ReadDbDataStoreTests.cs rename to Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/ReadDataStoreManageTests.cs index be5a57fb1..de15e762f 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/ReadDbDataStoreTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/ReadDataStoreManageTests.cs @@ -8,33 +8,33 @@ using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; using EdFi.Ods.AdminApi.Common.Infrastructure.Models; -using EdFi.Ods.AdminApi.V3.Features.DbDataStores; +using EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; using FakeItEasy; using NUnit.Framework; using Shouldly; -namespace EdFi.Ods.AdminApi.V3.UnitTests.Features.DbDataStores; +namespace EdFi.Ods.AdminApi.V3.UnitTests.Features.DataStores.Manage; [TestFixture] -public class ReadDbDataStoreTests +public class ReadDataStoreManageTests { [Test] - public async Task GetDbInstances_ReturnsOkWithMappedList() + public async Task GetDataStoreManages_ReturnsOkWithMappedList() { - var fakeQuery = A.Fake(); + var fakeQuery = A.Fake(); var queryParams = new CommonQueryParams(0, 10); - var queryResult = new List + var queryResult = new List { - new DbInstance { Id = 1, Name = "Instance A", Status = "Pending", DatabaseTemplate = "Minimal" } + new OdsInstanceManage { Id = 1, Name = "Instance A", Status = "Pending", DatabaseTemplate = "Minimal" } }; A.CallTo(() => fakeQuery.Execute(A._, null, null)).Returns(queryResult); - var result = await ReadDbDataStore.GetDbDataStores(fakeQuery, queryParams, null, null); + var result = await ReadDataStoreManage.GetDataStoreManages(fakeQuery, queryParams, null, null); - result.ShouldBeOfType>>(); - var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; + result.ShouldBeOfType>>(); + var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; okResult!.Value.ShouldNotBeNull(); okResult.Value.Count.ShouldBe(1); okResult.Value[0].Id.ShouldBe(1); @@ -44,17 +44,17 @@ public async Task GetDbInstances_ReturnsOkWithMappedList() } [Test] - public async Task GetDbInstance_ReturnsOkWithMappedModel() + public async Task GetDataStoreManage_ReturnsOkWithMappedModel() { - var fakeQuery = A.Fake(); - var queryResult = new DbInstance { Id = 5, Name = "Instance B", Status = "Completed", DatabaseTemplate = "Sample" }; + var fakeQuery = A.Fake(); + var queryResult = new OdsInstanceManage { Id = 5, Name = "Instance B", Status = "Completed", DatabaseTemplate = "Sample" }; A.CallTo(() => fakeQuery.Execute(5)).Returns(queryResult); - var result = await ReadDbDataStore.GetDbDataStore(fakeQuery, 5); + var result = await ReadDataStoreManage.GetDataStoreManage(fakeQuery, 5); - result.ShouldBeOfType>(); - var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok; + result.ShouldBeOfType>(); + var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok; okResult!.Value.ShouldNotBeNull(); okResult.Value.Id.ShouldBe(5); okResult.Value.Name.ShouldBe("Instance B"); @@ -63,71 +63,68 @@ public async Task GetDbInstance_ReturnsOkWithMappedModel() } [Test] - public void GetDbInstance_WhenNotFound_ThrowsNotFoundException() + public void GetDataStoreManage_WhenNotFound_ThrowsNotFoundException() { - var fakeQuery = A.Fake(); + var fakeQuery = A.Fake(); A.CallTo(() => fakeQuery.Execute(99)).Returns(null); Should.Throw>( - () => ReadDbDataStore.GetDbDataStore(fakeQuery, 99).GetAwaiter().GetResult()); + () => ReadDataStoreManage.GetDataStoreManage(fakeQuery, 99).GetAwaiter().GetResult()); } [Test] - public void GetDbInstances_WhenQueryThrows_ExceptionIsPropagated() + public void GetDataStoreManages_WhenQueryThrows_ExceptionIsPropagated() { - var fakeQuery = A.Fake(); + var fakeQuery = A.Fake(); A.CallTo(() => fakeQuery.Execute(A._, null, null)) .Throws(new System.Exception("Query failed")); Should.Throw(async () => - await ReadDbDataStore.GetDbDataStores(fakeQuery, new CommonQueryParams(0, 10), null, null)); + await ReadDataStoreManage.GetDataStoreManages(fakeQuery, new CommonQueryParams(0, 10), null, null)); } [Test] - public async Task GetDbInstances_ReturnsOkWithEmptyList() + public async Task GetDataStoreManages_ReturnsOkWithEmptyList() { - var fakeQuery = A.Fake(); + var fakeQuery = A.Fake(); var queryParams = new CommonQueryParams(0, 10); - A.CallTo(() => fakeQuery.Execute(A._, null, null)).Returns(new List()); + A.CallTo(() => fakeQuery.Execute(A._, null, null)).Returns(new List()); - var result = await ReadDbDataStore.GetDbDataStores(fakeQuery, queryParams, null, null); + var result = await ReadDataStoreManage.GetDataStoreManages(fakeQuery, queryParams, null, null); - result.ShouldBeOfType>>(); - var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; + result.ShouldBeOfType>>(); + var okResult = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; okResult!.Value.ShouldBeEmpty(); } [Test] - public async Task GetDbInstances_WithIdFilter_PassesIdToQuery() + public async Task GetDataStoreManages_WithIdFilter_PassesIdToQuery() { - var fakeQuery = A.Fake(); + var fakeQuery = A.Fake(); var queryParams = new CommonQueryParams(0, 10); - var queryResult = new List(); + var queryResult = new List(); A.CallTo(() => fakeQuery.Execute(A._, 42, null)).Returns(queryResult); - await ReadDbDataStore.GetDbDataStores(fakeQuery, queryParams, 42, null); + await ReadDataStoreManage.GetDataStoreManages(fakeQuery, queryParams, 42, null); A.CallTo(() => fakeQuery.Execute(A._, 42, null)).MustHaveHappenedOnceExactly(); } [Test] - public async Task GetDbInstances_WithNameFilter_PassesNameToQuery() + public async Task GetDataStoreManages_WithNameFilter_PassesNameToQuery() { - var fakeQuery = A.Fake(); + var fakeQuery = A.Fake(); var queryParams = new CommonQueryParams(0, 10); - var queryResult = new List(); + var queryResult = new List(); A.CallTo(() => fakeQuery.Execute(A._, null, "Instance A")).Returns(queryResult); - await ReadDbDataStore.GetDbDataStores(fakeQuery, queryParams, null, "Instance A"); + await ReadDataStoreManage.GetDataStoreManages(fakeQuery, queryParams, null, "Instance A"); A.CallTo(() => fakeQuery.Execute(A._, null, "Instance A")).MustHaveHappenedOnceExactly(); } } - - - diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/ReadEducationOrganizationsTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/ReadEducationOrganizationsTests.cs index d52b1355b..a0d4feabd 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/ReadEducationOrganizationsTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/ReadEducationOrganizationsTests.cs @@ -20,7 +20,7 @@ namespace EdFi.Ods.AdminApi.V3.UnitTests.Features.DataStores; public class ReadEducationOrganizationsTests { private IGetEducationOrganizationsQuery _getEdOrgsQuery = null!; - private IGetDbDataStoresQuery _getDbDataStoresQuery = null!; + private IGetDataStoreManagesQuery _getDataStoreManagesQuery = null!; private IGetDataStoreQuery _getDataStoreQuery = null!; private CommonQueryParams _queryParams; @@ -28,13 +28,13 @@ public class ReadEducationOrganizationsTests public void SetUp() { _getEdOrgsQuery = A.Fake(); - _getDbDataStoresQuery = A.Fake(); + _getDataStoreManagesQuery = A.Fake(); _getDataStoreQuery = A.Fake(); _queryParams = new CommonQueryParams(0, 10); } [Test] - public async Task GetEducationOrganizationsByDataStore_DoesNotAppendUnlinkedDbDataStores() + public async Task GetEducationOrganizationsByDataStore_DoesNotAppendUnlinkedDataStoreManages() { var dataStoreId = 3; A.CallTo(() => _getDataStoreQuery.Execute(dataStoreId)).Returns(new EdFi.Admin.DataAccess.Models.OdsInstance { OdsInstanceId = dataStoreId }); @@ -43,14 +43,14 @@ public async Task GetEducationOrganizationsByDataStore_DoesNotAppendUnlinkedDbDa { new() { Id = dataStoreId, Name = "DataStore3" } }); - A.CallTo(() => _getDbDataStoresQuery.Execute(A._, null, null)) - .Returns(new List + A.CallTo(() => _getDataStoreManagesQuery.Execute(A._, null, null)) + .Returns(new List { - new DbInstance { Id = 1, Name = "Unlinked", OdsInstanceId = null, Status = "PendingCreate" } + new OdsInstanceManage { Id = 1, Name = "Unlinked", OdsInstanceId = null, Status = "PendingCreate" } }); var result = await ReadEducationOrganizations.GetEducationOrganizationsByDataStore( - _getEdOrgsQuery, _getDataStoreQuery, _getDbDataStoresQuery, _queryParams, dataStoreId); + _getEdOrgsQuery, _getDataStoreQuery, _getDataStoreManagesQuery, _queryParams, dataStoreId); var ok = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; ok.ShouldNotBeNull(); @@ -59,7 +59,7 @@ public async Task GetEducationOrganizationsByDataStore_DoesNotAppendUnlinkedDbDa } [Test] - public async Task GetEducationOrganizationsByDataStore_EnrichesLinkedDbDataStoreFields() + public async Task GetEducationOrganizationsByDataStore_EnrichesLinkedDataStoreManageFields() { var dataStoreId = 7; A.CallTo(() => _getDataStoreQuery.Execute(dataStoreId)).Returns(new EdFi.Admin.DataAccess.Models.OdsInstance { OdsInstanceId = dataStoreId }); @@ -68,14 +68,14 @@ public async Task GetEducationOrganizationsByDataStore_EnrichesLinkedDbDataStore { new() { Id = dataStoreId, Name = "DataStore7" } }); - A.CallTo(() => _getDbDataStoresQuery.Execute(A._, null, null)) - .Returns(new List + A.CallTo(() => _getDataStoreManagesQuery.Execute(A._, null, null)) + .Returns(new List { - new DbInstance { Id = 5, OdsInstanceId = dataStoreId, Status = "Healthy", DatabaseTemplate = "Minimal", DatabaseName = "EdFi_Ods_7" } + new OdsInstanceManage { Id = 5, OdsInstanceId = dataStoreId, Status = "Healthy", DatabaseTemplate = "Minimal", DatabaseName = "EdFi_Ods_7" } }); var result = await ReadEducationOrganizations.GetEducationOrganizationsByDataStore( - _getEdOrgsQuery, _getDataStoreQuery, _getDbDataStoresQuery, _queryParams, dataStoreId); + _getEdOrgsQuery, _getDataStoreQuery, _getDataStoreManagesQuery, _queryParams, dataStoreId); var ok = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; ok.ShouldNotBeNull(); @@ -92,6 +92,6 @@ public void GetEducationOrganizationsByDataStore_WhenDataStoreNotFound_ThrowsNot Should.Throw>(async () => await ReadEducationOrganizations.GetEducationOrganizationsByDataStore( - _getEdOrgsQuery, _getDataStoreQuery, _getDbDataStoresQuery, _queryParams, 99)); + _getEdOrgsQuery, _getDataStoreQuery, _getDataStoreManagesQuery, _queryParams, 99)); } } diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/ReadTenantsTest.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/ReadTenantsTest.cs index 83154a4a3..a887a4ec9 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/ReadTenantsTest.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/ReadTenantsTest.cs @@ -26,15 +26,15 @@ public class ReadTenantsTest { private IGetDataStoresQuery _getDataStoresQuery = null!; private IGetEducationOrganizationQuery _getEducationOrganizationQuery = null!; - private IGetDbDataStoresQuery _getDbDataStoresQuery = null!; + private IGetDataStoreManagesQuery _getDataStoreManagesQuery = null!; [SetUp] public void SetUp() { _getDataStoresQuery = A.Fake(); _getEducationOrganizationQuery = A.Fake(); - _getDbDataStoresQuery = A.Fake(); - A.CallTo(() => _getDbDataStoresQuery.Execute(A._, A._, A.Ignored)) + _getDataStoreManagesQuery = A.Fake(); + A.CallTo(() => _getDataStoreManagesQuery.Execute(A._, A._, A.Ignored)) .Returns([]); } @@ -75,9 +75,9 @@ public async Task GetTenantEdOrgsByInstancesAsync_ReturnsOk_WhenTenantExists() A.CallTo(() => request.Path).Returns(new PathString("/tenants/tenant1/OdsInstances/edOrgs")); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, tenantName)).Returns(tenantDetailModel); - var result = await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, memoryCache, options, swaggerOptions, tenantName); + var result = await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, memoryCache, options, swaggerOptions, tenantName); result.ShouldNotBeNull(); } @@ -113,14 +113,14 @@ public async Task GetTenantEdOrgsByInstancesAsync_ReturnsNullId_WhenDataStoreIdI A.CallTo(() => request.Path).Returns(new PathString("/tenants/tenant1/OdsInstances/edOrgs")); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, tenantName)).Returns(tenantDetailModel); var result = await ReadTenants.GetTenantEdOrgsByDataStoresAsync( request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, - _getDbDataStoresQuery, + _getDataStoreManagesQuery, memoryCache, options, swaggerOptions, @@ -153,7 +153,7 @@ public void GetTenantEdOrgsByInstancesAsync_ThrowsValidationException_WhenTenant Should.ThrowAsync(async () => { - await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, memoryCache, options, swaggerOptions, tenantName); + await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, memoryCache, options, swaggerOptions, tenantName); }); } @@ -177,7 +177,7 @@ public void GetTenantEdOrgsByInstancesAsync_ThrowsValidationException_WhenTenant Should.ThrowAsync(async () => { - await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, memoryCache, options, swaggerOptions, tenantName); + await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, memoryCache, options, swaggerOptions, tenantName); }); } @@ -204,9 +204,9 @@ public async Task GetTenantEdOrgsByInstancesAsync_SkipsTenantHeaderValidation_Wh A.CallTo(() => request.Path).Returns(new PathString("/swagger/index.html")); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, tenantName)).Returns(tenantDetailModel); - var result = await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, memoryCache, options, swaggerOptions, tenantName); + var result = await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, memoryCache, options, swaggerOptions, tenantName); result.ShouldNotBeNull(); } @@ -234,9 +234,9 @@ public async Task GetTenantEdOrgsByInstancesAsync_SkipsTenantHeaderValidation_Wh A.CallTo(() => request.Path).Returns(new PathString("/tenants/tenant1/OdsInstances/edOrgs")); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, tenantName)).Returns(tenantDetailModel); - var result = await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, memoryCache, options, swaggerOptions, tenantName); + var result = await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, memoryCache, options, swaggerOptions, tenantName); result.ShouldNotBeNull(); } @@ -264,9 +264,9 @@ public async Task GetTenantEdOrgsByInstancesAsync_SkipsTenantHeaderValidation_Wh A.CallTo(() => request.Path).Returns(new PathString("/SWAGGER/index.html")); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, tenantName)).Returns(tenantDetailModel); - var result = await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, memoryCache, options, swaggerOptions, tenantName); + var result = await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, memoryCache, options, swaggerOptions, tenantName); result.ShouldNotBeNull(); } @@ -294,9 +294,9 @@ public async Task GetTenantEdOrgsByInstancesAsync_SkipsTenantHeaderValidation_Wh A.CallTo(() => request.Path).Returns(new PathString("/tenants/tenant1/OdsInstances/edOrgs")); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, tenantName)).Returns(tenantDetailModel); - var result = await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, memoryCache, options, swaggerOptions, tenantName); + var result = await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, memoryCache, options, swaggerOptions, tenantName); result.ShouldNotBeNull(); } @@ -321,7 +321,7 @@ public void GetTenantEdOrgsByInstancesAsync_EnforcesTenantHeaderValidation_WhenN Should.ThrowAsync(async () => { - await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, memoryCache, options, swaggerOptions, tenantName); + await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, memoryCache, options, swaggerOptions, tenantName); }); } @@ -348,9 +348,9 @@ public async Task GetTenantEdOrgsByInstancesAsync_SkipsTenantHeaderValidation_Wh A.CallTo(() => request.Path).Returns(new PathString()); A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres", MultiTenancy = true }); A.CallTo(() => swaggerOptions.Value).Returns(new SwaggerSettings { EnableSwagger = true }); - A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, tenantName)).Returns(tenantDetailModel); + A.CallTo(() => tenantsService.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, tenantName)).Returns(tenantDetailModel); - var result = await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, memoryCache, options, swaggerOptions, tenantName); + var result = await ReadTenants.GetTenantEdOrgsByDataStoresAsync(request, tenantsService, _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, memoryCache, options, swaggerOptions, tenantName); result.ShouldNotBeNull(); } diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/AddDbDataStoreCommandTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/AddDataStoreManageCommandTests.cs similarity index 74% rename from Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/AddDbDataStoreCommandTests.cs rename to Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/AddDataStoreManageCommandTests.cs index 42ce0098e..0bb86d70a 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/AddDbDataStoreCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/AddDataStoreManageCommandTests.cs @@ -19,12 +19,12 @@ namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Database.Commands; [TestFixture] -public class AddDbDataStoreCommandTests +public class AddDataStoreManageCommandTests { private static AdminApiDbContext CreateContext() { var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"AddDbInstanceCommand_{Guid.NewGuid()}") + .UseInMemoryDatabase(databaseName: $"AddDataStoreManageCommand_{Guid.NewGuid()}") .Options; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary @@ -36,11 +36,11 @@ private static AdminApiDbContext CreateContext() } [Test] - public void Execute_WithValidModel_PersistsDbInstance() + public void Execute_WithValidModel_PersistsOdsInstanceManage() { using var context = CreateContext(); - var command = new AddDbDataStoreCommand(context); - var model = new AddDbInstanceModelStub + var command = new AddDataStoreManageCommand(context); + var model = new AddDataStoreManageModelStub { Name = "Test Instance", DatabaseTemplate = "Minimal" @@ -49,16 +49,16 @@ public void Execute_WithValidModel_PersistsDbInstance() var result = command.Execute(model); result.Id.ShouldBeGreaterThan(0); - context.DbInstances.Any(d => d.Id == result.Id).ShouldBeTrue(); + context.OdsInstanceManages.Any(d => d.Id == result.Id).ShouldBeTrue(); } [Test] public void Execute_WithValidModel_SetsExpectedFieldValues() { using var context = CreateContext(); - var command = new AddDbDataStoreCommand(context); + var command = new AddDataStoreManageCommand(context); var before = DateTime.UtcNow; - var model = new AddDbInstanceModelStub + var model = new AddDataStoreManageModelStub { Name = " Test Instance ", DatabaseTemplate = " Minimal " @@ -68,7 +68,7 @@ public void Execute_WithValidModel_SetsExpectedFieldValues() result.Name.ShouldBe("Test Instance"); result.DatabaseTemplate.ShouldBe("Minimal"); - result.Status.ShouldBe(DbInstanceStatus.PendingCreate.ToString()); + result.Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); result.OdsInstanceId.ShouldBeNull(); result.OdsInstanceName.ShouldBeNull(); result.DatabaseName.ShouldBeNull(); @@ -81,8 +81,8 @@ public void Execute_WithValidModel_SetsExpectedFieldValues() public void Execute_WithSampleTemplate_PersistsWithCorrectTemplate() { using var context = CreateContext(); - var command = new AddDbDataStoreCommand(context); - var model = new AddDbInstanceModelStub + var command = new AddDataStoreManageCommand(context); + var model = new AddDataStoreManageModelStub { Name = "Sample Instance", DatabaseTemplate = "Sample" @@ -91,10 +91,10 @@ public void Execute_WithSampleTemplate_PersistsWithCorrectTemplate() var result = command.Execute(model); result.DatabaseTemplate.ShouldBe("Sample"); - context.DbInstances.Any(d => d.Id == result.Id && d.DatabaseTemplate == "Sample").ShouldBeTrue(); + context.OdsInstanceManages.Any(d => d.Id == result.Id && d.DatabaseTemplate == "Sample").ShouldBeTrue(); } - private sealed class AddDbInstanceModelStub : IAddDbDataStoreModel + private sealed class AddDataStoreManageModelStub : IAddDataStoreManageModel { public string? Name { get; set; } public string? DatabaseTemplate { get; set; } @@ -102,6 +102,3 @@ private sealed class AddDbInstanceModelStub : IAddDbDataStoreModel } #nullable restore - - - diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/DeleteDbDataStoreCommandTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/DeleteDataStoreManageCommandTests.cs similarity index 70% rename from Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/DeleteDbDataStoreCommandTests.cs rename to Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/DeleteDataStoreManageCommandTests.cs index f2e5716dc..1c592f4ef 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/DeleteDbDataStoreCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/DeleteDataStoreManageCommandTests.cs @@ -21,12 +21,12 @@ namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Database.Commands; [TestFixture] -public class DeleteDbDataStoreCommandTests +public class DeleteDataStoreManageCommandTests { private static AdminApiDbContext CreateContext() { var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"DeleteDbInstanceCommand_{Guid.NewGuid()}") + .UseInMemoryDatabase(databaseName: $"DeleteDataStoreManageCommand_{Guid.NewGuid()}") .Options; var configuration = new ConfigurationBuilder() .AddInMemoryCollection( @@ -40,21 +40,21 @@ private static AdminApiDbContext CreateContext() public void Execute_SetsStatusToPendingDelete() { using var context = CreateContext(); - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Test Instance", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow, }; - context.DbInstances.Add(instance); + context.OdsInstanceManages.Add(instance); context.SaveChanges(); - var command = new DeleteDbDataStoreCommand(context); + var command = new DeleteDataStoreManageCommand(context); command.Execute(instance.Id); - var updated = context.DbInstances.Single(d => d.Id == instance.Id); - updated.Status.ShouldBe(DbInstanceStatus.PendingDelete.ToString()); + var updated = context.OdsInstanceManages.Single(d => d.Id == instance.Id); + updated.Status.ShouldBe(OdsInstanceManageStatus.PendingDelete.ToString()); } [Test] @@ -62,20 +62,20 @@ public void Execute_UpdatesLastModifiedDate() { using var context = CreateContext(); var before = DateTime.UtcNow; - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Test Instance", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow, }; - context.DbInstances.Add(instance); + context.OdsInstanceManages.Add(instance); context.SaveChanges(); - var command = new DeleteDbDataStoreCommand(context); + var command = new DeleteDataStoreManageCommand(context); command.Execute(instance.Id); - var updated = context.DbInstances.Single(d => d.Id == instance.Id); + var updated = context.OdsInstanceManages.Single(d => d.Id == instance.Id); updated.LastModifiedDate.ShouldNotBeNull(); updated.LastModifiedDate!.Value.ShouldBeGreaterThanOrEqualTo(before); } @@ -84,7 +84,7 @@ public void Execute_UpdatesLastModifiedDate() public void Execute_WithNonExistentId_ThrowsNotFoundException() { using var context = CreateContext(); - var command = new DeleteDbDataStoreCommand(context); + var command = new DeleteDataStoreManageCommand(context); Should.Throw>(() => command.Execute(9999)); } @@ -93,23 +93,20 @@ public void Execute_WithNonExistentId_ThrowsNotFoundException() public void Execute_WhenStatusIsDeleted_ThrowsNotFoundException() { using var context = CreateContext(); - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Test Instance", - Status = DbInstanceStatus.Deleted.ToString(), + Status = OdsInstanceManageStatus.Deleted.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow, }; - context.DbInstances.Add(instance); + context.OdsInstanceManages.Add(instance); context.SaveChanges(); - var command = new DeleteDbDataStoreCommand(context); + var command = new DeleteDataStoreManageCommand(context); Should.Throw>(() => command.Execute(instance.Id)); } } #nullable restore - - - diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Queries/GetDataStoreManageByIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Queries/GetDataStoreManageByIdQueryTests.cs new file mode 100644 index 000000000..7427ca20b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Queries/GetDataStoreManageByIdQueryTests.cs @@ -0,0 +1,69 @@ +// 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. + +#nullable enable + +using System; +using System.Collections.Generic; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; +using EdFi.Ods.AdminApi.V3.Infrastructure; +using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Database.Queries; + +[TestFixture] +public class GetDataStoreManageByIdQueryTests +{ + private static AdminApiDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: $"GetDataStoreManageByIdQueryTests_{Guid.NewGuid()}") + .Options; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AppSettings:DatabaseEngine"] = "Postgres" + }) + .Build(); + + return new AdminApiDbContext(options, configuration); + } + + [Test] + public void Execute_WithExistingId_ReturnsOdsInstanceManage() + { + using var context = CreateContext(); + var odsInstanceManage = new OdsInstanceManage + { + Name = "Sandbox", + Status = "Healthy", + DatabaseTemplate = "Minimal" + }; + context.OdsInstanceManages.Add(odsInstanceManage); + context.SaveChanges(); + + var query = new GetDataStoreManageByIdQuery(context); + + var result = query.Execute(odsInstanceManage.Id); + + result.ShouldNotBeNull(); + result.Name.ShouldBe("Sandbox"); + } + + [Test] + public void Execute_WithUnknownId_ReturnsNull() + { + using var context = CreateContext(); + var query = new GetDataStoreManageByIdQuery(context); + + var result = query.Execute(999); + + result.ShouldBeNull(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Queries/GetDataStoreManagesQueryTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Queries/GetDataStoreManagesQueryTests.cs new file mode 100644 index 000000000..4778a0c12 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Queries/GetDataStoreManagesQueryTests.cs @@ -0,0 +1,78 @@ +// 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. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Models; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.V3.Infrastructure; +using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Database.Queries; + +[TestFixture] +public class GetDataStoreManagesQueryTests +{ + private static AdminApiDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: $"GetDataStoreManagesQueryTests_{Guid.NewGuid()}") + .Options; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AppSettings:DatabaseEngine"] = "Postgres" + }) + .Build(); + + return new AdminApiDbContext(options, configuration); + } + + private static IOptions DefaultOptions() => + Options.Create(new AppSettings { DatabaseEngine = "Postgres", DefaultPageSizeLimit = 25 }); + + [Test] + public void Execute_WithoutFilters_ReturnsAllOdsInstanceManages() + { + using var context = CreateContext(); + context.OdsInstanceManages.AddRange( + new OdsInstanceManage { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, + new OdsInstanceManage { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); + context.SaveChanges(); + + var query = new GetDataStoreManagesQuery(context, DefaultOptions()); + + var result = query.Execute(new CommonQueryParams(0, 25), null, null); + + result.Count.ShouldBe(2); + result.Select(x => x.Name).ShouldBe(["Sandbox A", "Sandbox B"], ignoreOrder: true); + } + + [Test] + public void Execute_WithNameFilter_ReturnsMatchingOdsInstanceManage() + { + using var context = CreateContext(); + context.OdsInstanceManages.AddRange( + new OdsInstanceManage { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, + new OdsInstanceManage { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); + context.SaveChanges(); + + var query = new GetDataStoreManagesQuery(context, DefaultOptions()); + + var result = query.Execute(new CommonQueryParams(0, 25), null, "Sandbox B"); + + result.Count.ShouldBe(1); + result.Single().Name.ShouldBe("Sandbox B"); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/CreateInstanceJobTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/CreateInstanceJobTests.cs index c095b9ec3..1192febda 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/CreateInstanceJobTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/CreateInstanceJobTests.cs @@ -15,9 +15,9 @@ using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; using EdFi.Ods.AdminApi.Common.Infrastructure.Providers.Interfaces; +using EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.V3.Features.DbDataStores; using EdFi.Ods.AdminApi.V3.Infrastructure; using EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs; using EdFi.Ods.AdminApi.V3.Infrastructure.Services.Tenants; @@ -74,13 +74,13 @@ private static SqlServerUsersContext CreateUsersContext(string databaseName) return new NonDisposingSqlServerUsersContext(options); } - private static IJobExecutionContext CreateJobExecutionContext(int dbInstanceId, string tenantName = null) + private static IJobExecutionContext CreateJobExecutionContext(int odsInstanceManageId, string tenantName = null) { var jobExecutionContext = A.Fake(); var jobDetail = A.Fake(); var jobDataMap = new JobDataMap { - { JobConstants.DbInstanceIdKey, dbInstanceId } + { JobConstants.OdsInstanceManageIdKey, odsInstanceManageId } }; if (!string.IsNullOrWhiteSpace(tenantName)) @@ -152,11 +152,11 @@ public void CreateInstanceJob_ShouldPreventConcurrentExecution() [TestCase("EdFi Ods", "Minimal", "EdFi_Ods_Minimal")] public void BuildDatabaseName_UsesCanonicalFormat(string name, string databaseTemplate, string expectedDatabaseName) { - DbDataStoreDatabaseNameFormatter.Build(name, databaseTemplate).ShouldBe(expectedDatabaseName); + DataStoreManageDatabaseNameFormatter.Build(name, databaseTemplate).ShouldBe(expectedDatabaseName); } [Test] - public async Task Execute_CreatesOdsInstance_AndCompletesDbInstance() + public async Task Execute_CreatesOdsInstance_AndCompletesOdsInstanceManage() { var configuration = CreateConfiguration(); using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}", configuration); @@ -173,16 +173,16 @@ public async Task Execute_CreatesOdsInstance_AndCompletesDbInstance() .Invokes((string connectionString, byte[] _) => plaintextConnectionString = connectionString) .ReturnsLazily((string connectionString, byte[] _) => $"encrypted::{connectionString}"); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -199,16 +199,16 @@ public async Task Execute_CreatesOdsInstance_AndCompletesDbInstance() configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - var persistedDbInstance = adminApiContext.DbInstances.Single(); + var persistedOdsInstanceManage = adminApiContext.OdsInstanceManages.Single(); var persistedOdsInstance = usersContext.OdsInstances.Single(); const string expectedDatabaseName = "EdFi_Ods_Sandbox_Minimal"; - persistedDbInstance.Status.ShouldBe(DbInstanceStatus.Created.ToString()); - persistedDbInstance.DatabaseName.ShouldBe(expectedDatabaseName); - persistedDbInstance.OdsInstanceId.ShouldNotBeNull(); - persistedDbInstance.OdsInstanceName.ShouldBe("Sandbox"); + persistedOdsInstanceManage.Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); + persistedOdsInstanceManage.DatabaseName.ShouldBe(expectedDatabaseName); + persistedOdsInstanceManage.OdsInstanceId.ShouldNotBeNull(); + persistedOdsInstanceManage.OdsInstanceName.ShouldBe("Sandbox"); persistedOdsInstance.Name.ShouldBe("Sandbox"); persistedOdsInstance.InstanceType.ShouldBe("Minimal"); A.CallTo(() => sandboxProvisioner.AddSandboxAsync(expectedDatabaseName, SandboxType.Minimal)) @@ -231,16 +231,16 @@ public async Task Execute_FormatsDatabaseName_WhenNameContainsSpaces() var encryptionProvider = A.Fake(); var sandboxProvisioner = A.Fake(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "My District", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -257,9 +257,9 @@ public async Task Execute_FormatsDatabaseName_WhenNameContainsSpaces() configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().DatabaseName.ShouldBe("EdFi_Ods_My_District_Minimal"); + adminApiContext.OdsInstanceManages.Single().DatabaseName.ShouldBe("EdFi_Ods_My_District_Minimal"); } [Test] @@ -281,17 +281,17 @@ public async Task Execute_ReusesExistingDatabaseName_WhenAlreadyAssigned() .Invokes((string connectionString, byte[] _) => plaintextConnectionString = connectionString) .ReturnsLazily((string connectionString, byte[] _) => $"encrypted::{connectionString}"); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", DatabaseName = existingDatabaseName, - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -308,16 +308,16 @@ public async Task Execute_ReusesExistingDatabaseName_WhenAlreadyAssigned() configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().DatabaseName.ShouldBe(existingDatabaseName); + adminApiContext.OdsInstanceManages.Single().DatabaseName.ShouldBe(existingDatabaseName); plaintextConnectionString.ShouldContain($"Initial Catalog={existingDatabaseName}"); A.CallTo(() => sandboxProvisioner.AddSandboxAsync(existingDatabaseName, SandboxType.Minimal)) .MustHaveHappenedOnceExactly(); } [Test] - public async Task Execute_DoesNothing_WhenDbInstanceIsNotPending() + public async Task Execute_DoesNothing_WhenOdsInstanceManageIsNotPending() { var configuration = CreateConfiguration(); using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}", configuration); @@ -329,16 +329,16 @@ public async Task Execute_DoesNothing_WhenDbInstanceIsNotPending() var encryptionProvider = A.Fake(); var sandboxProvisioner = A.Fake(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.Created.ToString(), + Status = OdsInstanceManageStatus.Created.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -355,16 +355,16 @@ public async Task Execute_DoesNothing_WhenDbInstanceIsNotPending() configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.Created.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); usersContext.OdsInstances.ShouldBeEmpty(); A.CallTo(() => sandboxProvisioner.AddSandboxAsync(A._, A._)).MustNotHaveHappened(); A.CallTo(() => encryptionProvider.Encrypt(A._, A._)).MustNotHaveHappened(); } [Test] - public async Task Execute_SetsDbInstanceToError_When_ProvisioningFails() + public async Task Execute_SetsOdsInstanceManageToError_When_ProvisioningFails() { var configuration = CreateConfiguration(); using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}", configuration); @@ -379,16 +379,16 @@ public async Task Execute_SetsDbInstanceToError_When_ProvisioningFails() A.CallTo(() => sandboxProvisioner.AddSandboxAsync(A._, A._)) .Throws(new InvalidOperationException("Provisioning failed.")); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -405,16 +405,16 @@ public async Task Execute_SetsDbInstanceToError_When_ProvisioningFails() configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.CreateFailed.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.CreateFailed.ToString()); usersContext.OdsInstances.ShouldBeEmpty(); A.CallTo(() => jobStatusService.SetStatusAsync(A._, QuartzJobStatus.Error, A._, A.That.Contains("Provisioning failed."))) .MustHaveHappenedOnceExactly(); } [Test] - public async Task Execute_SetsDbInstanceToError_WhenPendingStateAlreadyContainsOdsReferences() + public async Task Execute_SetsOdsInstanceManageToError_WhenPendingStateAlreadyContainsOdsReferences() { var configuration = CreateConfiguration(); using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}", configuration); @@ -426,18 +426,18 @@ public async Task Execute_SetsDbInstanceToError_WhenPendingStateAlreadyContainsO var encryptionProvider = A.Fake(); var sandboxProvisioner = A.Fake(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), OdsInstanceId = 42, OdsInstanceName = "Sandbox", LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -454,9 +454,9 @@ public async Task Execute_SetsDbInstanceToError_WhenPendingStateAlreadyContainsO configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.CreateFailed.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.CreateFailed.ToString()); usersContext.OdsInstances.ShouldBeEmpty(); A.CallTo(() => sandboxProvisioner.AddSandboxAsync(A._, A._)).MustNotHaveHappened(); A.CallTo(() => jobStatusService.SetStatusAsync(A._, QuartzJobStatus.Error, A._, A.That.Contains("invalid pending state"))) @@ -487,16 +487,16 @@ public async Task Execute_UsesTenantSpecificOdsConnectionString_WhenMultiTenancy .Invokes((string connectionString, byte[] _) => plaintextConnectionString = connectionString) .ReturnsLazily((string connectionString, byte[] _) => $"encrypted::{connectionString}"); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Sample", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - tenantAdminApiContext.DbInstances.Add(dbInstance); + tenantAdminApiContext.OdsInstanceManages.Add(odsInstanceManage); tenantAdminApiContext.SaveChanges(); var job = new CreateInstanceJob( @@ -513,14 +513,14 @@ public async Task Execute_UsesTenantSpecificOdsConnectionString_WhenMultiTenancy configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id, "tenant1")); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id, "tenant1")); - var persistedDbInstance = tenantAdminApiContext.DbInstances.Single(); + var persistedOdsInstanceManage = tenantAdminApiContext.OdsInstanceManages.Single(); var persistedOdsInstance = tenantUsersContext.OdsInstances.Single(); const string expectedDatabaseName = "EdFi_Ods_Sandbox_Sample"; - persistedDbInstance.Status.ShouldBe(DbInstanceStatus.Created.ToString()); - persistedDbInstance.DatabaseName.ShouldBe(expectedDatabaseName); + persistedOdsInstanceManage.Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); + persistedOdsInstanceManage.DatabaseName.ShouldBe(expectedDatabaseName); persistedOdsInstance.InstanceType.ShouldBe("Sample"); plaintextConnectionString.ShouldNotBeNull(); plaintextConnectionString.ShouldContain($"Initial Catalog={expectedDatabaseName}"); @@ -551,16 +551,16 @@ public async Task Execute_ReusesExistingOdsInstance_WhenFinalNameAlreadyExists() A.CallTo(() => encryptionProvider.Encrypt(A._, A._)) .Returns(encryptedConnectionString); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); usersContext.OdsInstances.Add(new OdsInstance @@ -585,16 +585,16 @@ public async Task Execute_ReusesExistingOdsInstance_WhenFinalNameAlreadyExists() configuration, new DbConnectionStringBuilderAdapterFactory(new SqlConnectionStringBuilderAdapter())); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - var persistedDbInstance = adminApiContext.DbInstances.Single(); + var persistedOdsInstanceManage = adminApiContext.OdsInstanceManages.Single(); var persistedOdsInstance = usersContext.OdsInstances.Single(); const string expectedDatabaseName = "EdFi_Ods_Sandbox_Minimal"; - persistedDbInstance.Status.ShouldBe(DbInstanceStatus.Created.ToString()); - persistedDbInstance.DatabaseName.ShouldBe(expectedDatabaseName); - persistedDbInstance.OdsInstanceId.ShouldBe(persistedOdsInstance.OdsInstanceId); - persistedDbInstance.OdsInstanceName.ShouldBe("Sandbox"); + persistedOdsInstanceManage.Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); + persistedOdsInstanceManage.DatabaseName.ShouldBe(expectedDatabaseName); + persistedOdsInstanceManage.OdsInstanceId.ShouldBe(persistedOdsInstance.OdsInstanceId); + persistedOdsInstanceManage.OdsInstanceName.ShouldBe("Sandbox"); persistedOdsInstance.ConnectionString.ShouldBe(encryptedConnectionString); usersContext.OdsInstances.Count().ShouldBe(1); A.CallTo(() => sandboxProvisioner.AddSandboxAsync(expectedDatabaseName, SandboxType.Minimal)) diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJobTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/CreatePendingDataStoreManagesDispatcherJobTests.cs similarity index 78% rename from Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJobTests.cs rename to Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/CreatePendingDataStoreManagesDispatcherJobTests.cs index 7d1da447e..b42228269 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJobTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/CreatePendingDataStoreManagesDispatcherJobTests.cs @@ -26,7 +26,7 @@ namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Services.Jobs; [TestFixture] -public class CreatePendingDbInstancesDispatcherJobTests +public class CreatePendingDataStoreManagesDispatcherJobTests { private sealed class NonDisposingAdminApiDbContext( DbContextOptions options, @@ -59,7 +59,7 @@ private static IOptions CreateOptions(bool multiTenancy = false, in { DatabaseEngine = "SqlServer", MultiTenancy = multiTenancy, - CreateDbInstancesMaxRetryAttempts = maxRetryAttempts + CreateOdsInstanceManagesMaxRetryAttempts = maxRetryAttempts }); private static IScheduler CreateScheduler(out List scheduledJobs) @@ -88,7 +88,7 @@ private static IJobExecutionContext CreateJobExecutionContext(IScheduler schedul jobDataMap.Put(JobConstants.TenantNameKey, tenantName); } - A.CallTo(() => jobDetail.Key).Returns(new JobKey(JobConstants.CreatePendingDbInstancesDispatcherJobName)); + A.CallTo(() => jobDetail.Key).Returns(new JobKey(JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName)); A.CallTo(() => jobExecutionContext.JobDetail).Returns(jobDetail); A.CallTo(() => jobExecutionContext.FireInstanceId).Returns(Guid.NewGuid().ToString()); A.CallTo(() => jobExecutionContext.MergedJobDataMap).Returns(jobDataMap); @@ -98,25 +98,25 @@ private static IJobExecutionContext CreateJobExecutionContext(IScheduler schedul } [Test] - public async Task Execute_SchedulesPendingDbInstance() + public async Task Execute_SchedulesPendingOdsInstanceManage() { using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}"); var tenantSpecificDbContextProvider = A.Fake(); var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out var scheduledJobs); - adminApiContext.DbInstances.Add(new Common.Infrastructure.Models.DbInstance + adminApiContext.OdsInstanceManages.Add(new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }); adminApiContext.SaveChanges(); - var job = new CreatePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new CreatePendingDataStoreManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, @@ -125,38 +125,38 @@ public async Task Execute_SchedulesPendingDbInstance() await job.Execute(CreateJobExecutionContext(scheduler)); scheduledJobs.Count.ShouldBe(1); - scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-{adminApiContext.DbInstances.Single().Id}"); + scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-{adminApiContext.OdsInstanceManages.Single().Id}"); } [Test] - public async Task Execute_RequeuesRetryableErrorDbInstance() + public async Task Execute_RequeuesRetryableErrorOdsInstanceManage() { using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}"); var tenantSpecificDbContextProvider = A.Fake(); var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out var scheduledJobs); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.CreateFailed.ToString(), + Status = OdsInstanceManageStatus.CreateFailed.ToString(), DatabaseName = "existingdb", LastRefreshed = DateTime.UtcNow.AddMinutes(-10), LastModifiedDate = DateTime.UtcNow.AddMinutes(-10) }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); adminApiContext.JobStatuses.Add(new JobStatus { - JobId = $"{CreateInstanceJob.BuildJobIdentity(dbInstance.Id, null)}_run-1", + JobId = $"{CreateInstanceJob.BuildJobIdentity(odsInstanceManage.Id, null)}_run-1", Status = QuartzJobStatus.Error.ToString() }); adminApiContext.SaveChanges(); - var job = new CreatePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new CreatePendingDataStoreManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, @@ -164,7 +164,7 @@ public async Task Execute_RequeuesRetryableErrorDbInstance() await job.Execute(CreateJobExecutionContext(scheduler)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.PendingCreate.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); scheduledJobs.Count.ShouldBe(1); } @@ -176,31 +176,31 @@ public async Task Execute_SetsCreateError_WhenRetryLimitIsReached() var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out var scheduledJobs); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.CreateFailed.ToString(), + Status = OdsInstanceManageStatus.CreateFailed.ToString(), LastRefreshed = DateTime.UtcNow.AddMinutes(-10), LastModifiedDate = DateTime.UtcNow.AddMinutes(-10) }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); for (var attempt = 1; attempt <= 3; attempt++) { adminApiContext.JobStatuses.Add(new JobStatus { - JobId = $"{CreateInstanceJob.BuildJobIdentity(dbInstance.Id, null)}_run-{attempt}", + JobId = $"{CreateInstanceJob.BuildJobIdentity(odsInstanceManage.Id, null)}_run-{attempt}", Status = QuartzJobStatus.Error.ToString() }); } adminApiContext.SaveChanges(); - var job = new CreatePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new CreatePendingDataStoreManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, @@ -208,7 +208,7 @@ public async Task Execute_SetsCreateError_WhenRetryLimitIsReached() await job.Execute(CreateJobExecutionContext(scheduler)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.CreateError.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.CreateError.ToString()); scheduledJobs.ShouldBeEmpty(); } @@ -224,18 +224,18 @@ public async Task Execute_UsesTenantSpecificContext_WhenMultiTenancyIsEnabled() A.CallTo(() => tenantSpecificDbContextProvider.GetAdminApiDbContext("tenant1")) .Returns(tenantAdminApiContext); - tenantAdminApiContext.DbInstances.Add(new Common.Infrastructure.Models.DbInstance + tenantAdminApiContext.OdsInstanceManages.Add(new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Sample", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }); tenantAdminApiContext.SaveChanges(); - var job = new CreatePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new CreatePendingDataStoreManagesDispatcherJob( + A.Fake>(), jobStatusService, defaultAdminApiContext, tenantSpecificDbContextProvider, @@ -244,7 +244,7 @@ public async Task Execute_UsesTenantSpecificContext_WhenMultiTenancyIsEnabled() await job.Execute(CreateJobExecutionContext(scheduler, "tenant1")); scheduledJobs.Count.ShouldBe(1); - scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-tenant1-{tenantAdminApiContext.DbInstances.Single().Id}"); + scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.CreateInstanceJobName}-tenant1-{tenantAdminApiContext.OdsInstanceManages.Single().Id}"); scheduledJobs[0].JobDataMap.GetString(JobConstants.TenantNameKey).ShouldBe("tenant1"); } } diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/DeleteInstanceJobTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/DeleteInstanceJobTests.cs index 0db3936f0..c34fd2d12 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/DeleteInstanceJobTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/DeleteInstanceJobTests.cs @@ -77,13 +77,13 @@ private static SqlServerUsersContext CreateUsersContext(string databaseName) return new NonDisposingSqlServerUsersContext(options); } - private static IJobExecutionContext CreateJobExecutionContext(int dbInstanceId, string tenantName = null) + private static IJobExecutionContext CreateJobExecutionContext(int odsInstanceManageId, string tenantName = null) { var jobExecutionContext = A.Fake(); var jobDetail = A.Fake(); var jobDataMap = new JobDataMap { - { JobConstants.DbInstanceIdKey, dbInstanceId } + { JobConstants.OdsInstanceManageIdKey, odsInstanceManageId } }; if (!string.IsNullOrWhiteSpace(tenantName)) @@ -149,18 +149,18 @@ public async Task Execute_DeletesDatabaseAndOdsInstance_AndSetsDeletedStatus() usersContext.OdsInstances.Add(odsInstance); usersContext.SaveChanges(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), DatabaseName = "EdFi_Ods_Sandbox_Minimal", OdsInstanceId = odsInstance.OdsInstanceId, LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new DeleteInstanceJob( @@ -174,9 +174,9 @@ public async Task Execute_DeletesDatabaseAndOdsInstance_AndSetsDeletedStatus() sandboxProvisioner, CreateOptions()); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.Deleted.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); usersContext.OdsInstances.ShouldBeEmpty(); A.CallTo(() => sandboxProvisioner.DeleteSandboxesAsync("EdFi_Ods_Sandbox_Minimal")) .MustHaveHappenedOnceExactly(); @@ -190,17 +190,17 @@ public async Task Execute_SkipsDatabaseDrop_WhenDatabaseNameIsNull() var jobStatusService = A.Fake(); var sandboxProvisioner = A.Fake(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), DatabaseName = null, LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new DeleteInstanceJob( @@ -214,9 +214,9 @@ public async Task Execute_SkipsDatabaseDrop_WhenDatabaseNameIsNull() sandboxProvisioner, CreateOptions()); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.Deleted.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); A.CallTo(() => sandboxProvisioner.DeleteSandboxesAsync(A._)).MustNotHaveHappened(); } @@ -228,18 +228,18 @@ public async Task Execute_SkipsOdsInstanceRemoval_WhenOdsInstanceIdIsNull() var jobStatusService = A.Fake(); var sandboxProvisioner = A.Fake(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), DatabaseName = "EdFi_Ods_Sandbox_Minimal", OdsInstanceId = null, LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new DeleteInstanceJob( @@ -253,31 +253,31 @@ public async Task Execute_SkipsOdsInstanceRemoval_WhenOdsInstanceIdIsNull() sandboxProvisioner, CreateOptions()); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.Deleted.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); usersContext.OdsInstances.ShouldBeEmpty(); } [Test] - public async Task Execute_DoesNothing_WhenDbInstanceIsNotPendingDelete() + public async Task Execute_DoesNothing_WhenOdsInstanceManageIsNotPendingDelete() { using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}"); using var usersContext = CreateUsersContext($"Users_{Guid.NewGuid()}"); var jobStatusService = A.Fake(); var sandboxProvisioner = A.Fake(); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.Created.ToString(), + Status = OdsInstanceManageStatus.Created.ToString(), DatabaseName = "EdFi_Ods_Sandbox_Minimal", LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new DeleteInstanceJob( @@ -291,9 +291,9 @@ public async Task Execute_DoesNothing_WhenDbInstanceIsNotPendingDelete() sandboxProvisioner, CreateOptions()); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.Created.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); A.CallTo(() => sandboxProvisioner.DeleteSandboxesAsync(A._)).MustNotHaveHappened(); } @@ -308,17 +308,17 @@ public async Task Execute_SetsDeleteFailed_WhenProvisionerThrows() A.CallTo(() => sandboxProvisioner.DeleteSandboxesAsync(A._)) .Throws(new InvalidOperationException("Drop failed.")); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), DatabaseName = "EdFi_Ods_Sandbox_Minimal", LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); var job = new DeleteInstanceJob( @@ -332,9 +332,9 @@ public async Task Execute_SetsDeleteFailed_WhenProvisionerThrows() sandboxProvisioner, CreateOptions()); - await job.Execute(CreateJobExecutionContext(dbInstance.Id)); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.DeleteFailed.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.DeleteFailed.ToString()); } [Test] @@ -354,17 +354,17 @@ public async Task Execute_UsesTenantSpecificContext_WhenMultiTenancyIsEnabled() A.CallTo(() => tenantSpecificDbContextProvider.GetUsersContext("tenant1")) .Returns(tenantUsersContext); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), DatabaseName = "EdFi_Ods_Sandbox_Minimal", LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }; - tenantAdminApiContext.DbInstances.Add(dbInstance); + tenantAdminApiContext.OdsInstanceManages.Add(odsInstanceManage); tenantAdminApiContext.SaveChanges(); var job = new DeleteInstanceJob( @@ -378,9 +378,9 @@ public async Task Execute_UsesTenantSpecificContext_WhenMultiTenancyIsEnabled() sandboxProvisioner, CreateOptions(multiTenancy: true)); - await job.Execute(CreateJobExecutionContext(dbInstance.Id, "tenant1")); + await job.Execute(CreateJobExecutionContext(odsInstanceManage.Id, "tenant1")); - tenantAdminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.Deleted.ToString()); - defaultAdminApiContext.DbInstances.ShouldBeEmpty(); + tenantAdminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); + defaultAdminApiContext.OdsInstanceManages.ShouldBeEmpty(); } } diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJobTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/DeletePendingDataStoreManagesDispatcherJobTests.cs similarity index 78% rename from Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJobTests.cs rename to Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/DeletePendingDataStoreManagesDispatcherJobTests.cs index dd6140e29..34fcd88b9 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJobTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs/DeletePendingDataStoreManagesDispatcherJobTests.cs @@ -26,7 +26,7 @@ namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Services.Jobs; [TestFixture] -public class DeletePendingDbInstancesDispatcherJobTests +public class DeletePendingDataStoreManagesDispatcherJobTests { private sealed class NonDisposingAdminApiDbContext( DbContextOptions options, @@ -59,7 +59,7 @@ private static IOptions CreateOptions(bool multiTenancy = false, in { DatabaseEngine = "SqlServer", MultiTenancy = multiTenancy, - DeleteDbInstancesMaxRetryAttempts = maxRetryAttempts + DeleteOdsInstanceManagesMaxRetryAttempts = maxRetryAttempts }); private static IScheduler CreateScheduler(out List scheduledJobs) @@ -88,7 +88,7 @@ private static IJobExecutionContext CreateJobExecutionContext(IScheduler schedul jobDataMap.Put(JobConstants.TenantNameKey, tenantName); } - A.CallTo(() => jobDetail.Key).Returns(new JobKey(JobConstants.DeletePendingDbInstancesDispatcherJobName)); + A.CallTo(() => jobDetail.Key).Returns(new JobKey(JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName)); A.CallTo(() => jobExecutionContext.JobDetail).Returns(jobDetail); A.CallTo(() => jobExecutionContext.FireInstanceId).Returns(Guid.NewGuid().ToString()); A.CallTo(() => jobExecutionContext.MergedJobDataMap).Returns(jobDataMap); @@ -98,25 +98,25 @@ private static IJobExecutionContext CreateJobExecutionContext(IScheduler schedul } [Test] - public async Task Execute_SchedulesPendingDeleteDbInstance() + public async Task Execute_SchedulesPendingDeleteOdsInstanceManage() { using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}"); var tenantSpecificDbContextProvider = A.Fake(); var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out var scheduledJobs); - adminApiContext.DbInstances.Add(new Common.Infrastructure.Models.DbInstance + adminApiContext.OdsInstanceManages.Add(new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }); adminApiContext.SaveChanges(); - var job = new DeletePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new DeletePendingDataStoreManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, @@ -125,38 +125,38 @@ public async Task Execute_SchedulesPendingDeleteDbInstance() await job.Execute(CreateJobExecutionContext(scheduler)); scheduledJobs.Count.ShouldBe(1); - scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.DeleteInstanceJobName}-{adminApiContext.DbInstances.Single().Id}"); + scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.DeleteInstanceJobName}-{adminApiContext.OdsInstanceManages.Single().Id}"); } [Test] - public async Task Execute_RequeuesRetryableDeleteFailedDbInstance() + public async Task Execute_RequeuesRetryableDeleteFailedOdsInstanceManage() { using var adminApiContext = CreateAdminApiContext($"Admin_{Guid.NewGuid()}"); var tenantSpecificDbContextProvider = A.Fake(); var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out var scheduledJobs); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.DeleteFailed.ToString(), + Status = OdsInstanceManageStatus.DeleteFailed.ToString(), DatabaseName = "EdFi_Ods_Sandbox_Minimal", LastRefreshed = DateTime.UtcNow.AddMinutes(-10), LastModifiedDate = DateTime.UtcNow.AddMinutes(-10) }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); adminApiContext.JobStatuses.Add(new JobStatus { - JobId = $"{DeleteInstanceJob.BuildJobIdentity(dbInstance.Id, null)}_run-1", + JobId = $"{DeleteInstanceJob.BuildJobIdentity(odsInstanceManage.Id, null)}_run-1", Status = QuartzJobStatus.Error.ToString() }); adminApiContext.SaveChanges(); - var job = new DeletePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new DeletePendingDataStoreManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, @@ -164,7 +164,7 @@ public async Task Execute_RequeuesRetryableDeleteFailedDbInstance() await job.Execute(CreateJobExecutionContext(scheduler)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.PendingDelete.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.PendingDelete.ToString()); scheduledJobs.Count.ShouldBe(1); } @@ -176,31 +176,31 @@ public async Task Execute_SetsDeleteError_WhenRetryLimitIsReached() var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out var scheduledJobs); - var dbInstance = new Common.Infrastructure.Models.DbInstance + var odsInstanceManage = new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.DeleteFailed.ToString(), + Status = OdsInstanceManageStatus.DeleteFailed.ToString(), LastRefreshed = DateTime.UtcNow.AddMinutes(-10), LastModifiedDate = DateTime.UtcNow.AddMinutes(-10) }; - adminApiContext.DbInstances.Add(dbInstance); + adminApiContext.OdsInstanceManages.Add(odsInstanceManage); adminApiContext.SaveChanges(); for (var attempt = 1; attempt <= 3; attempt++) { adminApiContext.JobStatuses.Add(new JobStatus { - JobId = $"{DeleteInstanceJob.BuildJobIdentity(dbInstance.Id, null)}_run-{attempt}", + JobId = $"{DeleteInstanceJob.BuildJobIdentity(odsInstanceManage.Id, null)}_run-{attempt}", Status = QuartzJobStatus.Error.ToString() }); } adminApiContext.SaveChanges(); - var job = new DeletePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new DeletePendingDataStoreManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, @@ -208,7 +208,7 @@ public async Task Execute_SetsDeleteError_WhenRetryLimitIsReached() await job.Execute(CreateJobExecutionContext(scheduler)); - adminApiContext.DbInstances.Single().Status.ShouldBe(DbInstanceStatus.DeleteError.ToString()); + adminApiContext.OdsInstanceManages.Single().Status.ShouldBe(OdsInstanceManageStatus.DeleteError.ToString()); scheduledJobs.ShouldBeEmpty(); } @@ -224,18 +224,18 @@ public async Task Execute_UsesTenantSpecificContext_WhenMultiTenancyIsEnabled() A.CallTo(() => tenantSpecificDbContextProvider.GetAdminApiDbContext("tenant1")) .Returns(tenantAdminApiContext); - tenantAdminApiContext.DbInstances.Add(new Common.Infrastructure.Models.DbInstance + tenantAdminApiContext.OdsInstanceManages.Add(new Common.Infrastructure.Models.OdsInstanceManage { Name = "Sandbox", DatabaseTemplate = "Minimal", - Status = DbInstanceStatus.PendingDelete.ToString(), + Status = OdsInstanceManageStatus.PendingDelete.ToString(), LastRefreshed = DateTime.UtcNow, LastModifiedDate = DateTime.UtcNow }); tenantAdminApiContext.SaveChanges(); - var job = new DeletePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new DeletePendingDataStoreManagesDispatcherJob( + A.Fake>(), jobStatusService, defaultAdminApiContext, tenantSpecificDbContextProvider, @@ -244,7 +244,7 @@ public async Task Execute_UsesTenantSpecificContext_WhenMultiTenancyIsEnabled() await job.Execute(CreateJobExecutionContext(scheduler, "tenant1")); scheduledJobs.Count.ShouldBe(1); - scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.DeleteInstanceJobName}-tenant1-{tenantAdminApiContext.DbInstances.Single().Id}"); + scheduledJobs[0].Key.Name.ShouldBe($"{JobConstants.DeleteInstanceJobName}-tenant1-{tenantAdminApiContext.OdsInstanceManages.Single().Id}"); scheduledJobs[0].JobDataMap.GetString(JobConstants.TenantNameKey).ShouldBe("tenant1"); } @@ -256,8 +256,8 @@ public async Task Execute_Throws_WhenTenantNameMissingAndMultiTenancyEnabled() var jobStatusService = A.Fake(); var scheduler = CreateScheduler(out _); - var job = new DeletePendingDbInstancesDispatcherJob( - A.Fake>(), + var job = new DeletePendingDataStoreManagesDispatcherJob( + A.Fake>(), jobStatusService, adminApiContext, tenantSpecificDbContextProvider, diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs index 64fdd7e95..72566be24 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs @@ -31,7 +31,7 @@ namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Services.Tenants; internal class TenantServiceTests { private static IEnumerable AllStatuses => - Enum.GetValues() + Enum.GetValues() .Select(status => status.ToString()); private IOptionsSnapshot _options = null!; @@ -39,7 +39,7 @@ internal class TenantServiceTests private AppSettingsFile _appSettings = null!; private IGetDataStoresQuery _getDataStoresQuery = null!; private IGetEducationOrganizationQuery _getEducationOrganizationQuery = null!; - private IGetDbDataStoresQuery _getDbDataStoresQuery = null!; + private IGetDataStoreManagesQuery _getDataStoreManagesQuery = null!; [SetUp] public void SetUp() @@ -48,8 +48,8 @@ public void SetUp() _memoryCache = new MemoryCache(new MemoryCacheOptions()); _getDataStoresQuery = A.Fake(); _getEducationOrganizationQuery = A.Fake(); - _getDbDataStoresQuery = A.Fake(); - A.CallTo(() => _getDbDataStoresQuery.Execute(A._, A._, A.Ignored)).Returns([]); + _getDataStoreManagesQuery = A.Fake(); + A.CallTo(() => _getDataStoreManagesQuery.Execute(A._, A._, A.Ignored)).Returns([]); _appSettings = new AppSettingsFile { AppSettings = new AppSettings @@ -222,7 +222,7 @@ public async Task GetTenantEdOrgsByInstancesAsync_Should_Return_Correct_TenantDe A.CallTo(() => _getEducationOrganizationQuery.Execute(A.That.Matches(ids => ids.Length == 1 && ids[0] == 101))) .Returns([educationOrganization]); - var tenant = await service.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, tenantName); + var tenant = await service.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, tenantName); tenant.ShouldNotBeNull(); tenant!.TenantName.ShouldBe(tenantName); @@ -243,13 +243,13 @@ public async Task GetTenantEdOrgsByInstancesAsync_Should_Return_Null_If_Not_Foun { var service = new TenantService(_options, _memoryCache); - var tenant = await service.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, "notfound"); + var tenant = await service.GetTenantEdOrgsByInstancesAsync(_getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, "notfound"); tenant.ShouldBeNull(); } [Test] - public async Task GetTenantEdOrgsByInstancesAsync_SetsStatusCreated_WhenDataStoreHasNoLinkedDbDataStore() + public async Task GetTenantEdOrgsByInstancesAsync_SetsStatusCreated_WhenDataStoreHasNoLinkedDataStoreManage() { _appSettings.AppSettings.MultiTenancy = false; var service = new TenantService(_options, _memoryCache); @@ -258,17 +258,17 @@ public async Task GetTenantEdOrgsByInstancesAsync_SetsStatusCreated_WhenDataStor A.CallTo(() => _getDataStoresQuery.Execute()).Returns([odsInstance]); var result = await service.GetTenantEdOrgsByInstancesAsync( - _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, Constants.DefaultTenantName); + _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, Constants.DefaultTenantName); result.ShouldNotBeNull(); result!.DataStores.Count.ShouldBe(1); - result.DataStores[0].Status.ShouldBe(DbInstanceStatus.Created.ToString()); + result.DataStores[0].Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); result.DataStores[0].DatabaseTemplate.ShouldBeNull(); result.DataStores[0].DatabaseName.ShouldBeNull(); } [Test] - public async Task GetTenantEdOrgsByInstancesAsync_EnrichesDataStore_WithLinkedDbDataStoreFields() + public async Task GetTenantEdOrgsByInstancesAsync_EnrichesDataStore_WithLinkedDataStoreManageFields() { _appSettings.AppSettings.MultiTenancy = false; var service = new TenantService(_options, _memoryCache); @@ -276,55 +276,55 @@ public async Task GetTenantEdOrgsByInstancesAsync_EnrichesDataStore_WithLinkedDb var odsInstance = new OdsInstance { OdsInstanceId = 2, Name = "DataStore2" }; A.CallTo(() => _getDataStoresQuery.Execute()).Returns([odsInstance]); - var dbDataStore = new DbInstance + var dataStoreManage = new OdsInstanceManage { Id = 10, - Name = "DbDataStore2", + Name = "DataStoreManage2", OdsInstanceId = 2, - Status = DbInstanceStatus.CreateInProgress.ToString(), + Status = OdsInstanceManageStatus.CreateInProgress.ToString(), DatabaseTemplate = "Minimal", DatabaseName = "EdFi_ODS_2", LastRefreshed = System.DateTime.UtcNow }; - A.CallTo(() => _getDbDataStoresQuery.Execute(A._, A._, A.Ignored)) - .Returns([dbDataStore]); + A.CallTo(() => _getDataStoreManagesQuery.Execute(A._, A._, A.Ignored)) + .Returns([dataStoreManage]); var result = await service.GetTenantEdOrgsByInstancesAsync( - _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, Constants.DefaultTenantName); + _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, Constants.DefaultTenantName); result.ShouldNotBeNull(); result!.DataStores.Count.ShouldBe(1); var dataStore = result.DataStores[0]; - dataStore.Status.ShouldBe(DbInstanceStatus.CreateInProgress.ToString()); + dataStore.Status.ShouldBe(OdsInstanceManageStatus.CreateInProgress.ToString()); dataStore.DatabaseTemplate.ShouldBe("Minimal"); dataStore.DatabaseName.ShouldBe("EdFi_ODS_2"); } [Test] - public async Task GetTenantEdOrgsByInstancesAsync_AddsUnlinkedDbDataStores_WithNullIds() + public async Task GetTenantEdOrgsByInstancesAsync_AddsUnlinkedDataStoreManages_WithNullIds() { _appSettings.AppSettings.MultiTenancy = false; var service = new TenantService(_options, _memoryCache); A.CallTo(() => _getDataStoresQuery.Execute()).Returns([]); - var unlinked1 = new DbInstance + var unlinked1 = new OdsInstanceManage { Id = 20, Name = "Unlinked-A", OdsInstanceId = null, - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), DatabaseTemplate = "Sample", LastRefreshed = System.DateTime.UtcNow }; - var unlinked2 = new DbInstance + var unlinked2 = new OdsInstanceManage { Id = 21, Name = "Unlinked-B", OdsInstanceId = null, - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = System.DateTime.UtcNow }; - A.CallTo(() => _getDbDataStoresQuery.Execute(A._, A._, A.Ignored)) + A.CallTo(() => _getDataStoreManagesQuery.Execute(A._, A._, A.Ignored)) .Returns([unlinked1, unlinked2]); var result = await service.GetTenantEdOrgsByInstancesAsync( - _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, Constants.DefaultTenantName); + _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, Constants.DefaultTenantName); result.ShouldNotBeNull(); result!.DataStores.Count.ShouldBe(2); @@ -341,49 +341,49 @@ public async Task GetTenantEdOrgsByInstancesAsync_MixedScenario_LinkedAndUnlinke var odsInstance = new OdsInstance { OdsInstanceId = 5, Name = "DataStore5" }; A.CallTo(() => _getDataStoresQuery.Execute()).Returns([odsInstance]); - var linked = new DbInstance + var linked = new OdsInstanceManage { Id = 30, Name = "Linked-5", OdsInstanceId = 5, - Status = DbInstanceStatus.Created.ToString(), + Status = OdsInstanceManageStatus.Created.ToString(), DatabaseTemplate = "Minimal", DatabaseName = "EdFi_ODS_5", LastRefreshed = System.DateTime.UtcNow }; - var unlinked = new DbInstance + var unlinked = new OdsInstanceManage { Id = 31, Name = "Unlinked-C", OdsInstanceId = null, - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), DatabaseTemplate = "Sample", LastRefreshed = System.DateTime.UtcNow }; - A.CallTo(() => _getDbDataStoresQuery.Execute(A._, A._, A.Ignored)) + A.CallTo(() => _getDataStoreManagesQuery.Execute(A._, A._, A.Ignored)) .Returns([linked, unlinked]); var result = await service.GetTenantEdOrgsByInstancesAsync( - _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, Constants.DefaultTenantName); + _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, Constants.DefaultTenantName); result.ShouldNotBeNull(); result!.DataStores.Count.ShouldBe(2); var linkedDataStore = result.DataStores.Single(d => d.DataStoreId == 5); - linkedDataStore.Status.ShouldBe(DbInstanceStatus.Created.ToString()); + linkedDataStore.Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); linkedDataStore.DatabaseTemplate.ShouldBe("Minimal"); linkedDataStore.DatabaseName.ShouldBe("EdFi_ODS_5"); var unlinkedDataStore = result.DataStores.Single(d => d.Name == "Unlinked-C"); unlinkedDataStore.Name.ShouldBe("Unlinked-C"); - unlinkedDataStore.Status.ShouldBe(DbInstanceStatus.PendingCreate.ToString()); + unlinkedDataStore.Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); unlinkedDataStore.DataStoreId.ShouldBeNull(); } [Test] [TestCaseSource(nameof(AllStatuses))] - public async Task GetTenantEdOrgsByInstancesAsync_AddsDbDataStore_WhenLinkedToMissingDataStore_ForAllStatuses(string status) + public async Task GetTenantEdOrgsByInstancesAsync_AddsDataStoreManage_WhenLinkedToMissingDataStore_ForAllStatuses(string status) { _appSettings.AppSettings.MultiTenancy = false; var service = new TenantService(_options, _memoryCache); A.CallTo(() => _getDataStoresQuery.Execute()).Returns([]); - var orphan = new DbInstance + var orphan = new OdsInstanceManage { Id = 42, Name = $"Orphan-{status}", @@ -393,11 +393,11 @@ public async Task GetTenantEdOrgsByInstancesAsync_AddsDbDataStore_WhenLinkedToMi DatabaseName = "EdFi_ODS_9002", LastRefreshed = System.DateTime.UtcNow }; - A.CallTo(() => _getDbDataStoresQuery.Execute(A._, A._, A.Ignored)) + A.CallTo(() => _getDataStoreManagesQuery.Execute(A._, A._, A.Ignored)) .Returns([orphan]); var result = await service.GetTenantEdOrgsByInstancesAsync( - _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, Constants.DefaultTenantName); + _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, Constants.DefaultTenantName); result.ShouldNotBeNull(); result!.DataStores.Count.ShouldBe(1); @@ -406,44 +406,44 @@ public async Task GetTenantEdOrgsByInstancesAsync_AddsDbDataStore_WhenLinkedToMi } [Test] - public async Task GetTenantEdOrgsByInstancesAsync_AppendsLatestDbDataStorePerMissingDataStoreId() + public async Task GetTenantEdOrgsByInstancesAsync_AppendsLatestDataStoreManagePerMissingDataStoreId() { _appSettings.AppSettings.MultiTenancy = false; var service = new TenantService(_options, _memoryCache); A.CallTo(() => _getDataStoresQuery.Execute()).Returns([]); - var older = new DbInstance + var older = new OdsInstanceManage { Id = 50, Name = "Orphan-Older", OdsInstanceId = 9003, - Status = DbInstanceStatus.CreateFailed.ToString(), + Status = OdsInstanceManageStatus.CreateFailed.ToString(), DatabaseTemplate = "Minimal", DatabaseName = "EdFi_ODS_9003_old", LastRefreshed = System.DateTime.UtcNow.AddMinutes(-10) }; - var newer = new DbInstance + var newer = new OdsInstanceManage { Id = 51, Name = "Orphan-Newer", OdsInstanceId = 9003, - Status = DbInstanceStatus.Deleted.ToString(), + Status = OdsInstanceManageStatus.Deleted.ToString(), DatabaseTemplate = "Minimal", DatabaseName = "EdFi_ODS_9003_new", LastModifiedDate = System.DateTime.UtcNow }; - A.CallTo(() => _getDbDataStoresQuery.Execute(A._, A._, A.Ignored)) + A.CallTo(() => _getDataStoreManagesQuery.Execute(A._, A._, A.Ignored)) .Returns([older, newer]); var result = await service.GetTenantEdOrgsByInstancesAsync( - _getDataStoresQuery, _getEducationOrganizationQuery, _getDbDataStoresQuery, Constants.DefaultTenantName); + _getDataStoresQuery, _getEducationOrganizationQuery, _getDataStoreManagesQuery, Constants.DefaultTenantName); result.ShouldNotBeNull(); result!.DataStores.Count.ShouldBe(1); - result.DataStores[0].Status.ShouldBe(DbInstanceStatus.Deleted.ToString()); + result.DataStores[0].Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); result.DataStores[0].Name.ShouldBe("Orphan-Newer"); } } From 62aea4e573f82796bb1b694e7b54822ddade8fed Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Tue, 28 Jul 2026 06:16:59 -0600 Subject: [PATCH 21/35] Rename v3 DBTests to DataStoreManage naming Renames EdFi.Ods.AdminApi.V3.DBTests files and internal references from the legacy DbDataStore/DbInstance naming to the DataStoreManage/ OdsInstanceManage naming introduced by the production rename. --- ...s.cs => AddDataStoreManageCommandTests.cs} | 32 +++++------ ...s => DeleteDataStoreManageCommandTests.cs} | 28 +++++----- ...cs => GetDataStoreManageByIdQueryTests.cs} | 14 ++--- ...ts.cs => GetDataStoreManagesQueryTests.cs} | 56 +++++++++---------- .../GetTenantEdOrgsByDataStoresTests.cs | 36 ++++++------ 5 files changed, 83 insertions(+), 83 deletions(-) rename Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/{AddDbDataStoreCommandTests.cs => AddDataStoreManageCommandTests.cs} (71%) rename Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/{DeleteDbDataStoreCommandTests.cs => DeleteDataStoreManageCommandTests.cs} (67%) rename Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/{GetDbDataStoreByIdQueryTests.cs => GetDataStoreManageByIdQueryTests.cs} (80%) rename Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/{GetDbDataStoresQueryTests.cs => GetDataStoreManagesQueryTests.cs} (58%) diff --git a/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDbDataStoreCommandTests.cs b/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDataStoreManageCommandTests.cs similarity index 71% rename from Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDbDataStoreCommandTests.cs rename to Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDataStoreManageCommandTests.cs index e67310954..9facff02f 100644 --- a/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDbDataStoreCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDataStoreManageCommandTests.cs @@ -14,29 +14,29 @@ namespace EdFi.Ods.AdminApi.V3.DBTests.Database.CommandTests; [TestFixture] -public class AddDbDataStoreCommandTests : AdminApiDbContextTestBase +public class AddDataStoreManageCommandTests : AdminApiDbContextTestBase { [Test] - public void ShouldAddDbInstance() + public void ShouldAddDataStoreManage() { - var model = new Mock(); + var model = new Mock(); model.Setup(x => x.Name).Returns("Test Instance"); model.Setup(x => x.DatabaseTemplate).Returns("Minimal"); var id = 0; Transaction(context => { - var command = new AddDbDataStoreCommand(context); + var command = new AddDataStoreManageCommand(context); id = command.Execute(model.Object).Id; id.ShouldBeGreaterThan(0); }); Transaction(context => { - var instance = context.DbInstances.Single(d => d.Id == id); + var instance = context.OdsInstanceManages.Single(d => d.Id == id); instance.Name.ShouldBe("Test Instance"); instance.DatabaseTemplate.ShouldBe("Minimal"); - instance.Status.ShouldBe(DbInstanceStatus.PendingCreate.ToString()); + instance.Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); instance.OdsInstanceId.ShouldBeNull(); instance.OdsInstanceName.ShouldBeNull(); instance.DatabaseName.ShouldBeNull(); @@ -44,23 +44,23 @@ public void ShouldAddDbInstance() } [Test] - public void ShouldAddDbInstanceWithSampleTemplate() + public void ShouldAddDataStoreManageWithSampleTemplate() { - var model = new Mock(); + var model = new Mock(); model.Setup(x => x.Name).Returns("Sample Instance"); model.Setup(x => x.DatabaseTemplate).Returns("Sample"); var id = 0; Transaction(context => { - var command = new AddDbDataStoreCommand(context); + var command = new AddDataStoreManageCommand(context); id = command.Execute(model.Object).Id; id.ShouldBeGreaterThan(0); }); Transaction(context => { - var instance = context.DbInstances.Single(d => d.Id == id); + var instance = context.OdsInstanceManages.Single(d => d.Id == id); instance.DatabaseTemplate.ShouldBe("Sample"); }); } @@ -68,20 +68,20 @@ public void ShouldAddDbInstanceWithSampleTemplate() [Test] public void ShouldTrimNameAndDatabaseTemplate() { - var model = new Mock(); + var model = new Mock(); model.Setup(x => x.Name).Returns(" Trimmed Instance "); model.Setup(x => x.DatabaseTemplate).Returns(" Minimal "); var id = 0; Transaction(context => { - var command = new AddDbDataStoreCommand(context); + var command = new AddDataStoreManageCommand(context); id = command.Execute(model.Object).Id; }); Transaction(context => { - var instance = context.DbInstances.Single(d => d.Id == id); + var instance = context.OdsInstanceManages.Single(d => d.Id == id); instance.Name.ShouldBe("Trimmed Instance"); instance.DatabaseTemplate.ShouldBe("Minimal"); }); @@ -90,7 +90,7 @@ public void ShouldTrimNameAndDatabaseTemplate() [Test] public void ShouldSetLastRefreshedAndLastModifiedDate() { - var model = new Mock(); + var model = new Mock(); model.Setup(x => x.Name).Returns("Timestamp Instance"); model.Setup(x => x.DatabaseTemplate).Returns("Minimal"); @@ -98,13 +98,13 @@ public void ShouldSetLastRefreshedAndLastModifiedDate() var id = 0; Transaction(context => { - var command = new AddDbDataStoreCommand(context); + var command = new AddDataStoreManageCommand(context); id = command.Execute(model.Object).Id; }); Transaction(context => { - var instance = context.DbInstances.Single(d => d.Id == id); + var instance = context.OdsInstanceManages.Single(d => d.Id == id); instance.LastRefreshed.ShouldBeGreaterThanOrEqualTo(before); instance.LastModifiedDate.ShouldNotBeNull(); instance.LastModifiedDate!.Value.ShouldBeGreaterThanOrEqualTo(before); diff --git a/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDbDataStoreCommandTests.cs b/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDataStoreManageCommandTests.cs similarity index 67% rename from Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDbDataStoreCommandTests.cs rename to Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDataStoreManageCommandTests.cs index 50a9ab06e..e3323b3eb 100644 --- a/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDbDataStoreCommandTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDataStoreManageCommandTests.cs @@ -14,15 +14,15 @@ namespace EdFi.Ods.AdminApi.V3.DBTests.Database.CommandTests; [TestFixture] -public class DeleteDbDataStoreCommandTests : AdminApiDbContextTestBase +public class DeleteDataStoreManageCommandTests : AdminApiDbContextTestBase { [Test] public void ShouldSetStatusToPendingDelete() { - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Delete Test Instance", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }; @@ -30,14 +30,14 @@ public void ShouldSetStatusToPendingDelete() Transaction(context => { - var command = new DeleteDbDataStoreCommand(context); + var command = new DeleteDataStoreManageCommand(context); command.Execute(instance.Id); }); Transaction(context => { - var updated = context.DbInstances.Single(d => d.Id == instance.Id); - updated.Status.ShouldBe(DbInstanceStatus.PendingDelete.ToString()); + var updated = context.OdsInstanceManages.Single(d => d.Id == instance.Id); + updated.Status.ShouldBe(OdsInstanceManageStatus.PendingDelete.ToString()); }); } @@ -45,10 +45,10 @@ public void ShouldSetStatusToPendingDelete() public void ShouldUpdateLastModifiedDate() { var before = DateTime.UtcNow; - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Timestamp Test Instance", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }; @@ -56,13 +56,13 @@ public void ShouldUpdateLastModifiedDate() Transaction(context => { - var command = new DeleteDbDataStoreCommand(context); + var command = new DeleteDataStoreManageCommand(context); command.Execute(instance.Id); }); Transaction(context => { - var updated = context.DbInstances.Single(d => d.Id == instance.Id); + var updated = context.OdsInstanceManages.Single(d => d.Id == instance.Id); updated.LastModifiedDate.ShouldNotBeNull(); updated.LastModifiedDate!.Value.ShouldBeGreaterThanOrEqualTo(before); }); @@ -71,10 +71,10 @@ public void ShouldUpdateLastModifiedDate() [Test] public void ShouldNotHardDeleteRecord() { - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "No Hard Delete Instance", - Status = DbInstanceStatus.PendingCreate.ToString(), + Status = OdsInstanceManageStatus.PendingCreate.ToString(), DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }; @@ -82,13 +82,13 @@ public void ShouldNotHardDeleteRecord() Transaction(context => { - var command = new DeleteDbDataStoreCommand(context); + var command = new DeleteDataStoreManageCommand(context); command.Execute(instance.Id); }); Transaction(context => { - var stillExists = context.DbInstances.Any(d => d.Id == instance.Id); + var stillExists = context.OdsInstanceManages.Any(d => d.Id == instance.Id); stillExists.ShouldBeTrue(); }); } diff --git a/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoreByIdQueryTests.cs b/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManageByIdQueryTests.cs similarity index 80% rename from Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoreByIdQueryTests.cs rename to Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManageByIdQueryTests.cs index 0ad543f83..ff7afbfba 100644 --- a/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoreByIdQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManageByIdQueryTests.cs @@ -12,23 +12,23 @@ namespace EdFi.Ods.AdminApi.V3.DBTests.Database.QueryTests; [TestFixture] -public class GetDbDataStoreByIdQueryTests : AdminApiDbContextTestBase +public class GetDataStoreManageByIdQueryTests : AdminApiDbContextTestBase { [Test] public void ShouldReturnNullForNonExistentId() { Transaction(context => { - var query = new GetDbDataStoreByIdQuery(context); + var query = new GetDataStoreManageByIdQuery(context); var result = query.Execute(0); result.ShouldBeNull(); }); } [Test] - public void ShouldGetDbInstanceById() + public void ShouldGetDataStoreManageById() { - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Test Instance", Status = "Pending", @@ -39,7 +39,7 @@ public void ShouldGetDbInstanceById() Transaction(context => { - var query = new GetDbDataStoreByIdQuery(context); + var query = new GetDataStoreManageByIdQuery(context); var result = query.Execute(instance.Id); result.ShouldNotBeNull(); result!.Id.ShouldBe(instance.Id); @@ -52,7 +52,7 @@ public void ShouldGetDbInstanceById() [Test] public void ShouldReturnNullWhenIdDoesNotMatch() { - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Another Instance", Status = "Pending", @@ -63,7 +63,7 @@ public void ShouldReturnNullWhenIdDoesNotMatch() Transaction(context => { - var query = new GetDbDataStoreByIdQuery(context); + var query = new GetDataStoreManageByIdQuery(context); var result = query.Execute(instance.Id + 9999); result.ShouldBeNull(); }); diff --git a/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoresQueryTests.cs b/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManagesQueryTests.cs similarity index 58% rename from Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoresQueryTests.cs rename to Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManagesQueryTests.cs index 41ad4e446..439b9cd20 100644 --- a/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoresQueryTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManagesQueryTests.cs @@ -16,15 +16,15 @@ namespace EdFi.Ods.AdminApi.V3.DBTests.Database.QueryTests; [TestFixture] -public class GetDbDataStoresQueryTests : AdminApiDbContextTestBase +public class GetDataStoreManagesQueryTests : AdminApiDbContextTestBase { private static IEnumerable AllStatuses => - Enum.GetValues().Select(status => status.ToString()); + Enum.GetValues().Select(status => status.ToString()); [Test] - public void ShouldRetrieveDbInstances() + public void ShouldRetrieveDataStoreManages() { - var instance = new DbInstance + var instance = new OdsInstanceManage { Name = "Test Instance", Status = "Pending", @@ -35,7 +35,7 @@ public void ShouldRetrieveDbInstances() Transaction(context => { - var query = new GetDbDataStoresQuery(context, Testing.GetAppSettings()); + var query = new GetDataStoreManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(), null, null); results.ShouldNotBeEmpty(); results.ShouldContain(d => d.Id == instance.Id && d.Name == "Test Instance"); @@ -43,11 +43,11 @@ public void ShouldRetrieveDbInstances() } [Test] - public void ShouldReturnEmptyListWhenNoDbInstances() + public void ShouldReturnEmptyListWhenNoDataStoreManages() { Transaction(context => { - var query = new GetDbDataStoresQuery(context, Testing.GetAppSettings()); + var query = new GetDataStoreManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(), null, null); results.ShouldBeEmpty(); }); @@ -57,13 +57,13 @@ public void ShouldReturnEmptyListWhenNoDbInstances() public void ShouldFilterByName() { Save( - new DbInstance { Name = "Instance Alpha", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "Instance Beta", Status = "Pending", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow } + new OdsInstanceManage { Name = "Instance Alpha", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "Instance Beta", Status = "Pending", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow } ); Transaction(context => { - var query = new GetDbDataStoresQuery(context, Testing.GetAppSettings()); + var query = new GetDataStoreManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(), null, "Instance Alpha"); results.Count.ShouldBe(1); results.Single().Name.ShouldBe("Instance Alpha"); @@ -73,13 +73,13 @@ public void ShouldFilterByName() [Test] public void ShouldFilterById() { - var instance1 = new DbInstance { Name = "Instance 1", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }; - var instance2 = new DbInstance { Name = "Instance 2", Status = "Pending", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow }; + var instance1 = new OdsInstanceManage { Name = "Instance 1", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }; + var instance2 = new OdsInstanceManage { Name = "Instance 2", Status = "Pending", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow }; Save(instance1, instance2); Transaction(context => { - var query = new GetDbDataStoresQuery(context, Testing.GetAppSettings()); + var query = new GetDataStoreManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(), instance1.Id, null); results.Count.ShouldBe(1); results.Single().Id.ShouldBe(instance1.Id); @@ -88,11 +88,11 @@ public void ShouldFilterById() } [Test] - public void ShouldRetrieveDbInstancesWithOffsetAndLimit() + public void ShouldRetrieveDataStoreManagesWithOffsetAndLimit() { for (var i = 1; i <= 5; i++) { - Save(new DbInstance + Save(new OdsInstanceManage { Name = $"Instance {i:D2}", Status = "Pending", @@ -103,7 +103,7 @@ public void ShouldRetrieveDbInstancesWithOffsetAndLimit() Transaction(context => { - var query = new GetDbDataStoresQuery(context, Testing.GetAppSettings()); + var query = new GetDataStoreManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, 2), null, null); results.Count.ShouldBe(2); @@ -117,11 +117,11 @@ public void ShouldRetrieveDbInstancesWithOffsetAndLimit() } [Test] - public void ShouldRetrieveAllDbInstancesWithoutLimit() + public void ShouldRetrieveAllDataStoreManagesWithoutLimit() { for (var i = 1; i <= 5; i++) { - Save(new DbInstance + Save(new OdsInstanceManage { Name = $"Instance {i:D2}", Status = "Pending", @@ -132,24 +132,24 @@ public void ShouldRetrieveAllDbInstancesWithoutLimit() Transaction(context => { - var query = new GetDbDataStoresQuery(context, Testing.GetAppSettings()); + var query = new GetDataStoreManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, null), null, null); results.Count.ShouldBe(5); }); } [Test] - public void ShouldRetrieveDbInstancesOrderedById() + public void ShouldRetrieveDataStoreManagesOrderedById() { Save( - new DbInstance { Name = "Instance C", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "Instance A", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "Instance B", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow } + new OdsInstanceManage { Name = "Instance C", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "Instance A", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "Instance B", Status = "Pending", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow } ); Transaction(context => { - var query = new GetDbDataStoresQuery(context, Testing.GetAppSettings()); + var query = new GetDataStoreManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(), null, null); var ids = results.Select(r => r.Id).ToList(); ids.ShouldBe(ids.OrderBy(x => x).ToList()); @@ -158,16 +158,16 @@ public void ShouldRetrieveDbInstancesOrderedById() [Test] [TestCaseSource(nameof(AllStatuses))] - public void ShouldIncludeDbDataStores_ForAllStatuses_WhenNoFiltersApplied(string status) + public void ShouldIncludeDataStoreManages_ForAllStatuses_WhenNoFiltersApplied(string status) { Save( - new DbInstance { Name = $"Status-{status}", OdsInstanceId = 111, Status = status, DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "Active DataStore", OdsInstanceId = 222, Status = "Created", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow } + new OdsInstanceManage { Name = $"Status-{status}", OdsInstanceId = 111, Status = status, DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "Active DataStore", OdsInstanceId = 222, Status = "Created", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow } ); Transaction(context => { - var query = new GetDbDataStoresQuery(context, Testing.GetAppSettings()); + var query = new GetDataStoreManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, null), null, null); results.Count.ShouldBe(2); results.ShouldContain(r => r.Name == $"Status-{status}" && r.Status == status); diff --git a/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetTenantEdOrgsByDataStoresTests.cs b/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetTenantEdOrgsByDataStoresTests.cs index 67c600d62..6484f3ce9 100644 --- a/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetTenantEdOrgsByDataStoresTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetTenantEdOrgsByDataStoresTests.cs @@ -17,9 +17,9 @@ namespace EdFi.Ods.AdminApi.V3.DBTests.Database.QueryTests; public class GetTenantEdOrgsByDataStoresTests : AdminApiDbContextTestBase { [Test] - public void ShouldDistinguishLinkedAndUnlinkedDbDataStores() + public void ShouldDistinguishLinkedAndUnlinkedDataStoreManages() { - var linked = new DbInstance + var linked = new OdsInstanceManage { Name = "Linked-DataStore", OdsInstanceId = 999, @@ -27,7 +27,7 @@ public void ShouldDistinguishLinkedAndUnlinkedDbDataStores() DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }; - var unlinked = new DbInstance + var unlinked = new OdsInstanceManage { Name = "Unlinked-DataStore", OdsInstanceId = null, @@ -39,7 +39,7 @@ public void ShouldDistinguishLinkedAndUnlinkedDbDataStores() Transaction(context => { - var query = new GetDbDataStoresQuery(context, Testing.GetAppSettings()); + var query = new GetDataStoreManagesQuery(context, Testing.GetAppSettings()); var allResults = query.Execute(new CommonQueryParams(0, null), null, null); var unlinkedResults = allResults.Where(d => d.OdsInstanceId == null).ToList(); @@ -56,26 +56,26 @@ public void ShouldDistinguishLinkedAndUnlinkedDbDataStores() } [Test] - public void ShouldReturnAllDbDataStores_WhenNoFiltersApplied() + public void ShouldReturnAllDataStoreManages_WhenNoFiltersApplied() { Save( - new DbInstance { Name = "A", OdsInstanceId = 1, Status = "Created", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "B", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "C", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow } + new OdsInstanceManage { Name = "A", OdsInstanceId = 1, Status = "Created", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "B", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "C", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow } ); Transaction(context => { - var query = new GetDbDataStoresQuery(context, Testing.GetAppSettings()); + var query = new GetDataStoreManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, null), null, null); results.Count.ShouldBe(3); }); } [Test] - public void ShouldReturnAllDbDataStoreFields_ForLinkedDataStore() + public void ShouldReturnAllDataStoreManageFields_ForLinkedDataStore() { - var dbDataStore = new DbInstance + var dataStoreManage = new OdsInstanceManage { Name = "Fully-Linked", OdsInstanceId = 42, @@ -84,11 +84,11 @@ public void ShouldReturnAllDbDataStoreFields_ForLinkedDataStore() DatabaseName = "EdFi_ODS_42", LastRefreshed = DateTime.UtcNow }; - Save(dbDataStore); + Save(dataStoreManage); Transaction(context => { - var query = new GetDbDataStoresQuery(context, Testing.GetAppSettings()); + var query = new GetDataStoreManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, null), null, null); results.Count.ShouldBe(1); @@ -101,11 +101,11 @@ public void ShouldReturnAllDbDataStoreFields_ForLinkedDataStore() } [Test] - public void ShouldReturnEmptyList_WhenNoDbDataStoresExist() + public void ShouldReturnEmptyList_WhenNoDataStoreManagesExist() { Transaction(context => { - var query = new GetDbDataStoresQuery(context, Testing.GetAppSettings()); + var query = new GetDataStoreManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, null), null, null); results.ShouldBeEmpty(); }); @@ -115,13 +115,13 @@ public void ShouldReturnEmptyList_WhenNoDbDataStoresExist() public void ShouldReturnMultipleUnlinkedDataStores_InIdOrder() { Save( - new DbInstance { Name = "Z-Unlinked", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, - new DbInstance { Name = "A-Unlinked", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow } + new OdsInstanceManage { Name = "Z-Unlinked", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Minimal", LastRefreshed = DateTime.UtcNow }, + new OdsInstanceManage { Name = "A-Unlinked", OdsInstanceId = null, Status = "PendingCreate", DatabaseTemplate = "Sample", LastRefreshed = DateTime.UtcNow } ); Transaction(context => { - var query = new GetDbDataStoresQuery(context, Testing.GetAppSettings()); + var query = new GetDataStoreManagesQuery(context, Testing.GetAppSettings()); var results = query.Execute(new CommonQueryParams(0, null), null, null); var ids = results.Select(r => r.Id).ToList(); ids.ShouldBe(ids.OrderBy(x => x).ToList()); From 5be52848501ea9eecd9ab9344c41c085ae4a3485 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Tue, 28 Jul 2026 06:36:57 -0600 Subject: [PATCH 22/35] Rename v3 Bruno E2E DbDataStores collection to DataStores/Manage Mirrors Task 9's v2 rename. Moves and rewrites all 14 files under Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores to v3/DataStores/Manage, updating meta.name, request URLs (/v3/dbDataStores -> /v3/dataStores/manage), bru variable names (CreatedDbDataStoreId -> CreatedDataStoreManageId, etc.), and test assertion description strings. The not-found title assertion is updated to check for "datastoremanage", matching the actual resource name literal ("dataStoreManage") passed to NotFoundException in ReadDataStoreManage.cs/DeleteDataStoreManage.cs. --- ...DELETE - DataStore Manage - Not Found.bru} | 14 ++--- ...- DataStore Manage - Success.bru.disabled} | 60 +++++++++---------- ... - DataStores Manage - Filter by Name.bru} | 10 ++-- .../GET - DataStores Manage - Not Found.bru} | 16 ++--- ...ores Manage - Without Limit and Offset.bru | 25 ++++++++ ...T - DataStores Manage - Without Limit.bru} | 8 +-- ... - DataStores Manage - Without Offset.bru} | 8 +-- .../Manage/GET - DataStores Manage by ID.bru} | 14 ++--- .../Manage/GET - DataStores Manage.bru} | 12 ++-- ...es Manage - Invalid Database Template.bru} | 16 ++--- .../POST - DataStores Manage - Invalid.bru} | 16 ++--- ... - DataStores Manage - Sample Template.bru | 51 ++++++++++++++++ .../Manage/POST - DataStores Manage.bru} | 12 ++-- .../Manage}/folder.bru | 2 +- ...bDataStores - Without Limit and Offset.bru | 25 -------- .../POST - DbDataStores - Sample Template.bru | 51 ---------------- 16 files changed, 170 insertions(+), 170 deletions(-) rename Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/{DbDataStores/DELETE - DbDataStore - Not Found.bru => DataStores/Manage/DELETE - DataStore Manage - Not Found.bru} (52%) rename Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/{DbDataStores/DELETE - DbDataStore - Success.bru.disabled => DataStores/Manage/DELETE - DataStore Manage - Success.bru.disabled} (55%) rename Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/{DbDataStores/GET - DbDataStores - Filter by Name.bru => DataStores/Manage/GET - DataStores Manage - Filter by Name.bru} (53%) rename Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/{DbDataStores/GET - DbDataStores - Not Found.bru => DataStores/Manage/GET - DataStores Manage - Not Found.bru} (50%) create mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Limit and Offset.bru rename Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/{DbDataStores/GET - DbDataStores - Without Limit.bru => DataStores/Manage/GET - DataStores Manage - Without Limit.bru} (50%) rename Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/{DbDataStores/GET - DbDataStores - Without Offset.bru => DataStores/Manage/GET - DataStores Manage - Without Offset.bru} (50%) rename Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/{DbDataStores/GET - DbDataStores by ID.bru => DataStores/Manage/GET - DataStores Manage by ID.bru} (67%) rename Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/{DbDataStores/GET - DbDataStores.bru => DataStores/Manage/GET - DataStores Manage.bru} (73%) rename Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/{DbDataStores/POST - DbDataStores - Invalid Database Template.bru => DataStores/Manage/POST - DataStores Manage - Invalid Database Template.bru} (56%) rename Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/{DbDataStores/POST - DbDataStores - Invalid.bru => DataStores/Manage/POST - DataStores Manage - Invalid.bru} (58%) create mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Sample Template.bru rename Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/{DbDataStores/POST - DbDataStores.bru => DataStores/Manage/POST - DataStores Manage.bru} (62%) rename Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/{DbDataStores => DataStores/Manage}/folder.bru (68%) delete mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Limit and Offset.bru delete mode 100644 Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Sample Template.bru diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/DELETE - DbDataStore - Not Found.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/DELETE - DataStore Manage - Not Found.bru similarity index 52% rename from Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/DELETE - DbDataStore - Not Found.bru rename to Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/DELETE - DataStore Manage - Not Found.bru index 0bf339938..3735e88ff 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/DELETE - DbDataStore - Not Found.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/DELETE - DataStore Manage - Not Found.bru @@ -1,31 +1,31 @@ meta { - name: DbDataStores - Delete - Not Found + name: DataStores Manage - Delete - Not Found type: http seq: 12 } delete { - url: {{API_URL}}/v3/dbDataStores/0 + url: {{API_URL}}/v3/dataStores/manage/0 body: none auth: inherit } script:post-response { - test("DELETE DbDataStore Not Found: Status code is Not Found", function () { + test("DELETE DataStore Manage Not Found: Status code is Not Found", function () { expect(res.getStatus()).to.equal(404); }); const response = res.getBody(); - test("DELETE DbDataStore Not Found: Response matches error format", function () { + test("DELETE DataStore Manage Not Found: Response matches error format", function () { expect(response).to.have.property("title"); }); - test("DELETE DbDataStore Not Found: Response title is helpful and accurate", function () { + test("DELETE DataStore Manage Not Found: Response title is helpful and accurate", function () { expect(response.title.toLowerCase()).to.contain("not found"); }); - - test("DELETE DbDataStore Not Found: type URN is correct", function () { + + test("DELETE DataStore Manage Not Found: type URN is correct", function () { const response = res.getBody(); expect(response.type).to.equal("urn:ed-fi:management-api:not-found"); }); diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/DELETE - DbDataStore - Success.bru.disabled b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/DELETE - DataStore Manage - Success.bru.disabled similarity index 55% rename from Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/DELETE - DbDataStore - Success.bru.disabled rename to Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/DELETE - DataStore Manage - Success.bru.disabled index 6ffcd7f02..5b227651d 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/DELETE - DbDataStore - Success.bru.disabled +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/DELETE - DataStore Manage - Success.bru.disabled @@ -1,5 +1,5 @@ meta { - name: DbDataStores - Delete - Success + name: DataStores Manage - Delete - Success type: http seq: 13 skip: true @@ -8,7 +8,7 @@ meta { script:pre-request { // DISABLED: The CI pipeline does not seed the Minimal/Sample ODS template databases // (EdFi_Ods_Minimal_Template / EdFi_Ods_Populated_Template) before running the E2E suite. - // Without those source databases the provisioner cannot copy them and the DbDataStore + // Without those source databases the provisioner cannot copy them and the DataStore Manage // transitions to Error status, causing this pre-request to throw and block the whole run. // To re-enable: remove this early-return block and remove 'skip: true' from meta. // See docs/design/INSTANCE-MANAGEMENT-Quartz.md for background on the provisioning job design. @@ -27,7 +27,7 @@ script:pre-request { const axios = require('axios'); const API_URL = bru.getEnvVar("API_URL"); const TOKEN = bru.getEnvVar("TOKEN"); - const deleteDbDataStoreName = `Delete Test DB Instance ${Date.now()}`; + const deleteDataStoreManageName = `Delete Test DB Instance ${Date.now()}`; const requestTimeoutMs = 30000; const provisioningTimeoutMs = 180000; const pollingIntervalMs = 5000; @@ -56,66 +56,66 @@ script:pre-request { const sleep = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)); try { - console.log(`Creating delete test DbDataStore: ${deleteDbDataStoreName}`); + console.log(`Creating delete test DataStore Manage: ${deleteDataStoreManageName}`); - const createResponse = await axios.post(`${API_URL}/v3/dbDataStores`, { - name: deleteDbDataStoreName, + const createResponse = await axios.post(`${API_URL}/v3/dataStores/manage`, { + name: deleteDataStoreManageName, databaseTemplate: "Minimal" }, axiosConfig); if (createResponse.status !== 202) { - logAxiosFailure("Failed to create delete test DbDataStore", createResponse); - throw new Error(`Failed to create delete test DbDataStore - Status: ${createResponse.status}`); + logAxiosFailure("Failed to create delete test DataStore Manage", createResponse); + throw new Error(`Failed to create delete test DataStore Manage - Status: ${createResponse.status}`); } const locationHeader = createResponse.headers.location; const locationValue = Array.isArray(locationHeader) ? locationHeader[0] : locationHeader; if (!locationValue) { - console.log("❌ Create delete test DbDataStore response did not include a location header"); + console.log("❌ Create delete test DataStore Manage response did not include a location header"); console.log("Headers:", JSON.stringify(createResponse.headers, null, 2)); - throw new Error("Create delete test DbDataStore response did not include a location header."); + throw new Error("Create delete test DataStore Manage response did not include a location header."); } - const freshPendingDbDataStoreId = locationValue.split("/").pop(); + const freshPendingDataStoreManageId = locationValue.split("/").pop(); - if (!freshPendingDbDataStoreId) { - throw new Error(`Could not parse DbDataStore id from location header '${locationValue}'.`); + if (!freshPendingDataStoreManageId) { + throw new Error(`Could not parse DataStore Manage id from location header '${locationValue}'.`); } - bru.setVar("FreshPendingDbDataStoreId", freshPendingDbDataStoreId); - bru.setVar("DeleteDbDataStoreName", deleteDbDataStoreName); + bru.setVar("FreshPendingDataStoreManageId", freshPendingDataStoreManageId); + bru.setVar("DeleteDataStoreManageName", deleteDataStoreManageName); const provisioningDeadline = Date.now() + provisioningTimeoutMs; let isReadyToDelete = false; while (Date.now() < provisioningDeadline) { - const statusResponse = await axios.get(`${API_URL}/v3/dbDataStores/${freshPendingDbDataStoreId}`, axiosConfig); + const statusResponse = await axios.get(`${API_URL}/v3/dataStores/manage/${freshPendingDataStoreManageId}`, axiosConfig); if (statusResponse.status !== 200) { - logAxiosFailure("Failed to poll delete test DbDataStore", statusResponse); - throw new Error(`Failed to poll delete test DbDataStore - Status: ${statusResponse.status}`); + logAxiosFailure("Failed to poll delete test DataStore Manage", statusResponse); + throw new Error(`Failed to poll delete test DataStore Manage - Status: ${statusResponse.status}`); } const currentStatus = statusResponse.data?.status; - console.log(`Delete test DbDataStore ${freshPendingDbDataStoreId} status: ${currentStatus}`); + console.log(`Delete test DataStore Manage ${freshPendingDataStoreManageId} status: ${currentStatus}`); if (currentStatus === "Completed" || currentStatus === "DeleteFailed") { - console.log(`✅ Delete test DbDataStore ${freshPendingDbDataStoreId} is ready to be deleted`); + console.log(`✅ Delete test DataStore Manage ${freshPendingDataStoreManageId} is ready to be deleted`); isReadyToDelete = true; break; } if (currentStatus === "Error") { - logAxiosFailure("Delete test DbDataStore provisioning ended in Error", statusResponse); - throw new Error(`Delete test DbDataStore provisioning ended in Error for id ${freshPendingDbDataStoreId}.`); + logAxiosFailure("Delete test DataStore Manage provisioning ended in Error", statusResponse); + throw new Error(`Delete test DataStore Manage provisioning ended in Error for id ${freshPendingDataStoreManageId}.`); } await sleep(pollingIntervalMs); } if (!isReadyToDelete) { - throw new Error(`Timed out waiting for delete test DbDataStore ${freshPendingDbDataStoreId} to become deletable.`); + throw new Error(`Timed out waiting for delete test DataStore Manage ${freshPendingDataStoreManageId} to become deletable.`); } } catch (error) { console.log("Error in pre-request setup:", error.message); @@ -124,7 +124,7 @@ script:pre-request { } delete { - url: {{API_URL}}/v3/dbDataStores/{{FreshPendingDbDataStoreId}} + url: {{API_URL}}/v3/dataStores/manage/{{FreshPendingDataStoreManageId}} body: none auth: inherit } @@ -137,19 +137,19 @@ script:post-response { const responseStatus = res.getStatus(); if (responseStatus !== expectedStatus) { - console.log("❌ DELETE DbDataStore Success request failed"); - console.log("DbDataStoreId:", bru.getVar("FreshPendingDbDataStoreId")); - console.log("DbDataStoreName:", bru.getVar("DeleteDbDataStoreName")); + console.log("❌ DELETE DataStore Manage Success request failed"); + console.log("DataStoreManageId:", bru.getVar("FreshPendingDataStoreManageId")); + console.log("DataStoreManageName:", bru.getVar("DeleteDataStoreManageName")); console.log("Status:", responseStatus); console.log("Response body:", JSON.stringify(res.getBody(), null, 2)); } - test("DELETE DbDataStore Success: Status code is No Content", function () { + test("DELETE DataStore Manage Success: Status code is No Content", function () { expect(responseStatus).to.equal(expectedStatus); }); - bru.deleteVar("FreshPendingDbDataStoreId"); - bru.deleteVar("DeleteDbDataStoreName"); + bru.deleteVar("FreshPendingDataStoreManageId"); + bru.deleteVar("DeleteDataStoreManageName"); } settings { diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Filter by Name.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Filter by Name.bru similarity index 53% rename from Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Filter by Name.bru rename to Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Filter by Name.bru index 81279136e..a6f102be2 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Filter by Name.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Filter by Name.bru @@ -1,11 +1,11 @@ meta { - name: DbDataStores - Filter by Name + name: DataStores Manage - Filter by Name type: http seq: 8 } get { - url: {{API_URL}}/v3/dbDataStores?name=Test DB Instance + url: {{API_URL}}/v3/dataStores/manage?name=Test DB Instance body: none auth: inherit } @@ -15,15 +15,15 @@ params:query { } script:post-response { - test("GET DbDataStores Filter by Name: Status code is OK", function () { + test("GET DataStores Manage Filter by Name: Status code is OK", function () { expect(res.getStatus()).to.equal(200); }); - test("GET DbDataStores Filter by Name: Response is an array", function () { + test("GET DataStores Manage Filter by Name: Response is an array", function () { expect(res.getBody()).to.be.an("array"); }); - test("GET DbDataStores Filter by Name: All results match the name filter", function () { + test("GET DataStores Manage Filter by Name: All results match the name filter", function () { const results = res.getBody(); results.forEach(function (item) { expect(item.name).to.equal("Test DB Instance"); diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Not Found.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Not Found.bru similarity index 50% rename from Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Not Found.bru rename to Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Not Found.bru index fd1b545c3..dd45d00ba 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Not Found.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Not Found.bru @@ -1,32 +1,32 @@ meta { - name: DbDataStores - Not Found + name: DataStores Manage - Not Found type: http seq: 7 } get { - url: {{API_URL}}/v3/dbDataStores/0 + url: {{API_URL}}/v3/dataStores/manage/0 body: none auth: inherit } script:post-response { - test("GET DbDataStore Not Found: Status code is Not Found", function () { + test("GET DataStore Manage Not Found: Status code is Not Found", function () { expect(res.getStatus()).to.equal(404); }); const response = res.getBody(); - test("GET DbDataStore Not Found: Response matches error format", function () { + test("GET DataStore Manage Not Found: Response matches error format", function () { expect(response).to.have.property("title"); }); - test("GET DbDataStore Not Found: Response title is helpful and accurate", function () { + test("GET DataStore Manage Not Found: Response title is helpful and accurate", function () { expect(response.title).to.contain("Not found"); - expect(response.title.toLowerCase()).to.contain("dbdatastore"); + expect(response.title.toLowerCase()).to.contain("datastoremanage"); }); - - test("GET DbDataStore Not Found: type URN is correct", function () { + + test("GET DataStore Manage Not Found: type URN is correct", function () { const response = res.getBody(); expect(response.type).to.equal("urn:ed-fi:management-api:not-found"); }); diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Limit and Offset.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Limit and Offset.bru new file mode 100644 index 000000000..c7df85b73 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Limit and Offset.bru @@ -0,0 +1,25 @@ +meta { + name: DataStores Manage - Without Limit and Offset + type: http + seq: 9 +} + +get { + url: {{API_URL}}/v3/dataStores/manage + body: none + auth: inherit +} + +script:post-response { + test("GET DataStores Manage Without Limit and Offset: Status code is OK", function () { + expect(res.getStatus()).to.equal(200); + }); + + test("GET DataStores Manage Without Limit and Offset: Response is an array", function () { + expect(res.getBody()).to.be.an("array"); + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Limit.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Limit.bru similarity index 50% rename from Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Limit.bru rename to Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Limit.bru index 436f69be2..305d44e81 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Limit.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Limit.bru @@ -1,11 +1,11 @@ meta { - name: DbDataStores - Without Limit + name: DataStores Manage - Without Limit type: http seq: 10 } get { - url: {{API_URL}}/v3/dbDataStores?offset={{offset}} + url: {{API_URL}}/v3/dataStores/manage?offset={{offset}} body: none auth: inherit } @@ -15,11 +15,11 @@ params:query { } script:post-response { - test("GET DbDataStores Without Limit: Status code is OK", function () { + test("GET DataStores Manage Without Limit: Status code is OK", function () { expect(res.getStatus()).to.equal(200); }); - test("GET DbDataStores Without Limit: Response is an array", function () { + test("GET DataStores Manage Without Limit: Response is an array", function () { expect(res.getBody()).to.be.an("array"); }); } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Offset.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Offset.bru similarity index 50% rename from Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Offset.bru rename to Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Offset.bru index f5f1f23a0..26b11a74c 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Offset.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Offset.bru @@ -1,11 +1,11 @@ meta { - name: DbDataStores - Without Offset + name: DataStores Manage - Without Offset type: http seq: 11 } get { - url: {{API_URL}}/v3/dbDataStores?limit={{limit}} + url: {{API_URL}}/v3/dataStores/manage?limit={{limit}} body: none auth: inherit } @@ -15,11 +15,11 @@ params:query { } script:post-response { - test("GET DbDataStores Without Offset: Status code is OK", function () { + test("GET DataStores Manage Without Offset: Status code is OK", function () { expect(res.getStatus()).to.equal(200); }); - test("GET DbDataStores Without Offset: Response is an array", function () { + test("GET DataStores Manage Without Offset: Response is an array", function () { expect(res.getBody()).to.be.an("array"); }); } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores by ID.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage by ID.bru similarity index 67% rename from Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores by ID.bru rename to Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage by ID.bru index d1035372b..82e8795f1 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores by ID.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage by ID.bru @@ -1,27 +1,27 @@ meta { - name: DbDataStores by ID + name: DataStores Manage by ID type: http seq: 6 } get { - url: {{API_URL}}/v3/dbDataStores/{{CreatedDbDataStoreId}} + url: {{API_URL}}/v3/dataStores/manage/{{CreatedDataStoreManageId}} body: none auth: inherit } script:post-response { - test("GET DbDataStore by ID: Status code is OK", function () { + test("GET DataStore Manage by ID: Status code is OK", function () { expect(res.getStatus()).to.equal(200); }); const result = res.getBody(); - test("GET DbDataStore by ID: Response id matches", function () { - expect(result.id).to.equal(parseInt(bru.getVar("CreatedDbDataStoreId"))); + test("GET DataStore Manage by ID: Response id matches", function () { + expect(result.id).to.equal(parseInt(bru.getVar("CreatedDataStoreManageId"))); }); - test("GET DbDataStore by ID: Response contains expected fields", function () { + test("GET DataStore Manage by ID: Response contains expected fields", function () { expect(result).to.have.property("name"); expect(result).to.have.property("status"); expect(result).to.have.property("databaseTemplate"); @@ -46,7 +46,7 @@ script:post-response { required: ["id", "name", "status", "databaseTemplate"] }; - test("GET DbDataStore by ID: Response matches schema", function () { + test("GET DataStore Manage by ID: Response matches schema", function () { const valid = ajv.validate(schema, result); expect(valid).to.be.true; }); diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage.bru similarity index 73% rename from Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores.bru rename to Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage.bru index 543faea2d..8f658a9ff 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage.bru @@ -1,11 +1,11 @@ meta { - name: DbDataStores - Get All + name: DataStores Manage - Get All type: http seq: 5 } get { - url: {{API_URL}}/v3/dbDataStores?offset={{offset}}&limit={{limit}} + url: {{API_URL}}/v3/dataStores/manage?offset={{offset}}&limit={{limit}} body: none auth: inherit } @@ -16,15 +16,15 @@ params:query { } script:post-response { - test("GET DbDataStores: Status code is OK", function () { + test("GET DataStores Manage: Status code is OK", function () { expect(res.getStatus()).to.equal(200); }); - test("GET DbDataStores: Response is an array", function () { + test("GET DataStores Manage: Response is an array", function () { expect(res.getBody()).to.be.an("array"); }); - test("GET DbDataStores: Response includes created instance", function () { + test("GET DataStores Manage: Response includes created instance", function () { expect(res.getBody().length).to.be.greaterThan(0); }); @@ -50,7 +50,7 @@ script:post-response { } }; - test("GET DbDataStores: Response matches schema", function () { + test("GET DataStores Manage: Response matches schema", function () { const valid = ajv.validate(schema, res.getBody()); expect(valid).to.be.true; }); diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Invalid Database Template.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Invalid Database Template.bru similarity index 56% rename from Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Invalid Database Template.bru rename to Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Invalid Database Template.bru index 233a6b87b..52aa24216 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Invalid Database Template.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Invalid Database Template.bru @@ -1,11 +1,11 @@ meta { - name: DbDataStores - Invalid Database Template + name: DataStores Manage - Invalid Database Template type: http seq: 3 } post { - url: {{API_URL}}/v3/dbDataStores + url: {{API_URL}}/v3/dataStores/manage body: json auth: inherit } @@ -18,27 +18,27 @@ body:json { } script:post-response { - test("POST DbDataStores Invalid Template: Status code is Bad Request", function () { + test("POST DataStores Manage Invalid Template: Status code is Bad Request", function () { expect(res.getStatus()).to.equal(400); }); const response = res.getBody(); - test("POST DbDataStores Invalid Template: Response matches error format", function () { + test("POST DataStores Manage Invalid Template: Response matches error format", function () { expect(response).to.have.property("title"); expect(response).to.have.property("errors"); }); - test("POST DbDataStores Invalid Template: Response errors include message for DatabaseTemplate", function () { + test("POST DataStores Manage Invalid Template: Response errors include message for DatabaseTemplate", function () { expect(response.errors["DatabaseTemplate"].length).to.be.greaterThan(0); }); - test("POST DbDataStores Invalid Template: Error message mentions allowed values", function () { + test("POST DataStores Manage Invalid Template: Error message mentions allowed values", function () { const errorMessage = response.errors["DatabaseTemplate"][0].toLowerCase(); expect(errorMessage).to.match(/minimal|sample/); }); - - test("POST DbDataStores Invalid Template: type URN is correct", function () { + + test("POST DataStores Manage Invalid Template: type URN is correct", function () { const response = res.getBody(); expect(response.type).to.equal("urn:ed-fi:management-api:bad-request:validation"); }); diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Invalid.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Invalid.bru similarity index 58% rename from Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Invalid.bru rename to Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Invalid.bru index 4056845f8..35d53e78f 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Invalid.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Invalid.bru @@ -1,11 +1,11 @@ meta { - name: DbDataStores - Invalid + name: DataStores Manage - Invalid type: http seq: 2 } post { - url: {{API_URL}}/v3/dbDataStores + url: {{API_URL}}/v3/dataStores/manage body: json auth: inherit } @@ -18,27 +18,27 @@ body:json { } script:post-response { - test("POST DbDataStores Invalid: Status code is Bad Request", function () { + test("POST DataStores Manage Invalid: Status code is Bad Request", function () { expect(res.getStatus()).to.equal(400); }); const response = res.getBody(); - test("POST DbDataStores Invalid: Response matches error format", function () { + test("POST DataStores Manage Invalid: Response matches error format", function () { expect(response).to.have.property("title"); expect(response).to.have.property("errors"); }); - test("POST DbDataStores Invalid: Response title is helpful and accurate", function () { + test("POST DataStores Manage Invalid: Response title is helpful and accurate", function () { expect(response.title.toLowerCase()).to.contain("validation"); }); - test("POST DbDataStores Invalid: Response errors include messages by property", function () { + test("POST DataStores Manage Invalid: Response errors include messages by property", function () { expect(response.errors["Name"].length).to.be.greaterThan(0); expect(response.errors["DatabaseTemplate"].length).to.be.greaterThan(0); }); - - test("POST DbDataStores Invalid: type URN is correct", function () { + + test("POST DataStores Manage Invalid: type URN is correct", function () { const response = res.getBody(); expect(response.type).to.equal("urn:ed-fi:management-api:bad-request:validation"); }); diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Sample Template.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Sample Template.bru new file mode 100644 index 000000000..5b70ac888 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Sample Template.bru @@ -0,0 +1,51 @@ +meta { + name: DataStores Manage - Sample Template + type: http + seq: 4 +} + +script:pre-request { + const SampleDataStoreManageName = `Test DB Instance Sample ${Date.now()}`; + bru.setVar("SampleDataStoreManageName", SampleDataStoreManageName); + console.log(`Using unique Sample DataStore Manage name: ${SampleDataStoreManageName}`); +} + +post { + url: {{API_URL}}/v3/dataStores/manage + body: json + auth: inherit +} + +body:json { + { + "name": "{{SampleDataStoreManageName}}", + "databaseTemplate": "Sample" + } +} + +script:post-response { + const expectedStatus = 202; + const responseStatus = res.getStatus(); + + if (responseStatus !== expectedStatus) { + console.log("❌ POST DataStores Manage Sample request failed"); + console.log("Status:", responseStatus); + console.log("Response body:", JSON.stringify(res.getBody(), null, 2)); + } + + test("POST DataStores Manage Sample: Status code is Accepted", function () { + expect(responseStatus).to.equal(expectedStatus); + }); + + test("POST DataStores Manage Sample: Response includes location in header", function () { + expect(res.getHeaders()).to.have.property("location"); + const id = res.getHeader("location").split("/").pop(); + if (id) { + bru.setVar("CreatedDataStoreManageIdSample", id); + } + }); +} + +settings { + encodeUrl: true +} diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage.bru similarity index 62% rename from Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores.bru rename to Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage.bru index acd70b98e..f452497df 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage.bru @@ -1,11 +1,11 @@ meta { - name: DbDataStores + name: DataStores Manage type: http seq: 1 } post { - url: {{API_URL}}/v3/dbDataStores + url: {{API_URL}}/v3/dataStores/manage body: json auth: inherit } @@ -18,20 +18,20 @@ body:json { } script:post-response { - test("POST DbDataStores: Status code is Accepted", function () { + test("POST DataStores Manage: Status code is Accepted", function () { expect(res.getStatus()).to.equal(202); }); - test("POST DbDataStores: Response includes location in header", function () { + test("POST DataStores Manage: Response includes location in header", function () { expect(res.getHeaders()).to.have.property("location"); const location = res.getHeader("location"); const id = location.split("/").pop(); if (id) { - bru.setVar("CreatedDbDataStoreId", id); + bru.setVar("CreatedDataStoreManageId", id); } }); - test("POST DbDataStores: Location header is absolute url", function () { + test("POST DataStores Manage: Location header is absolute url", function () { const location = res.getHeaders().location; expect(location).to.match(/^https?:\/\/.+/); }); diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/folder.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/folder.bru similarity index 68% rename from Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/folder.bru rename to Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/folder.bru index dd3bc65a4..cffed6757 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/folder.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/folder.bru @@ -1,5 +1,5 @@ meta { - name: DbDataStores + name: Manage seq: 99 } diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Limit and Offset.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Limit and Offset.bru deleted file mode 100644 index a93c69682..000000000 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Limit and Offset.bru +++ /dev/null @@ -1,25 +0,0 @@ -meta { - name: DbDataStores - Without Limit and Offset - type: http - seq: 9 -} - -get { - url: {{API_URL}}/v3/dbDataStores - body: none - auth: inherit -} - -script:post-response { - test("GET DbDataStores Without Limit and Offset: Status code is OK", function () { - expect(res.getStatus()).to.equal(200); - }); - - test("GET DbDataStores Without Limit and Offset: Response is an array", function () { - expect(res.getBody()).to.be.an("array"); - }); -} - -settings { - encodeUrl: true -} diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Sample Template.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Sample Template.bru deleted file mode 100644 index 63198ce9e..000000000 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Sample Template.bru +++ /dev/null @@ -1,51 +0,0 @@ -meta { - name: DbDataStores - Sample Template - type: http - seq: 4 -} - -script:pre-request { - const SampleDbDataStoreName = `Test DB Instance Sample ${Date.now()}`; - bru.setVar("SampleDbDataStoreName", SampleDbDataStoreName); - console.log(`Using unique Sample DbDataStore name: ${SampleDbDataStoreName}`); -} - -post { - url: {{API_URL}}/v3/dbDataStores - body: json - auth: inherit -} - -body:json { - { - "name": "{{SampleDbDataStoreName}}", - "databaseTemplate": "Sample" - } -} - -script:post-response { - const expectedStatus = 202; - const responseStatus = res.getStatus(); - - if (responseStatus !== expectedStatus) { - console.log("❌ POST DbDataStores Sample request failed"); - console.log("Status:", responseStatus); - console.log("Response body:", JSON.stringify(res.getBody(), null, 2)); - } - - test("POST DbDataStores Sample: Status code is Accepted", function () { - expect(responseStatus).to.equal(expectedStatus); - }); - - test("POST DbDataStores Sample: Response includes location in header", function () { - expect(res.getHeaders()).to.have.property("location"); - const id = res.getHeader("location").split("/").pop(); - if (id) { - bru.setVar("CreatedDbDataStoreIdSample", id); - } - }); -} - -settings { - encodeUrl: true -} From 5ab7ccc4d8d4b0a948a786a72eb300a73e99619e Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Tue, 28 Jul 2026 07:10:13 -0600 Subject: [PATCH 23/35] Rename docs/http/dbinstances.http to odsinstances-manage.http, update routes Updates request URLs from /v2/dbinstances to /v2/odsinstances/manage to match the renamed v2 endpoint, and renames @name block labels and their references accordingly. No v3/dbDataStores requests existed in this file. --- ...nstances.http => odsinstances-manage.http} | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) rename docs/http/{dbinstances.http => odsinstances-manage.http} (75%) diff --git a/docs/http/dbinstances.http b/docs/http/odsinstances-manage.http similarity index 75% rename from docs/http/dbinstances.http rename to docs/http/odsinstances-manage.http index 0da789c99..f5c33115a 100644 --- a/docs/http/dbinstances.http +++ b/docs/http/odsinstances-manage.http @@ -23,8 +23,8 @@ grant_type=client_credentials&scope=edfi_admin_api/full_access ### Create a DB instance (Minimal template) -# @name createDbInstanceMinimal -POST {{adminapi_url}}/v2/dbinstances +# @name createOdsInstanceManageMinimal +POST {{adminapi_url}}/v2/odsinstances/manage Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} @@ -35,8 +35,8 @@ Tenant: {{tenant}} } ### Create a DB instance (Sample template) -# @name createDbInstanceSample -POST {{adminapi_url}}/v2/dbinstances +# @name createOdsInstanceManageSample +POST {{adminapi_url}}/v2/odsinstances/manage Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} @@ -47,8 +47,8 @@ Tenant: {{tenant}} } ### Create a DB instance (Minimal template) -# @name createDbInstanceMinimal -POST {{adminapi_url}}/v2/dbinstances +# @name createOdsInstanceManageMinimal +POST {{adminapi_url}}/v2/odsinstances/manage Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} @@ -59,20 +59,20 @@ Tenant: {{tenant}} } ### DELETE a DB instance (Minimal) -DELETE {{adminapi_url}}/v2{{createDbInstanceMinimal.response.headers.location}} +DELETE {{adminapi_url}}/v2{{createOdsInstanceManageMinimal.response.headers.location}} Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} ### DELETE a DB instance (Sample) -DELETE {{adminapi_url}}/v2{{createDbInstanceSample.response.headers.location}} +DELETE {{adminapi_url}}/v2{{createOdsInstanceManageSample.response.headers.location}} Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} ### Get all DB instances -GET {{adminapi_url}}/v2/dbinstances +GET {{adminapi_url}}/v2/odsinstances/manage Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} @@ -84,37 +84,37 @@ Authorization: bearer {{token}} Tenant: {{tenant}} ### Get all DB instances with pagination -GET {{adminapi_url}}/v2/dbinstances?offset=0&limit=10 +GET {{adminapi_url}}/v2/odsinstances/manage?offset=0&limit=10 Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} ### Get all DB instances filtered by name -GET {{adminapi_url}}/v2/dbinstances?name=My DB Instance +GET {{adminapi_url}}/v2/odsinstances/manage?name=My DB Instance Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} ### Get DB instance by ID (Minimal) -GET {{adminapi_url}}/v2{{createDbInstanceMinimal.response.headers.location}} +GET {{adminapi_url}}/v2{{createOdsInstanceManageMinimal.response.headers.location}} Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} ### Get DB instance by ID (Sample) -GET {{adminapi_url}}/v2{{createDbInstanceSample.response.headers.location}} +GET {{adminapi_url}}/v2{{createOdsInstanceManageSample.response.headers.location}} Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} ### Get DB instance by ID - not found -GET {{adminapi_url}}/v2/dbinstances/0 +GET {{adminapi_url}}/v2/odsinstances/manage/0 Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} ### Create DB instance - invalid (missing required fields) -POST {{adminapi_url}}/v2/dbinstances +POST {{adminapi_url}}/v2/odsinstances/manage Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} @@ -125,7 +125,7 @@ Tenant: {{tenant}} } ### Create DB instance - invalid (invalid name) -POST {{adminapi_url}}/v2/dbinstances +POST {{adminapi_url}}/v2/odsinstances/manage Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} @@ -136,7 +136,7 @@ Tenant: {{tenant}} } ### Create DB instance - invalid template -POST {{adminapi_url}}/v2/dbinstances +POST {{adminapi_url}}/v2/odsinstances/manage Content-Type: application/json Authorization: bearer {{token}} Tenant: {{tenant}} From d8e28aef3bd15d3e38f35530d564d1fdb3cc0285 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Tue, 28 Jul 2026 07:40:06 -0600 Subject: [PATCH 24/35] Fix stale dbInstanceId references from final rename review Update three v2 Bruno E2E schema assertions to use the renamed odsInstanceManageId property instead of the old dbInstanceId name, and add the missing DataStoreManageId assertion to the v3 ReadEducationOrganizationsTests linked-instance test for parity with the equivalent v2 test. Co-Authored-By: Claude Sonnet 5 --- .../Features/DataStores/ReadEducationOrganizationsTests.cs | 3 ++- .../OdsInstances/GET - OdsInstances - EdOrgs By InstanceId.bru | 2 +- .../GET - Tenants EdOrgs by Tenant Name - Multitenant.bru | 2 +- .../GET - Tenants EdOrgs by Tenant Name - Singletenant.bru | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/ReadEducationOrganizationsTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/ReadEducationOrganizationsTests.cs index a0d4feabd..751587a03 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/ReadEducationOrganizationsTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/ReadEducationOrganizationsTests.cs @@ -79,7 +79,8 @@ public async Task GetEducationOrganizationsByDataStore_EnrichesLinkedDataStoreMa var ok = result as Microsoft.AspNetCore.Http.HttpResults.Ok>; ok.ShouldNotBeNull(); - ok.Value![0].Status.ShouldBe("Healthy"); + ok.Value![0].DataStoreManageId.ShouldBe(5); + ok.Value[0].Status.ShouldBe("Healthy"); ok.Value[0].DatabaseTemplate.ShouldBe("Minimal"); ok.Value[0].DatabaseName.ShouldBe("EdFi_Ods_7"); } diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/GET - OdsInstances - EdOrgs By InstanceId.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/GET - OdsInstances - EdOrgs By InstanceId.bru index 9ac922685..c8e4e2912 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/GET - OdsInstances - EdOrgs By InstanceId.bru +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/GET - OdsInstances - EdOrgs By InstanceId.bru @@ -78,7 +78,7 @@ script:post-response { "type": "object", "properties": { "id": { "type": "integer" }, - "dbInstanceId": { "type": ["integer", "null"] }, + "odsInstanceManageId": { "type": ["integer", "null"] }, "name": { "type": "string" }, "instanceType": { "type": ["string", "null"] }, "status": { "type": ["string", "null"] }, diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru index 6abec2aef..30b84fce1 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru @@ -97,7 +97,7 @@ script:post-response { "id": { "type": ["integer", "null"] }, - "dbInstanceId": { + "odsInstanceManageId": { "type": ["integer", "null"] }, "name": { diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru index 77e8620b3..923bf9991 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru @@ -96,7 +96,7 @@ script:post-response { "id": { "type": ["integer", "null"] }, - "dbInstanceId": { + "odsInstanceManageId": { "type": ["integer", "null"] }, "name": { From cc99bc887286557b11dc11eec6ec79d7ed1641b7 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Tue, 28 Jul 2026 08:23:21 -0600 Subject: [PATCH 25/35] [ADMINAPI-1477] Fix location-header ID extraction in POST OdsInstances Manage E2E test The route changed from /v2/dbinstances/{id} to /v2/odsinstances/manage/{id} during the rename, adding a path segment. The old hardcoded split("/")[2] pulled "manage" instead of the id, leaving CreatedOdsInstanceManageId unset and breaking every downstream test that depends on it. Switched to split("/").pop(), matching the pattern already used by the Sample Template variant and by v3's equivalent test. Co-Authored-By: Claude Sonnet 5 --- .../v2/OdsInstances/Manage/POST - OdsInstances Manage.bru | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage.bru b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage.bru index 89f3b35f1..0b9be7ffd 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage.bru +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage.bru @@ -24,7 +24,7 @@ script:post-response { test("POST OdsInstances Manage: Response includes location in header", function () { expect(res.getHeaders()).to.have.property("location"); - const id = res.getHeader("location").split("/")[2]; + const id = res.getHeader("location").split("/").pop(); if (id) { bru.setVar("CreatedOdsInstanceManageId", id); } From ed9b465a65d8ac77a132c5a34a353b258fc36afb Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Tue, 28 Jul 2026 13:33:00 -0600 Subject: [PATCH 26/35] Add design spec for v3 tenant DataStoreManageId parity fix Documents the plan to add DataStoreManageId to v3's TenantDataStoreModel, closing a parity gap with v2's OdsInstanceManageId on the tenants/edOrgs endpoint. Co-Authored-By: Claude Sonnet 5 --- ...28-v3-tenant-datastore-manage-id-design.md | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-28-v3-tenant-datastore-manage-id-design.md diff --git a/docs/superpowers/specs/2026-07-28-v3-tenant-datastore-manage-id-design.md b/docs/superpowers/specs/2026-07-28-v3-tenant-datastore-manage-id-design.md new file mode 100644 index 000000000..3615b8ab6 --- /dev/null +++ b/docs/superpowers/specs/2026-07-28-v3-tenant-datastore-manage-id-design.md @@ -0,0 +1,110 @@ +# Add DataStoreManageId to v3 Tenants/DataStores/EdOrgs response + +Date: 2026-07-28 + +## Summary + +v2's `/tenants/{tenantName}/odsInstances/edOrgs` endpoint returns `OdsInstanceManageId` on each +ods-instance entry — the id of the linked `OdsInstanceManage` record (provisioning status, +database template/name). v3's equivalent endpoint, `/tenants/{tenantName}/dataStores/edOrgs`, +never got the matching field on `TenantDataStoreModel`. This is a pre-existing parity gap, not a +new feature: the underlying query (`IGetDataStoreManagesQuery`) and DI wiring are already fully +present in v3 — only the model field and two assignments are missing. + +This change adds `DataStoreManageId` (v3's naming counterpart to v2's `OdsInstanceManageId`) to +`TenantDataStoreModel`, populates it in `TenantService` and `TenantMapper`, and updates unit tests +and Bruno E2E schemas to match. While in the Bruno E2E schema file, it also closes an unrelated +pre-existing gap where the v3 schema was missing `status`/`databaseTemplate`/`databaseName` +properties that `TenantDataStoreModel` already returns and v2's schema already validates. + +## Background / current state + +- v2: `TenantOdsInstanceModel.OdsInstanceManageId` (`Application/EdFi.Ods.AdminApi/Features/Tenants/TenantDetailModel.cs:34`) + is populated in two places: + - `TenantService.GetTenantEdOrgsByInstancesAsync` (`Application/EdFi.Ods.AdminApi/Infrastructure/Services/Tenants/TenantService.cs:156`) + sets it for instances linked to an `OdsInstanceManage` record. + - `TenantMapper.ToUnlinkedOdsInstanceManageModel` (`Application/EdFi.Ods.AdminApi/Features/Tenants/TenantMapper.cs:33`) + sets it for orphaned `OdsInstanceManage` records with no matching ods instance. +- v3: `TenantDataStoreModel` (`Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs`) + has no equivalent field. The parallel code in `TenantService` + (`Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs:152-164`) and + `TenantMapper.ToUnlinkedDataStoreManageModel` + (`Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs:28`) sets `Status`, + `DatabaseTemplate`, `DatabaseName` but never an id. +- `ReadTenants.cs` (both versions) and the query/DI layer already match structurally — no changes + needed there. +- v3's single-instance endpoint (`/dataStores/{id}/edOrgs`, + `DataStoreWithEducationOrganizationsModel`) already has `DataStoreManageId` correctly — this gap + is specific to the tenant-level `edOrgs` endpoint. +- Neither v2 nor v3 has any DBTests coverage for the tenants/edOrgs endpoint today, so DBTests are + not part of this change. +- JSON serialization uses the default camelCase policy (no explicit `JsonPropertyName` needed) — + `DataStoreManageId` will serialize as `dataStoreManageId`, matching the `odsInstanceManageId` / + `dataStoreId` casing convention already used on this model. + +## Naming + +Per the existing v2→v3 renaming convention on this model (`OdsInstance` → `DataStore`, +`OdsInstanceId` → `DataStoreId`), the new field is named `DataStoreManageId`. + +## Code changes + +1. **`Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs`** + Add `public int? DataStoreManageId { get; set; }` to `TenantDataStoreModel`, immediately after + `DataStoreId`, mirroring v2's field order. + +2. **`Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs`** + In `ToUnlinkedDataStoreManageModel`, add `DataStoreManageId = source.Id` (mirrors v2's + `ToUnlinkedOdsInstanceManageModel`). + +3. **`Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs`** + In `GetTenantEdOrgsByInstancesAsync`, in the linked-data-store loop, add + `dataStore.DataStoreManageId = dataStoreManage.Id;` alongside the existing `Status` / + `DatabaseTemplate` / `DatabaseName` assignments. + +No changes to `ReadTenants.cs`, queries, or DI registration. + +## Test changes + +### `Application/EdFi.Ods.AdminApi.V3.UnitTests` + +- **`Infrastructure/Services/Tenants/TenantServiceTests.cs`** — add `DataStoreManageId` assertions + (mirroring the equivalent v2 assertions already present in + `EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs`) to: + - `GetTenantEdOrgsByInstancesAsync_SetsStatusCreated_WhenDataStoreHasNoLinkedDataStoreManage` — + assert `DataStoreManageId.ShouldBeNull()`. + - `GetTenantEdOrgsByInstancesAsync_EnrichesDataStore_WithLinkedDataStoreManageFields` — assert + `DataStoreManageId.ShouldBe(10)`. + - `GetTenantEdOrgsByInstancesAsync_AddsUnlinkedDataStoreManages_WithNullIds` — extend the + `ShouldContain` predicates to include `DataStoreManageId == 20` / `== 21`. + - `GetTenantEdOrgsByInstancesAsync_MixedScenario_LinkedAndUnlinked` — assert + `DataStoreManageId.ShouldBe(30)` on the linked entry and `.ShouldBe(31)` on the unlinked entry. + - `GetTenantEdOrgsByInstancesAsync_AddsDataStoreManage_WhenLinkedToMissingDataStore_ForAllStatuses` + — assert `DataStoreManageId.ShouldBe(42)`. + - `GetTenantEdOrgsByInstancesAsync_AppendsLatestDataStoreManagePerMissingDataStoreId` — assert + `DataStoreManageId.ShouldBe(51)`. + +- **`Features/Tenants/TenantDetailModelTests.cs`** — in `Properties_ShouldBeSettable`, set + `DataStoreManageId = 10` on the constructed `TenantDataStoreModel` and assert it round-trips, + mirroring v2's equivalent test. + +### DBTests + +No changes — no existing DBTests coverage for this feature in either version. + +### Bruno E2E (`Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/`) + +In both `GET - Tenants EdOrgs by Tenant Name - Multitenant.bru` and +`GET - Tenants EdOrgs by Tenant Name - Singletenant.bru`, update `GetTenantDataStoresEdOrgsSchema` +to add, matching v2's `GetTenantOdsInstancesEdOrgsSchema`: +- `dataStoreManageId`: `["integer", "null"]` +- `status`: `["string", "null"]` +- `databaseTemplate`: `["string", "null"]` +- `databaseName`: `["string", "null"]` + +## Out of scope + +- v3's single-instance `/dataStores/{id}/edOrgs` endpoint — already has `DataStoreManageId` + correctly. +- DBTests for the tenants/edOrgs endpoint (neither version has any today). +- Any other pre-existing v2/v3 parity gaps not touched by this endpoint's response model. From 6dcfce36325f975a79bc602e0627572825b70c15 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Tue, 28 Jul 2026 13:46:14 -0600 Subject: [PATCH 27/35] Add DataStoreManageId property to v3 TenantDataStoreModel --- .../Features/Tenants/TenantDetailModelTests.cs | 2 ++ .../EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/TenantDetailModelTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/TenantDetailModelTests.cs index 04428cbe7..f9742bfb0 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/TenantDetailModelTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/TenantDetailModelTests.cs @@ -41,6 +41,7 @@ public void Properties_ShouldBeSettable() var odsInstance = new TenantDataStoreModel() { DataStoreId = 1, + DataStoreManageId = 10, EducationOrganizations = [educationOrganization] }; @@ -53,6 +54,7 @@ public void Properties_ShouldBeSettable() // Assert tenantDetailModel.TenantName.ShouldBe(tenantName); tenantDetailModel.DataStores.ShouldBe([odsInstance]); + tenantDetailModel.DataStores[0].DataStoreManageId.ShouldBe(10); tenantDetailModel.DataStores[0].EducationOrganizations.ShouldBe([educationOrganization]); } diff --git a/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs b/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs index 771d25288..3b8e10d0b 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs @@ -31,6 +31,7 @@ public class TenantDataStoreModel { [JsonPropertyName("id")] public int? DataStoreId { get; set; } + public int? DataStoreManageId { get; set; } public string Name { get; set; } public string? DataStoreType { get; set; } public string? Status { get; set; } From ce909e1f0160d44da82c33f4428491ab78a59df3 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Tue, 28 Jul 2026 13:53:29 -0600 Subject: [PATCH 28/35] Populate DataStoreManageId on v3 tenant data stores --- .../Services/Tenants/TenantServiceTests.cs | 10 ++++++++-- .../Features/Tenants/TenantMapper.cs | 1 + .../Infrastructure/Services/Tenants/TenantService.cs | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs index 72566be24..3f2acde17 100644 --- a/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs @@ -263,6 +263,7 @@ public async Task GetTenantEdOrgsByInstancesAsync_SetsStatusCreated_WhenDataStor result.ShouldNotBeNull(); result!.DataStores.Count.ShouldBe(1); result.DataStores[0].Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); + result.DataStores[0].DataStoreManageId.ShouldBeNull(); result.DataStores[0].DatabaseTemplate.ShouldBeNull(); result.DataStores[0].DatabaseName.ShouldBeNull(); } @@ -295,6 +296,7 @@ public async Task GetTenantEdOrgsByInstancesAsync_EnrichesDataStore_WithLinkedDa result.ShouldNotBeNull(); result!.DataStores.Count.ShouldBe(1); var dataStore = result.DataStores[0]; + dataStore.DataStoreManageId.ShouldBe(10); dataStore.Status.ShouldBe(OdsInstanceManageStatus.CreateInProgress.ToString()); dataStore.DatabaseTemplate.ShouldBe("Minimal"); dataStore.DatabaseName.ShouldBe("EdFi_ODS_2"); @@ -328,8 +330,8 @@ public async Task GetTenantEdOrgsByInstancesAsync_AddsUnlinkedDataStoreManages_W result.ShouldNotBeNull(); result!.DataStores.Count.ShouldBe(2); - result.DataStores.ShouldContain(d => d.DataStoreId == null && d.Name == "Unlinked-A"); - result.DataStores.ShouldContain(d => d.DataStoreId == null && d.Name == "Unlinked-B"); + result.DataStores.ShouldContain(d => d.DataStoreId == null && d.Name == "Unlinked-A" && d.DataStoreManageId == 20); + result.DataStores.ShouldContain(d => d.DataStoreId == null && d.Name == "Unlinked-B" && d.DataStoreManageId == 21); } [Test] @@ -364,11 +366,13 @@ public async Task GetTenantEdOrgsByInstancesAsync_MixedScenario_LinkedAndUnlinke result!.DataStores.Count.ShouldBe(2); var linkedDataStore = result.DataStores.Single(d => d.DataStoreId == 5); + linkedDataStore.DataStoreManageId.ShouldBe(30); linkedDataStore.Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); linkedDataStore.DatabaseTemplate.ShouldBe("Minimal"); linkedDataStore.DatabaseName.ShouldBe("EdFi_ODS_5"); var unlinkedDataStore = result.DataStores.Single(d => d.Name == "Unlinked-C"); + unlinkedDataStore.DataStoreManageId.ShouldBe(31); unlinkedDataStore.Name.ShouldBe("Unlinked-C"); unlinkedDataStore.Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); unlinkedDataStore.DataStoreId.ShouldBeNull(); @@ -402,6 +406,7 @@ public async Task GetTenantEdOrgsByInstancesAsync_AddsDataStoreManage_WhenLinked result.ShouldNotBeNull(); result!.DataStores.Count.ShouldBe(1); result.DataStores[0].DataStoreId.ShouldBeNull(); + result.DataStores[0].DataStoreManageId.ShouldBe(42); result.DataStores[0].Status.ShouldBe(status); } @@ -443,6 +448,7 @@ public async Task GetTenantEdOrgsByInstancesAsync_AppendsLatestDataStoreManagePe result.ShouldNotBeNull(); result!.DataStores.Count.ShouldBe(1); + result.DataStores[0].DataStoreManageId.ShouldBe(51); result.DataStores[0].Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); result.DataStores[0].Name.ShouldBe("Orphan-Newer"); } diff --git a/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs b/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs index 689f85ee1..b3ab06d3b 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs @@ -30,6 +30,7 @@ public static TenantDataStoreModel ToUnlinkedDataStoreManageModel(OdsInstanceMan return new TenantDataStoreModel { DataStoreId = null, + DataStoreManageId = source.Id, Name = source.Name, Status = source.Status, DatabaseTemplate = source.DatabaseTemplate, diff --git a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs index 56f1e670b..155912bac 100644 --- a/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs +++ b/Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs @@ -153,6 +153,7 @@ public async Task> GetTenantsAsync(bool fromCache = false) { if (dataStore.DataStoreId is int dataStoreId && linkedDataStoreManagesByDataStoreId.TryGetValue(dataStoreId, out var dataStoreManage)) { + dataStore.DataStoreManageId = dataStoreManage.Id; dataStore.Status = dataStoreManage.Status; dataStore.DatabaseTemplate = dataStoreManage.DatabaseTemplate; dataStore.DatabaseName = dataStoreManage.DatabaseName; From e2972e92fb45324e1ad214dab56b01a57b8dcf37 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Tue, 28 Jul 2026 13:59:22 -0600 Subject: [PATCH 29/35] Add dataStoreManageId and status fields to v3 tenant edOrgs E2E schema Co-Authored-By: Claude Sonnet 5 --- ...- Tenants EdOrgs by Tenant Name - Multitenant.bru | 12 ++++++++++++ ... Tenants EdOrgs by Tenant Name - Singletenant.bru | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru index 43338df17..494b13c13 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru @@ -97,12 +97,24 @@ script:post-response { "id": { "type": ["integer", "null"] }, + "dataStoreManageId": { + "type": ["integer", "null"] + }, "name": { "type": "string" }, "dataStoreType": { "type": ["string", "null"] }, + "status": { + "type": ["string", "null"] + }, + "databaseTemplate": { + "type": ["string", "null"] + }, + "databaseName": { + "type": ["string", "null"] + }, "educationOrganizations": { "type": "array", "items": { diff --git a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru index 29367c5dc..dd93a75b9 100644 --- a/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru +++ b/Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru @@ -96,12 +96,24 @@ script:post-response { "id": { "type": ["integer", "null"] }, + "dataStoreManageId": { + "type": ["integer", "null"] + }, "name": { "type": "string" }, "dataStoreType": { "type": ["string", "null"] }, + "status": { + "type": ["string", "null"] + }, + "databaseTemplate": { + "type": ["string", "null"] + }, + "databaseName": { + "type": ["string", "null"] + }, "educationOrganizations": { "type": "array", "items": { From 380cf7c9f7924ee794be9db91a1bc438f2637346 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Tue, 28 Jul 2026 14:12:27 -0600 Subject: [PATCH 30/35] Add implementation plan for v3 tenant DataStoreManageId parity fix Companion plan document for the design spec, executed via subagent-driven-development across Tasks 1-4. Co-Authored-By: Claude Sonnet 5 --- ...026-07-28-v3-tenant-datastore-manage-id.md | 476 ++++++++++++++++++ 1 file changed, 476 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-28-v3-tenant-datastore-manage-id.md diff --git a/docs/superpowers/plans/2026-07-28-v3-tenant-datastore-manage-id.md b/docs/superpowers/plans/2026-07-28-v3-tenant-datastore-manage-id.md new file mode 100644 index 000000000..0922a9e09 --- /dev/null +++ b/docs/superpowers/plans/2026-07-28-v3-tenant-datastore-manage-id.md @@ -0,0 +1,476 @@ +# v3 Tenant DataStoreManageId Parity Fix Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add `DataStoreManageId` to v3's `TenantDataStoreModel` so the `/tenants/{tenantName}/dataStores/edOrgs` endpoint reaches parity with v2's `OdsInstanceManageId` on `/tenants/{tenantName}/odsInstances/edOrgs`. + +**Architecture:** v3's `TenantService`, `TenantMapper`, and the underlying `IGetDataStoreManagesQuery`/DI wiring already exist and already populate `Status`/`DatabaseTemplate`/`DatabaseName` from the linked `OdsInstanceManage` record — they just never captured its `Id`. This plan mirrors the existing v2 code paths (`Application/EdFi.Ods.AdminApi/Infrastructure/Services/Tenants/TenantService.cs` and `Features/Tenants/TenantMapper.cs`) into their v3 counterparts, one property and two one-line assignments, then extends the existing unit tests and Bruno E2E schemas to assert on it. + +**Tech Stack:** C# / .NET, NUnit + Shouldly + FakeItEasy (unit tests), Bruno (`.bru`) E2E collections with `ajv` JSON-schema assertions. + +## Global Constraints + +- Field name is `DataStoreManageId` (v3's naming counterpart to v2's `OdsInstanceManageId`), per `docs/superpowers/specs/2026-07-28-v3-tenant-datastore-manage-id-design.md`. +- No `[JsonPropertyName]` override needed — the app's default camelCase policy serializes `DataStoreManageId` as `dataStoreManageId`. +- No changes to `ReadTenants.cs`, queries, or DI registration in either version. +- No DBTests — neither v2 nor v3 has DBTests coverage for this endpoint today. +- v3's single-instance `/dataStores/{id}/edOrgs` endpoint (`DataStoreWithEducationOrganizationsModel`) already has `DataStoreManageId` correctly and must not be touched. +- While updating the Bruno E2E schema, also add `status`, `databaseTemplate`, `databaseName` (each `["string", "null"]`) to close a separate pre-existing gap where v3's schema never validated fields `TenantDataStoreModel` already returns and v2's schema already checks. + +--- + +### Task 1: Add `DataStoreManageId` property to `TenantDataStoreModel` + +**Files:** +- Modify: `Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs` +- Test: `Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/TenantDetailModelTests.cs` + +**Interfaces:** +- Produces: `TenantDataStoreModel.DataStoreManageId` (`int?`, settable property) — consumed by Task 2's `TenantMapper` and `TenantService` changes, and by Task 2's `TenantServiceTests` assertions. + +- [ ] **Step 1: Write the failing test** + + In `Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/TenantDetailModelTests.cs`, update the `Properties_ShouldBeSettable` test (this will fail to compile until Task 1 Step 3 adds the property): + + Replace: + ```csharp + var odsInstance = new TenantDataStoreModel() + { + DataStoreId = 1, + EducationOrganizations = [educationOrganization] + }; + + var tenantDetailModel = new TenantDetailModel() + { + TenantName = tenantName, + DataStores = [odsInstance] + }; + + // Assert + tenantDetailModel.TenantName.ShouldBe(tenantName); + tenantDetailModel.DataStores.ShouldBe([odsInstance]); + tenantDetailModel.DataStores[0].EducationOrganizations.ShouldBe([educationOrganization]); + } + ``` + + With: + ```csharp + var odsInstance = new TenantDataStoreModel() + { + DataStoreId = 1, + DataStoreManageId = 10, + EducationOrganizations = [educationOrganization] + }; + + var tenantDetailModel = new TenantDetailModel() + { + TenantName = tenantName, + DataStores = [odsInstance] + }; + + // Assert + tenantDetailModel.TenantName.ShouldBe(tenantName); + tenantDetailModel.DataStores.ShouldBe([odsInstance]); + tenantDetailModel.DataStores[0].DataStoreManageId.ShouldBe(10); + tenantDetailModel.DataStores[0].EducationOrganizations.ShouldBe([educationOrganization]); + } + ``` + +- [ ] **Step 2: Run test to verify it fails** + + Run: `dotnet test "Application/EdFi.Ods.AdminApi.V3.UnitTests/EdFi.Ods.AdminApi.V3.UnitTests.csproj" --filter "FullyQualifiedName~TenantDetailModelTests" --nologo` + + Expected: Build FAILS with `CS0117: 'TenantDataStoreModel' does not contain a definition for 'DataStoreManageId'`. + +- [ ] **Step 3: Add the property** + + In `Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs`, in `TenantDataStoreModel`: + + Replace: + ```csharp + [JsonPropertyName("id")] + public int? DataStoreId { get; set; } + public string Name { get; set; } + ``` + + With: + ```csharp + [JsonPropertyName("id")] + public int? DataStoreId { get; set; } + public int? DataStoreManageId { get; set; } + public string Name { get; set; } + ``` + +- [ ] **Step 4: Run test to verify it passes** + + Run: `dotnet test "Application/EdFi.Ods.AdminApi.V3.UnitTests/EdFi.Ods.AdminApi.V3.UnitTests.csproj" --filter "FullyQualifiedName~TenantDetailModelTests" --nologo` + + Expected: PASS (2 tests). + +- [ ] **Step 5: Commit** + + ```bash + git add "Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/TenantDetailModelTests.cs" + git commit -m "Add DataStoreManageId property to v3 TenantDataStoreModel" + ``` + +--- + +### Task 2: Populate `DataStoreManageId` in `TenantMapper` and `TenantService` + +**Files:** +- Modify: `Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs` +- Modify: `Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs` +- Test: `Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs` + +**Interfaces:** +- Consumes: `TenantDataStoreModel.DataStoreManageId` (`int?`, from Task 1). +- Produces: `TenantMapper.ToUnlinkedDataStoreManageModel(OdsInstanceManage)` now sets `DataStoreManageId`; `TenantService.GetTenantEdOrgsByInstancesAsync(...)` now sets `DataStoreManageId` on linked data stores. No signature changes — later tasks consume the same method names/signatures as before. + +- [ ] **Step 1: Write the failing tests** + + In `Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs`, apply these six edits (each adds a `DataStoreManageId` assertion mirroring the equivalent `OdsInstanceManageId` assertion already present in `Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs`): + + **a) `GetTenantEdOrgsByInstancesAsync_SetsStatusCreated_WhenDataStoreHasNoLinkedDataStoreManage`** + + Replace: + ```csharp + result.ShouldNotBeNull(); + result!.DataStores.Count.ShouldBe(1); + result.DataStores[0].Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); + result.DataStores[0].DatabaseTemplate.ShouldBeNull(); + result.DataStores[0].DatabaseName.ShouldBeNull(); + } + ``` + + With: + ```csharp + result.ShouldNotBeNull(); + result!.DataStores.Count.ShouldBe(1); + result.DataStores[0].Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); + result.DataStores[0].DataStoreManageId.ShouldBeNull(); + result.DataStores[0].DatabaseTemplate.ShouldBeNull(); + result.DataStores[0].DatabaseName.ShouldBeNull(); + } + ``` + + **b) `GetTenantEdOrgsByInstancesAsync_EnrichesDataStore_WithLinkedDataStoreManageFields`** + + Replace: + ```csharp + result.ShouldNotBeNull(); + result!.DataStores.Count.ShouldBe(1); + var dataStore = result.DataStores[0]; + dataStore.Status.ShouldBe(OdsInstanceManageStatus.CreateInProgress.ToString()); + dataStore.DatabaseTemplate.ShouldBe("Minimal"); + dataStore.DatabaseName.ShouldBe("EdFi_ODS_2"); + } + ``` + + With: + ```csharp + result.ShouldNotBeNull(); + result!.DataStores.Count.ShouldBe(1); + var dataStore = result.DataStores[0]; + dataStore.DataStoreManageId.ShouldBe(10); + dataStore.Status.ShouldBe(OdsInstanceManageStatus.CreateInProgress.ToString()); + dataStore.DatabaseTemplate.ShouldBe("Minimal"); + dataStore.DatabaseName.ShouldBe("EdFi_ODS_2"); + } + ``` + + **c) `GetTenantEdOrgsByInstancesAsync_AddsUnlinkedDataStoreManages_WithNullIds`** + + Replace: + ```csharp + result.ShouldNotBeNull(); + result!.DataStores.Count.ShouldBe(2); + result.DataStores.ShouldContain(d => d.DataStoreId == null && d.Name == "Unlinked-A"); + result.DataStores.ShouldContain(d => d.DataStoreId == null && d.Name == "Unlinked-B"); + } + ``` + + With: + ```csharp + result.ShouldNotBeNull(); + result!.DataStores.Count.ShouldBe(2); + result.DataStores.ShouldContain(d => d.DataStoreId == null && d.Name == "Unlinked-A" && d.DataStoreManageId == 20); + result.DataStores.ShouldContain(d => d.DataStoreId == null && d.Name == "Unlinked-B" && d.DataStoreManageId == 21); + } + ``` + + **d) `GetTenantEdOrgsByInstancesAsync_MixedScenario_LinkedAndUnlinked`** + + Replace: + ```csharp + var linkedDataStore = result.DataStores.Single(d => d.DataStoreId == 5); + linkedDataStore.Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); + linkedDataStore.DatabaseTemplate.ShouldBe("Minimal"); + linkedDataStore.DatabaseName.ShouldBe("EdFi_ODS_5"); + + var unlinkedDataStore = result.DataStores.Single(d => d.Name == "Unlinked-C"); + unlinkedDataStore.Name.ShouldBe("Unlinked-C"); + unlinkedDataStore.Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); + unlinkedDataStore.DataStoreId.ShouldBeNull(); + } + ``` + + With: + ```csharp + var linkedDataStore = result.DataStores.Single(d => d.DataStoreId == 5); + linkedDataStore.DataStoreManageId.ShouldBe(30); + linkedDataStore.Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); + linkedDataStore.DatabaseTemplate.ShouldBe("Minimal"); + linkedDataStore.DatabaseName.ShouldBe("EdFi_ODS_5"); + + var unlinkedDataStore = result.DataStores.Single(d => d.Name == "Unlinked-C"); + unlinkedDataStore.DataStoreManageId.ShouldBe(31); + unlinkedDataStore.Name.ShouldBe("Unlinked-C"); + unlinkedDataStore.Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); + unlinkedDataStore.DataStoreId.ShouldBeNull(); + } + ``` + + **e) `GetTenantEdOrgsByInstancesAsync_AddsDataStoreManage_WhenLinkedToMissingDataStore_ForAllStatuses`** + + Replace: + ```csharp + result.ShouldNotBeNull(); + result!.DataStores.Count.ShouldBe(1); + result.DataStores[0].DataStoreId.ShouldBeNull(); + result.DataStores[0].Status.ShouldBe(status); + } + ``` + + With: + ```csharp + result.ShouldNotBeNull(); + result!.DataStores.Count.ShouldBe(1); + result.DataStores[0].DataStoreId.ShouldBeNull(); + result.DataStores[0].DataStoreManageId.ShouldBe(42); + result.DataStores[0].Status.ShouldBe(status); + } + ``` + + **f) `GetTenantEdOrgsByInstancesAsync_AppendsLatestDataStoreManagePerMissingDataStoreId`** + + Replace: + ```csharp + result.ShouldNotBeNull(); + result!.DataStores.Count.ShouldBe(1); + result.DataStores[0].Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); + result.DataStores[0].Name.ShouldBe("Orphan-Newer"); + } + ``` + + With: + ```csharp + result.ShouldNotBeNull(); + result!.DataStores.Count.ShouldBe(1); + result.DataStores[0].DataStoreManageId.ShouldBe(51); + result.DataStores[0].Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); + result.DataStores[0].Name.ShouldBe("Orphan-Newer"); + } + ``` + +- [ ] **Step 2: Run tests to verify they fail** + + Run: `dotnet test "Application/EdFi.Ods.AdminApi.V3.UnitTests/EdFi.Ods.AdminApi.V3.UnitTests.csproj" --filter "FullyQualifiedName~TenantServiceTests" --nologo` + + Expected: FAIL — the 6 edited tests fail with Shouldly mismatches (e.g. `dataStore.DataStoreManageId` is `null`, expected `10`). + +- [ ] **Step 3: Populate `DataStoreManageId` in `TenantMapper`** + + In `Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs`: + + Replace: + ```csharp + public static TenantDataStoreModel ToUnlinkedDataStoreManageModel(OdsInstanceManage source) + { + return new TenantDataStoreModel + { + DataStoreId = null, + Name = source.Name, + Status = source.Status, + DatabaseTemplate = source.DatabaseTemplate, + DatabaseName = source.DatabaseName, + }; + } + ``` + + With: + ```csharp + public static TenantDataStoreModel ToUnlinkedDataStoreManageModel(OdsInstanceManage source) + { + return new TenantDataStoreModel + { + DataStoreId = null, + DataStoreManageId = source.Id, + Name = source.Name, + Status = source.Status, + DatabaseTemplate = source.DatabaseTemplate, + DatabaseName = source.DatabaseName, + }; + } + ``` + +- [ ] **Step 4: Populate `DataStoreManageId` in `TenantService`** + + In `Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs`, in `GetTenantEdOrgsByInstancesAsync`: + + Replace: + ```csharp + foreach (var dataStore in tenantDetails.DataStores) + { + if (dataStore.DataStoreId is int dataStoreId && linkedDataStoreManagesByDataStoreId.TryGetValue(dataStoreId, out var dataStoreManage)) + { + dataStore.Status = dataStoreManage.Status; + dataStore.DatabaseTemplate = dataStoreManage.DatabaseTemplate; + dataStore.DatabaseName = dataStoreManage.DatabaseName; + } + else + { + dataStore.Status = OdsInstanceManageStatus.Created.ToString(); + } + } + ``` + + With: + ```csharp + foreach (var dataStore in tenantDetails.DataStores) + { + if (dataStore.DataStoreId is int dataStoreId && linkedDataStoreManagesByDataStoreId.TryGetValue(dataStoreId, out var dataStoreManage)) + { + dataStore.DataStoreManageId = dataStoreManage.Id; + dataStore.Status = dataStoreManage.Status; + dataStore.DatabaseTemplate = dataStoreManage.DatabaseTemplate; + dataStore.DatabaseName = dataStoreManage.DatabaseName; + } + else + { + dataStore.Status = OdsInstanceManageStatus.Created.ToString(); + } + } + ``` + +- [ ] **Step 5: Run tests to verify they pass** + + Run: `dotnet test "Application/EdFi.Ods.AdminApi.V3.UnitTests/EdFi.Ods.AdminApi.V3.UnitTests.csproj" --filter "FullyQualifiedName~TenantServiceTests" --nologo` + + Expected: PASS (all tests in the fixture, including the `[TestCaseSource(nameof(AllStatuses))]` parameterized cases). + +- [ ] **Step 6: Commit** + + ```bash + git add "Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs" + git commit -m "Populate DataStoreManageId on v3 tenant data stores" + ``` + +--- + +### Task 3: Update Bruno E2E schema assertions + +**Files:** +- Modify: `Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru` +- Modify: `Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru` + +**Interfaces:** +- Consumes: the JSON response shape produced by Task 2 (`dataStoreManageId`, `status`, `databaseTemplate`, `databaseName` now present on each `dataStores[]` entry). +- Produces: nothing consumed by later tasks — this is the last task in the plan. + +- [ ] **Step 1: Update the Multitenant schema** + + In `Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru`, inside `script:post-response`, in `GetTenantDataStoresEdOrgsSchema`: + + Replace: + ```javascript + "dataStores": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": ["integer", "null"] + }, + "name": { + "type": "string" + }, + "dataStoreType": { + "type": ["string", "null"] + }, + "educationOrganizations": { + ``` + + With: + ```javascript + "dataStores": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": ["integer", "null"] + }, + "dataStoreManageId": { + "type": ["integer", "null"] + }, + "name": { + "type": "string" + }, + "dataStoreType": { + "type": ["string", "null"] + }, + "status": { + "type": ["string", "null"] + }, + "databaseTemplate": { + "type": ["string", "null"] + }, + "databaseName": { + "type": ["string", "null"] + }, + "educationOrganizations": { + ``` + +- [ ] **Step 2: Update the Singletenant schema** + + In `Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru`, apply the identical replacement from Step 1 (same `GetTenantDataStoresEdOrgsSchema` block, same before/after text). + +- [ ] **Step 3: Run the v3 multitenant Bruno E2E suite** + + Run: `./eng/run-e2e-bruno.ps1 -ApiVersion 3 -TenantMode multitenant -TearDown` + + Expected: PASS, including `GET Tenants DataStores EdOrgs: Response matches schema` in both the Multitenant and Singletenant collection runs (Singletenant coverage runs as part of the same suite in single-tenant mode — if your local setup only runs one mode at a time, also run `./eng/run-e2e-bruno.ps1 -ApiVersion 3 -TenantMode singletenant -TearDown`). + + Note: this requires local Docker/DB setup per `docs/developer.md`. If that environment isn't available, at minimum re-read both edited `.bru` files to confirm the JSON is well-formed (matching brace/bracket structure) before committing. + +- [ ] **Step 4: Commit** + + ```bash + git add "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru" + git commit -m "Add dataStoreManageId and status fields to v3 tenant edOrgs E2E schema" + ``` + +--- + +### Task 4: Full verification + +**Files:** none (verification only) + +- [ ] **Step 1: Run the full v3 unit test project** + + Run: `dotnet test "Application/EdFi.Ods.AdminApi.V3.UnitTests/EdFi.Ods.AdminApi.V3.UnitTests.csproj" --nologo` + + Expected: PASS, 0 failures. + +- [ ] **Step 2: Run the full solution unit test suite** + + Run: `./build.ps1 -Command UnitTest` + + Expected: PASS, 0 failures across all `*.UnitTests` projects (confirms the v2 `TenantServiceTests`/`TenantDetailModelTests` — untouched by this plan — still pass, i.e. no accidental cross-version regression). + +- [ ] **Step 3: Confirm no unrelated files changed** + + Run: `git status` + + Expected: working tree clean (all changes already committed across Tasks 1–3); no unexpected modified files outside the ones listed in this plan's tasks. From fec1c61a8c1bdaf25b76ce0e23253c6967b870cd Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Tue, 28 Jul 2026 15:08:31 -0600 Subject: [PATCH 31/35] Removes documentation --- ...026-07-28-v3-tenant-datastore-manage-id.md | 476 ------------------ ...28-v3-tenant-datastore-manage-id-design.md | 110 ---- 2 files changed, 586 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-28-v3-tenant-datastore-manage-id.md delete mode 100644 docs/superpowers/specs/2026-07-28-v3-tenant-datastore-manage-id-design.md diff --git a/docs/superpowers/plans/2026-07-28-v3-tenant-datastore-manage-id.md b/docs/superpowers/plans/2026-07-28-v3-tenant-datastore-manage-id.md deleted file mode 100644 index 0922a9e09..000000000 --- a/docs/superpowers/plans/2026-07-28-v3-tenant-datastore-manage-id.md +++ /dev/null @@ -1,476 +0,0 @@ -# v3 Tenant DataStoreManageId Parity Fix Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add `DataStoreManageId` to v3's `TenantDataStoreModel` so the `/tenants/{tenantName}/dataStores/edOrgs` endpoint reaches parity with v2's `OdsInstanceManageId` on `/tenants/{tenantName}/odsInstances/edOrgs`. - -**Architecture:** v3's `TenantService`, `TenantMapper`, and the underlying `IGetDataStoreManagesQuery`/DI wiring already exist and already populate `Status`/`DatabaseTemplate`/`DatabaseName` from the linked `OdsInstanceManage` record — they just never captured its `Id`. This plan mirrors the existing v2 code paths (`Application/EdFi.Ods.AdminApi/Infrastructure/Services/Tenants/TenantService.cs` and `Features/Tenants/TenantMapper.cs`) into their v3 counterparts, one property and two one-line assignments, then extends the existing unit tests and Bruno E2E schemas to assert on it. - -**Tech Stack:** C# / .NET, NUnit + Shouldly + FakeItEasy (unit tests), Bruno (`.bru`) E2E collections with `ajv` JSON-schema assertions. - -## Global Constraints - -- Field name is `DataStoreManageId` (v3's naming counterpart to v2's `OdsInstanceManageId`), per `docs/superpowers/specs/2026-07-28-v3-tenant-datastore-manage-id-design.md`. -- No `[JsonPropertyName]` override needed — the app's default camelCase policy serializes `DataStoreManageId` as `dataStoreManageId`. -- No changes to `ReadTenants.cs`, queries, or DI registration in either version. -- No DBTests — neither v2 nor v3 has DBTests coverage for this endpoint today. -- v3's single-instance `/dataStores/{id}/edOrgs` endpoint (`DataStoreWithEducationOrganizationsModel`) already has `DataStoreManageId` correctly and must not be touched. -- While updating the Bruno E2E schema, also add `status`, `databaseTemplate`, `databaseName` (each `["string", "null"]`) to close a separate pre-existing gap where v3's schema never validated fields `TenantDataStoreModel` already returns and v2's schema already checks. - ---- - -### Task 1: Add `DataStoreManageId` property to `TenantDataStoreModel` - -**Files:** -- Modify: `Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs` -- Test: `Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/TenantDetailModelTests.cs` - -**Interfaces:** -- Produces: `TenantDataStoreModel.DataStoreManageId` (`int?`, settable property) — consumed by Task 2's `TenantMapper` and `TenantService` changes, and by Task 2's `TenantServiceTests` assertions. - -- [ ] **Step 1: Write the failing test** - - In `Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/TenantDetailModelTests.cs`, update the `Properties_ShouldBeSettable` test (this will fail to compile until Task 1 Step 3 adds the property): - - Replace: - ```csharp - var odsInstance = new TenantDataStoreModel() - { - DataStoreId = 1, - EducationOrganizations = [educationOrganization] - }; - - var tenantDetailModel = new TenantDetailModel() - { - TenantName = tenantName, - DataStores = [odsInstance] - }; - - // Assert - tenantDetailModel.TenantName.ShouldBe(tenantName); - tenantDetailModel.DataStores.ShouldBe([odsInstance]); - tenantDetailModel.DataStores[0].EducationOrganizations.ShouldBe([educationOrganization]); - } - ``` - - With: - ```csharp - var odsInstance = new TenantDataStoreModel() - { - DataStoreId = 1, - DataStoreManageId = 10, - EducationOrganizations = [educationOrganization] - }; - - var tenantDetailModel = new TenantDetailModel() - { - TenantName = tenantName, - DataStores = [odsInstance] - }; - - // Assert - tenantDetailModel.TenantName.ShouldBe(tenantName); - tenantDetailModel.DataStores.ShouldBe([odsInstance]); - tenantDetailModel.DataStores[0].DataStoreManageId.ShouldBe(10); - tenantDetailModel.DataStores[0].EducationOrganizations.ShouldBe([educationOrganization]); - } - ``` - -- [ ] **Step 2: Run test to verify it fails** - - Run: `dotnet test "Application/EdFi.Ods.AdminApi.V3.UnitTests/EdFi.Ods.AdminApi.V3.UnitTests.csproj" --filter "FullyQualifiedName~TenantDetailModelTests" --nologo` - - Expected: Build FAILS with `CS0117: 'TenantDataStoreModel' does not contain a definition for 'DataStoreManageId'`. - -- [ ] **Step 3: Add the property** - - In `Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs`, in `TenantDataStoreModel`: - - Replace: - ```csharp - [JsonPropertyName("id")] - public int? DataStoreId { get; set; } - public string Name { get; set; } - ``` - - With: - ```csharp - [JsonPropertyName("id")] - public int? DataStoreId { get; set; } - public int? DataStoreManageId { get; set; } - public string Name { get; set; } - ``` - -- [ ] **Step 4: Run test to verify it passes** - - Run: `dotnet test "Application/EdFi.Ods.AdminApi.V3.UnitTests/EdFi.Ods.AdminApi.V3.UnitTests.csproj" --filter "FullyQualifiedName~TenantDetailModelTests" --nologo` - - Expected: PASS (2 tests). - -- [ ] **Step 5: Commit** - - ```bash - git add "Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/TenantDetailModelTests.cs" - git commit -m "Add DataStoreManageId property to v3 TenantDataStoreModel" - ``` - ---- - -### Task 2: Populate `DataStoreManageId` in `TenantMapper` and `TenantService` - -**Files:** -- Modify: `Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs` -- Modify: `Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs` -- Test: `Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs` - -**Interfaces:** -- Consumes: `TenantDataStoreModel.DataStoreManageId` (`int?`, from Task 1). -- Produces: `TenantMapper.ToUnlinkedDataStoreManageModel(OdsInstanceManage)` now sets `DataStoreManageId`; `TenantService.GetTenantEdOrgsByInstancesAsync(...)` now sets `DataStoreManageId` on linked data stores. No signature changes — later tasks consume the same method names/signatures as before. - -- [ ] **Step 1: Write the failing tests** - - In `Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs`, apply these six edits (each adds a `DataStoreManageId` assertion mirroring the equivalent `OdsInstanceManageId` assertion already present in `Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs`): - - **a) `GetTenantEdOrgsByInstancesAsync_SetsStatusCreated_WhenDataStoreHasNoLinkedDataStoreManage`** - - Replace: - ```csharp - result.ShouldNotBeNull(); - result!.DataStores.Count.ShouldBe(1); - result.DataStores[0].Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); - result.DataStores[0].DatabaseTemplate.ShouldBeNull(); - result.DataStores[0].DatabaseName.ShouldBeNull(); - } - ``` - - With: - ```csharp - result.ShouldNotBeNull(); - result!.DataStores.Count.ShouldBe(1); - result.DataStores[0].Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); - result.DataStores[0].DataStoreManageId.ShouldBeNull(); - result.DataStores[0].DatabaseTemplate.ShouldBeNull(); - result.DataStores[0].DatabaseName.ShouldBeNull(); - } - ``` - - **b) `GetTenantEdOrgsByInstancesAsync_EnrichesDataStore_WithLinkedDataStoreManageFields`** - - Replace: - ```csharp - result.ShouldNotBeNull(); - result!.DataStores.Count.ShouldBe(1); - var dataStore = result.DataStores[0]; - dataStore.Status.ShouldBe(OdsInstanceManageStatus.CreateInProgress.ToString()); - dataStore.DatabaseTemplate.ShouldBe("Minimal"); - dataStore.DatabaseName.ShouldBe("EdFi_ODS_2"); - } - ``` - - With: - ```csharp - result.ShouldNotBeNull(); - result!.DataStores.Count.ShouldBe(1); - var dataStore = result.DataStores[0]; - dataStore.DataStoreManageId.ShouldBe(10); - dataStore.Status.ShouldBe(OdsInstanceManageStatus.CreateInProgress.ToString()); - dataStore.DatabaseTemplate.ShouldBe("Minimal"); - dataStore.DatabaseName.ShouldBe("EdFi_ODS_2"); - } - ``` - - **c) `GetTenantEdOrgsByInstancesAsync_AddsUnlinkedDataStoreManages_WithNullIds`** - - Replace: - ```csharp - result.ShouldNotBeNull(); - result!.DataStores.Count.ShouldBe(2); - result.DataStores.ShouldContain(d => d.DataStoreId == null && d.Name == "Unlinked-A"); - result.DataStores.ShouldContain(d => d.DataStoreId == null && d.Name == "Unlinked-B"); - } - ``` - - With: - ```csharp - result.ShouldNotBeNull(); - result!.DataStores.Count.ShouldBe(2); - result.DataStores.ShouldContain(d => d.DataStoreId == null && d.Name == "Unlinked-A" && d.DataStoreManageId == 20); - result.DataStores.ShouldContain(d => d.DataStoreId == null && d.Name == "Unlinked-B" && d.DataStoreManageId == 21); - } - ``` - - **d) `GetTenantEdOrgsByInstancesAsync_MixedScenario_LinkedAndUnlinked`** - - Replace: - ```csharp - var linkedDataStore = result.DataStores.Single(d => d.DataStoreId == 5); - linkedDataStore.Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); - linkedDataStore.DatabaseTemplate.ShouldBe("Minimal"); - linkedDataStore.DatabaseName.ShouldBe("EdFi_ODS_5"); - - var unlinkedDataStore = result.DataStores.Single(d => d.Name == "Unlinked-C"); - unlinkedDataStore.Name.ShouldBe("Unlinked-C"); - unlinkedDataStore.Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); - unlinkedDataStore.DataStoreId.ShouldBeNull(); - } - ``` - - With: - ```csharp - var linkedDataStore = result.DataStores.Single(d => d.DataStoreId == 5); - linkedDataStore.DataStoreManageId.ShouldBe(30); - linkedDataStore.Status.ShouldBe(OdsInstanceManageStatus.Created.ToString()); - linkedDataStore.DatabaseTemplate.ShouldBe("Minimal"); - linkedDataStore.DatabaseName.ShouldBe("EdFi_ODS_5"); - - var unlinkedDataStore = result.DataStores.Single(d => d.Name == "Unlinked-C"); - unlinkedDataStore.DataStoreManageId.ShouldBe(31); - unlinkedDataStore.Name.ShouldBe("Unlinked-C"); - unlinkedDataStore.Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); - unlinkedDataStore.DataStoreId.ShouldBeNull(); - } - ``` - - **e) `GetTenantEdOrgsByInstancesAsync_AddsDataStoreManage_WhenLinkedToMissingDataStore_ForAllStatuses`** - - Replace: - ```csharp - result.ShouldNotBeNull(); - result!.DataStores.Count.ShouldBe(1); - result.DataStores[0].DataStoreId.ShouldBeNull(); - result.DataStores[0].Status.ShouldBe(status); - } - ``` - - With: - ```csharp - result.ShouldNotBeNull(); - result!.DataStores.Count.ShouldBe(1); - result.DataStores[0].DataStoreId.ShouldBeNull(); - result.DataStores[0].DataStoreManageId.ShouldBe(42); - result.DataStores[0].Status.ShouldBe(status); - } - ``` - - **f) `GetTenantEdOrgsByInstancesAsync_AppendsLatestDataStoreManagePerMissingDataStoreId`** - - Replace: - ```csharp - result.ShouldNotBeNull(); - result!.DataStores.Count.ShouldBe(1); - result.DataStores[0].Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); - result.DataStores[0].Name.ShouldBe("Orphan-Newer"); - } - ``` - - With: - ```csharp - result.ShouldNotBeNull(); - result!.DataStores.Count.ShouldBe(1); - result.DataStores[0].DataStoreManageId.ShouldBe(51); - result.DataStores[0].Status.ShouldBe(OdsInstanceManageStatus.Deleted.ToString()); - result.DataStores[0].Name.ShouldBe("Orphan-Newer"); - } - ``` - -- [ ] **Step 2: Run tests to verify they fail** - - Run: `dotnet test "Application/EdFi.Ods.AdminApi.V3.UnitTests/EdFi.Ods.AdminApi.V3.UnitTests.csproj" --filter "FullyQualifiedName~TenantServiceTests" --nologo` - - Expected: FAIL — the 6 edited tests fail with Shouldly mismatches (e.g. `dataStore.DataStoreManageId` is `null`, expected `10`). - -- [ ] **Step 3: Populate `DataStoreManageId` in `TenantMapper`** - - In `Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs`: - - Replace: - ```csharp - public static TenantDataStoreModel ToUnlinkedDataStoreManageModel(OdsInstanceManage source) - { - return new TenantDataStoreModel - { - DataStoreId = null, - Name = source.Name, - Status = source.Status, - DatabaseTemplate = source.DatabaseTemplate, - DatabaseName = source.DatabaseName, - }; - } - ``` - - With: - ```csharp - public static TenantDataStoreModel ToUnlinkedDataStoreManageModel(OdsInstanceManage source) - { - return new TenantDataStoreModel - { - DataStoreId = null, - DataStoreManageId = source.Id, - Name = source.Name, - Status = source.Status, - DatabaseTemplate = source.DatabaseTemplate, - DatabaseName = source.DatabaseName, - }; - } - ``` - -- [ ] **Step 4: Populate `DataStoreManageId` in `TenantService`** - - In `Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs`, in `GetTenantEdOrgsByInstancesAsync`: - - Replace: - ```csharp - foreach (var dataStore in tenantDetails.DataStores) - { - if (dataStore.DataStoreId is int dataStoreId && linkedDataStoreManagesByDataStoreId.TryGetValue(dataStoreId, out var dataStoreManage)) - { - dataStore.Status = dataStoreManage.Status; - dataStore.DatabaseTemplate = dataStoreManage.DatabaseTemplate; - dataStore.DatabaseName = dataStoreManage.DatabaseName; - } - else - { - dataStore.Status = OdsInstanceManageStatus.Created.ToString(); - } - } - ``` - - With: - ```csharp - foreach (var dataStore in tenantDetails.DataStores) - { - if (dataStore.DataStoreId is int dataStoreId && linkedDataStoreManagesByDataStoreId.TryGetValue(dataStoreId, out var dataStoreManage)) - { - dataStore.DataStoreManageId = dataStoreManage.Id; - dataStore.Status = dataStoreManage.Status; - dataStore.DatabaseTemplate = dataStoreManage.DatabaseTemplate; - dataStore.DatabaseName = dataStoreManage.DatabaseName; - } - else - { - dataStore.Status = OdsInstanceManageStatus.Created.ToString(); - } - } - ``` - -- [ ] **Step 5: Run tests to verify they pass** - - Run: `dotnet test "Application/EdFi.Ods.AdminApi.V3.UnitTests/EdFi.Ods.AdminApi.V3.UnitTests.csproj" --filter "FullyQualifiedName~TenantServiceTests" --nologo` - - Expected: PASS (all tests in the fixture, including the `[TestCaseSource(nameof(AllStatuses))]` parameterized cases). - -- [ ] **Step 6: Commit** - - ```bash - git add "Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs" - git commit -m "Populate DataStoreManageId on v3 tenant data stores" - ``` - ---- - -### Task 3: Update Bruno E2E schema assertions - -**Files:** -- Modify: `Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru` -- Modify: `Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru` - -**Interfaces:** -- Consumes: the JSON response shape produced by Task 2 (`dataStoreManageId`, `status`, `databaseTemplate`, `databaseName` now present on each `dataStores[]` entry). -- Produces: nothing consumed by later tasks — this is the last task in the plan. - -- [ ] **Step 1: Update the Multitenant schema** - - In `Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru`, inside `script:post-response`, in `GetTenantDataStoresEdOrgsSchema`: - - Replace: - ```javascript - "dataStores": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": ["integer", "null"] - }, - "name": { - "type": "string" - }, - "dataStoreType": { - "type": ["string", "null"] - }, - "educationOrganizations": { - ``` - - With: - ```javascript - "dataStores": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": ["integer", "null"] - }, - "dataStoreManageId": { - "type": ["integer", "null"] - }, - "name": { - "type": "string" - }, - "dataStoreType": { - "type": ["string", "null"] - }, - "status": { - "type": ["string", "null"] - }, - "databaseTemplate": { - "type": ["string", "null"] - }, - "databaseName": { - "type": ["string", "null"] - }, - "educationOrganizations": { - ``` - -- [ ] **Step 2: Update the Singletenant schema** - - In `Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru`, apply the identical replacement from Step 1 (same `GetTenantDataStoresEdOrgsSchema` block, same before/after text). - -- [ ] **Step 3: Run the v3 multitenant Bruno E2E suite** - - Run: `./eng/run-e2e-bruno.ps1 -ApiVersion 3 -TenantMode multitenant -TearDown` - - Expected: PASS, including `GET Tenants DataStores EdOrgs: Response matches schema` in both the Multitenant and Singletenant collection runs (Singletenant coverage runs as part of the same suite in single-tenant mode — if your local setup only runs one mode at a time, also run `./eng/run-e2e-bruno.ps1 -ApiVersion 3 -TenantMode singletenant -TearDown`). - - Note: this requires local Docker/DB setup per `docs/developer.md`. If that environment isn't available, at minimum re-read both edited `.bru` files to confirm the JSON is well-formed (matching brace/bracket structure) before committing. - -- [ ] **Step 4: Commit** - - ```bash - git add "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Multitenant.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/GET - Tenants EdOrgs by Tenant Name - Singletenant.bru" - git commit -m "Add dataStoreManageId and status fields to v3 tenant edOrgs E2E schema" - ``` - ---- - -### Task 4: Full verification - -**Files:** none (verification only) - -- [ ] **Step 1: Run the full v3 unit test project** - - Run: `dotnet test "Application/EdFi.Ods.AdminApi.V3.UnitTests/EdFi.Ods.AdminApi.V3.UnitTests.csproj" --nologo` - - Expected: PASS, 0 failures. - -- [ ] **Step 2: Run the full solution unit test suite** - - Run: `./build.ps1 -Command UnitTest` - - Expected: PASS, 0 failures across all `*.UnitTests` projects (confirms the v2 `TenantServiceTests`/`TenantDetailModelTests` — untouched by this plan — still pass, i.e. no accidental cross-version regression). - -- [ ] **Step 3: Confirm no unrelated files changed** - - Run: `git status` - - Expected: working tree clean (all changes already committed across Tasks 1–3); no unexpected modified files outside the ones listed in this plan's tasks. diff --git a/docs/superpowers/specs/2026-07-28-v3-tenant-datastore-manage-id-design.md b/docs/superpowers/specs/2026-07-28-v3-tenant-datastore-manage-id-design.md deleted file mode 100644 index 3615b8ab6..000000000 --- a/docs/superpowers/specs/2026-07-28-v3-tenant-datastore-manage-id-design.md +++ /dev/null @@ -1,110 +0,0 @@ -# Add DataStoreManageId to v3 Tenants/DataStores/EdOrgs response - -Date: 2026-07-28 - -## Summary - -v2's `/tenants/{tenantName}/odsInstances/edOrgs` endpoint returns `OdsInstanceManageId` on each -ods-instance entry — the id of the linked `OdsInstanceManage` record (provisioning status, -database template/name). v3's equivalent endpoint, `/tenants/{tenantName}/dataStores/edOrgs`, -never got the matching field on `TenantDataStoreModel`. This is a pre-existing parity gap, not a -new feature: the underlying query (`IGetDataStoreManagesQuery`) and DI wiring are already fully -present in v3 — only the model field and two assignments are missing. - -This change adds `DataStoreManageId` (v3's naming counterpart to v2's `OdsInstanceManageId`) to -`TenantDataStoreModel`, populates it in `TenantService` and `TenantMapper`, and updates unit tests -and Bruno E2E schemas to match. While in the Bruno E2E schema file, it also closes an unrelated -pre-existing gap where the v3 schema was missing `status`/`databaseTemplate`/`databaseName` -properties that `TenantDataStoreModel` already returns and v2's schema already validates. - -## Background / current state - -- v2: `TenantOdsInstanceModel.OdsInstanceManageId` (`Application/EdFi.Ods.AdminApi/Features/Tenants/TenantDetailModel.cs:34`) - is populated in two places: - - `TenantService.GetTenantEdOrgsByInstancesAsync` (`Application/EdFi.Ods.AdminApi/Infrastructure/Services/Tenants/TenantService.cs:156`) - sets it for instances linked to an `OdsInstanceManage` record. - - `TenantMapper.ToUnlinkedOdsInstanceManageModel` (`Application/EdFi.Ods.AdminApi/Features/Tenants/TenantMapper.cs:33`) - sets it for orphaned `OdsInstanceManage` records with no matching ods instance. -- v3: `TenantDataStoreModel` (`Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs`) - has no equivalent field. The parallel code in `TenantService` - (`Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs:152-164`) and - `TenantMapper.ToUnlinkedDataStoreManageModel` - (`Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs:28`) sets `Status`, - `DatabaseTemplate`, `DatabaseName` but never an id. -- `ReadTenants.cs` (both versions) and the query/DI layer already match structurally — no changes - needed there. -- v3's single-instance endpoint (`/dataStores/{id}/edOrgs`, - `DataStoreWithEducationOrganizationsModel`) already has `DataStoreManageId` correctly — this gap - is specific to the tenant-level `edOrgs` endpoint. -- Neither v2 nor v3 has any DBTests coverage for the tenants/edOrgs endpoint today, so DBTests are - not part of this change. -- JSON serialization uses the default camelCase policy (no explicit `JsonPropertyName` needed) — - `DataStoreManageId` will serialize as `dataStoreManageId`, matching the `odsInstanceManageId` / - `dataStoreId` casing convention already used on this model. - -## Naming - -Per the existing v2→v3 renaming convention on this model (`OdsInstance` → `DataStore`, -`OdsInstanceId` → `DataStoreId`), the new field is named `DataStoreManageId`. - -## Code changes - -1. **`Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantDetailModel.cs`** - Add `public int? DataStoreManageId { get; set; }` to `TenantDataStoreModel`, immediately after - `DataStoreId`, mirroring v2's field order. - -2. **`Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs`** - In `ToUnlinkedDataStoreManageModel`, add `DataStoreManageId = source.Id` (mirrors v2's - `ToUnlinkedOdsInstanceManageModel`). - -3. **`Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs`** - In `GetTenantEdOrgsByInstancesAsync`, in the linked-data-store loop, add - `dataStore.DataStoreManageId = dataStoreManage.Id;` alongside the existing `Status` / - `DatabaseTemplate` / `DatabaseName` assignments. - -No changes to `ReadTenants.cs`, queries, or DI registration. - -## Test changes - -### `Application/EdFi.Ods.AdminApi.V3.UnitTests` - -- **`Infrastructure/Services/Tenants/TenantServiceTests.cs`** — add `DataStoreManageId` assertions - (mirroring the equivalent v2 assertions already present in - `EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs`) to: - - `GetTenantEdOrgsByInstancesAsync_SetsStatusCreated_WhenDataStoreHasNoLinkedDataStoreManage` — - assert `DataStoreManageId.ShouldBeNull()`. - - `GetTenantEdOrgsByInstancesAsync_EnrichesDataStore_WithLinkedDataStoreManageFields` — assert - `DataStoreManageId.ShouldBe(10)`. - - `GetTenantEdOrgsByInstancesAsync_AddsUnlinkedDataStoreManages_WithNullIds` — extend the - `ShouldContain` predicates to include `DataStoreManageId == 20` / `== 21`. - - `GetTenantEdOrgsByInstancesAsync_MixedScenario_LinkedAndUnlinked` — assert - `DataStoreManageId.ShouldBe(30)` on the linked entry and `.ShouldBe(31)` on the unlinked entry. - - `GetTenantEdOrgsByInstancesAsync_AddsDataStoreManage_WhenLinkedToMissingDataStore_ForAllStatuses` - — assert `DataStoreManageId.ShouldBe(42)`. - - `GetTenantEdOrgsByInstancesAsync_AppendsLatestDataStoreManagePerMissingDataStoreId` — assert - `DataStoreManageId.ShouldBe(51)`. - -- **`Features/Tenants/TenantDetailModelTests.cs`** — in `Properties_ShouldBeSettable`, set - `DataStoreManageId = 10` on the constructed `TenantDataStoreModel` and assert it round-trips, - mirroring v2's equivalent test. - -### DBTests - -No changes — no existing DBTests coverage for this feature in either version. - -### Bruno E2E (`Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/Tenants/`) - -In both `GET - Tenants EdOrgs by Tenant Name - Multitenant.bru` and -`GET - Tenants EdOrgs by Tenant Name - Singletenant.bru`, update `GetTenantDataStoresEdOrgsSchema` -to add, matching v2's `GetTenantOdsInstancesEdOrgsSchema`: -- `dataStoreManageId`: `["integer", "null"]` -- `status`: `["string", "null"]` -- `databaseTemplate`: `["string", "null"]` -- `databaseName`: `["string", "null"]` - -## Out of scope - -- v3's single-instance `/dataStores/{id}/edOrgs` endpoint — already has `DataStoreManageId` - correctly. -- DBTests for the tenants/edOrgs endpoint (neither version has any today). -- Any other pre-existing v2/v3 parity gaps not touched by this endpoint's response model. From f70055fe0c17462091fbb97369d6989dc5b630e0 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Tue, 28 Jul 2026 15:10:12 -0600 Subject: [PATCH 32/35] Removes documentation --- ...rename-dbinstances-to-odsinstancemanage.md | 4051 ----------------- ...dbinstances-to-odsinstancemanage-design.md | 236 - 2 files changed, 4287 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-27-rename-dbinstances-to-odsinstancemanage.md delete mode 100644 docs/superpowers/specs/2026-07-27-rename-dbinstances-to-odsinstancemanage-design.md diff --git a/docs/superpowers/plans/2026-07-27-rename-dbinstances-to-odsinstancemanage.md b/docs/superpowers/plans/2026-07-27-rename-dbinstances-to-odsinstancemanage.md deleted file mode 100644 index 16b47cf30..000000000 --- a/docs/superpowers/plans/2026-07-27-rename-dbinstances-to-odsinstancemanage.md +++ /dev/null @@ -1,4051 +0,0 @@ -# Rename DbInstances/DbDataStores to OdsInstanceManage/DataStoreManage Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Rename the shared `DbInstance` entity/table to `OdsInstanceManage`, move v2's `Features\DbInstances` into `Features\OdsInstances\Manage` (`/v2/odsInstances/manage`), move v3's `Features\DbDataStores` into `Features\DataStores\Manage` (`/v3/dataStores/manage`), and update every dependent identifier, config key, test, and E2E artifact. - -**Architecture:** No new architecture — this is a mechanical rename+move refactor across two parallel, already-symmetric API version trees (v2 and v3) that share one entity/table in the `EdFi.Ods.AdminApi.Common` project. Each task renames one cohesive slice (shared layer, then v2, then v3, mirroring the same steps) and ends with a build+test checkpoint. - -**Tech Stack:** .NET / ASP.NET Core minimal APIs, EF Core, Quartz.NET, FluentValidation, NUnit + Shouldly, Bruno E2E collections, raw SQL migrations (MSSQL + PostgreSQL). - -## Global Constraints - -- Breaking change: old routes (`/v2/dbInstances/*`, `/v3/dbDataStores/*`) and old `AppSettings` config keys are removed outright — no back-compat aliases, no deprecation shims. -- Table rename is in-place (`sp_rename` / `ALTER TABLE ... RENAME TO`) to preserve existing data and identity/serial sequences — never drop-and-recreate. -- New migration artifact uses the next sequential number `00007-...sql`, added in all four locations: `Application\EdFi.Ods.AdminApi\Artifacts\{MsSql,PgSql}\Structure\Admin\` and `Application\EdFi.Ods.AdminApi.V3\Artifacts\{MsSql,PgSql}\Structure\Admin\` (only the v2 copy is actually consumed by `Install-AdminApiTables`/Docker init scripts; the v3 copy is a documentation-only duplicate kept in sync by convention). -- `CreateInstanceJob`/`DeleteInstanceJob` (both v2's and v3's own copies) are NOT renamed — generic names that don't contain "DbInstance"/"DbDataStore". -- v2's FK-style field is named `OdsInstanceManageId`; v3's is named `DataStoreManageId` — they are DTO-level fields on different response models, not the same property. -- New v3 unit tests are added for `GetDataStoreManagesQuery`/`GetDataStoreManageByIdQuery` to close a pre-existing coverage gap (v2 has these, v3 didn't). -- A new `DataStoreManageId` field is added to v3's `DataStoreWithEducationOrganizationsModel` for parity with v2's `OdsInstanceManageId`. -- `appsettings.json`/`appsettings.Development.json` config keys are renamed; `appsettings.Development.json` currently has uncommitted local edits (personal dev environment values) — preserve those values, only rename the keys. -- `docs/http/dbinstances.http` (renamed to `odsinstances-manage.http`) currently has uncommitted manual-testing edits — rebase the rename on top of those, don't discard them. -- Spec: `docs/superpowers/specs/2026-07-27-rename-dbinstances-to-odsinstancemanage-design.md`. -- v3's original `AddDbDataStore.cs`/`DeleteDbDataStore.cs` used parameter names that matched their type names in capitalization (e.g. `AddDbDataStoreCommand AddDbDataStoreCommand`, `IGetDbDataStoreByIdQuery GetDbDataStoreByIdQuery`) — an inconsistency with v2's lowerCamelCase convention. Tasks 11's rewritten code normalizes these to standard lowerCamelCase (`addDataStoreManageCommand`, `getDataStoreManageByIdQuery`) as an incidental, same-line cleanup, not a separate refactor. - ---- - -## Master Rename Table - -Reference this table from every task below instead of repeating it. Every occurrence of the "Old" token (as a whole identifier/word, not substring-inside-unrelated-word) becomes "New" in the files that task touches. - -### Shared (`EdFi.Ods.AdminApi.Common` project — affects both v2 and v3) - -| Old | New | -|---|---| -| `DbInstance` (entity class) | `OdsInstanceManage` | -| `DbInstances` (DbSet property / table name) | `OdsInstanceManages` | -| `DbInstanceStatus` (enum) | `OdsInstanceManageStatus` | -| `JobConstants.DbInstanceIdKey` | `JobConstants.OdsInstanceManageIdKey` | -| `JobConstants.CreatePendingDbInstancesDispatcherJobName` | `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName` | -| `JobConstants.DeletePendingDbInstancesDispatcherJobName` | `JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName` | -| `AppSettings.CreateDbInstancesSweepIntervalInMins` | `AppSettings.CreateOdsInstanceManagesSweepIntervalInMins` | -| `AppSettings.CreateDbInstancesMaxRetryAttempts` | `AppSettings.CreateOdsInstanceManagesMaxRetryAttempts` | -| `AppSettings.DeleteDbInstancesSweepIntervalInMins` | `AppSettings.DeleteOdsInstanceManagesSweepIntervalInMins` | -| `AppSettings.DeleteDbInstancesMaxRetryAttempts` | `AppSettings.DeleteOdsInstanceManagesMaxRetryAttempts` | -| Table `adminapi.DbInstances` | `adminapi.OdsInstanceManages` | -| Index `IX_DbInstances_Name` | `IX_OdsInstanceManages_Name` | -| Index `IX_DbInstances_OdsInstanceId` | `IX_OdsInstanceManages_OdsInstanceId` | -| Constraint `PK_DbInstances` | `PK_OdsInstanceManages` | - -### v2-specific (`EdFi.Ods.AdminApi` project) - -| Old | New | -|---|---| -| Namespace `EdFi.Ods.AdminApi.Features.DbInstances` | `EdFi.Ods.AdminApi.Features.OdsInstances.Manage` | -| `AddDbInstance` (class/file) | `AddOdsInstanceManage` | -| `ReadDbInstance` (class/file) | `ReadOdsInstanceManage` | -| `DeleteDbInstance` (class/file) | `DeleteOdsInstanceManage` | -| `DbInstanceModel` (class/file) | `OdsInstanceManageModel` | -| `DbInstanceMapper` (class/file) | `OdsInstanceManageMapper` | -| `DbInstanceDatabaseNameFormatter` (class/file) | `OdsInstanceManageDatabaseNameFormatter` | -| `AddDbInstanceRequest` | `AddOdsInstanceManageRequest` | -| `IAddDbInstanceModel` | `IAddOdsInstanceManageModel` | -| `AddDbInstanceCommand` | `AddOdsInstanceManageCommand` | -| `IDeleteDbInstanceCommand` / `DeleteDbInstanceCommand` | `IDeleteOdsInstanceManageCommand` / `DeleteOdsInstanceManageCommand` | -| `IGetDbInstancesQuery` / `GetDbInstancesQuery` | `IGetOdsInstanceManagesQuery` / `GetOdsInstanceManagesQuery` | -| `IGetDbInstanceByIdQuery` / `GetDbInstanceByIdQuery` | `IGetOdsInstanceManageByIdQuery` / `GetOdsInstanceManageByIdQuery` | -| `CreatePendingDbInstancesDispatcherJob` (v2 copy) | `CreatePendingOdsInstanceManagesDispatcherJob` | -| `DeletePendingDbInstancesDispatcherJob` (v2 copy) | `DeletePendingOdsInstanceManagesDispatcherJob` | -| `MaxDbInstanceNameLength` | `MaxOdsInstanceManageNameLength` | -| `_validDbInstanceNamePattern` | `_validOdsInstanceManageNamePattern` | -| Route `/dbInstances` | `/odsInstances/manage` | -| `OdsInstanceWithEducationOrganizationsModel.DbInstanceId` | `OdsInstanceManageId` | -| `MergeDbInstanceData` | `MergeOdsInstanceManageData` | -| `TenantOdsInstanceModel.DbInstanceId` (`Features\Tenants\TenantDetailModel.cs`) | `OdsInstanceManageId` | -| `TenantMapper.ToUnlinkedDbInstanceModel` | `TenantMapper.ToUnlinkedOdsInstanceManageModel` | - -### v3-specific (`EdFi.Ods.AdminApi.V3` project) - -| Old | New | -|---|---| -| Namespace `EdFi.Ods.AdminApi.V3.Features.DbDataStores` | `EdFi.Ods.AdminApi.V3.Features.DataStores.Manage` | -| `AddDbDataStore` (class/file) | `AddDataStoreManage` | -| `ReadDbDataStore` (class/file) | `ReadDataStoreManage` | -| `DeleteDbDataStore` (class/file) | `DeleteDataStoreManage` | -| `DbDataStoreModel` (class/file) | `DataStoreManageModel` | -| `DbDataStoreMapper` (class/file) | `DataStoreManageMapper` | -| `DbDataStoreDatabaseNameFormatter` (class/file) | `DataStoreManageDatabaseNameFormatter` | -| `AddDbDataStoreRequest` | `AddDataStoreManageRequest` | -| `IAddDbDataStoreModel` | `IAddDataStoreManageModel` | -| `AddDbDataStoreCommand` | `AddDataStoreManageCommand` | -| `IDeleteDbDataStoreCommand` / `DeleteDbDataStoreCommand` | `IDeleteDataStoreManageCommand` / `DeleteDataStoreManageCommand` | -| `IGetDbDataStoresQuery` / `GetDbDataStoresQuery` | `IGetDataStoreManagesQuery` / `GetDataStoreManagesQuery` | -| `IGetDbDataStoreByIdQuery` / `GetDbDataStoreByIdQuery` | `IGetDataStoreManageByIdQuery` / `GetDataStoreManageByIdQuery` | -| `CreatePendingDbInstancesDispatcherJob` (v3 copy) | `CreatePendingDataStoreManagesDispatcherJob` | -| `DeletePendingDbInstancesDispatcherJob` (v3 copy) | `DeletePendingDataStoreManagesDispatcherJob` | -| `MaxDbDataStoreNameLength` | `MaxDataStoreManageNameLength` | -| `_validDbDataStoreNamePattern` | `_validDataStoreManageNamePattern` | -| Route `/dbDataStores` | `/dataStores/manage` | -| `MergeDbDataStoreData` | `MergeDataStoreManageData` | -| *(new field, no old counterpart)* | `DataStoreWithEducationOrganizationsModel.DataStoreManageId` | -| `TenantMapper.ToUnlinkedDbDataStoreModel` | `TenantMapper.ToUnlinkedDataStoreManageModel` | - -Note: `DataStoreModel.DataStoreId`/`DataStoreModel.DataStoreType` (the existing `DataStore`'s own DTO fields, unrelated file) and `DbDataStoreModel.DataStoreId`/`DataStoreName` (already-renamed-at-DTO-level fields carried over unchanged into `DataStoreManageModel`) are **not** touched — only tokens literally containing `DbInstance`/`DbDataStore` change. - ---- - -### Task 1: Migration — rename `adminapi.DbInstances` to `adminapi.OdsInstanceManages` - -**Files:** -- Create: `Application\EdFi.Ods.AdminApi\Artifacts\MsSql\Structure\Admin\00007-RenameDbInstancesToOdsInstanceManages.sql` -- Create: `Application\EdFi.Ods.AdminApi\Artifacts\PgSql\Structure\Admin\00007-RenameDbInstancesToOdsInstanceManages.sql` -- Create: `Application\EdFi.Ods.AdminApi.V3\Artifacts\MsSql\Structure\Admin\00007-RenameDbInstancesToOdsInstanceManages.sql` -- Create: `Application\EdFi.Ods.AdminApi.V3\Artifacts\PgSql\Structure\Admin\00007-RenameDbInstancesToOdsInstanceManages.sql` - -**Interfaces:** -- Produces: table `adminapi.OdsInstanceManages` with the same columns as the old `adminapi.DbInstances` (`Id, Name, OdsInstanceId, OdsInstanceName, Status, DatabaseTemplate, DatabaseName, LastRefreshed, LastModifiedDate`), indexes `IX_OdsInstanceManages_Name` / `IX_OdsInstanceManages_OdsInstanceId`. Task 2's EF Core mapping depends on this table name existing. - -- [ ] **Step 1: Write the MSSQL migration script** - -```sql --- 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. - -IF EXISTS (SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = 'adminapi' AND TABLE_NAME = 'DbInstances') - AND NOT EXISTS (SELECT 1 FROM [INFORMATION_SCHEMA].[TABLES] WHERE TABLE_SCHEMA = 'adminapi' AND TABLE_NAME = 'OdsInstanceManages') -BEGIN - EXEC sp_rename 'adminapi.DbInstances', 'OdsInstanceManages'; - EXEC sp_rename 'adminapi.PK_DbInstances', 'PK_OdsInstanceManages'; - EXEC sp_rename 'adminapi.OdsInstanceManages.IX_DbInstances_Name', 'IX_OdsInstanceManages_Name', 'INDEX'; - EXEC sp_rename 'adminapi.OdsInstanceManages.IX_DbInstances_OdsInstanceId', 'IX_OdsInstanceManages_OdsInstanceId', 'INDEX'; -END -``` - -Save this file to all two MSSQL locations (`Application\EdFi.Ods.AdminApi\Artifacts\MsSql\Structure\Admin\` and `Application\EdFi.Ods.AdminApi.V3\Artifacts\MsSql\Structure\Admin\`) — byte-identical, matching the existing convention where `00005-CreateDbInstances.sql` is duplicated across both trees. - -- [ ] **Step 2: Write the PostgreSQL migration script** - -```sql --- 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. - -DO $$ -BEGIN - IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'adminapi' AND table_name = 'dbinstances') - AND NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'adminapi' AND table_name = 'odsinstancemanages') - THEN - ALTER TABLE adminapi.DbInstances RENAME TO OdsInstanceManages; - ALTER TABLE adminapi.OdsInstanceManages RENAME CONSTRAINT pk_dbinstances TO pk_odsinstancemanages; - ALTER INDEX adminapi.idx_dbinstances_name RENAME TO idx_odsinstancemanages_name; - ALTER INDEX adminapi.idx_dbinstances_odsinstanceid RENAME TO idx_odsinstancemanages_odsinstanceid; - END IF; -END $$; -``` - -Save this file to both PgSql locations (`Application\EdFi.Ods.AdminApi\Artifacts\PgSql\Structure\Admin\` and `Application\EdFi.Ods.AdminApi.V3\Artifacts\PgSql\Structure\Admin\`) — byte-identical. - -- [ ] **Step 3: Verify against a local database** - -Run: `./eng/run-dbup-migrations.ps1` (or the equivalent Docker init flow already used locally) against a database that has the old `00001`-`00006` scripts applied, then confirm: -- MSSQL: `SELECT name FROM sys.tables WHERE schema_id = SCHEMA_ID('adminapi');` shows `OdsInstanceManages`, not `DbInstances`. -- PostgreSQL: `\dt adminapi.*` shows `odsinstancemanages`, not `dbinstances`. -- Existing rows (if any were present before the rename) are unchanged in count and content. -- Re-running the script a second time is a no-op (idempotency guard prevents re-running `sp_rename`/`ALTER TABLE RENAME` once already renamed). - -- [ ] **Step 4: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi/Artifacts/MsSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql" \ - "Application/EdFi.Ods.AdminApi/Artifacts/PgSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql" \ - "Application/EdFi.Ods.AdminApi.V3/Artifacts/MsSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql" \ - "Application/EdFi.Ods.AdminApi.V3/Artifacts/PgSql/Structure/Admin/00007-RenameDbInstancesToOdsInstanceManages.sql" -git commit -m "Add migration renaming adminapi.DbInstances to adminapi.OdsInstanceManages" -``` - ---- - -### Task 2: Rename shared entity, enum, JobConstants, AppSettings (Common project + both DbContexts) - -**Files:** -- Modify: `Application\EdFi.Ods.AdminApi.Common\Infrastructure\Models\DbInstance.cs` → rename file to `OdsInstanceManage.cs` -- Modify: `Application\EdFi.Ods.AdminApi.Common\Constants\Constants.cs` -- Modify: `Application\EdFi.Ods.AdminApi.Common\Infrastructure\Jobs\JobConstants.cs` -- Modify: `Application\EdFi.Ods.AdminApi.Common\Settings\AppSettings.cs` -- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\AdminApiDbContext.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\AdminApiDbContext.cs` -- Modify: `Application\EdFi.Ods.AdminApi\appsettings.json` -- Modify: `Application\EdFi.Ods.AdminApi\appsettings.Development.json` -- Modify: `Application\EdFi.Ods.AdminApi.V3\appsettings.json` - -**Interfaces:** -- Produces: `OdsInstanceManage` entity class (namespace `EdFi.Ods.AdminApi.Common.Infrastructure.Models`), `OdsInstanceManageStatus` enum (namespace `EdFi.Ods.AdminApi.Common.Constants`), `JobConstants.OdsInstanceManageIdKey`/`CreatePendingOdsInstanceManagesDispatcherJobName`/`DeletePendingOdsInstanceManagesDispatcherJobName`, `AppSettings.CreateOdsInstanceManagesSweepIntervalInMins`/`CreateOdsInstanceManagesMaxRetryAttempts`/`DeleteOdsInstanceManagesSweepIntervalInMins`/`DeleteOdsInstanceManagesMaxRetryAttempts`. Every later task in this plan consumes these exact names. - -- [ ] **Step 1: Rename the entity file and class** - -```bash -git mv "Application/EdFi.Ods.AdminApi.Common/Infrastructure/Models/DbInstance.cs" "Application/EdFi.Ods.AdminApi.Common/Infrastructure/Models/OdsInstanceManage.cs" -``` - -Edit the file content to: - -```csharp -// 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.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace EdFi.Ods.AdminApi.Common.Infrastructure.Models; - -public class OdsInstanceManage -{ - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } - - [Required] - [StringLength(100)] - public string Name { get; set; } = string.Empty; - - public int? OdsInstanceId { get; set; } - - [StringLength(100)] - public string? OdsInstanceName { get; set; } - - [Required] - [StringLength(75)] - public string Status { get; set; } = string.Empty; - - [Required] - [StringLength(100)] - public string DatabaseTemplate { get; set; } = string.Empty; - - [StringLength(255)] - public string? DatabaseName { get; set; } - - [Required] - public DateTime LastRefreshed { get; set; } = DateTime.UtcNow; - - public DateTime? LastModifiedDate { get; set; } -} -``` - -- [ ] **Step 2: Rename the `DbInstanceStatus` enum in `Constants.cs`** - -In `Application\EdFi.Ods.AdminApi.Common\Constants\Constants.cs`, replace: - -```csharp -public enum DbInstanceStatus -``` - -with: - -```csharp -public enum OdsInstanceManageStatus -``` - -(the enum's member list — `PendingCreate, Created, CreateInProgress, CreateFailed, CreateError, PendingDelete, DeleteInProgress, Deleted, DeleteFailed, DeleteError` — is unchanged.) - -- [ ] **Step 3: Rename `JobConstants` members** - -In `Application\EdFi.Ods.AdminApi.Common\Infrastructure\Jobs\JobConstants.cs`, replace: - -```csharp - public const string DbInstanceIdKey = "DbInstanceId"; - public const string OdsInstanceIdKey = "OdsInstanceId"; - public const string CreateInstanceJobName = "CreateInstanceJob"; - public const string CreatePendingDbInstancesDispatcherJobName = "CreatePendingDbInstancesDispatcherJob"; - public const string DeleteInstanceJobName = "DeleteInstanceJob"; - public const string DeletePendingDbInstancesDispatcherJobName = "DeletePendingDbInstancesDispatcherJob"; -``` - -with: - -```csharp - public const string OdsInstanceManageIdKey = "OdsInstanceManageId"; - public const string OdsInstanceIdKey = "OdsInstanceId"; - public const string CreateInstanceJobName = "CreateInstanceJob"; - public const string CreatePendingOdsInstanceManagesDispatcherJobName = "CreatePendingOdsInstanceManagesDispatcherJob"; - public const string DeleteInstanceJobName = "DeleteInstanceJob"; - public const string DeletePendingOdsInstanceManagesDispatcherJobName = "DeletePendingOdsInstanceManagesDispatcherJob"; -``` - -(`OdsInstanceIdKey`, `CreateInstanceJobName`, `DeleteInstanceJobName`, `RefreshEducationOrganizationsJobName`, `RunIdKey`, `JobTypeKey`, `TenantNameKey` are unrelated and unchanged.) - -- [ ] **Step 4: Rename `AppSettings` properties** - -In `Application\EdFi.Ods.AdminApi.Common\Settings\AppSettings.cs`, replace: - -```csharp - public int CreateDbInstancesSweepIntervalInMins { get; set; } = 5; - public int CreateDbInstancesMaxRetryAttempts { get; set; } = 3; - public int DeleteDbInstancesSweepIntervalInMins { get; set; } = 5; - public int DeleteDbInstancesMaxRetryAttempts { get; set; } = 3; -``` - -with: - -```csharp - public int CreateOdsInstanceManagesSweepIntervalInMins { get; set; } = 5; - public int CreateOdsInstanceManagesMaxRetryAttempts { get; set; } = 3; - public int DeleteOdsInstanceManagesSweepIntervalInMins { get; set; } = 5; - public int DeleteOdsInstanceManagesMaxRetryAttempts { get; set; } = 3; -``` - -- [ ] **Step 5: Update both `AdminApiDbContext` classes** - -In `Application\EdFi.Ods.AdminApi\Infrastructure\AdminApiDbContext.cs` (and identically in `Application\EdFi.Ods.AdminApi.V3\Infrastructure\AdminApiDbContext.cs`), replace: - -```csharp - public DbSet DbInstances { get; set; } -``` - -with: - -```csharp - public DbSet OdsInstanceManages { get; set; } -``` - -and replace: - -```csharp - modelBuilder.Entity().ToTable("DbInstances").HasKey(t => t.Id); -``` - -with: - -```csharp - modelBuilder.Entity().ToTable("OdsInstanceManages").HasKey(t => t.Id); -``` - -- [ ] **Step 6: Update `appsettings.json` (v2) config keys** - -In `Application\EdFi.Ods.AdminApi\appsettings.json`, replace the four keys (preserving their existing values `120`/`3`/`120`/`3`): - -```json - "CreateDbInstancesSweepIntervalInMins": 120, - "CreateDbInstancesMaxRetryAttempts": 3, - "DeleteDbInstancesSweepIntervalInMins": 120, - "DeleteDbInstancesMaxRetryAttempts": 3, -``` - -with: - -```json - "CreateOdsInstanceManagesSweepIntervalInMins": 120, - "CreateOdsInstanceManagesMaxRetryAttempts": 3, - "DeleteOdsInstanceManagesSweepIntervalInMins": 120, - "DeleteOdsInstanceManagesMaxRetryAttempts": 3, -``` - -- [ ] **Step 7: Update `appsettings.Development.json` (v2) config keys** - -This file currently has uncommitted local edits. Read the file first to get its current exact values, then replace only the four key names (`CreateDbInstancesSweepIntervalInMins`, `CreateDbInstancesMaxRetryAttempts`, `DeleteDbInstancesSweepIntervalInMins`, `DeleteDbInstancesMaxRetryAttempts`) with their `OdsInstanceManages`-named equivalents, preserving whatever values are currently present (as of this plan's writing: `5`, `3`, `5`, `3`) and every other uncommitted edit in the file untouched. - -- [ ] **Step 8: Update `appsettings.json` (v3) config keys** - -Same four-key rename as Step 6, applied to `Application\EdFi.Ods.AdminApi.V3\appsettings.json` (existing values `120`/`3`/`120`/`3`). - -- [ ] **Step 9: Build to confirm no compile errors yet from callers** - -Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` (or the solution file this repo uses — check for a `.sln` at the repo root or under `Application\`). -Expected: FAILS — callers in v2/v3 Features/Infrastructure/Jobs still reference the old `DbInstance`/`DbInstanceStatus`/old `JobConstants`/old `AppSettings` names. This is expected at this checkpoint; Tasks 3–15 fix each caller. Confirm the failures are all in files this plan's later tasks will touch (grep the build output for `DbInstance`/`DbDataStore` to sanity-check no unexpected file is affected). - -- [ ] **Step 10: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi.Common/Infrastructure/Models/OdsInstanceManage.cs" \ - "Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs" \ - "Application/EdFi.Ods.AdminApi.Common/Infrastructure/Jobs/JobConstants.cs" \ - "Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs" \ - "Application/EdFi.Ods.AdminApi/Infrastructure/AdminApiDbContext.cs" \ - "Application/EdFi.Ods.AdminApi.V3/Infrastructure/AdminApiDbContext.cs" \ - "Application/EdFi.Ods.AdminApi/appsettings.json" \ - "Application/EdFi.Ods.AdminApi/appsettings.Development.json" \ - "Application/EdFi.Ods.AdminApi.V3/appsettings.json" -git commit -m "Rename shared DbInstance entity/enum/config keys to OdsInstanceManage" -``` - -Note: this commit intentionally leaves the solution non-building until Task 3 onward completes — that's expected for a rename this wide; each subsequent task is reviewed independently but the "build passes" checkpoint only becomes true again at the end of Task 9 (v2 side fully done) and again at the end of Task 15 (v3 side fully done). If your workflow requires green-build-per-commit, squash Tasks 2–9 (or 2–15) before merging instead of committing at each intermediate step — call this out to whoever reviews the branch. - ---- - -### Task 3: v2 Infrastructure layer rename (Queries, Commands, Jobs) - -**Files:** -- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Database\Queries\GetDbInstancesQuery.cs` → rename to `GetOdsInstanceManagesQuery.cs` -- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Database\Queries\GetDbInstanceByIdQuery.cs` → rename to `GetOdsInstanceManageByIdQuery.cs` -- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Database\Commands\AddDbInstanceCommand.cs` → rename to `AddOdsInstanceManageCommand.cs` -- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Database\Commands\DeleteDbInstanceCommand.cs` → rename to `DeleteOdsInstanceManageCommand.cs` -- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Jobs\CreatePendingDbInstancesDispatcherJob.cs` → rename to `CreatePendingOdsInstanceManagesDispatcherJob.cs` -- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Jobs\DeletePendingDbInstancesDispatcherJob.cs` → rename to `DeletePendingOdsInstanceManagesDispatcherJob.cs` -- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Jobs\CreateInstanceJob.cs` (renamed in place, filename unchanged) -- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Jobs\DeleteInstanceJob.cs` (renamed in place, filename unchanged) - -**Interfaces:** -- Consumes: `OdsInstanceManage` entity, `OdsInstanceManageStatus` enum, `JobConstants.OdsInstanceManageIdKey`, `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName`/`DeletePendingOdsInstanceManagesDispatcherJobName`, `AppSettings.CreateOdsInstanceManagesMaxRetryAttempts`/`DeleteOdsInstanceManagesMaxRetryAttempts` (all from Task 2), `AdminApiDbContext.OdsInstanceManages` (from Task 2). -- Produces: `IGetOdsInstanceManagesQuery`/`GetOdsInstanceManagesQuery`, `IGetOdsInstanceManageByIdQuery`/`GetOdsInstanceManageByIdQuery`, `AddOdsInstanceManageCommand`/`IAddOdsInstanceManageModel`, `IDeleteOdsInstanceManageCommand`/`DeleteOdsInstanceManageCommand`, `CreatePendingOdsInstanceManagesDispatcherJob`, `DeletePendingOdsInstanceManagesDispatcherJob` — consumed by Task 4 (feature files) and Task 6 (wiring). - -- [ ] **Step 1: Rename and update the Queries** - -```bash -git mv "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetDbInstancesQuery.cs" "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManagesQuery.cs" -git mv "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetDbInstanceByIdQuery.cs" "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQuery.cs" -``` - -`GetOdsInstanceManagesQuery.cs`: - -```csharp -// 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.Infrastructure; -using EdFi.Ods.AdminApi.Common.Infrastructure.Models; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Infrastructure.Extensions; -using Microsoft.Extensions.Options; - -namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; - -public interface IGetOdsInstanceManagesQuery -{ - List Execute(CommonQueryParams commonQueryParams, int? id, string? name); -} - -public class GetOdsInstanceManagesQuery : IGetOdsInstanceManagesQuery -{ - private readonly AdminApiDbContext _context; - private readonly IOptions _options; - - public GetOdsInstanceManagesQuery(AdminApiDbContext context, IOptions options) - { - _context = context; - _options = options; - } - - public List Execute(CommonQueryParams commonQueryParams, int? id, string? name) - { - return _context.OdsInstanceManages - .Where(d => id == null || d.Id == id) - .Where(d => name == null || d.Name == name) - .OrderBy(d => d.Id) - .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) - .ToList(); - } -} -``` - -`GetOdsInstanceManageByIdQuery.cs`: - -```csharp -// 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.Infrastructure.Models; - -namespace EdFi.Ods.AdminApi.Infrastructure.Database.Queries; - -public interface IGetOdsInstanceManageByIdQuery -{ - OdsInstanceManage? Execute(int id); -} - -public class GetOdsInstanceManageByIdQuery : IGetOdsInstanceManageByIdQuery -{ - private readonly AdminApiDbContext _context; - - public GetOdsInstanceManageByIdQuery(AdminApiDbContext context) - { - _context = context; - } - - public OdsInstanceManage? Execute(int id) - { - return _context.OdsInstanceManages.SingleOrDefault(d => d.Id == id); - } -} -``` - -- [ ] **Step 2: Rename and update the Commands** - -```bash -git mv "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddDbInstanceCommand.cs" "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceManageCommand.cs" -git mv "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteDbInstanceCommand.cs" "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommand.cs" -``` - -`AddOdsInstanceManageCommand.cs`: - -```csharp -// 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.Constants; -using EdFi.Ods.AdminApi.Common.Infrastructure.Models; - -namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; - -public class AddOdsInstanceManageCommand -{ - private readonly AdminApiDbContext _context; - - public AddOdsInstanceManageCommand(AdminApiDbContext context) - { - _context = context; - } - - public OdsInstanceManage Execute(IAddOdsInstanceManageModel model) - { - if (string.IsNullOrWhiteSpace(model.Name)) - throw new ArgumentException("Name is required.", nameof(model)); - if (string.IsNullOrWhiteSpace(model.DatabaseTemplate)) - throw new ArgumentException("DatabaseTemplate is required.", nameof(model)); - - var now = DateTime.UtcNow; - - var odsInstanceManage = new OdsInstanceManage - { - Name = model.Name.Trim(), - DatabaseTemplate = model.DatabaseTemplate.Trim(), - Status = OdsInstanceManageStatus.PendingCreate.ToString(), - OdsInstanceId = null, - OdsInstanceName = null, - DatabaseName = null, - LastRefreshed = now, - LastModifiedDate = now - }; - - _context.OdsInstanceManages.Add(odsInstanceManage); - _context.SaveChanges(); - return odsInstanceManage; - } -} - -public interface IAddOdsInstanceManageModel -{ - string? Name { get; } - string? DatabaseTemplate { get; } -} -``` - -`DeleteOdsInstanceManageCommand.cs`: - -```csharp -// 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.Constants; -using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; - -namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; - -public interface IDeleteOdsInstanceManageCommand -{ - void Execute(int id); -} - -public class DeleteOdsInstanceManageCommand : IDeleteOdsInstanceManageCommand -{ - private readonly AdminApiDbContext _context; - - public DeleteOdsInstanceManageCommand(AdminApiDbContext context) - { - _context = context; - } - - public void Execute(int id) - { - var odsInstanceManage = - _context.OdsInstanceManages.Find(id) - ?? throw new NotFoundException("odsInstanceManage", id); - - if (odsInstanceManage.Status != OdsInstanceManageStatus.Created.ToString()) - throw new NotFoundException("odsInstanceManage", id); - - odsInstanceManage.Status = OdsInstanceManageStatus.PendingDelete.ToString(); - odsInstanceManage.LastModifiedDate = DateTime.UtcNow; - - _context.SaveChanges(); - } -} -``` - -- [ ] **Step 3: Rename and update the dispatcher Jobs** - -```bash -git mv "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJob.cs" "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJob.cs" -git mv "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJob.cs" "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJob.cs" -``` - -`CreatePendingOdsInstanceManagesDispatcherJob.cs` (apply the Master Rename Table to the existing content — class name, constructor param, local variable names `eligibleDbInstances`→`eligibleOdsInstanceManages`, `dbInstance`→`odsInstanceManage` loop variable, `JobConstants.DbInstanceIdKey`→`JobConstants.OdsInstanceManageIdKey`, `DbInstanceStatus`→`OdsInstanceManageStatus`, `_options.Value.CreateDbInstancesMaxRetryAttempts`→`_options.Value.CreateOdsInstanceManagesMaxRetryAttempts`, `adminApiDbContext.DbInstances`→`adminApiDbContext.OdsInstanceManages`): - -```csharp -// 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.Admin.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Common.Constants; -using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; -using EdFi.Ods.AdminApi.Common.Infrastructure.Models; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; -using Quartz; - -namespace EdFi.Ods.AdminApi.Infrastructure.Services.Jobs; - -[DisallowConcurrentExecution] -public class CreatePendingOdsInstanceManagesDispatcherJob( - ILogger logger, - IJobStatusService jobStatusService, - AdminApiDbContext dbContext, - ITenantSpecificDbContextProvider tenantSpecificDbContextProvider, - IOptions options) - : AdminApiQuartzJobBase(logger, jobStatusService) -{ - private const int DefaultMaxRetryAttempts = 3; - - private readonly AdminApiDbContext _dbContext = dbContext; - private readonly ITenantSpecificDbContextProvider _tenantSpecificDbContextProvider = tenantSpecificDbContextProvider; - private readonly IOptions _options = options; - - protected override async Task ExecuteJobAsync(IJobExecutionContext context) - { - var multiTenancyEnabled = _options.Value.MultiTenancy; - var tenantName = GetTenantName(context, multiTenancyEnabled); - AdminApiDbContext? tenantAdminApiDbContext = null; - var adminApiDbContext = _dbContext; - - try - { - if (multiTenancyEnabled) - { - tenantAdminApiDbContext = _tenantSpecificDbContextProvider.GetAdminApiDbContext(tenantName!); - adminApiDbContext = tenantAdminApiDbContext; - } - - var eligibleOdsInstanceManages = await adminApiDbContext.OdsInstanceManages - .Where(instance => instance.Status == OdsInstanceManageStatus.PendingCreate.ToString() || instance.Status == OdsInstanceManageStatus.CreateFailed.ToString()) - .OrderBy(instance => instance.Id) - .ToListAsync(); - - foreach (var odsInstanceManage in eligibleOdsInstanceManages) - { - if (string.Equals(odsInstanceManage.Status, OdsInstanceManageStatus.PendingCreate.ToString(), StringComparison.OrdinalIgnoreCase)) - { - await ScheduleCreateJobAsync(context, odsInstanceManage.Id, tenantName); - continue; - } - - if (!await IsRetryEligibleAsync(adminApiDbContext, odsInstanceManage, tenantName)) - { - odsInstanceManage.Status = OdsInstanceManageStatus.CreateError.ToString(); - odsInstanceManage.LastModifiedDate = DateTime.UtcNow; - odsInstanceManage.LastRefreshed = DateTime.UtcNow; - await adminApiDbContext.SaveChangesAsync(); - continue; - } - - odsInstanceManage.Status = OdsInstanceManageStatus.PendingCreate.ToString(); - odsInstanceManage.LastModifiedDate = DateTime.UtcNow; - odsInstanceManage.LastRefreshed = DateTime.UtcNow; - await adminApiDbContext.SaveChangesAsync(); - - await ScheduleCreateJobAsync(context, odsInstanceManage.Id, tenantName); - } - } - finally - { - if (tenantAdminApiDbContext is not null) - { - await tenantAdminApiDbContext.DisposeAsync(); - } - } - } - - private async Task IsRetryEligibleAsync(AdminApiDbContext adminApiDbContext, OdsInstanceManage odsInstanceManage, string? tenantName) - { - var maxRetryAttempts = _options.Value.CreateOdsInstanceManagesMaxRetryAttempts > 0 - ? _options.Value.CreateOdsInstanceManagesMaxRetryAttempts - : DefaultMaxRetryAttempts; - - var jobIdPrefix = $"{CreateInstanceJob.BuildJobIdentity(odsInstanceManage.Id, tenantName)}_"; - var errorCount = await adminApiDbContext.JobStatuses - .CountAsync(status => status.JobId.StartsWith(jobIdPrefix) && status.Status == QuartzJobStatus.Error.ToString()); - - return errorCount < maxRetryAttempts; - } - - private static async Task ScheduleCreateJobAsync(IJobExecutionContext context, int odsInstanceManageId, string? tenantName) - { - var jobData = new Dictionary - { - [JobConstants.OdsInstanceManageIdKey] = odsInstanceManageId - }; - - if (!string.IsNullOrWhiteSpace(tenantName)) - { - jobData[JobConstants.TenantNameKey] = tenantName; - } - - await QuartzJobScheduler.ScheduleJob( - context.Scheduler, - CreateInstanceJob.CreateJobKey(odsInstanceManageId, tenantName), - jobData, - startImmediately: true); - } - - private static string? GetTenantName(IJobExecutionContext context, bool multiTenancyEnabled) - { - if (!multiTenancyEnabled) - { - return null; - } - - var tenantName = context.MergedJobDataMap.ContainsKey(JobConstants.TenantNameKey) - ? context.MergedJobDataMap.GetString(JobConstants.TenantNameKey) - : null; - - if (string.IsNullOrWhiteSpace(tenantName)) - { - throw new InvalidOperationException( - $"{JobConstants.TenantNameKey} must be provided when multi-tenancy is enabled."); - } - - return tenantName; - } -} -``` - -`DeletePendingOdsInstanceManagesDispatcherJob.cs` — apply the identical transformation (class name, `eligibleOdsInstanceManages`, `odsInstanceManage` loop var, `DeleteOdsInstanceManagesMaxRetryAttempts`, `PendingDelete`/`DeleteFailed`/`DeleteError` status branches instead of Create's) mirroring `CreatePendingOdsInstanceManagesDispatcherJob.cs` above but keeping the Delete-specific status logic and calling `DeleteInstanceJob` (unchanged name) instead of `CreateInstanceJob`. - -- [ ] **Step 4: Update `CreateInstanceJob.cs` and `DeleteInstanceJob.cs` in place (filenames unchanged, class names unchanged — only internal references)** - -In `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Jobs\CreateInstanceJob.cs`: -- Replace `using EdFi.Ods.AdminApi.Features.DbInstances;` with `using EdFi.Ods.AdminApi.Features.OdsInstances.Manage;` (namespace of `OdsInstanceManageDatabaseNameFormatter`, produced by Task 4 — this file will not compile until Task 4 completes; that's expected, note it in the PR). -- Replace every `DbInstance? dbInstance` / `DbInstance dbInstance` type reference with `OdsInstanceManage? odsInstanceManage` / `OdsInstanceManage odsInstanceManage` (rename the local variable throughout the method body too, e.g. `dbInstance.Status`→`odsInstanceManage.Status`). -- Replace `adminApiDbContext.DbInstances` with `adminApiDbContext.OdsInstanceManages`. -- Replace `DbInstanceStatus` with `OdsInstanceManageStatus` (all switch/enum references). -- Replace `JobConstants.DbInstanceIdKey` with `JobConstants.OdsInstanceManageIdKey`. -- Replace `DbInstanceDatabaseNameFormatter` with `OdsInstanceManageDatabaseNameFormatter`. -- Replace `int dbInstanceId` parameter names with `int odsInstanceManageId` in `CreateJobKey`/`BuildJobIdentity`. -- Update the comment `// The CreatePendingDbInstancesDispatcherJob may have already scheduled...` (if present) and any other prose comment mentioning "DbInstance" to say "OdsInstanceManage" instead, and `CreatePendingDbInstancesDispatcherJob` to `CreatePendingOdsInstanceManagesDispatcherJob`. - -In `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Jobs\DeleteInstanceJob.cs`: apply the identical set of replacements (this file doesn't reference `DbInstanceDatabaseNameFormatter`, so skip that one). - -- [ ] **Step 5: Build to confirm the Infrastructure layer compiles in isolation** - -Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` -Expected: still FAILS — `Features\DbInstances\*` (Task 4), `Features\OdsInstances\*` (Task 5), `Program.cs`/`WebApplicationBuilderExtensions.cs` (Task 6) haven't been updated yet. Confirm the remaining errors are now confined to those files only (no errors left in `Infrastructure\Database\*` or `Infrastructure\Services\Jobs\*`). - -- [ ] **Step 6: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManagesQuery.cs" \ - "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQuery.cs" \ - "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddOdsInstanceManageCommand.cs" \ - "Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommand.cs" \ - "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJob.cs" \ - "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJob.cs" \ - "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/CreateInstanceJob.cs" \ - "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Jobs/DeleteInstanceJob.cs" -git commit -m "Rename v2 Infrastructure Queries/Commands/Jobs to OdsInstanceManage naming" -``` - ---- - -### Task 4: v2 Feature folder move — `Features\DbInstances` → `Features\OdsInstances\Manage` - -**Files:** -- Create: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\Manage\AddOdsInstanceManage.cs` -- Create: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\Manage\ReadOdsInstanceManage.cs` -- Create: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\Manage\DeleteOdsInstanceManage.cs` -- Create: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\Manage\OdsInstanceManageModel.cs` -- Create: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\Manage\OdsInstanceManageMapper.cs` -- Create: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\Manage\OdsInstanceManageDatabaseNameFormatter.cs` -- Delete: `Application\EdFi.Ods.AdminApi\Features\DbInstances\` (entire folder, all 6 old files) - -**Interfaces:** -- Consumes: `AddOdsInstanceManageCommand`/`IAddOdsInstanceManageModel`, `IGetOdsInstanceManagesQuery`/`IGetOdsInstanceManageByIdQuery`, `IDeleteOdsInstanceManageCommand` (Task 3), `OdsInstanceManage`/`OdsInstanceManageStatus` (Task 2), `JobConstants.OdsInstanceManageIdKey` (Task 2), `CreateInstanceJob`/`DeleteInstanceJob` (Task 3, unchanged names). -- Produces: routes `POST/GET/DELETE /odsInstances/manage` under `EdFi.Ods.AdminApi.Features.OdsInstances.Manage`, consumed by Task 9 (Bruno E2E) and Task 17 (`.http` file). - -- [ ] **Step 1: Move the folder with git so history follows, then delete leftovers** - -```bash -git mv "Application/EdFi.Ods.AdminApi/Features/DbInstances/AddDbInstance.cs" "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/AddOdsInstanceManage.cs" -git mv "Application/EdFi.Ods.AdminApi/Features/DbInstances/ReadDbInstance.cs" "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/ReadOdsInstanceManage.cs" -git mv "Application/EdFi.Ods.AdminApi/Features/DbInstances/DeleteDbInstance.cs" "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/DeleteOdsInstanceManage.cs" -git mv "Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceModel.cs" "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageModel.cs" -git mv "Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceMapper.cs" "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageMapper.cs" -git mv "Application/EdFi.Ods.AdminApi/Features/DbInstances/DbInstanceDatabaseNameFormatter.cs" "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage/OdsInstanceManageDatabaseNameFormatter.cs" -``` - -Confirm `Application\EdFi.Ods.AdminApi\Features\DbInstances\` is now empty and remove the empty folder if git/your OS leaves it behind. - -- [ ] **Step 2: Replace `AddOdsInstanceManage.cs` content** - -```csharp -// 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.RegularExpressions; - -using EdFi.Admin.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Common.Features; -using EdFi.Ods.AdminApi.Common.Infrastructure; -using EdFi.Ods.AdminApi.Common.Infrastructure.Context; -using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; -using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; -using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Infrastructure; -using EdFi.Ods.AdminApi.Common.Constants; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using EdFi.Ods.AdminApi.Infrastructure.Services.Jobs; -using FluentValidation; -using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Quartz; -using Swashbuckle.AspNetCore.Annotations; - -namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; - -public class AddOdsInstanceManage : IFeature -{ - private const int MaxSynchronizedNameLength = 100; - private const int MaxOdsInstanceManageNameLength = MaxSynchronizedNameLength; - private static readonly Regex _validOdsInstanceManageNamePattern = new( - "^[A-Za-z0-9 _]+$", - RegexOptions.Compiled | RegexOptions.CultureInvariant); - - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder - .MapPost(endpoints, "/odsInstances/manage", Handle) - .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponseCode(202)) - .BuildForVersions(AdminApiVersions.V2); - } - - public async static Task Handle( - Validator validator, - AddOdsInstanceManageCommand addOdsInstanceManageCommand, - [FromServices] ISchedulerFactory schedulerFactory, - [FromServices] IContextProvider tenantConfigurationProvider, - [FromServices] IOptions options, - AddOdsInstanceManageRequest request) - { - await validator.GuardAsync(request); - - var added = addOdsInstanceManageCommand.Execute(request); - - var tenantIdentifier = options.Value.MultiTenancy - ? tenantConfigurationProvider.Get()?.TenantIdentifier - : null; - - var jobBuilder = JobBuilder.Create() - .WithIdentity(CreateInstanceJob.CreateJobKey(added.Id, tenantIdentifier)) - .UsingJobData(JobConstants.OdsInstanceManageIdKey, added.Id); - - if (!string.IsNullOrWhiteSpace(tenantIdentifier)) - { - jobBuilder = jobBuilder.UsingJobData(JobConstants.TenantNameKey, tenantIdentifier); - } - - var trigger = TriggerBuilder.Create() - .StartNow() - .Build(); - - var scheduler = await schedulerFactory.GetScheduler(); - - try - { - await scheduler.ScheduleJob(jobBuilder.Build(), trigger); - } - catch (ObjectAlreadyExistsException) - { - // The CreatePendingOdsInstanceManagesDispatcherJob may have already scheduled this job - // (e.g. it fired between the DB insert and this ScheduleJob call). Treat duplicate - // scheduling as success — the job is already queued and will process the OdsInstanceManage. - } - - return Results.Accepted($"/odsinstances/manage/{added.Id}", null); - } - - [SwaggerSchema(Title = "AddOdsInstanceManageRequest")] - public class AddOdsInstanceManageRequest : IAddOdsInstanceManageModel - { - [SwaggerSchema(Description = "Name of the database instance", Nullable = false)] - public string? Name { get; set; } - - [SwaggerSchema(Description = "Database template to use for the instance", Nullable = false)] - public string? DatabaseTemplate { get; set; } - } - - public class Validator : AbstractValidator - { - private static readonly string[] _validDatabaseTemplates = Enum.GetNames(); - private readonly AdminApiDbContext _adminApiDbContext; - private readonly IUsersContext _usersContext; - - public Validator(AdminApiDbContext adminApiDbContext, IUsersContext usersContext) - { - _adminApiDbContext = adminApiDbContext; - _usersContext = usersContext; - - RuleFor(m => m.Name) - .NotEmpty() - .MaximumLength(MaxOdsInstanceManageNameLength) - .WithMessage($"'{{PropertyName}}' must be {MaxOdsInstanceManageNameLength} characters or fewer so the synchronized ODS instance name fits within {MaxSynchronizedNameLength} characters.") - .Matches(_validOdsInstanceManageNamePattern) - .WithMessage("'{PropertyName}' may only contain letters, numbers, spaces, and underscores."); - - RuleFor(m => m.DatabaseTemplate).NotEmpty().MaximumLength(100) - .Must(t => t != null && _validDatabaseTemplates.Contains(t)) - .WithMessage($"'{{PropertyValue}}' is not a valid database template. Allowed values are: {string.Join(", ", _validDatabaseTemplates)}."); - - RuleFor(m => m).CustomAsync(async (request, context, cancellationToken) => - { - if (string.IsNullOrWhiteSpace(request.Name) - || string.IsNullOrWhiteSpace(request.DatabaseTemplate) - || request.Name.Length > MaxOdsInstanceManageNameLength - || !_validOdsInstanceManageNamePattern.IsMatch(request.Name) - || !_validDatabaseTemplates.Contains(request.DatabaseTemplate)) - { - return; - } - - var normalizedName = request.Name.Trim(); - - if (await _adminApiDbContext.OdsInstanceManages.AnyAsync(instance => instance.Name == normalizedName && instance.Status != OdsInstanceManageStatus.Deleted.ToString(), cancellationToken)) - { - context.AddFailure( - nameof(AddOdsInstanceManageRequest.Name), - $"An OdsInstanceManage named '{normalizedName}' already exists."); - return; - } - - if (await _usersContext.OdsInstances.AnyAsync(instance => instance.Name == normalizedName, cancellationToken)) - { - context.AddFailure( - nameof(AddOdsInstanceManageRequest.Name), - $"An OdsInstance named '{normalizedName}' already exists."); - return; - } - - var databaseName = OdsInstanceManageDatabaseNameFormatter.Build(request.Name, request.DatabaseTemplate); - - if (databaseName.Length > OdsInstanceManageDatabaseNameFormatter.MaxPortableDatabaseNameLength) - { - context.AddFailure( - nameof(AddOdsInstanceManageRequest.Name), - $"The generated database name '{databaseName}' exceeds the portable limit of {OdsInstanceManageDatabaseNameFormatter.MaxPortableDatabaseNameLength} characters. Shorten Name or DatabaseTemplate."); - } - }); - } - } -} -``` - -- [ ] **Step 3: Replace `ReadOdsInstanceManage.cs` content** - -```csharp -// 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.ErrorHandling; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; - -namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; - -public class ReadOdsInstanceManage : IFeature -{ - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstances/manage", GetOdsInstanceManages) - .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V2); - - AdminApiEndpointBuilder.MapGet(endpoints, "/odsInstances/manage/{id}", GetOdsInstanceManage) - .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V2); - } - - public static Task GetOdsInstanceManages(IGetOdsInstanceManagesQuery query, - [AsParameters] CommonQueryParams commonQueryParams, int? id, string? name) - { - var list = OdsInstanceManageMapper.ToModelList(query.Execute(commonQueryParams, id, name)); - return Task.FromResult(Results.Ok(list)); - } - - public static Task GetOdsInstanceManage(IGetOdsInstanceManageByIdQuery query, int id) - { - var odsInstanceManage = query.Execute(id); - if (odsInstanceManage == null) - { - throw new NotFoundException("odsInstanceManage", id); - } - var model = OdsInstanceManageMapper.ToModel(odsInstanceManage); - return Task.FromResult(Results.Ok(model)); - } -} -``` - -- [ ] **Step 4: Replace `DeleteOdsInstanceManage.cs` content** - -```csharp -// 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.Constants; -using EdFi.Ods.AdminApi.Common.Features; -using EdFi.Ods.AdminApi.Common.Infrastructure; -using EdFi.Ods.AdminApi.Common.Infrastructure.Context; -using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; -using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; -using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using EdFi.Ods.AdminApi.Infrastructure.Services.Jobs; -using FluentValidation; -using FluentValidation.Results; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Quartz; - -namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; - -public class DeleteOdsInstanceManage : IFeature -{ - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder - .MapDelete(endpoints, "/odsInstances/manage/{id}", Handle) - .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponseCode(204)) - .BuildForVersions(AdminApiVersions.V2); - } - - public static async Task Handle( - IGetOdsInstanceManageByIdQuery getOdsInstanceManageByIdQuery, - IDeleteOdsInstanceManageCommand deleteOdsInstanceManageCommand, - [FromServices] ISchedulerFactory schedulerFactory, - [FromServices] IContextProvider tenantConfigurationProvider, - [FromServices] IOptions options, - int id - ) - { - var odsInstanceManage = getOdsInstanceManageByIdQuery.Execute(id); - if (odsInstanceManage is null) - throw new NotFoundException("odsInstanceManage", id); - - if (odsInstanceManage.Status == OdsInstanceManageStatus.Deleted.ToString()) - throw new NotFoundException("odsInstanceManage", id); - - var blockingMessage = GetBlockingStatusMessage(odsInstanceManage.Status); - if (blockingMessage is not null) - throw new ValidationException([new ValidationFailure(nameof(id), blockingMessage)]); - - deleteOdsInstanceManageCommand.Execute(id); - - var tenantName = options.Value.MultiTenancy - ? tenantConfigurationProvider.Get()?.TenantIdentifier - : null; - var jobData = new Dictionary - { - [JobConstants.OdsInstanceManageIdKey] = id - }; - - if (!string.IsNullOrWhiteSpace(tenantName)) - { - jobData[JobConstants.TenantNameKey] = tenantName; - } - - var scheduler = await schedulerFactory.GetScheduler(); - - try - { - await QuartzJobScheduler.ScheduleJob( - scheduler, - DeleteInstanceJob.CreateJobKey(id, tenantName), - jobData, - startImmediately: true); - } - catch (ObjectAlreadyExistsException) - { - // The DeletePendingOdsInstanceManagesDispatcherJob may have already scheduled this job. - // Treat duplicate scheduling as success — the job is already queued. - } - - return Results.NoContent(); - } - - private static string? GetBlockingStatusMessage(string status) - { - if (Enum.TryParse(status, ignoreCase: true, out var parsed)) - { - return parsed switch - { - OdsInstanceManageStatus.PendingCreate => "OdsInstanceManage is being provisioned. Wait for creation to complete.", - OdsInstanceManageStatus.CreateInProgress => "OdsInstanceManage is currently being provisioned. Wait for creation to complete.", - OdsInstanceManageStatus.CreateFailed => "OdsInstanceManage creation failed. It will be retried automatically by the background job.", - OdsInstanceManageStatus.CreateError => "OdsInstanceManage creation failed permanently. Manual database intervention required before deleting.", - OdsInstanceManageStatus.PendingDelete => "OdsInstanceManage is already queued for deletion.", - OdsInstanceManageStatus.DeleteInProgress => "OdsInstanceManage is currently being deleted.", - OdsInstanceManageStatus.DeleteFailed => "OdsInstanceManage deletion failed. It will be retried automatically by the background job.", - OdsInstanceManageStatus.DeleteError => "OdsInstanceManage deletion failed permanently. Manual database intervention required.", - _ => null, - }; - } - - return null; - } -} -``` - -- [ ] **Step 5: Replace `OdsInstanceManageModel.cs` content** - -```csharp -// 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 Swashbuckle.AspNetCore.Annotations; - -namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; - -[SwaggerSchema(Title = "OdsInstanceManage")] -public class OdsInstanceManageModel -{ - public int? Id { get; set; } - public string? Name { get; set; } - public int? OdsInstanceId { get; set; } - public string? OdsInstanceName { get; set; } - public string? Status { get; set; } - public string? DatabaseTemplate { get; set; } - public string? DatabaseName { get; set; } - public DateTime? LastRefreshed { get; set; } - public DateTime? LastModifiedDate { get; set; } -} -``` - -- [ ] **Step 6: Replace `OdsInstanceManageMapper.cs` content** - -```csharp -// 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.Infrastructure.Models; - -namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; - -public static class OdsInstanceManageMapper -{ - public static OdsInstanceManageModel ToModel(OdsInstanceManage source) - { - return new OdsInstanceManageModel - { - Id = source.Id, - Name = source.Name, - OdsInstanceId = source.OdsInstanceId, - OdsInstanceName = source.OdsInstanceName, - Status = source.Status, - DatabaseTemplate = source.DatabaseTemplate, - DatabaseName = source.DatabaseName, - LastRefreshed = source.LastRefreshed, - LastModifiedDate = source.LastModifiedDate, - }; - } - - public static List ToModelList(IEnumerable source) - { - return source.Select(ToModel).ToList(); - } -} -``` - -- [ ] **Step 7: Replace `OdsInstanceManageDatabaseNameFormatter.cs` content** - -```csharp -// 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.RegularExpressions; - -namespace EdFi.Ods.AdminApi.Features.OdsInstances.Manage; - -internal static class OdsInstanceManageDatabaseNameFormatter -{ - private const string CanonicalPrefix = "EdFi_Ods"; - - // Use PostgreSQL's identifier limit as the portable ceiling so the persisted - // DatabaseName always matches the real provisioned database across engines. - internal const int MaxPortableDatabaseNameLength = 63; - - private static readonly Regex _leadingCanonicalPrefixPattern = new( - @"^(?:(?:edfi_+ods)(?:_+|$))+", - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - - internal static string Build(string instanceName, string databaseTemplate) - { - ArgumentException.ThrowIfNullOrWhiteSpace(instanceName); - ArgumentException.ThrowIfNullOrWhiteSpace(databaseTemplate); - - var normalizedName = NormalizeSegment(instanceName); - var normalizedDatabaseTemplate = NormalizeSegment(databaseTemplate); - var normalizedNameWithoutPrefix = _leadingCanonicalPrefixPattern.Replace(normalizedName, string.Empty).Trim('_'); - - return string.IsNullOrWhiteSpace(normalizedNameWithoutPrefix) - ? $"{CanonicalPrefix}_{normalizedDatabaseTemplate}" - : $"{CanonicalPrefix}_{normalizedNameWithoutPrefix}_{normalizedDatabaseTemplate}"; - } - - private static string NormalizeSegment(string value) - => value.Replace(' ', '_').Trim('_'); -} -``` - -- [ ] **Step 8: Build** - -Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` -Expected: remaining errors confined to `Features\OdsInstances\OdsInstanceWithEducationOrganizationsModel.cs`/`ReadEducationOrganizations.cs` (Task 5) and `Program.cs`/`WebApplicationBuilderExtensions.cs` (Task 6). - -- [ ] **Step 9: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi/Features/OdsInstances/Manage" \ - "Application/EdFi.Ods.AdminApi/Features/DbInstances" -git commit -m "Move v2 DbInstances feature into OdsInstances/Manage, rename routes to /odsInstances/manage" -``` - ---- - -### Task 5: v2 existing `OdsInstances` and `Tenants` files — update references to the renamed query/enum - -**Amendment (discovered during Task 3 implementation):** the original plan missed a whole consumer area — `Features\Tenants\*` and `Infrastructure\Services\Tenants\TenantService.cs` also inject `IGetDbInstancesQuery`/`DbInstance`/`DbInstanceStatus` to build the `/tenants/{tenantName}/odsInstances/edOrgs` response. This section folds that fix into Task 5 rather than adding a new task number. - -**Files:** -- Modify: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\OdsInstanceWithEducationOrganizationsModel.cs` -- Modify: `Application\EdFi.Ods.AdminApi\Features\OdsInstances\ReadEducationOrganizations.cs` -- Modify: `Application\EdFi.Ods.AdminApi\Features\Tenants\TenantDetailModel.cs` -- Modify: `Application\EdFi.Ods.AdminApi\Features\Tenants\TenantMapper.cs` -- Modify: `Application\EdFi.Ods.AdminApi\Features\Tenants\ReadTenants.cs` -- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\Services\Tenants\TenantService.cs` - -**Interfaces:** -- Consumes: `IGetOdsInstanceManagesQuery` (Task 3), `OdsInstanceManageStatus` (Task 2). -- Produces: `OdsInstanceWithEducationOrganizationsModel.OdsInstanceManageId` (renamed from `DbInstanceId`), `TenantOdsInstanceModel.OdsInstanceManageId` (renamed from `DbInstanceId`), `TenantMapper.ToUnlinkedOdsInstanceManageModel` (renamed from `ToUnlinkedDbInstanceModel`), `ITenantsService.GetTenantEdOrgsByInstancesAsync(..., IGetOdsInstanceManagesQuery, ...)` — all public API response shape / internal wiring only, consumed by nothing else in this plan. - -- [ ] **Step 1: Rename the `DbInstanceId` property** - -In `OdsInstanceWithEducationOrganizationsModel.cs`, replace: - -```csharp - [SwaggerSchema(Description = "DbInstance identifier for this ODS instance")] - public int? DbInstanceId { get; set; } -``` - -with: - -```csharp - [SwaggerSchema(Description = "OdsInstanceManage identifier for this ODS instance")] - public int? OdsInstanceManageId { get; set; } -``` - -- [ ] **Step 2: Update `ReadEducationOrganizations.cs`** - -Replace the full file content with: - -```csharp -// 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.Constants; -using EdFi.Ods.AdminApi.Common.Features; -using EdFi.Ods.AdminApi.Common.Infrastructure; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using Microsoft.AspNetCore.Mvc; - -namespace EdFi.Ods.AdminApi.Features.OdsInstances; - -public class ReadEducationOrganizations : IFeature -{ - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder - .MapGet(endpoints, "/odsInstances/{instanceId}/edOrgs", GetEducationOrganizationsByInstance) - .WithSummaryAndDescription( - "Retrieves education organizations for a specific ODS instance", - "Returns all education organizations for the specified ODS instance in a nested structure" - ) - .WithRouteOptions(b => b.WithResponse>(200)) - .BuildForVersions(AdminApiVersions.V2); - } - - public static async Task GetEducationOrganizationsByInstance( - [FromServices] IGetEducationOrganizationsQuery getEducationOrganizationsQuery, - [FromServices] IGetOdsInstanceQuery getOdsInstanceQuery, - [FromServices] IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery, - [AsParameters] CommonQueryParams commonQueryParams, - int instanceId) - { - getOdsInstanceQuery.Execute(instanceId); - - var educationOrganizations = await getEducationOrganizationsQuery.ExecuteAsync( - commonQueryParams, - instanceId: instanceId); - - MergeOdsInstanceManageData(educationOrganizations, getOdsInstanceManagesQuery); - return Results.Ok(educationOrganizations); - } - - private static void MergeOdsInstanceManageData( - List instances, - IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery) - { - var allOdsInstanceManages = getOdsInstanceManagesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); - - var linkedById = allOdsInstanceManages - .Where(d => d.OdsInstanceId is not null) - .GroupBy(d => d.OdsInstanceId!.Value) - .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); - - foreach (var instance in instances) - { - if (instance.Id is int instanceId && linkedById.TryGetValue(instanceId, out var odsInstanceManage)) - { - instance.OdsInstanceManageId = odsInstanceManage.Id; - instance.Status = odsInstanceManage.Status; - instance.DatabaseTemplate = odsInstanceManage.DatabaseTemplate; - instance.DatabaseName = odsInstanceManage.DatabaseName; - } - else - { - instance.Status = OdsInstanceManageStatus.Created.ToString(); - } - } - } -} -``` - -- [ ] **Step 3: Rename `TenantOdsInstanceModel.DbInstanceId` in `TenantDetailModel.cs`** - -Replace: - -```csharp - [JsonPropertyName("id")] - public int? OdsInstanceId { get; set; } - public int? DbInstanceId { get; set; } -``` - -with: - -```csharp - [JsonPropertyName("id")] - public int? OdsInstanceId { get; set; } - public int? OdsInstanceManageId { get; set; } -``` - -- [ ] **Step 4: Update `TenantMapper.cs`** - -Replace the full file content with: - -```csharp -// 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.Admin.DataAccess.Models; -using EdFi.Ods.AdminApi.Common.Infrastructure.Models; - -namespace EdFi.Ods.AdminApi.Features.Tenants; - -public static class TenantMapper -{ - public static TenantOdsInstanceModel ToOdsInstanceModel(OdsInstance source) - { - return new TenantOdsInstanceModel - { - OdsInstanceId = source.OdsInstanceId, - Name = source.Name, - InstanceType = source.InstanceType, - }; - } - - public static List ToOdsInstanceModelList(IEnumerable source) - { - return source.Select(ToOdsInstanceModel).ToList(); - } - - public static TenantOdsInstanceModel ToUnlinkedOdsInstanceManageModel(OdsInstanceManage source) - { - return new TenantOdsInstanceModel - { - OdsInstanceId = null, - OdsInstanceManageId = source.Id, - Name = source.Name, - Status = source.Status, - DatabaseTemplate = source.DatabaseTemplate, - DatabaseName = source.DatabaseName, - }; - } -} -``` - -- [ ] **Step 5: Update `ReadTenants.cs`** - -Replace: - -```csharp - IGetDbInstancesQuery getDbInstancesQuery, -``` - -with: - -```csharp - IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery, -``` - -and replace: - -```csharp - var tenant = await tenantsService.GetTenantEdOrgsByInstancesAsync( - getOdsInstancesQuery, getEducationOrganizationQuery, getDbInstancesQuery, tenantName); -``` - -with: - -```csharp - var tenant = await tenantsService.GetTenantEdOrgsByInstancesAsync( - getOdsInstancesQuery, getEducationOrganizationQuery, getOdsInstanceManagesQuery, tenantName); -``` - -Add `using EdFi.Ods.AdminApi.Infrastructure.Database.Queries;` if not already present (it already is — `IGetOdsInstanceManagesQuery` lives in the same namespace as `IGetOdsInstancesQuery`/`IGetEducationOrganizationQuery`, both already imported in this file). - -- [ ] **Step 6: Update `TenantService.cs`** - -Replace the interface line: - -```csharp - Task GetTenantEdOrgsByInstancesAsync(IGetOdsInstancesQuery getOdsInstancesQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, IGetDbInstancesQuery getDbInstancesQuery, string tenantName); -``` - -with: - -```csharp - Task GetTenantEdOrgsByInstancesAsync(IGetOdsInstancesQuery getOdsInstancesQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery, string tenantName); -``` - -Replace the method signature: - -```csharp - public async Task GetTenantEdOrgsByInstancesAsync( - IGetOdsInstancesQuery getOdsInstancesQuery, - IGetEducationOrganizationQuery getEducationOrganizationQuery, - IGetDbInstancesQuery getDbInstancesQuery, - string tenantName) -``` - -with: - -```csharp - public async Task GetTenantEdOrgsByInstancesAsync( - IGetOdsInstancesQuery getOdsInstancesQuery, - IGetEducationOrganizationQuery getEducationOrganizationQuery, - IGetOdsInstanceManagesQuery getOdsInstanceManagesQuery, - string tenantName) -``` - -Replace the method body's use of the renamed query and entity: - -```csharp - var allDbInstances = getDbInstancesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); - - var linkedDbInstancesByOdsId = allDbInstances - .Where(d => d.OdsInstanceId is not null) - .GroupBy(d => d.OdsInstanceId!.Value) - .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); - - foreach (var odsInstance in tenantDetails.OdsInstances) - { - if (odsInstance.OdsInstanceId is int odsInstanceId && linkedDbInstancesByOdsId.TryGetValue(odsInstanceId, out var dbInstance)) - { - odsInstance.DbInstanceId = dbInstance.Id; - odsInstance.Status = dbInstance.Status; - odsInstance.DatabaseTemplate = dbInstance.DatabaseTemplate; - odsInstance.DatabaseName = dbInstance.DatabaseName; - } - else - { - odsInstance.Status = DbInstanceStatus.Created.ToString(); - } - } - - var existingOdsInstanceIds = tenantDetails.OdsInstances - .Where(i => i.OdsInstanceId is int) - .Select(i => i.OdsInstanceId!.Value) - .ToHashSet(); - - var unlinkedDbInstances = allDbInstances - .Where(d => d.OdsInstanceId is null) - .Concat(allDbInstances - .Where(d => d.OdsInstanceId is not null && !existingOdsInstanceIds.Contains(d.OdsInstanceId.Value)) - .GroupBy(d => d.OdsInstanceId!.Value) - .Select(g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First())) - .ToList(); - foreach (var dbInstance in unlinkedDbInstances) - { - tenantDetails.OdsInstances.Add(TenantMapper.ToUnlinkedDbInstanceModel(dbInstance)); - } -``` - -with: - -```csharp - var allOdsInstanceManages = getOdsInstanceManagesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); - - var linkedOdsInstanceManagesByOdsId = allOdsInstanceManages - .Where(d => d.OdsInstanceId is not null) - .GroupBy(d => d.OdsInstanceId!.Value) - .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); - - foreach (var odsInstance in tenantDetails.OdsInstances) - { - if (odsInstance.OdsInstanceId is int odsInstanceId && linkedOdsInstanceManagesByOdsId.TryGetValue(odsInstanceId, out var odsInstanceManage)) - { - odsInstance.OdsInstanceManageId = odsInstanceManage.Id; - odsInstance.Status = odsInstanceManage.Status; - odsInstance.DatabaseTemplate = odsInstanceManage.DatabaseTemplate; - odsInstance.DatabaseName = odsInstanceManage.DatabaseName; - } - else - { - odsInstance.Status = OdsInstanceManageStatus.Created.ToString(); - } - } - - var existingOdsInstanceIds = tenantDetails.OdsInstances - .Where(i => i.OdsInstanceId is int) - .Select(i => i.OdsInstanceId!.Value) - .ToHashSet(); - - var unlinkedOdsInstanceManages = allOdsInstanceManages - .Where(d => d.OdsInstanceId is null) - .Concat(allOdsInstanceManages - .Where(d => d.OdsInstanceId is not null && !existingOdsInstanceIds.Contains(d.OdsInstanceId.Value)) - .GroupBy(d => d.OdsInstanceId!.Value) - .Select(g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First())) - .ToList(); - foreach (var odsInstanceManage in unlinkedOdsInstanceManages) - { - tenantDetails.OdsInstances.Add(TenantMapper.ToUnlinkedOdsInstanceManageModel(odsInstanceManage)); - } -``` - -- [ ] **Step 7: Build** - -Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` -Expected: remaining errors confined to `Program.cs`/`WebApplicationBuilderExtensions.cs` (Task 6) and all of v3 (Tasks 10-13, not yet done). - -- [ ] **Step 8: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi/Features/OdsInstances/OdsInstanceWithEducationOrganizationsModel.cs" \ - "Application/EdFi.Ods.AdminApi/Features/OdsInstances/ReadEducationOrganizations.cs" \ - "Application/EdFi.Ods.AdminApi/Features/Tenants/TenantDetailModel.cs" \ - "Application/EdFi.Ods.AdminApi/Features/Tenants/TenantMapper.cs" \ - "Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs" \ - "Application/EdFi.Ods.AdminApi/Infrastructure/Services/Tenants/TenantService.cs" -git commit -m "Update v2 OdsInstances and Tenants EdOrgs merges to use renamed OdsInstanceManage query" -``` - ---- - -### Task 6: v2 wiring — `Program.cs` and `WebApplicationBuilderExtensions.cs` - -**Files:** -- Modify: `Application\EdFi.Ods.AdminApi\Program.cs` -- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\WebApplicationBuilderExtensions.cs` - -**Interfaces:** -- Consumes: `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName`/`DeletePendingOdsInstanceManagesDispatcherJobName` (Task 2), `AppSettings.CreateOdsInstanceManagesSweepIntervalInMins`/`DeleteOdsInstanceManagesSweepIntervalInMins` (Task 2), `CreatePendingOdsInstanceManagesDispatcherJob`/`DeletePendingOdsInstanceManagesDispatcherJob` (Task 3). -- Produces: fully wired v2 Quartz scheduling — this is the last v2-side file needed for the solution to build; Task 7's build checkpoint depends on this completing cleanly. - -- [ ] **Step 1: Update config key lookups in `Program.cs`** - -Replace: - -```csharp -var createDbInstancesSweepIntervalInMins = app.Configuration.GetValue( - "AppSettings:CreateDbInstancesSweepIntervalInMins" -); -var deleteDbInstancesSweepIntervalInMins = app.Configuration.GetValue( - "AppSettings:DeleteDbInstancesSweepIntervalInMins" -); -``` - -with: - -```csharp -var createOdsInstanceManagesSweepIntervalInMins = app.Configuration.GetValue( - "AppSettings:CreateOdsInstanceManagesSweepIntervalInMins" -); -var deleteOdsInstanceManagesSweepIntervalInMins = app.Configuration.GetValue( - "AppSettings:DeleteOdsInstanceManagesSweepIntervalInMins" -); -``` - -- [ ] **Step 2: Rename every local variable derived from those two lines, in both the `AdminApiMode.V2` and `AdminApiMode.V3` branches** - -Throughout both branches, rename: -- `createDbInstancesSweepIntervalInMins` → `createOdsInstanceManagesSweepIntervalInMins` -- `deleteDbInstancesSweepIntervalInMins` → `deleteOdsInstanceManagesSweepIntervalInMins` -- `createDbInstancesSweepInterval` → `createOdsInstanceManagesSweepInterval` -- `deleteDbInstancesSweepInterval` → `deleteOdsInstanceManagesSweepInterval` - -(these are `double.TryParse(..., out var createDbInstancesSweepInterval)`-style declarations and their later `TimeSpan.FromMinutes(...)` usages — rename the declaration and every usage site.) - -- [ ] **Step 3: Update `JobKey`/job-type references in the `AdminApiMode.V2` branch** - -Replace every occurrence in the V2 branch of: - -```csharp -await QuartzJobScheduler.ScheduleJob( - scheduler, - jobKey: new JobKey($"{JobConstants.CreatePendingDbInstancesDispatcherJobName}_{tenantName}"), -``` - -and the non-multi-tenant equivalent - -```csharp -await QuartzJobScheduler.ScheduleJob( - scheduler, - jobKey: new JobKey(JobConstants.CreatePendingDbInstancesDispatcherJobName), -``` - -with the `CreatePendingOdsInstanceManagesDispatcherJob` class and `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName` constant (both multi-tenant and non-multi-tenant branches). Do the same for the `Delete...` pair (`DeletePendingDbInstancesDispatcherJob` → `DeletePendingOdsInstanceManagesDispatcherJob`, `JobConstants.DeletePendingDbInstancesDispatcherJobName` → `JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName`). - -Also update the two `_logger.Error(...)` messages: - -```csharp -_logger.Error("Invalid value for CreateDbInstancesSweepIntervalInMins. Please ensure it is a valid number."); -... -_logger.Error("Invalid value for DeleteDbInstancesSweepIntervalInMins. Please ensure it is a valid number."); -``` - -to: - -```csharp -_logger.Error("Invalid value for CreateOdsInstanceManagesSweepIntervalInMins. Please ensure it is a valid number."); -... -_logger.Error("Invalid value for DeleteOdsInstanceManagesSweepIntervalInMins. Please ensure it is a valid number."); -``` - -(these log messages appear once in the V2 branch and once in the V3 branch — update both; Task 13 covers the V3-branch job-type swap to `V3Jobs.CreatePendingDataStoreManagesDispatcherJob`/`DeletePendingDataStoreManagesDispatcherJob`, but the log message text and interval variable renames happen here since they're shared local variables computed once before the `if/else if` branches.) - -- [ ] **Step 4: Update `WebApplicationBuilderExtensions.cs` DI registrations for the v2 (`else`) branch** - -Find the `else` branch of `RegisterQuartzServices` (the non-V3 branch, currently registering `CreateInstanceJob`, `CreatePendingDbInstancesDispatcherJob`, `RefreshEducationOrganizationsJob`, etc. — grep for `webApplicationBuilder.Services.AddTransient();`) and replace: - -```csharp - webApplicationBuilder.Services.AddTransient(); -``` - -with: - -```csharp - webApplicationBuilder.Services.AddTransient(); -``` - -Also find and rename the corresponding `DeletePendingDbInstancesDispatcherJob` registration (grep the same file for it — it sits near the `Create...` registration in the same `else` block) to `DeletePendingOdsInstanceManagesDispatcherJob`. - -- [ ] **Step 5: Build the full solution** - -Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` -Expected: v2 side (everything except v3's `EdFi.Ods.AdminApi.V3` project) now compiles cleanly. Remaining errors should be entirely inside `Application\EdFi.Ods.AdminApi.V3\` (fixed by Tasks 10–13) and its `.UnitTests`/`.DBTests` counterparts (Tasks 14–15) and `Application\EdFi.Ods.AdminApi.UnitTests`/`.DBTests` (Tasks 7–8, not yet done). - -- [ ] **Step 6: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi/Program.cs" \ - "Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs" -git commit -m "Update v2 Quartz wiring to use renamed OdsInstanceManage jobs and config keys" -``` - ---- - -### Task 7: v2 Unit tests rename (`EdFi.Ods.AdminApi.UnitTests`) - -**Files:** -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Features\DbInstances\AddDbInstanceTests.cs` → move+rename to `Application\EdFi.Ods.AdminApi.UnitTests\Features\OdsInstances\Manage\AddOdsInstanceManageTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Features\DbInstances\ReadDbInstanceTests.cs` → move+rename to `Application\EdFi.Ods.AdminApi.UnitTests\Features\OdsInstances\Manage\ReadOdsInstanceManageTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Features\DbInstances\DeleteDbInstanceTests.cs` → move+rename to `Application\EdFi.Ods.AdminApi.UnitTests\Features\OdsInstances\Manage\DeleteOdsInstanceManageTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Database\Commands\AddDbInstanceCommandTests.cs` → rename to `AddOdsInstanceManageCommandTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Database\Commands\DeleteDbInstanceCommandTests.cs` → rename to `DeleteOdsInstanceManageCommandTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Database\Queries\GetDbInstancesQueryTests.cs` → rename to `GetOdsInstanceManagesQueryTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Database\Queries\GetDbInstanceByIdQueryTests.cs` → rename to `GetOdsInstanceManageByIdQueryTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Jobs\CreatePendingDbInstancesDispatcherJobTests.cs` → rename to `CreatePendingOdsInstanceManagesDispatcherJobTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Jobs\DeletePendingDbInstancesDispatcherJobTests.cs` → rename to `DeletePendingOdsInstanceManagesDispatcherJobTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Jobs\CreateInstanceJobTests.cs` (renamed in place, filename unchanged) -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Jobs\DeleteInstanceJobTests.cs` (renamed in place, filename unchanged) -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Features\Tenants\ReadTenantsTest.cs` (filename unchanged — update references to Task 5's renamed Tenants types) -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Features\Tenants\TenantDetailModelTests.cs` (filename unchanged — update references) -- Modify: `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Tenants\TenantServiceTests.cs` (filename unchanged — update references) - -**Amendment (same gap as Task 5):** the three Tenants test files above were missed in the original plan — they test `Features\Tenants\*`/`TenantService.cs`, which Task 5 (amended) now renames. Their production counterparts changed in Task 5, so these tests need matching updates or they won't compile. - -**Interfaces:** -- Consumes: every production type from Tasks 2–6 (`OdsInstanceManage`, `OdsInstanceManageStatus`, `IGetOdsInstanceManagesQuery`, `AddOdsInstanceManageCommand`, `AddOdsInstanceManage`/`ReadOdsInstanceManage`/`DeleteOdsInstanceManage` features, `CreatePendingOdsInstanceManagesDispatcherJob`, etc.), plus Task 5's renamed `TenantOdsInstanceModel.OdsInstanceManageId`, `TenantMapper.ToUnlinkedOdsInstanceManageModel`, `ITenantsService.GetTenantEdOrgsByInstancesAsync(..., IGetOdsInstanceManagesQuery, ...)`. - -- [ ] **Step 1: Move and rename the query test files (full content known — apply directly)** - -```bash -git mv "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetDbInstancesQueryTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManagesQueryTests.cs" -git mv "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetDbInstanceByIdQueryTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQueryTests.cs" -``` - -`GetOdsInstanceManagesQueryTests.cs`: - -```csharp -// 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. - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Linq; -using EdFi.Ods.AdminApi.Common.Infrastructure; -using EdFi.Ods.AdminApi.Common.Infrastructure.Models; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Infrastructure; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; -using NUnit.Framework; -using Shouldly; - -namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Database.Queries; - -[TestFixture] -public class GetOdsInstanceManagesQueryTests -{ - private static AdminApiDbContext CreateContext() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"GetOdsInstanceManagesQueryTests_{Guid.NewGuid()}") - .Options; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["AppSettings:DatabaseEngine"] = "Postgres" - }) - .Build(); - - return new AdminApiDbContext(options, configuration); - } - - private static IOptions DefaultOptions() => - Options.Create(new AppSettings { DatabaseEngine = "Postgres", DefaultPageSizeLimit = 25 }); - - [Test] - public void Execute_WithoutFilters_ReturnsAllOdsInstanceManages() - { - using var context = CreateContext(); - context.OdsInstanceManages.AddRange( - new OdsInstanceManage { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, - new OdsInstanceManage { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); - context.SaveChanges(); - - var query = new GetOdsInstanceManagesQuery(context, DefaultOptions()); - - var result = query.Execute(new CommonQueryParams(0, 25), null, null); - - result.Count.ShouldBe(2); - result.Select(x => x.Name).ShouldBe(["Sandbox A", "Sandbox B"], ignoreOrder: true); - } - - [Test] - public void Execute_WithNameFilter_ReturnsMatchingOdsInstanceManage() - { - using var context = CreateContext(); - context.OdsInstanceManages.AddRange( - new OdsInstanceManage { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, - new OdsInstanceManage { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); - context.SaveChanges(); - - var query = new GetOdsInstanceManagesQuery(context, DefaultOptions()); - - var result = query.Execute(new CommonQueryParams(0, 25), null, "Sandbox B"); - - result.Count.ShouldBe(1); - result.Single().Name.ShouldBe("Sandbox B"); - } -} -``` - -`GetOdsInstanceManageByIdQueryTests.cs`: - -```csharp -// 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. - -#nullable enable - -using System; -using System.Collections.Generic; -using EdFi.Ods.AdminApi.Common.Infrastructure.Models; -using EdFi.Ods.AdminApi.Infrastructure; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using NUnit.Framework; -using Shouldly; - -namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Database.Queries; - -[TestFixture] -public class GetOdsInstanceManageByIdQueryTests -{ - private static AdminApiDbContext CreateContext() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"GetOdsInstanceManageByIdQueryTests_{Guid.NewGuid()}") - .Options; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["AppSettings:DatabaseEngine"] = "Postgres" - }) - .Build(); - - return new AdminApiDbContext(options, configuration); - } - - [Test] - public void Execute_WithExistingId_ReturnsOdsInstanceManage() - { - using var context = CreateContext(); - var odsInstanceManage = new OdsInstanceManage - { - Name = "Sandbox", - Status = "Healthy", - DatabaseTemplate = "Minimal" - }; - context.OdsInstanceManages.Add(odsInstanceManage); - context.SaveChanges(); - - var query = new GetOdsInstanceManageByIdQuery(context); - - var result = query.Execute(odsInstanceManage.Id); - - result.ShouldNotBeNull(); - result.Name.ShouldBe("Sandbox"); - } - - [Test] - public void Execute_WithUnknownId_ReturnsNull() - { - using var context = CreateContext(); - var query = new GetOdsInstanceManageByIdQuery(context); - - var result = query.Execute(999); - - result.ShouldBeNull(); - } -} -``` - -- [ ] **Step 2: Move and rename the remaining v2 unit test files, applying the Master Rename Table** - -```bash -git mv "Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/AddDbInstanceTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/AddOdsInstanceManageTests.cs" -git mv "Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/ReadDbInstanceTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/ReadOdsInstanceManageTests.cs" -git mv "Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances/DeleteDbInstanceTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage/DeleteOdsInstanceManageTests.cs" -git mv "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddDbInstanceCommandTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddOdsInstanceManageCommandTests.cs" -git mv "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteDbInstanceCommandTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommandTests.cs" -git mv "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJobTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJobTests.cs" -git mv "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJobTests.cs" "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJobTests.cs" -``` - -For each moved file and for the two in-place files (`CreateInstanceJobTests.cs`, `DeleteInstanceJobTests.cs`), open it and apply every substitution from the "v2-specific" and "Shared" Master Rename Table sections above (class name matching the new filename, `namespace ...UnitTests.Features.DbInstances` → `...UnitTests.Features.OdsInstances.Manage`, every `DbInstance`/`OdsInstance...`-adjacent identifier, string literal route fragments like `"/dbInstances"` → `"/odsInstances/manage"` if any test asserts on the route string, and any `[TestFixture] public class AddDbInstanceTests` → `AddOdsInstanceManageTests`). - -- [ ] **Step 3: Update the three Tenants-area test files (filenames unchanged)** - -Open `Application\EdFi.Ods.AdminApi.UnitTests\Features\Tenants\ReadTenantsTest.cs`, `Application\EdFi.Ods.AdminApi.UnitTests\Features\Tenants\TenantDetailModelTests.cs`, and `Application\EdFi.Ods.AdminApi.UnitTests\Infrastructure\Services\Tenants\TenantServiceTests.cs`. Update every reference to match Task 5's renames: `IGetDbInstancesQuery`/`getDbInstancesQuery` → `IGetOdsInstanceManagesQuery`/`getOdsInstanceManagesQuery`, `DbInstance`/`DbInstanceStatus` → `OdsInstanceManage`/`OdsInstanceManageStatus` (mock/fixture setups), `TenantOdsInstanceModel.DbInstanceId` → `.OdsInstanceManageId`, `TenantMapper.ToUnlinkedDbInstanceModel` → `.ToUnlinkedOdsInstanceManageModel`. Do not rename the files or their test class names — only internal references. - -- [ ] **Step 4: Run the v2 unit test suite** - -Run: `dotnet test Application/EdFi.Ods.AdminApi.UnitTests/EdFi.Ods.AdminApi.UnitTests.csproj` -Expected: PASS, same test count as before the rename (compare `git stash`'s pre-change `dotnet test` output count if unsure, or just confirm no failures/errors and no tests silently vanished — count should match the pre-rename baseline exactly since this task renames tests, not delete/add them). - -- [ ] **Step 5: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi.UnitTests/Features/OdsInstances/Manage" \ - "Application/EdFi.Ods.AdminApi.UnitTests/Features/DbInstances" \ - "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddOdsInstanceManageCommandTests.cs" \ - "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteOdsInstanceManageCommandTests.cs" \ - "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManagesQueryTests.cs" \ - "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Queries/GetOdsInstanceManageByIdQueryTests.cs" \ - "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreatePendingOdsInstanceManagesDispatcherJobTests.cs" \ - "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeletePendingOdsInstanceManagesDispatcherJobTests.cs" \ - "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/CreateInstanceJobTests.cs" \ - "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Jobs/DeleteInstanceJobTests.cs" \ - "Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/ReadTenantsTest.cs" \ - "Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/TenantDetailModelTests.cs" \ - "Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs" -git commit -m "Rename v2 unit tests to OdsInstanceManage naming" -``` - ---- - -### Task 8: v2 DBTests rename (`EdFi.Ods.AdminApi.DBTests`) - -**Files:** -- Modify: `Application\EdFi.Ods.AdminApi.DBTests\Database\CommandTests\AddDbInstanceCommandTests.cs` → rename to `AddOdsInstanceManageCommandTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.DBTests\Database\CommandTests\DeleteDbInstanceCommandTests.cs` → rename to `DeleteOdsInstanceManageCommandTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.DBTests\Database\QueryTests\GetDbInstanceByIdQueryTests.cs` → rename to `GetOdsInstanceManageByIdQueryTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.DBTests\Database\QueryTests\GetDbInstancesQueryTests.cs` → rename to `GetOdsInstanceManagesQueryTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.DBTests\...\GetTenantEdOrgsByInstancesTests.cs` (filename unchanged — update internal references only) - -**Interfaces:** -- Consumes: same production types as Task 7, against a real database (this project is this repo's integration-test layer). - -- [ ] **Step 1: Move and rename** - -```bash -git mv "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddDbInstanceCommandTests.cs" "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceManageCommandTests.cs" -git mv "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteDbInstanceCommandTests.cs" "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceManageCommandTests.cs" -git mv "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetDbInstanceByIdQueryTests.cs" "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManageByIdQueryTests.cs" -git mv "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetDbInstancesQueryTests.cs" "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManagesQueryTests.cs" -``` - -Apply the Master Rename Table to each moved file's content (class name, `AdminApiDbContext.DbInstances`→`.OdsInstanceManages`, `DbInstance`/`DbInstanceStatus`→`OdsInstanceManage`/`OdsInstanceManageStatus`, command/query type names). In `GetTenantEdOrgsByInstancesTests.cs` (filename unchanged, locate it first with a search since its exact path wasn't confirmed during research — search `Application\EdFi.Ods.AdminApi.DBTests` for `GetTenantEdOrgsByInstancesTests`), update only the internal references to renamed types (`IGetOdsInstanceManagesQuery`, `OdsInstanceManage`, etc.), not the filename or class name. - -- [ ] **Step 2: Run the v2 DB test suite** - -Run: `dotnet test Application/EdFi.Ods.AdminApi.DBTests/EdFi.Ods.AdminApi.DBTests.csproj` (requires a local test database per `docs/developer.md` DB migration instructions — apply Task 1's migration first). -Expected: PASS, same test count as the pre-rename baseline. - -- [ ] **Step 3: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddOdsInstanceManageCommandTests.cs" \ - "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteOdsInstanceManageCommandTests.cs" \ - "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManageByIdQueryTests.cs" \ - "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetOdsInstanceManagesQueryTests.cs" \ - "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddDbInstanceCommandTests.cs" \ - "Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteDbInstanceCommandTests.cs" \ - "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetDbInstanceByIdQueryTests.cs" \ - "Application/EdFi.Ods.AdminApi.DBTests/Database/QueryTests/GetDbInstancesQueryTests.cs" -git commit -m "Rename v2 DBTests to OdsInstanceManage naming" -``` - ---- - -### Task 9: v2 Bruno E2E collection rename - -**Files:** -- Modify: `Application\EdFi.Ods.AdminApi\E2E Tests\V2\Bruno Admin API E2E 2.0 refactor\v2\DbInstances\` → move all files to `...\v2\OdsInstances\Manage\` - -**Interfaces:** -- Consumes: routes `/v2/odsInstances/manage*` (Task 4). - -- [ ] **Step 1: Move the folder and rename every `.bru` file** - -```bash -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/folder.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/folder.bru" -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage.bru" -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Sample Template.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Sample Template.bru" -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Invalid.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Invalid.bru" -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/POST - DbInstances - Invalid Database Template.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/POST - OdsInstances Manage - Invalid Database Template.bru" -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage.bru" -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances by ID.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage by ID.bru" -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Offset.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Offset.bru" -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Limit.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Limit.bru" -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Without Limit and Offset.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Without Limit and Offset.bru" -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Not Found.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Not Found.bru" -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/GET - DbInstances - Filter by Name.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/GET - OdsInstances Manage - Filter by Name.bru" -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/DELETE - DbInstance - Success.bru.disabled" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/DELETE - OdsInstance Manage - Success.bru.disabled" -git mv "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances/DELETE - DbInstance - Not Found.bru" "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage/DELETE - OdsInstance Manage - Not Found.bru" -``` - -- [ ] **Step 2: Update `folder.bru`** - -``` -meta { - name: Manage - seq: 99 -} - -auth { - mode: inherit -} -``` - -- [ ] **Step 3: Update every moved `.bru` file's content** - -For each file: change `meta { name: DbInstances ... }` → `meta { name: OdsInstances Manage ... }` (or the per-file variant, e.g. `DbInstance` singular in the DELETE files → `OdsInstance Manage`), change the request URL from `{{API_URL}}/v2/dbinstances...` to `{{API_URL}}/v2/odsinstances/manage...` (preserve any `/{id}` suffix or query string), and rename any `bru.setVar("CreatedDbInstanceId", ...)` / `{{CreatedDbInstanceId}}` variable references to `CreatedOdsInstanceManageId`, and update `test("POST DbInstances: ...")`-style assertion description strings to say `OdsInstances Manage` instead of `DbInstances`. - -For example, `POST - OdsInstances Manage.bru`: - -``` -meta { - name: OdsInstances Manage - type: http - seq: 1 -} - -post { - url: {{API_URL}}/v2/odsinstances/manage - body: json - auth: inherit -} - -body:json { - { - "name": "Test DB Instance", - "databaseTemplate": "Minimal" - } -} - -script:post-response { - test("POST OdsInstances Manage: Status code is Accepted", function () { - expect(res.getStatus()).to.equal(202); - }); - - test("POST OdsInstances Manage: Response includes location in header", function () { - expect(res.getHeaders()).to.have.property("location"); - const id = res.getHeader("location").split("/")[2]; - if (id) { - bru.setVar("CreatedOdsInstanceManageId", id); - } - }); -} - -settings { - encodeUrl: true -} -``` - -Apply the same URL/variable/assertion-text substitution pattern to the remaining 11 files (`GET`, `DELETE`, invalid-input variants) based on each file's current content. - -- [ ] **Step 4: Run the v2 Bruno E2E suite** - -Run: `./eng/run-e2e-bruno.ps1 -ApiVersion 2 -TenantMode singletenant -TearDown` (adjust `-TenantMode` to match this repo's default if different — check `docs/developer.md` for the exact invocation). -Expected: PASS, all `OdsInstances Manage` requests succeed with the same assertions as before the rename. - -- [ ] **Step 5: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/OdsInstances/Manage" \ - "Application/EdFi.Ods.AdminApi/E2E Tests/V2/Bruno Admin API E2E 2.0 refactor/v2/DbInstances" -git commit -m "Rename v2 Bruno E2E DbInstances collection to OdsInstances/Manage" -``` - ---- - -### Task 10: v3 Infrastructure layer rename (Queries, Commands, Jobs) - -**Files:** -- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Database\Queries\GetDbDataStoresQuery.cs` → rename to `GetDataStoreManagesQuery.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Database\Queries\GetDbDataStoreByIdQuery.cs` → rename to `GetDataStoreManageByIdQuery.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Database\Commands\AddDbDataStoreCommand.cs` → rename to `AddDataStoreManageCommand.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Database\Commands\DeleteDbDataStoreCommand.cs` → rename to `DeleteDataStoreManageCommand.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Services\Jobs\CreatePendingDbInstancesDispatcherJob.cs` → rename to `CreatePendingDataStoreManagesDispatcherJob.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Services\Jobs\DeletePendingDbInstancesDispatcherJob.cs` → rename to `DeletePendingDataStoreManagesDispatcherJob.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Services\Jobs\CreateInstanceJob.cs` (renamed in place, filename unchanged) -- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Services\Jobs\DeleteInstanceJob.cs` (renamed in place, filename unchanged) - -**Interfaces:** -- Consumes: `OdsInstanceManage` entity, `OdsInstanceManageStatus` enum, `JobConstants.OdsInstanceManageIdKey` (Task 2 — shared, so v3 also uses the `OdsInstanceManage`-prefixed shared names even though its own feature-layer naming is `DataStoreManage`), `AppSettings.CreateOdsInstanceManagesMaxRetryAttempts`/`DeleteOdsInstanceManagesMaxRetryAttempts` (Task 2), `AdminApiDbContext.OdsInstanceManages` (Task 2, V3's own DbContext). -- Produces: `IGetDataStoreManagesQuery`/`GetDataStoreManagesQuery`, `IGetDataStoreManageByIdQuery`/`GetDataStoreManageByIdQuery`, `AddDataStoreManageCommand`/`IAddDataStoreManageModel`, `IDeleteDataStoreManageCommand`/`DeleteDataStoreManageCommand`, `CreatePendingDataStoreManagesDispatcherJob`, `DeletePendingDataStoreManagesDispatcherJob` — consumed by Task 11 (feature files) and Task 13 (wiring). - -- [ ] **Step 1: Rename and update the Queries** - -```bash -git mv "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDbDataStoresQuery.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManagesQuery.cs" -git mv "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDbDataStoreByIdQuery.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManageByIdQuery.cs" -``` - -`GetDataStoreManagesQuery.cs`: - -```csharp -// 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.Infrastructure; -using EdFi.Ods.AdminApi.Common.Infrastructure.Models; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.V3.Infrastructure.Extensions; -using Microsoft.Extensions.Options; - -namespace EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; - -public interface IGetDataStoreManagesQuery -{ - List Execute(CommonQueryParams commonQueryParams, int? id, string? name); -} - -public class GetDataStoreManagesQuery : IGetDataStoreManagesQuery -{ - private readonly AdminApiDbContext _context; - private readonly IOptions _options; - - public GetDataStoreManagesQuery(AdminApiDbContext context, IOptions options) - { - _context = context; - _options = options; - } - - public List Execute(CommonQueryParams commonQueryParams, int? id, string? name) - { - return _context.OdsInstanceManages - .Where(d => id == null || d.Id == id) - .Where(d => name == null || d.Name == name) - .OrderBy(d => d.Id) - .Paginate(commonQueryParams.Offset, commonQueryParams.Limit, _options) - .ToList(); - } -} -``` - -`GetDataStoreManageByIdQuery.cs`: - -```csharp -// 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.Infrastructure.Models; - -namespace EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; - -public interface IGetDataStoreManageByIdQuery -{ - OdsInstanceManage? Execute(int id); -} - -public class GetDataStoreManageByIdQuery : IGetDataStoreManageByIdQuery -{ - private readonly AdminApiDbContext _context; - - public GetDataStoreManageByIdQuery(AdminApiDbContext context) - { - _context = context; - } - - public OdsInstanceManage? Execute(int id) - { - return _context.OdsInstanceManages.SingleOrDefault(d => d.Id == id); - } -} -``` - -- [ ] **Step 2: Rename and update the Commands** - -```bash -git mv "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/AddDbDataStoreCommand.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/AddDataStoreManageCommand.cs" -git mv "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/DeleteDbDataStoreCommand.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/DeleteDataStoreManageCommand.cs" -``` - -`AddDataStoreManageCommand.cs`: - -```csharp -// 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.Constants; -using EdFi.Ods.AdminApi.Common.Infrastructure.Models; - -namespace EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; - -public class AddDataStoreManageCommand -{ - private readonly AdminApiDbContext _context; - - public AddDataStoreManageCommand(AdminApiDbContext context) - { - _context = context; - } - - public OdsInstanceManage Execute(IAddDataStoreManageModel model) - { - if (string.IsNullOrWhiteSpace(model.Name)) - throw new ArgumentException("Name is required.", nameof(model)); - if (string.IsNullOrWhiteSpace(model.DatabaseTemplate)) - throw new ArgumentException("DatabaseTemplate is required.", nameof(model)); - - var now = DateTime.UtcNow; - - var odsInstanceManage = new OdsInstanceManage - { - Name = model.Name.Trim(), - DatabaseTemplate = model.DatabaseTemplate.Trim(), - Status = OdsInstanceManageStatus.PendingCreate.ToString(), - OdsInstanceId = null, - OdsInstanceName = null, - DatabaseName = null, - LastRefreshed = now, - LastModifiedDate = now - }; - - _context.OdsInstanceManages.Add(odsInstanceManage); - _context.SaveChanges(); - return odsInstanceManage; - } -} - -public interface IAddDataStoreManageModel -{ - string? Name { get; } - string? DatabaseTemplate { get; } -} -``` - -`DeleteDataStoreManageCommand.cs`: - -```csharp -// 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.Constants; -using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; - -namespace EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; - -public interface IDeleteDataStoreManageCommand -{ - void Execute(int id); -} - -public class DeleteDataStoreManageCommand : IDeleteDataStoreManageCommand -{ - private readonly AdminApiDbContext _context; - - public DeleteDataStoreManageCommand(AdminApiDbContext context) - { - _context = context; - } - - public void Execute(int id) - { - var odsInstanceManage = - _context.OdsInstanceManages.Find(id) - ?? throw new NotFoundException("dataStoreManage", id); - - if (odsInstanceManage.Status == OdsInstanceManageStatus.Deleted.ToString()) - throw new NotFoundException("dataStoreManage", id); - - odsInstanceManage.Status = OdsInstanceManageStatus.PendingDelete.ToString(); - odsInstanceManage.LastModifiedDate = DateTime.UtcNow; - - _context.SaveChanges(); - } -} -``` - -- [ ] **Step 3: Rename and update the dispatcher Jobs** - -```bash -git mv "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreatePendingDbInstancesDispatcherJob.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreatePendingDataStoreManagesDispatcherJob.cs" -git mv "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeletePendingDbInstancesDispatcherJob.cs" "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeletePendingDataStoreManagesDispatcherJob.cs" -``` - -`CreatePendingDataStoreManagesDispatcherJob.cs` (same transformation pattern as v2's `CreatePendingOdsInstanceManagesDispatcherJob.cs` from Task 3 — class renamed to `CreatePendingDataStoreManagesDispatcherJob`, everything else identical since v3's dispatcher logic was byte-identical to v2's): - -```csharp -// 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.Admin.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Common.Constants; -using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; -using EdFi.Ods.AdminApi.Common.Infrastructure.Models; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.V3.Infrastructure.Services.Tenants; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Options; -using Quartz; - -namespace EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs; - -[DisallowConcurrentExecution] -public class CreatePendingDataStoreManagesDispatcherJob( - ILogger logger, - IJobStatusService jobStatusService, - AdminApiDbContext dbContext, - ITenantSpecificDbContextProvider tenantSpecificDbContextProvider, - IOptions options) - : AdminApiQuartzJobBase(logger, jobStatusService) -{ - private const int DefaultMaxRetryAttempts = 3; - - private readonly AdminApiDbContext _dbContext = dbContext; - private readonly ITenantSpecificDbContextProvider _tenantSpecificDbContextProvider = tenantSpecificDbContextProvider; - private readonly IOptions _options = options; - - protected override async Task ExecuteJobAsync(IJobExecutionContext context) - { - var multiTenancyEnabled = _options.Value.MultiTenancy; - var tenantName = GetTenantName(context, multiTenancyEnabled); - AdminApiDbContext? tenantAdminApiDbContext = null; - var adminApiDbContext = _dbContext; - - try - { - if (multiTenancyEnabled) - { - tenantAdminApiDbContext = _tenantSpecificDbContextProvider.GetAdminApiDbContext(tenantName!); - adminApiDbContext = tenantAdminApiDbContext; - } - - var eligibleOdsInstanceManages = await adminApiDbContext.OdsInstanceManages - .Where(instance => instance.Status == OdsInstanceManageStatus.PendingCreate.ToString() || instance.Status == OdsInstanceManageStatus.CreateFailed.ToString()) - .OrderBy(instance => instance.Id) - .ToListAsync(); - - foreach (var odsInstanceManage in eligibleOdsInstanceManages) - { - if (string.Equals(odsInstanceManage.Status, OdsInstanceManageStatus.PendingCreate.ToString(), StringComparison.OrdinalIgnoreCase)) - { - await ScheduleCreateJobAsync(context, odsInstanceManage.Id, tenantName); - continue; - } - - if (!await IsRetryEligibleAsync(adminApiDbContext, odsInstanceManage, tenantName)) - { - odsInstanceManage.Status = OdsInstanceManageStatus.CreateError.ToString(); - odsInstanceManage.LastModifiedDate = DateTime.UtcNow; - odsInstanceManage.LastRefreshed = DateTime.UtcNow; - await adminApiDbContext.SaveChangesAsync(); - continue; - } - - odsInstanceManage.Status = OdsInstanceManageStatus.PendingCreate.ToString(); - odsInstanceManage.LastModifiedDate = DateTime.UtcNow; - odsInstanceManage.LastRefreshed = DateTime.UtcNow; - await adminApiDbContext.SaveChangesAsync(); - - await ScheduleCreateJobAsync(context, odsInstanceManage.Id, tenantName); - } - } - finally - { - if (tenantAdminApiDbContext is not null) - { - await tenantAdminApiDbContext.DisposeAsync(); - } - } - } - - private async Task IsRetryEligibleAsync(AdminApiDbContext adminApiDbContext, OdsInstanceManage odsInstanceManage, string? tenantName) - { - var maxRetryAttempts = _options.Value.CreateOdsInstanceManagesMaxRetryAttempts > 0 - ? _options.Value.CreateOdsInstanceManagesMaxRetryAttempts - : DefaultMaxRetryAttempts; - - var jobIdPrefix = $"{CreateInstanceJob.BuildJobIdentity(odsInstanceManage.Id, tenantName)}_"; - var errorCount = await adminApiDbContext.JobStatuses - .CountAsync(status => status.JobId.StartsWith(jobIdPrefix) && status.Status == QuartzJobStatus.Error.ToString()); - - return errorCount < maxRetryAttempts; - } - - private static async Task ScheduleCreateJobAsync(IJobExecutionContext context, int odsInstanceManageId, string? tenantName) - { - var jobData = new Dictionary - { - [JobConstants.OdsInstanceManageIdKey] = odsInstanceManageId - }; - - if (!string.IsNullOrWhiteSpace(tenantName)) - { - jobData[JobConstants.TenantNameKey] = tenantName; - } - - await QuartzJobScheduler.ScheduleJob( - context.Scheduler, - CreateInstanceJob.CreateJobKey(odsInstanceManageId, tenantName), - jobData, - startImmediately: true); - } - - private static string? GetTenantName(IJobExecutionContext context, bool multiTenancyEnabled) - { - if (!multiTenancyEnabled) - { - return null; - } - - var tenantName = context.MergedJobDataMap.ContainsKey(JobConstants.TenantNameKey) - ? context.MergedJobDataMap.GetString(JobConstants.TenantNameKey) - : null; - - if (string.IsNullOrWhiteSpace(tenantName)) - { - throw new InvalidOperationException( - $"{JobConstants.TenantNameKey} must be provided when multi-tenancy is enabled."); - } - - return tenantName; - } -} -``` - -`DeletePendingDataStoreManagesDispatcherJob.cs` — apply the identical transformation (mirroring v2's `DeletePendingOdsInstanceManagesDispatcherJob.cs` from Task 3, renamed to `DeletePendingDataStoreManagesDispatcherJob`, using `DeleteOdsInstanceManagesMaxRetryAttempts` and calling `DeleteInstanceJob`). - -- [ ] **Step 4: Update `CreateInstanceJob.cs` and `DeleteInstanceJob.cs` in place (v3 copies — filenames and class names unchanged, only internal references)** - -Apply the same substitutions as Task 3 Step 4, but for the v3 files: replace `using EdFi.Ods.AdminApi.V3.Features.DbDataStores;` with `using EdFi.Ods.AdminApi.V3.Features.DataStores.Manage;` (namespace of `DataStoreManageDatabaseNameFormatter`, produced by Task 11), rename `DbInstance`/`dbInstance` → `OdsInstanceManage`/`odsInstanceManage`, `adminApiDbContext.DbInstances` → `.OdsInstanceManages`, `DbInstanceStatus` → `OdsInstanceManageStatus`, `JobConstants.DbInstanceIdKey` → `JobConstants.OdsInstanceManageIdKey`, and (in `CreateInstanceJob.cs` only) `DbDataStoreDatabaseNameFormatter` → `DataStoreManageDatabaseNameFormatter`. - -- [ ] **Step 5: Build to confirm the v3 Infrastructure layer compiles in isolation** - -Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` (or the appropriate project reference — confirm the v3 project's build command from `docs/developer.md`). -Expected: remaining errors confined to `Features\DbDataStores\*` (Task 11), `Features\DataStores\*` (Task 12), and `Program.cs`/`WebApplicationBuilderExtensions.cs` (Task 13, shared file already partially updated in Task 6). - -- [ ] **Step 6: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManagesQuery.cs" \ - "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Queries/GetDataStoreManageByIdQuery.cs" \ - "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/AddDataStoreManageCommand.cs" \ - "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Database/Commands/DeleteDataStoreManageCommand.cs" \ - "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreatePendingDataStoreManagesDispatcherJob.cs" \ - "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeletePendingDataStoreManagesDispatcherJob.cs" \ - "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/CreateInstanceJob.cs" \ - "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Jobs/DeleteInstanceJob.cs" -git commit -m "Rename v3 Infrastructure Queries/Commands/Jobs to DataStoreManage naming" -``` - ---- - -### Task 11: v3 Feature folder move — `Features\DbDataStores` → `Features\DataStores\Manage` - -**Files:** -- Create: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\Manage\AddDataStoreManage.cs` -- Create: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\Manage\ReadDataStoreManage.cs` -- Create: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\Manage\DeleteDataStoreManage.cs` -- Create: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\Manage\DataStoreManageModel.cs` -- Create: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\Manage\DataStoreManageMapper.cs` -- Create: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\Manage\DataStoreManageDatabaseNameFormatter.cs` -- Delete: `Application\EdFi.Ods.AdminApi.V3\Features\DbDataStores\` (entire folder, all 6 old files) - -**Interfaces:** -- Consumes: `AddDataStoreManageCommand`/`IAddDataStoreManageModel`, `IGetDataStoreManagesQuery`/`IGetDataStoreManageByIdQuery`, `IDeleteDataStoreManageCommand` (Task 10), `OdsInstanceManage`/`OdsInstanceManageStatus` (Task 2), `JobConstants.OdsInstanceManageIdKey` (Task 2), `CreateInstanceJob`/`DeleteInstanceJob` (Task 10, unchanged names). -- Produces: routes `POST/GET/DELETE /dataStores/manage` under `EdFi.Ods.AdminApi.V3.Features.DataStores.Manage`, consumed by Task 16 (Bruno E2E) and Task 17 (`.http` file). - -- [ ] **Step 1: Move the folder with git** - -```bash -git mv "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/AddDbDataStore.cs" "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/AddDataStoreManage.cs" -git mv "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/ReadDbDataStore.cs" "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/ReadDataStoreManage.cs" -git mv "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DeleteDbDataStore.cs" "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DeleteDataStoreManage.cs" -git mv "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreModel.cs" "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageModel.cs" -git mv "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreMapper.cs" "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageMapper.cs" -git mv "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores/DbDataStoreDatabaseNameFormatter.cs" "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage/DataStoreManageDatabaseNameFormatter.cs" -``` - -- [ ] **Step 2: Replace `AddDataStoreManage.cs` content** - -```csharp -// 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.RegularExpressions; - -using EdFi.Admin.DataAccess.Contexts; -using EdFi.Ods.AdminApi.Common.Features; -using EdFi.Ods.AdminApi.Common.Infrastructure; -using EdFi.Ods.AdminApi.Common.Infrastructure.Context; -using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; -using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; -using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Common.Constants; -using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; -using EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs; -using FluentValidation; -using Microsoft.EntityFrameworkCore; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Quartz; -using Swashbuckle.AspNetCore.Annotations; -using EdFi.Ods.AdminApi.V3.Infrastructure; - -namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; - -public class AddDataStoreManage : IFeature -{ - private const int MaxSynchronizedNameLength = 100; - private const int MaxDataStoreManageNameLength = MaxSynchronizedNameLength; - private static readonly Regex _validDataStoreManageNamePattern = new( - "^[A-Za-z0-9 _]+$", - RegexOptions.Compiled | RegexOptions.CultureInvariant); - - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder - .MapPost(endpoints, "/dataStores/manage", Handle) - .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponseCode(202)) - .BuildForVersions(AdminApiVersions.V3); - } - - public async static Task Handle( - Validator validator, - AddDataStoreManageCommand addDataStoreManageCommand, - [FromServices] ISchedulerFactory schedulerFactory, - [FromServices] IContextProvider tenantConfigurationProvider, - [FromServices] IOptions options, - AddDataStoreManageRequest request, - HttpContext httpContext) - { - await validator.GuardAsync(request); - - var added = addDataStoreManageCommand.Execute(request); - - var tenantIdentifier = options.Value.MultiTenancy - ? tenantConfigurationProvider.Get()?.TenantIdentifier - : null; - - var jobBuilder = JobBuilder.Create() - .WithIdentity(CreateInstanceJob.CreateJobKey(added.Id, tenantIdentifier)) - .UsingJobData(JobConstants.OdsInstanceManageIdKey, added.Id); - - if (!string.IsNullOrWhiteSpace(tenantIdentifier)) - { - jobBuilder = jobBuilder.UsingJobData(JobConstants.TenantNameKey, tenantIdentifier); - } - - var trigger = TriggerBuilder.Create() - .StartNow() - .Build(); - - var scheduler = await schedulerFactory.GetScheduler(); - - try - { - await scheduler.ScheduleJob(jobBuilder.Build(), trigger); - } - catch (ObjectAlreadyExistsException) - { - // The CreatePendingDataStoreManagesDispatcherJob may have already scheduled this job - // (e.g. it fired between the DB insert and this ScheduleJob call). Treat duplicate - // scheduling as success — the job is already queued and will process the OdsInstanceManage. - } - - var absoluteLocation = ResourceUrlHelper.BuildAbsoluteResourceUrl(httpContext, AdminApiMode.V3, $"/dataStores/manage/{added.Id}"); - return Results.Accepted(absoluteLocation, null); - } - - [SwaggerSchema(Title = "AddDataStoreManageRequest")] - public class AddDataStoreManageRequest : IAddDataStoreManageModel - { - [SwaggerSchema(Description = "Name of the DataStore database", Nullable = false)] - public string? Name { get; set; } - - [SwaggerSchema(Description = "Database template to use for the DataStore database", Nullable = false)] - public string? DatabaseTemplate { get; set; } - } - - public class Validator : AbstractValidator - { - private static readonly string[] _validDatabaseTemplates = Enum.GetNames(); - private readonly AdminApiDbContext _adminApiDbContext; - private readonly IUsersContext _usersContext; - - public Validator(AdminApiDbContext adminApiDbContext, IUsersContext usersContext) - { - _adminApiDbContext = adminApiDbContext; - _usersContext = usersContext; - - RuleFor(m => m.Name) - .NotEmpty() - .MaximumLength(MaxDataStoreManageNameLength) - .WithMessage($"'{{PropertyName}}' must be {MaxDataStoreManageNameLength} characters or fewer so the synchronized DataStore name fits within {MaxSynchronizedNameLength} characters.") - .Matches(_validDataStoreManageNamePattern) - .WithMessage("'{PropertyName}' may only contain letters, numbers, spaces, and underscores."); - - RuleFor(m => m.DatabaseTemplate).NotEmpty().MaximumLength(100) - .Must(t => t != null && _validDatabaseTemplates.Contains(t)) - .WithMessage($"'{{PropertyValue}}' is not a valid database template. Allowed values are: {string.Join(", ", _validDatabaseTemplates)}."); - - RuleFor(m => m).CustomAsync(async (request, context, cancellationToken) => - { - if (string.IsNullOrWhiteSpace(request.Name) - || string.IsNullOrWhiteSpace(request.DatabaseTemplate) - || request.Name.Length > MaxDataStoreManageNameLength - || !_validDataStoreManageNamePattern.IsMatch(request.Name) - || !_validDatabaseTemplates.Contains(request.DatabaseTemplate)) - { - return; - } - - var normalizedName = request.Name.Trim(); - - if (await _adminApiDbContext.OdsInstanceManages.AnyAsync(instance => instance.Name == normalizedName && instance.Status != OdsInstanceManageStatus.Deleted.ToString(), cancellationToken)) - { - context.AddFailure( - nameof(AddDataStoreManageRequest.Name), - $"A DataStoreManage named '{normalizedName}' already exists."); - return; - } - - if (await _usersContext.OdsInstances.AnyAsync(instance => instance.Name == normalizedName, cancellationToken)) - { - context.AddFailure( - nameof(AddDataStoreManageRequest.Name), - $"A DataStore named '{normalizedName}' already exists."); - return; - } - - var databaseName = DataStoreManageDatabaseNameFormatter.Build(request.Name, request.DatabaseTemplate); - - if (databaseName.Length > DataStoreManageDatabaseNameFormatter.MaxPortableDatabaseNameLength) - { - context.AddFailure( - nameof(AddDataStoreManageRequest.Name), - $"The generated database name '{databaseName}' exceeds the portable limit of {DataStoreManageDatabaseNameFormatter.MaxPortableDatabaseNameLength} characters. Shorten Name or DatabaseTemplate."); - } - }); - } - } -} -``` - -- [ ] **Step 3: Replace `ReadDataStoreManage.cs` content** - -```csharp -// 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.ErrorHandling; -using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; - -namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; - -public class ReadDataStoreManage : IFeature -{ - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder.MapGet(endpoints, "/dataStores/manage", GetDataStoreManages) - .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V3); - - AdminApiEndpointBuilder.MapGet(endpoints, "/dataStores/manage/{id}", GetDataStoreManage) - .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponse(200)) - .BuildForVersions(AdminApiVersions.V3); - } - - public static Task GetDataStoreManages(IGetDataStoreManagesQuery query, - [AsParameters] CommonQueryParams commonQueryParams, int? id, string? name) - { - var list = DataStoreManageMapper.ToModelList(query.Execute(commonQueryParams, id, name)); - return Task.FromResult(Results.Ok(list)); - } - - public static Task GetDataStoreManage(IGetDataStoreManageByIdQuery query, int id) - { - var dataStoreManage = query.Execute(id); - if (dataStoreManage == null) - { - throw new NotFoundException("dataStoreManage", id); - } - var model = DataStoreManageMapper.ToModel(dataStoreManage); - return Task.FromResult(Results.Ok(model)); - } -} -``` - -- [ ] **Step 4: Replace `DeleteDataStoreManage.cs` content** - -```csharp -// 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.Constants; -using EdFi.Ods.AdminApi.Common.Features; -using EdFi.Ods.AdminApi.Common.Infrastructure; -using EdFi.Ods.AdminApi.Common.Infrastructure.Context; -using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; -using EdFi.Ods.AdminApi.Common.Infrastructure.Jobs; -using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; -using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; -using EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs; -using FluentValidation; -using FluentValidation.Results; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; -using Quartz; - -namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; - -public class DeleteDataStoreManage : IFeature -{ - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder - .MapDelete(endpoints, "/dataStores/manage/{id}", Handle) - .WithDefaultSummaryAndDescription() - .WithRouteOptions(b => b.WithResponseCode(204)) - .BuildForVersions(AdminApiVersions.V3); - } - - public static async Task Handle( - IGetDataStoreManageByIdQuery getDataStoreManageByIdQuery, - IDeleteDataStoreManageCommand deleteDataStoreManageCommand, - [FromServices] ISchedulerFactory schedulerFactory, - [FromServices] IContextProvider tenantConfigurationProvider, - [FromServices] IOptions options, - int id - ) - { - var dataStoreManage = getDataStoreManageByIdQuery.Execute(id); - if (dataStoreManage is null) - throw new NotFoundException("dataStoreManage", id); - - if (dataStoreManage.Status == OdsInstanceManageStatus.Deleted.ToString()) - throw new NotFoundException("dataStoreManage", id); - - var blockingMessage = GetBlockingStatusMessage(dataStoreManage.Status); - if (blockingMessage is not null) - throw new ValidationException([new ValidationFailure(nameof(id), blockingMessage)]); - - deleteDataStoreManageCommand.Execute(id); - - var tenantName = options.Value.MultiTenancy - ? tenantConfigurationProvider.Get()?.TenantIdentifier - : null; - var jobData = new Dictionary - { - [JobConstants.OdsInstanceManageIdKey] = id - }; - - if (!string.IsNullOrWhiteSpace(tenantName)) - { - jobData[JobConstants.TenantNameKey] = tenantName; - } - - var scheduler = await schedulerFactory.GetScheduler(); - - try - { - await QuartzJobScheduler.ScheduleJob( - scheduler, - DeleteInstanceJob.CreateJobKey(id, tenantName), - jobData, - startImmediately: true); - } - catch (ObjectAlreadyExistsException) - { - // The DeletePendingDataStoreManagesDispatcherJob may have already scheduled this job. - // Treat duplicate scheduling as success — the job is already queued. - } - - return Results.NoContent(); - } - - private static string? GetBlockingStatusMessage(string status) - { - if (Enum.TryParse(status, out var parsed)) - { - return parsed switch - { - OdsInstanceManageStatus.PendingCreate => "OdsInstanceManage is being provisioned. Wait for creation to complete.", - OdsInstanceManageStatus.CreateInProgress => "OdsInstanceManage is currently being provisioned. Wait for creation to complete.", - OdsInstanceManageStatus.CreateFailed => "OdsInstanceManage creation failed. It will be retried automatically by the background job.", - OdsInstanceManageStatus.CreateError => "OdsInstanceManage creation failed permanently. Manual database intervention required before deleting.", - OdsInstanceManageStatus.PendingDelete => "OdsInstanceManage is already queued for deletion.", - OdsInstanceManageStatus.DeleteInProgress => "OdsInstanceManage is currently being deleted.", - OdsInstanceManageStatus.DeleteFailed => "OdsInstanceManage deletion failed. It will be retried automatically by the background job.", - OdsInstanceManageStatus.DeleteError => "OdsInstanceManage deletion failed permanently. Manual database intervention required.", - _ => null, - }; - } - - return null; - } -} -``` - -- [ ] **Step 5: Replace `DataStoreManageModel.cs` content** - -```csharp -// 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 Swashbuckle.AspNetCore.Annotations; - -namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; - -[SwaggerSchema(Title = "DataStoreManage")] -public class DataStoreManageModel -{ - public int? Id { get; set; } - public string? Name { get; set; } - public int? DataStoreId { get; set; } - public string? DataStoreName { get; set; } - public string? Status { get; set; } - public string? DatabaseTemplate { get; set; } - public string? DatabaseName { get; set; } - public DateTime? LastRefreshed { get; set; } - public DateTime? LastModifiedDate { get; set; } -} -``` - -- [ ] **Step 6: Replace `DataStoreManageMapper.cs` content** - -```csharp -// 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.Infrastructure.Models; - -namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; - -public static class DataStoreManageMapper -{ - public static DataStoreManageModel ToModel(OdsInstanceManage source) - { - return new DataStoreManageModel - { - Id = source.Id, - Name = source.Name, - DataStoreId = source.OdsInstanceId, - DataStoreName = source.OdsInstanceName, - Status = source.Status, - DatabaseTemplate = source.DatabaseTemplate, - DatabaseName = source.DatabaseName, - LastRefreshed = source.LastRefreshed, - LastModifiedDate = source.LastModifiedDate, - }; - } - - public static List ToModelList(IEnumerable source) - { - return source.Select(ToModel).ToList(); - } -} -``` - -- [ ] **Step 7: Replace `DataStoreManageDatabaseNameFormatter.cs` content** - -```csharp -// 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.RegularExpressions; - -namespace EdFi.Ods.AdminApi.V3.Features.DataStores.Manage; - -internal static class DataStoreManageDatabaseNameFormatter -{ - private const string CanonicalPrefix = "EdFi_Ods"; - - // Use PostgreSQL's identifier limit as the portable ceiling so the persisted - // DatabaseName always matches the real provisioned database across engines. - internal const int MaxPortableDatabaseNameLength = 63; - - private static readonly Regex _leadingCanonicalPrefixPattern = new( - @"^(?:(?:edfi_+ods)(?:_+|$))+", - RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - - internal static string Build(string dataStoreName, string databaseTemplate) - { - ArgumentException.ThrowIfNullOrWhiteSpace(dataStoreName); - ArgumentException.ThrowIfNullOrWhiteSpace(databaseTemplate); - - var normalizedName = NormalizeSegment(dataStoreName); - var normalizedDatabaseTemplate = NormalizeSegment(databaseTemplate); - var normalizedNameWithoutPrefix = _leadingCanonicalPrefixPattern.Replace(normalizedName, string.Empty).Trim('_'); - - return string.IsNullOrWhiteSpace(normalizedNameWithoutPrefix) - ? $"{CanonicalPrefix}_{normalizedDatabaseTemplate}" - : $"{CanonicalPrefix}_{normalizedNameWithoutPrefix}_{normalizedDatabaseTemplate}"; - } - - private static string NormalizeSegment(string value) - => value.Replace(' ', '_').Trim('_'); -} -``` - -- [ ] **Step 8: Build** - -Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` -Expected: remaining errors confined to `Features\DataStores\DataStoreWithEducationOrganizationsModel.cs`/`ReadEducationOrganizations.cs` (Task 12) and `Program.cs`/`WebApplicationBuilderExtensions.cs` V3 branches (Task 13). - -- [ ] **Step 9: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/Manage" \ - "Application/EdFi.Ods.AdminApi.V3/Features/DbDataStores" -git commit -m "Move v3 DbDataStores feature into DataStores/Manage, rename routes to /dataStores/manage" -``` - ---- - -### Task 12: v3 existing `DataStores` and `Tenants` files — update references and close the `DataStoreManageId` parity gap - -**Amendment (same gap as Task 5, mirrored on the v3 side):** `Features\Tenants\*` and `Infrastructure\Services\Tenants\TenantService.cs` inject `IGetDbDataStoresQuery`/`DbInstance`/`DbInstanceStatus` to build the `/tenants/{tenantName}/dataStores/edOrgs` response. v3's `TenantDetailModel.cs` does NOT reference these old names (its `TenantDataStoreModel` has no linking-ID field equivalent to v2's `DbInstanceId` — that's a pre-existing v2/v3 asymmetry, not something to fix here), so only `TenantMapper.cs`, `ReadTenants.cs`, and `TenantService.cs` need changes. - -**Files:** -- Modify: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\DataStoreWithEducationOrganizationsModel.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3\Features\DataStores\ReadEducationOrganizations.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3\Features\Tenants\TenantMapper.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3\Features\Tenants\ReadTenants.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3\Infrastructure\Services\Tenants\TenantService.cs` - -**Interfaces:** -- Consumes: `IGetDataStoreManagesQuery` (Task 10), `OdsInstanceManageStatus` (Task 2). -- Produces: `DataStoreWithEducationOrganizationsModel.DataStoreManageId` (new field, parity with v2's `OdsInstanceManageId` from Task 5), `TenantMapper.ToUnlinkedDataStoreManageModel` (renamed from `ToUnlinkedDbDataStoreModel`), `ITenantsService.GetTenantEdOrgsByInstancesAsync(..., IGetDataStoreManagesQuery, ...)`. - -- [ ] **Step 1: Add the `DataStoreManageId` field** - -In `DataStoreWithEducationOrganizationsModel.cs`, add a new property alongside `Id` (mirroring v2's `OdsInstanceWithEducationOrganizationsModel.OdsInstanceManageId` from Task 5): - -```csharp - [SwaggerSchema(Description = "Data store identifier")] - public int? Id { get; set; } - - [SwaggerSchema(Description = "DataStoreManage identifier for this data store")] - public int? DataStoreManageId { get; set; } -``` - -(insert the new property immediately after `Id`, before `Name`.) - -- [ ] **Step 2: Update `ReadEducationOrganizations.cs`** - -Replace the full file content with: - -```csharp -// 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.Constants; -using EdFi.Ods.AdminApi.Common.Features; -using EdFi.Ods.AdminApi.Common.Infrastructure; -using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; -using Microsoft.AspNetCore.Mvc; - -namespace EdFi.Ods.AdminApi.V3.Features.DataStores; - -public class ReadEducationOrganizations : IFeature -{ - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder - .MapGet(endpoints, "/dataStores/{dataStoreId}/edOrgs", GetEducationOrganizationsByDataStore) - .WithSummaryAndDescription( - "Retrieves education organizations for a specific data store", - "Returns all education organizations for the specified data store in a nested structure" - ) - .WithRouteOptions(b => b.WithResponse>(200)) - .BuildForVersions(AdminApiVersions.V3); - } - - public static async Task GetEducationOrganizationsByDataStore( - [FromServices] IGetEducationOrganizationsQuery getEducationOrganizationsQuery, - [FromServices] IGetDataStoreQuery getDataStoreQuery, - [FromServices] IGetDataStoreManagesQuery getDataStoreManagesQuery, - [AsParameters] CommonQueryParams commonQueryParams, - int dataStoreId) - { - getDataStoreQuery.Execute(dataStoreId); - - var educationOrganizations = await getEducationOrganizationsQuery.ExecuteAsync( - commonQueryParams, - dataStoreId: dataStoreId); - - MergeDataStoreManageData(educationOrganizations, getDataStoreManagesQuery); - return Results.Ok(educationOrganizations); - } - - private static void MergeDataStoreManageData( - List instances, - IGetDataStoreManagesQuery getDataStoreManagesQuery) - { - var allDataStoreManages = getDataStoreManagesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); - - var linkedById = allDataStoreManages - .Where(d => d.OdsInstanceId is not null) - .GroupBy(d => d.OdsInstanceId!.Value) - .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); - - foreach (var instance in instances) - { - if (instance.Id is int dataStoreId && linkedById.TryGetValue(dataStoreId, out var dataStoreManage)) - { - instance.DataStoreManageId = dataStoreManage.Id; - instance.Status = dataStoreManage.Status; - instance.DatabaseTemplate = dataStoreManage.DatabaseTemplate; - instance.DatabaseName = dataStoreManage.DatabaseName; - } - else - { - instance.Status = OdsInstanceManageStatus.Created.ToString(); - } - } - } -} -``` - -(this adds `instance.DataStoreManageId = dataStoreManage.Id;` in the linked branch, which is the new parity behavior — v2's equivalent method already sets `instance.OdsInstanceManageId` the same way.) - -- [ ] **Step 3: Update `TenantMapper.cs`** - -Replace the full file content with: - -```csharp -// 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.Admin.DataAccess.Models; -using EdFi.Ods.AdminApi.Common.Infrastructure.Models; - -namespace EdFi.Ods.AdminApi.V3.Features.Tenants; - -public static class TenantMapper -{ - public static TenantDataStoreModel ToDataStoreModel(OdsInstance source) - { - return new TenantDataStoreModel - { - DataStoreId = source.OdsInstanceId, - Name = source.Name, - DataStoreType = source.InstanceType, - }; - } - - public static List ToDataStoreModelList(IEnumerable source) - { - return source.Select(ToDataStoreModel).ToList(); - } - - public static TenantDataStoreModel ToUnlinkedDataStoreManageModel(OdsInstanceManage source) - { - return new TenantDataStoreModel - { - DataStoreId = null, - Name = source.Name, - Status = source.Status, - DatabaseTemplate = source.DatabaseTemplate, - DatabaseName = source.DatabaseName, - }; - } -} -``` - -- [ ] **Step 4: Update `ReadTenants.cs`** - -Replace: - -```csharp - IGetDbDataStoresQuery getDbDataStoresQuery, -``` - -with: - -```csharp - IGetDataStoreManagesQuery getDataStoreManagesQuery, -``` - -and replace: - -```csharp - var tenant = await tenantsService.GetTenantEdOrgsByInstancesAsync( - getDataStoresQuery, getEducationOrganizationQuery, getDbDataStoresQuery, tenantName); -``` - -with: - -```csharp - var tenant = await tenantsService.GetTenantEdOrgsByInstancesAsync( - getDataStoresQuery, getEducationOrganizationQuery, getDataStoreManagesQuery, tenantName); -``` - -(`IGetDataStoreManagesQuery` lives in the same `EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries` namespace already imported in this file — no new using needed.) - -- [ ] **Step 5: Update `TenantService.cs`** - -Replace the interface line: - -```csharp - Task GetTenantEdOrgsByInstancesAsync(IGetDataStoresQuery getDataStoresQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, IGetDbDataStoresQuery getDbDataStoresQuery, string tenantName); -``` - -with: - -```csharp - Task GetTenantEdOrgsByInstancesAsync(IGetDataStoresQuery getDataStoresQuery, IGetEducationOrganizationQuery getEducationOrganizationQuery, IGetDataStoreManagesQuery getDataStoreManagesQuery, string tenantName); -``` - -Replace the method signature: - -```csharp - public async Task GetTenantEdOrgsByInstancesAsync( - IGetDataStoresQuery getDataStoresQuery, - IGetEducationOrganizationQuery getEducationOrganizationQuery, - IGetDbDataStoresQuery getDbDataStoresQuery, - string tenantName) -``` - -with: - -```csharp - public async Task GetTenantEdOrgsByInstancesAsync( - IGetDataStoresQuery getDataStoresQuery, - IGetEducationOrganizationQuery getEducationOrganizationQuery, - IGetDataStoreManagesQuery getDataStoreManagesQuery, - string tenantName) -``` - -Replace the method body's use of the renamed query/entity: - -```csharp - var allDbDataStores = getDbDataStoresQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); - - var linkedDbDataStoresByDataStoreId = allDbDataStores - .Where(d => d.OdsInstanceId is not null) - .GroupBy(d => d.OdsInstanceId!.Value) - .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); - - foreach (var dataStore in tenantDetails.DataStores) - { - if (dataStore.DataStoreId is int dataStoreId && linkedDbDataStoresByDataStoreId.TryGetValue(dataStoreId, out var dbDataStore)) - { - dataStore.Status = dbDataStore.Status; - dataStore.DatabaseTemplate = dbDataStore.DatabaseTemplate; - dataStore.DatabaseName = dbDataStore.DatabaseName; - } - else - { - dataStore.Status = DbInstanceStatus.Created.ToString(); - } - } - - var existingDataStoreIds = tenantDetails.DataStores - .Where(i => i.DataStoreId is int) - .Select(i => i.DataStoreId!.Value) - .ToHashSet(); - - var unlinkedDbDataStores = allDbDataStores - .Where(d => d.OdsInstanceId is null) - .Concat(allDbDataStores - .Where(d => d.OdsInstanceId is not null && !existingDataStoreIds.Contains(d.OdsInstanceId.Value)) - .GroupBy(d => d.OdsInstanceId!.Value) - .Select(g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First())) - .ToList(); - foreach (var dbDataStore in unlinkedDbDataStores) - { - tenantDetails.DataStores.Add(TenantMapper.ToUnlinkedDbDataStoreModel(dbDataStore)); - } -``` - -with: - -```csharp - var allDataStoreManages = getDataStoreManagesQuery.Execute(new CommonQueryParams(0, int.MaxValue), null, null); - - var linkedDataStoreManagesByDataStoreId = allDataStoreManages - .Where(d => d.OdsInstanceId is not null) - .GroupBy(d => d.OdsInstanceId!.Value) - .ToDictionary(g => g.Key, g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First()); - - foreach (var dataStore in tenantDetails.DataStores) - { - if (dataStore.DataStoreId is int dataStoreId && linkedDataStoreManagesByDataStoreId.TryGetValue(dataStoreId, out var dataStoreManage)) - { - dataStore.Status = dataStoreManage.Status; - dataStore.DatabaseTemplate = dataStoreManage.DatabaseTemplate; - dataStore.DatabaseName = dataStoreManage.DatabaseName; - } - else - { - dataStore.Status = OdsInstanceManageStatus.Created.ToString(); - } - } - - var existingDataStoreIds = tenantDetails.DataStores - .Where(i => i.DataStoreId is int) - .Select(i => i.DataStoreId!.Value) - .ToHashSet(); - - var unlinkedDataStoreManages = allDataStoreManages - .Where(d => d.OdsInstanceId is null) - .Concat(allDataStoreManages - .Where(d => d.OdsInstanceId is not null && !existingDataStoreIds.Contains(d.OdsInstanceId.Value)) - .GroupBy(d => d.OdsInstanceId!.Value) - .Select(g => g.OrderByDescending(d => d.LastModifiedDate ?? d.LastRefreshed).First())) - .ToList(); - foreach (var dataStoreManage in unlinkedDataStoreManages) - { - tenantDetails.DataStores.Add(TenantMapper.ToUnlinkedDataStoreManageModel(dataStoreManage)); - } -``` - -- [ ] **Step 6: Build** - -Run: `dotnet build Application/Ed-Fi-ODS-AdminApi.sln` -Expected: remaining errors confined to `Program.cs`/`WebApplicationBuilderExtensions.cs` V3 branches (Task 13). - -- [ ] **Step 7: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/DataStoreWithEducationOrganizationsModel.cs" \ - "Application/EdFi.Ods.AdminApi.V3/Features/DataStores/ReadEducationOrganizations.cs" \ - "Application/EdFi.Ods.AdminApi.V3/Features/Tenants/TenantMapper.cs" \ - "Application/EdFi.Ods.AdminApi.V3/Features/Tenants/ReadTenants.cs" \ - "Application/EdFi.Ods.AdminApi.V3/Infrastructure/Services/Tenants/TenantService.cs" -git commit -m "Update v3 DataStores and Tenants EdOrgs merges to use renamed DataStoreManage query, add DataStoreManageId for v2 parity" -``` - ---- - -### Task 13: v3 wiring — `Program.cs` V3 branch and `WebApplicationBuilderExtensions.cs` V3 branch - -**Files:** -- Modify: `Application\EdFi.Ods.AdminApi\Program.cs` (V3 branch only — the shared interval-variable renames were already done in Task 6) -- Modify: `Application\EdFi.Ods.AdminApi\Infrastructure\WebApplicationBuilderExtensions.cs` (V3 branch only) - -**Interfaces:** -- Consumes: `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName`/`DeletePendingOdsInstanceManagesDispatcherJobName` (Task 2 — same shared constants v2 uses, since `JobConstants` isn't version-specific), `CreatePendingDataStoreManagesDispatcherJob`/`DeletePendingDataStoreManagesDispatcherJob` (Task 10, the V3-namespaced classes aliased as `V3Jobs.*`). -- Produces: fully wired v3 Quartz scheduling — the last v3-side wiring file; Task 14's build checkpoint depends on this. - -- [ ] **Step 1: Update job-type references in the `AdminApiMode.V3` branch of `Program.cs`** - -Replace every occurrence in the V3 branch of: - -```csharp -await QuartzJobScheduler.ScheduleJob( - scheduler, - jobKey: new JobKey($"{JobConstants.CreatePendingDbInstancesDispatcherJobName}_{tenantName}"), -``` - -and the non-multi-tenant equivalent - -```csharp -await QuartzJobScheduler.ScheduleJob( - scheduler, - jobKey: new JobKey(JobConstants.CreatePendingDbInstancesDispatcherJobName), -``` - -with `V3Jobs.CreatePendingDataStoreManagesDispatcherJob` and `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName` (both branches). Do the same for `V3Jobs.DeletePendingDbInstancesDispatcherJob` → `V3Jobs.DeletePendingDataStoreManagesDispatcherJob` with `JobConstants.DeletePendingOdsInstanceManagesDispatcherJobName`. - -Note: `JobConstants.CreatePendingOdsInstanceManagesDispatcherJobName`/`DeletePendingOdsInstanceManagesDispatcherJobName` are the same shared string constants v2 uses (from Task 2) — only the generic type parameter (`V3Jobs.CreatePendingDataStoreManagesDispatcherJob` vs. v2's bare `CreatePendingOdsInstanceManagesDispatcherJob`) differs between branches, matching the existing pre-rename pattern where both branches already used the same `JobConstants.CreatePendingDbInstancesDispatcherJobName` string. - -- [ ] **Step 2: Update `WebApplicationBuilderExtensions.cs` DI registrations for the V3 branch** - -Replace: - -```csharp - webApplicationBuilder.Services.AddTransient< - EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.CreatePendingDbInstancesDispatcherJob - >(); -``` - -with: - -```csharp - webApplicationBuilder.Services.AddTransient< - EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.CreatePendingDataStoreManagesDispatcherJob - >(); -``` - -and replace: - -```csharp - webApplicationBuilder.Services.AddTransient< - EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.DeletePendingDbInstancesDispatcherJob - >(); -``` - -with: - -```csharp - webApplicationBuilder.Services.AddTransient< - EdFi.Ods.AdminApi.V3.Infrastructure.Services.Jobs.DeletePendingDataStoreManagesDispatcherJob - >(); -``` - -(the surrounding `CreateInstanceJob`/`DeleteInstanceJob`/`JobStatusService` registrations in this same V3 block are unchanged — only the two dispatcher job type references change.) - -- [ ] **Step 3: Build the full solution** - -Run: `dotnet build` at the repository root (build everything — both v2 and v3 solutions/projects). -Expected: PASS with zero errors. Grep the full source tree for `DbInstance` and `DbDataStore` (`grep -rn "DbInstance\|DbDataStore" Application --include=*.cs`) — the only remaining matches at this point should be inside test projects (Tasks 7, 8, 14, 15 — not yet done) and E2E/`.http` artifacts (Tasks 9, 16, 17 — not yet done). No matches should remain in any non-test `.cs` file under `Application\EdFi.Ods.AdminApi\` or `Application\EdFi.Ods.AdminApi.V3\` (excluding their `E2E Tests` folders, which Tasks 9/16 handle). - -- [ ] **Step 4: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi/Program.cs" \ - "Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs" -git commit -m "Update v3 Quartz wiring to use renamed DataStoreManage dispatcher jobs" -``` - ---- - -### Task 14: v3 Unit tests rename + add missing query-test coverage (`EdFi.Ods.AdminApi.V3.UnitTests`) - -**Files:** -- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DbDataStores\AddDbDataStoreTests.cs` → move+rename to `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DataStores\Manage\AddDataStoreManageTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DbDataStores\ReadDbDataStoreTests.cs` → move+rename to `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DataStores\Manage\ReadDataStoreManageTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DbDataStores\DeleteDbDataStoreTests.cs` → move+rename to `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DataStores\Manage\DeleteDataStoreManageTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Database\Commands\AddDbDataStoreCommandTests.cs` → rename to `AddDataStoreManageCommandTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Database\Commands\DeleteDbDataStoreCommandTests.cs` → rename to `DeleteDataStoreManageCommandTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\CreatePendingDbInstancesDispatcherJobTests.cs` → rename to `CreatePendingDataStoreManagesDispatcherJobTests.cs` (search first to confirm exact path — the initial research report listed this file but it wasn't independently verified in this plan's file reads) -- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\DeletePendingDbInstancesDispatcherJobTests.cs` → rename to `DeletePendingDataStoreManagesDispatcherJobTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\CreateInstanceJobTests.cs` (filename unchanged — update `DbDataStore`/`DbInstance` references only) -- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\DeleteInstanceJobTests.cs` (filename unchanged — update references only) -- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DataStores\ReadEducationOrganizationsTests.cs` (filename unchanged — update `IGetDbDataStoresQuery`/`DbInstance` references) -- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Tenants\TenantServiceTests.cs` (filename unchanged — update references) -- Modify: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\Tenants\ReadTenantsTest.cs` (filename unchanged — update references) -- Create: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Database\Queries\GetDataStoreManagesQueryTests.cs` (new — closes the coverage gap) -- Create: `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Database\Queries\GetDataStoreManageByIdQueryTests.cs` (new — closes the coverage gap) - -**Interfaces:** -- Consumes: every v3 production type from Tasks 10–13. - -- [ ] **Step 1: Move and rename the Command test files (full content known — apply directly)** - -```bash -git mv "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/AddDbDataStoreCommandTests.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/AddDataStoreManageCommandTests.cs" -git mv "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/DeleteDbDataStoreCommandTests.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/DeleteDataStoreManageCommandTests.cs" -``` - -`AddDataStoreManageCommandTests.cs`: - -```csharp -// 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.Collections.Generic; -using System.Linq; -using EdFi.Ods.AdminApi.Common.Constants; -using EdFi.Ods.AdminApi.V3.Infrastructure; -using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using NUnit.Framework; -using Shouldly; - -#nullable enable - -namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Database.Commands; - -[TestFixture] -public class AddDataStoreManageCommandTests -{ - private static AdminApiDbContext CreateContext() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"AddDataStoreManageCommand_{Guid.NewGuid()}") - .Options; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["AppSettings:DatabaseEngine"] = "SqlServer" - }) - .Build(); - return new AdminApiDbContext(options, configuration); - } - - [Test] - public void Execute_WithValidModel_PersistsOdsInstanceManage() - { - using var context = CreateContext(); - var command = new AddDataStoreManageCommand(context); - var model = new AddDataStoreManageModelStub - { - Name = "Test Instance", - DatabaseTemplate = "Minimal" - }; - - var result = command.Execute(model); - - result.Id.ShouldBeGreaterThan(0); - context.OdsInstanceManages.Any(d => d.Id == result.Id).ShouldBeTrue(); - } - - [Test] - public void Execute_WithValidModel_SetsExpectedFieldValues() - { - using var context = CreateContext(); - var command = new AddDataStoreManageCommand(context); - var before = DateTime.UtcNow; - var model = new AddDataStoreManageModelStub - { - Name = " Test Instance ", - DatabaseTemplate = " Minimal " - }; - - var result = command.Execute(model); - - result.Name.ShouldBe("Test Instance"); - result.DatabaseTemplate.ShouldBe("Minimal"); - result.Status.ShouldBe(OdsInstanceManageStatus.PendingCreate.ToString()); - result.OdsInstanceId.ShouldBeNull(); - result.OdsInstanceName.ShouldBeNull(); - result.DatabaseName.ShouldBeNull(); - result.LastRefreshed.ShouldBeGreaterThanOrEqualTo(before); - result.LastModifiedDate.ShouldNotBeNull(); - result.LastModifiedDate!.Value.ShouldBeGreaterThanOrEqualTo(before); - } - - [Test] - public void Execute_WithSampleTemplate_PersistsWithCorrectTemplate() - { - using var context = CreateContext(); - var command = new AddDataStoreManageCommand(context); - var model = new AddDataStoreManageModelStub - { - Name = "Sample Instance", - DatabaseTemplate = "Sample" - }; - - var result = command.Execute(model); - - result.DatabaseTemplate.ShouldBe("Sample"); - context.OdsInstanceManages.Any(d => d.Id == result.Id && d.DatabaseTemplate == "Sample").ShouldBeTrue(); - } - - private sealed class AddDataStoreManageModelStub : IAddDataStoreManageModel - { - public string? Name { get; set; } - public string? DatabaseTemplate { get; set; } - } -} - -#nullable restore -``` - -`DeleteDataStoreManageCommandTests.cs`: - -```csharp -// 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.Collections.Generic; -using System.Linq; -using EdFi.Ods.AdminApi.Common.Constants; -using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; -using EdFi.Ods.AdminApi.Common.Infrastructure.Models; -using EdFi.Ods.AdminApi.V3.Infrastructure; -using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Commands; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using NUnit.Framework; -using Shouldly; - -#nullable enable - -namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Database.Commands; - -[TestFixture] -public class DeleteDataStoreManageCommandTests -{ - private static AdminApiDbContext CreateContext() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"DeleteDataStoreManageCommand_{Guid.NewGuid()}") - .Options; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection( - new Dictionary { ["AppSettings:DatabaseEngine"] = "SqlServer" } - ) - .Build(); - return new AdminApiDbContext(options, configuration); - } - - [Test] - public void Execute_SetsStatusToPendingDelete() - { - using var context = CreateContext(); - var instance = new OdsInstanceManage - { - Name = "Test Instance", - Status = OdsInstanceManageStatus.PendingCreate.ToString(), - DatabaseTemplate = "Minimal", - LastRefreshed = DateTime.UtcNow, - }; - context.OdsInstanceManages.Add(instance); - context.SaveChanges(); - - var command = new DeleteDataStoreManageCommand(context); - command.Execute(instance.Id); - - var updated = context.OdsInstanceManages.Single(d => d.Id == instance.Id); - updated.Status.ShouldBe(OdsInstanceManageStatus.PendingDelete.ToString()); - } - - [Test] - public void Execute_UpdatesLastModifiedDate() - { - using var context = CreateContext(); - var before = DateTime.UtcNow; - var instance = new OdsInstanceManage - { - Name = "Test Instance", - Status = OdsInstanceManageStatus.PendingCreate.ToString(), - DatabaseTemplate = "Minimal", - LastRefreshed = DateTime.UtcNow, - }; - context.OdsInstanceManages.Add(instance); - context.SaveChanges(); - - var command = new DeleteDataStoreManageCommand(context); - command.Execute(instance.Id); - - var updated = context.OdsInstanceManages.Single(d => d.Id == instance.Id); - updated.LastModifiedDate.ShouldNotBeNull(); - updated.LastModifiedDate!.Value.ShouldBeGreaterThanOrEqualTo(before); - } - - [Test] - public void Execute_WithNonExistentId_ThrowsNotFoundException() - { - using var context = CreateContext(); - var command = new DeleteDataStoreManageCommand(context); - - Should.Throw>(() => command.Execute(9999)); - } - - [Test] - public void Execute_WhenStatusIsDeleted_ThrowsNotFoundException() - { - using var context = CreateContext(); - var instance = new OdsInstanceManage - { - Name = "Test Instance", - Status = OdsInstanceManageStatus.Deleted.ToString(), - DatabaseTemplate = "Minimal", - LastRefreshed = DateTime.UtcNow, - }; - context.OdsInstanceManages.Add(instance); - context.SaveChanges(); - - var command = new DeleteDataStoreManageCommand(context); - - Should.Throw>(() => command.Execute(instance.Id)); - } -} - -#nullable restore -``` - -- [ ] **Step 2: Write the new `GetDataStoreManagesQueryTests.cs` (closes the pre-existing v3 coverage gap)** - -```csharp -// 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. - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Linq; -using EdFi.Ods.AdminApi.Common.Infrastructure; -using EdFi.Ods.AdminApi.Common.Infrastructure.Models; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.V3.Infrastructure; -using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; -using NUnit.Framework; -using Shouldly; - -namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Database.Queries; - -[TestFixture] -public class GetDataStoreManagesQueryTests -{ - private static AdminApiDbContext CreateContext() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"GetDataStoreManagesQueryTests_{Guid.NewGuid()}") - .Options; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["AppSettings:DatabaseEngine"] = "Postgres" - }) - .Build(); - - return new AdminApiDbContext(options, configuration); - } - - private static IOptions DefaultOptions() => - Options.Create(new AppSettings { DatabaseEngine = "Postgres", DefaultPageSizeLimit = 25 }); - - [Test] - public void Execute_WithoutFilters_ReturnsAllOdsInstanceManages() - { - using var context = CreateContext(); - context.OdsInstanceManages.AddRange( - new OdsInstanceManage { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, - new OdsInstanceManage { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); - context.SaveChanges(); - - var query = new GetDataStoreManagesQuery(context, DefaultOptions()); - - var result = query.Execute(new CommonQueryParams(0, 25), null, null); - - result.Count.ShouldBe(2); - result.Select(x => x.Name).ShouldBe(["Sandbox A", "Sandbox B"], ignoreOrder: true); - } - - [Test] - public void Execute_WithNameFilter_ReturnsMatchingOdsInstanceManage() - { - using var context = CreateContext(); - context.OdsInstanceManages.AddRange( - new OdsInstanceManage { Name = "Sandbox A", Status = "Healthy", DatabaseTemplate = "Minimal" }, - new OdsInstanceManage { Name = "Sandbox B", Status = "Healthy", DatabaseTemplate = "Minimal" }); - context.SaveChanges(); - - var query = new GetDataStoreManagesQuery(context, DefaultOptions()); - - var result = query.Execute(new CommonQueryParams(0, 25), null, "Sandbox B"); - - result.Count.ShouldBe(1); - result.Single().Name.ShouldBe("Sandbox B"); - } -} -``` - -- [ ] **Step 3: Write the new `GetDataStoreManageByIdQueryTests.cs` (closes the pre-existing v3 coverage gap)** - -```csharp -// 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. - -#nullable enable - -using System; -using System.Collections.Generic; -using EdFi.Ods.AdminApi.Common.Infrastructure.Models; -using EdFi.Ods.AdminApi.V3.Infrastructure; -using EdFi.Ods.AdminApi.V3.Infrastructure.Database.Queries; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using NUnit.Framework; -using Shouldly; - -namespace EdFi.Ods.AdminApi.V3.UnitTests.Infrastructure.Database.Queries; - -[TestFixture] -public class GetDataStoreManageByIdQueryTests -{ - private static AdminApiDbContext CreateContext() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: $"GetDataStoreManageByIdQueryTests_{Guid.NewGuid()}") - .Options; - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - ["AppSettings:DatabaseEngine"] = "Postgres" - }) - .Build(); - - return new AdminApiDbContext(options, configuration); - } - - [Test] - public void Execute_WithExistingId_ReturnsOdsInstanceManage() - { - using var context = CreateContext(); - var odsInstanceManage = new OdsInstanceManage - { - Name = "Sandbox", - Status = "Healthy", - DatabaseTemplate = "Minimal" - }; - context.OdsInstanceManages.Add(odsInstanceManage); - context.SaveChanges(); - - var query = new GetDataStoreManageByIdQuery(context); - - var result = query.Execute(odsInstanceManage.Id); - - result.ShouldNotBeNull(); - result.Name.ShouldBe("Sandbox"); - } - - [Test] - public void Execute_WithUnknownId_ReturnsNull() - { - using var context = CreateContext(); - var query = new GetDataStoreManageByIdQuery(context); - - var result = query.Execute(999); - - result.ShouldBeNull(); - } -} -``` - -- [ ] **Step 4: Move and rename the remaining v3 unit test files, applying the Master Rename Table** - -```bash -git mv "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/AddDbDataStoreTests.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/AddDataStoreManageTests.cs" -git mv "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/ReadDbDataStoreTests.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/ReadDataStoreManageTests.cs" -git mv "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores/DeleteDbDataStoreTests.cs" "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage/DeleteDataStoreManageTests.cs" -``` - -Search for the exact dispatcher-job-test filenames first (`Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\`) — rename `CreatePendingDbInstancesDispatcherJobTests.cs`/`DeletePendingDbInstancesDispatcherJobTests.cs` to `CreatePendingDataStoreManagesDispatcherJobTests.cs`/`DeletePendingDataStoreManagesDispatcherJobTests.cs` if present. - -For each moved file, apply every substitution from the Master Rename Table (class name matching new filename, `namespace ...V3.UnitTests.Features.DbDataStores` → `...V3.UnitTests.Features.DataStores.Manage`, route string literals `"/dbDataStores"` → `"/dataStores/manage"` if asserted, mock setups referencing `IGetDbDataStoresQuery`/`AddDbDataStoreCommand`/etc. → their renamed equivalents). - -- [ ] **Step 5: Update the four non-renamed files that reference `DbDataStore`/`DbInstance` internally** - -Open each of the following and replace every `DbDataStore`/`DbInstance`-family token per the Master Rename Table (mock setups, fixture data, assertions) without changing the filename or test class name: -- `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\CreateInstanceJobTests.cs` -- `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Jobs\DeleteInstanceJobTests.cs` -- `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\DataStores\ReadEducationOrganizationsTests.cs` -- `Application\EdFi.Ods.AdminApi.V3.UnitTests\Infrastructure\Services\Tenants\TenantServiceTests.cs` -- `Application\EdFi.Ods.AdminApi.V3.UnitTests\Features\Tenants\ReadTenantsTest.cs` - -- [ ] **Step 6: Run the v3 unit test suite** - -Run: `dotnet test Application/EdFi.Ods.AdminApi.V3.UnitTests/EdFi.Ods.AdminApi.V3.UnitTests.csproj` -Expected: PASS. Test count should be the pre-rename baseline **plus 4** (the two new `GetDataStoreManagesQueryTests` tests and two new `GetDataStoreManageByIdQueryTests` tests). - -- [ ] **Step 7: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/Manage" \ - "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DbDataStores" \ - "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/AddDataStoreManageCommandTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Commands/DeleteDataStoreManageCommandTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Queries/GetDataStoreManagesQueryTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Database/Queries/GetDataStoreManageByIdQueryTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Jobs" \ - "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/DataStores/ReadEducationOrganizationsTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.UnitTests/Features/Tenants/ReadTenantsTest.cs" -git commit -m "Rename v3 unit tests to DataStoreManage naming, add missing query test coverage" -``` - ---- - -### Task 15: v3 DBTests rename (`EdFi.Ods.AdminApi.V3.DBTests`) - -**Files:** -- Modify: `Application\EdFi.Ods.AdminApi.V3.DBTests\Database\CommandTests\AddDbDataStoreCommandTests.cs` → rename to `AddDataStoreManageCommandTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3.DBTests\Database\CommandTests\DeleteDbDataStoreCommandTests.cs` → rename to `DeleteDataStoreManageCommandTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3.DBTests\Database\QueryTests\GetDbDataStoreByIdQueryTests.cs` → rename to `GetDataStoreManageByIdQueryTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3.DBTests\Database\QueryTests\GetDbDataStoresQueryTests.cs` → rename to `GetDataStoreManagesQueryTests.cs` -- Modify: `Application\EdFi.Ods.AdminApi.V3.DBTests\Database\QueryTests\GetTenantEdOrgsByDataStoresTests.cs` (filename unchanged — update internal references only) - -**Interfaces:** -- Consumes: same v3 production types as Task 14, against a real database. - -- [ ] **Step 1: Move and rename** - -```bash -git mv "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDbDataStoreCommandTests.cs" "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDataStoreManageCommandTests.cs" -git mv "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDbDataStoreCommandTests.cs" "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDataStoreManageCommandTests.cs" -git mv "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoreByIdQueryTests.cs" "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManageByIdQueryTests.cs" -git mv "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoresQueryTests.cs" "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManagesQueryTests.cs" -``` - -Apply the Master Rename Table to each moved file's content (class name, `AdminApiDbContext.DbInstances`→`.OdsInstanceManages`, `DbInstance`/`DbInstanceStatus`→`OdsInstanceManage`/`OdsInstanceManageStatus`, command/query type names to their `DataStoreManage`-prefixed v3 equivalents). In `GetTenantEdOrgsByDataStoresTests.cs` (filename unchanged), update only the internal references to renamed types, not the filename or class name. - -- [ ] **Step 2: Run the v3 DB test suite** - -Run: `dotnet test Application/EdFi.Ods.AdminApi.V3.DBTests/EdFi.Ods.AdminApi.V3.DBTests.csproj` (requires the local test database with Task 1's migration applied). -Expected: PASS, same test count as the pre-rename baseline. - -- [ ] **Step 3: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDataStoreManageCommandTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDataStoreManageCommandTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManageByIdQueryTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDataStoreManagesQueryTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetTenantEdOrgsByDataStoresTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/AddDbDataStoreCommandTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/CommandTests/DeleteDbDataStoreCommandTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoreByIdQueryTests.cs" \ - "Application/EdFi.Ods.AdminApi.V3.DBTests/Database/QueryTests/GetDbDataStoresQueryTests.cs" -git commit -m "Rename v3 DBTests to DataStoreManage naming" -``` - ---- - -### Task 16: v3 Bruno E2E collection rename - -**Files:** -- Modify: `Application\EdFi.Ods.AdminApi.V3\E2E Tests\Bruno Admin API E2E 3.0\v3\DbDataStores\` → move all files to `...\v3\DataStores\Manage\` - -**Interfaces:** -- Consumes: routes `/v3/dataStores/manage*` (Task 11). - -- [ ] **Step 1: Move the folder and rename every `.bru` file** - -```bash -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/folder.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/folder.bru" -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage.bru" -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Sample Template.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Sample Template.bru" -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Invalid.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Invalid.bru" -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/POST - DbDataStores - Invalid Database Template.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/POST - DataStores Manage - Invalid Database Template.bru" -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage.bru" -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores by ID.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage by ID.bru" -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Offset.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Offset.bru" -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Limit.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Limit.bru" -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Without Limit and Offset.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Without Limit and Offset.bru" -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Not Found.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Not Found.bru" -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/GET - DbDataStores - Filter by Name.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/GET - DataStores Manage - Filter by Name.bru" -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/DELETE - DbDataStore - Success.bru.disabled" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/DELETE - DataStore Manage - Success.bru.disabled" -git mv "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores/DELETE - DbDataStore - Not Found.bru" "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage/DELETE - DataStore Manage - Not Found.bru" -``` - -- [ ] **Step 2: Update `folder.bru`** - -``` -meta { - name: Manage - seq: 99 -} - -auth { - mode: inherit -} -``` - -- [ ] **Step 3: Update every moved `.bru` file's content** - -Same substitution pattern as Task 9 Step 3: `meta { name: DbDataStores ... }` → `meta { name: DataStores Manage ... }`, request URL `{{API_URL}}/v3/dbDataStores...` → `{{API_URL}}/v3/dataStores/manage...`, any `bru.setVar`/`{{...}}` variable named `CreatedDbDataStoreId`-style → `CreatedDataStoreManageId`, and `test("POST DbDataStores: ...")`-style description strings → `DataStores Manage`. - -For example, `POST - DataStores Manage.bru`: - -``` -meta { - name: DataStores Manage - type: http - seq: 1 -} - -post { - url: {{API_URL}}/v3/dataStores/manage - body: json - auth: inherit -} - -body:json { - { - "name": "Test DB Instance", - "databaseTemplate": "Minimal" - } -} - -script:post-response { - test("POST DataStores Manage: Status code is Accepted", function () { - expect(res.getStatus()).to.equal(202); - }); - - test("POST DataStores Manage: Response includes location in header", function () { - expect(res.getHeaders()).to.have.property("location"); - const id = res.getHeader("location").split("/").pop(); - if (id) { - bru.setVar("CreatedDataStoreManageId", id); - } - }); -} - -settings { - encodeUrl: true -} -``` - -Apply the same URL/variable/assertion-text substitution pattern to the remaining files based on each file's current content (note v3's POST uses an absolute `Location` header via `ResourceUrlHelper.BuildAbsoluteResourceUrl`, unlike v2's relative path — preserve whatever ID-extraction logic the original `.bru` file used). - -- [ ] **Step 4: Run the v3 Bruno E2E suite** - -Run: `./eng/run-e2e-bruno.ps1 -ApiVersion 3 -TenantMode singletenant -TearDown` (adjust `-TenantMode` per `docs/developer.md`). -Expected: PASS, all `DataStores Manage` requests succeed with the same assertions as before the rename. - -- [ ] **Step 5: Commit** - -```bash -git add "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DataStores/Manage" \ - "Application/EdFi.Ods.AdminApi.V3/E2E Tests/Bruno Admin API E2E 3.0/v3/DbDataStores" -git commit -m "Rename v3 Bruno E2E DbDataStores collection to DataStores/Manage" -``` - ---- - -### Task 17: Rename and update `docs/http/dbinstances.http` - -**Files:** -- Modify: `docs\http\dbinstances.http` → rename to `docs\http\odsinstances-manage.http` - -**Interfaces:** -- Consumes: routes `/v2/odsInstances/manage*` (Task 4) and `/v3/dataStores/manage*` (Task 11). - -- [ ] **Step 1: Read the file's current (uncommitted-edits-included) content first** - -Read `docs\http\dbinstances.http` fresh (don't rely on the diff seen earlier in this conversation — more local edits may have landed since) to get the exact current content, since this file has uncommitted personal edits that must be preserved. - -- [ ] **Step 2: Rename the file** - -```bash -git mv "docs/http/dbinstances.http" "docs/http/odsinstances-manage.http" -``` - -- [ ] **Step 3: Update every request URL in the file** - -Replace every occurrence of: -- `{{adminapi_url}}/v2/dbinstances` → `{{adminapi_url}}/v2/odsinstances/manage` -- `{{adminapi_url}}/v3/dbDataStores` → `{{adminapi_url}}/v3/dataStores/manage` - -preserving any path suffix (`/1`, `?offset=0&limit=10`, etc.), any `@name` block labels that reference `createDbInstance...` (rename to `createOdsInstanceManage...` for v2 blocks, `createDataStoreManage...` for v3 blocks), and every other line in the file exactly as currently written (commented-out `@adminapi_url` lines, tenant headers, existing `/v2/odsinstances` and `/v3/dataStores` blocks that were already correct, etc.). - -- [ ] **Step 4: Manually smoke-test a couple of requests** - -Using the VS Code REST Client (or whichever `.http` runner this repo's contributors use per `docs/developer.md`), run the renamed `POST`/`GET`/`DELETE` blocks against a locally running v2 and v3 instance and confirm they hit `/odsInstances/manage`/`/dataStores/manage` successfully. - -- [ ] **Step 5: Commit** - -```bash -git add "docs/http/odsinstances-manage.http" "docs/http/dbinstances.http" -git commit -m "Rename docs/http/dbinstances.http to odsinstances-manage.http, update routes" -``` - ---- - -### Task 18: Full solution verification - -**Files:** none (verification only). - -- [ ] **Step 1: Full clean build** - -Run: `dotnet build` at the repository root (or `./build.ps1 build` per this repo's build script convention). -Expected: zero errors, zero warnings introduced by this change. - -- [ ] **Step 2: Full unit test suite** - -Run: `./build.ps1 -Command UnitTest` -Expected: PASS. Total test count = pre-rename baseline + 4 (Task 14's new v3 query tests). - -- [ ] **Step 3: Full integration (DBTests) suite** - -Run the DBTests projects per `docs/developer.md`'s integration-test instructions (apply Task 1's migration to the test database first if not already applied). -Expected: PASS, same test count as pre-rename baseline. - -- [ ] **Step 4: Full-repo grep for any remaining stray references** - -Run: `grep -rn "DbInstance\|DbDataStore" --include=*.cs --include=*.sql --include=*.json --include=*.bru --include=*.http .` from the repository root. -Expected: zero matches, except: -- Historical migration scripts `00001`–`00006` (which correctly still say `DbInstances` — they describe the table's state at that point in history and must not be edited). -- This plan document and the design spec document themselves (`docs/superpowers/plans/...`, `docs/superpowers/specs/...`), which describe the rename and legitimately mention the old names. - -- [ ] **Step 5: Run both v2 and v3 Bruno E2E suites end-to-end** - -Run: `./eng/run-e2e-bruno.ps1 -ApiVersion 2 -TenantMode singletenant -TearDown` and `./eng/run-e2e-bruno.ps1 -ApiVersion 3 -TenantMode singletenant -TearDown` (and the multitenant variants if this repo's CI runs those too — check `docs/developer.md`). -Expected: PASS. - -- [ ] **Step 6: Final review pass** - -Re-read the Master Rename Table at the top of this plan against the grep output from Step 4 to confirm every listed identifier was actually renamed everywhere it appeared — no partial renames left (e.g. a class renamed but a lingering XML doc comment or log message still saying the old name). - -No commit for this task — it's a verification checkpoint. If Step 4 or 6 turns up a stray reference, fix it in place and amend the most relevant task's commit (or add a small follow-up commit), then re-run the affected verification step. - - - - diff --git a/docs/superpowers/specs/2026-07-27-rename-dbinstances-to-odsinstancemanage-design.md b/docs/superpowers/specs/2026-07-27-rename-dbinstances-to-odsinstancemanage-design.md deleted file mode 100644 index 4f54684d1..000000000 --- a/docs/superpowers/specs/2026-07-27-rename-dbinstances-to-odsinstancemanage-design.md +++ /dev/null @@ -1,236 +0,0 @@ -# Rename DbInstances (v2) / DbDataStores (v3) to OdsInstances/Manage and DataStores/Manage - -Date: 2026-07-27 - -## Summary - -`v2`'s `DbInstances` feature and `v3`'s `DbDataStores` feature are two separate presentation -layers over the exact same physical table (`adminapi.DbInstances`) and the exact same EF Core -entity class (`DbInstance`, defined once in the shared `EdFi.Ods.AdminApi.Common` project and -consumed by both version-specific `AdminApiDbContext` classes). - -This change: - -- Renames the shared entity/table to `OdsInstanceManage` / `adminapi.OdsInstanceManages`. -- Moves and renames the v2 feature folder `Features\DbInstances` into a new - `Features\OdsInstances\Manage` subfolder, changing routes from `/v2/dbInstances` to - `/v2/odsInstances/manage`. -- Moves and renames the v3 feature folder `Features\DbDataStores` into a new - `Features\DataStores\Manage` subfolder, changing routes from `/v3/dbDataStores` to - `/v3/dataStores/manage`. -- Renames all identifiers containing `DbInstance`/`DbDataStore` across production code, tests, - Bruno E2E collections, and `.http` scratch files to match. -- Is a **breaking change**: old routes and old `AppSettings` config keys are removed outright, - with no deprecated aliases or backward-compatibility shims. - -## Background / current state - -- v2's `OdsInstances` feature already depends on `DbInstances` data: it merges `DbInstance` - status/database-name into its "OdsInstance + EdOrgs" response - (`OdsInstanceWithEducationOrganizationsModel.DbInstanceId`, - `ReadEducationOrganizations.MergeDbInstanceData`). -- v3's `DataStores` feature does the analogous merge - (`ReadEducationOrganizations.MergeDbDataStoreData`), but its - `DataStoreWithEducationOrganizationsModel` is missing a field equivalent to v2's - `DbInstanceId` — a pre-existing parity gap this change closes. -- The migration artifact tree is duplicated: `Application\EdFi.Ods.AdminApi\Artifacts\` (MsSql + - PgSql) is the one actually consumed — the `.csproj` includes - ``, and - `eng\run-dbup-migrations.ps1` → `Install-AdminApiTables` runs against that published output. - `Application\EdFi.Ods.AdminApi.V3\Artifacts\` is a documentation-only duplicate (the V3 project - has `IsPublishable=false` and no matching `Content` include) that has historically been kept in - sync by hand at each prior migration step. This change keeps that convention: the new migration - is added to both locations, but only the v2 copy has any functional effect. -- There is no FK constraint between this table and `OdsInstances` — `OdsInstanceId` is a plain - nullable, unenforced column populated once async provisioning completes. - -## Naming scheme - -| Concept | Old (v2) | Old (v3) | New (shared, Common project) | -|---|---|---|---| -| Entity class | `DbInstance` | `DbInstance` | `OdsInstanceManage` | -| Table | `adminapi.DbInstances` | `adminapi.DbInstances` | `adminapi.OdsInstanceManages` | -| DbSet property | `DbInstances` | `DbInstances` | `OdsInstanceManages` | -| Status enum | `DbInstanceStatus` | `DbInstanceStatus` | `OdsInstanceManageStatus` | - -Per-version feature-layer naming stays distinct, matching the existing convention where v3 -already renames DTO-level fields (`OdsInstanceId`/`OdsInstanceName` → `DataStoreId`/ -`DataStoreName`) while sharing the underlying entity: - -| Concept | v2 (new) | v3 (new) | -|---|---|---| -| Feature folder | `Features\OdsInstances\Manage\` | `Features\DataStores\Manage\` | -| Route prefix | `/v2/odsInstances/manage` | `/v3/dataStores/manage` | -| FK-style `Id` field (in the OdsInstance/DataStore + EdOrgs response models) | `OdsInstanceManageId` | `DataStoreManageId` | -| Query interfaces | `IGetOdsInstanceManagesQuery`, `IGetOdsInstanceManageByIdQuery` | `IGetDataStoreManagesQuery`, `IGetDataStoreManageByIdQuery` | -| Command classes | `AddOdsInstanceManageCommand`, `DeleteOdsInstanceManageCommand` | `AddDataStoreManageCommand`, `DeleteDataStoreManageCommand` | -| Dispatcher jobs | `CreatePendingOdsInstanceManagesDispatcherJob`, `DeletePendingOdsInstanceManagesDispatcherJob` | `CreatePendingDataStoreManagesDispatcherJob`, `DeletePendingDataStoreManagesDispatcherJob` | - -Shared `JobConstants`/`AppSettings` keys (Common project) follow the entity name: - -- `JobConstants.DbInstanceIdKey` → `OdsInstanceManageIdKey` -- `JobConstants.CreatePendingDbInstancesDispatcherJobName` / - `DeletePendingDbInstancesDispatcherJobName` → - `CreatePendingOdsInstanceManagesDispatcherJobName` / `DeletePendingOdsInstanceManagesDispatcherJobName` -- `AppSettings.CreateDbInstancesSweepIntervalInMins`, `CreateDbInstancesMaxRetryAttempts`, - `DeleteDbInstancesSweepIntervalInMins`, `DeleteDbInstancesMaxRetryAttempts` → - `CreateOdsInstanceManagesSweepIntervalInMins`, `CreateOdsInstanceManagesMaxRetryAttempts`, - `DeleteOdsInstanceManagesSweepIntervalInMins`, `DeleteOdsInstanceManagesMaxRetryAttempts` - (breaking config-key rename — call out in release notes so deployers update - `appsettings.json`/environment overrides). - -### Explicitly out of scope (left unchanged) - -- `CreateInstanceJob` / `DeleteInstanceJob` (both v2's and v3's own copies) — generic names that - don't contain "DbInstance"/"DbDataStore". -- The table's own `OdsInstanceId`/`OdsInstanceName` columns (link a row to an actual ODS - instance) — unrelated to the FK-style `Id` field being renamed. -- No backward-compatible routes, no deprecated aliases, no config-key fallback shims. -- `NuGet.config` files (per repository convention — not touched unless explicitly requested). - -## Migration - -New artifact `00007-RenameDbInstancesToOdsInstanceManages.sql`, added in all four locations -(MsSql/PgSql × v2/v3 Artifacts trees), using an **in-place rename** to preserve existing data and -identity/serial sequences: - -- MSSQL: `sp_rename 'adminapi.DbInstances', 'OdsInstanceManages'`, plus `sp_rename` for the two - indexes (`IX_DbInstances_Name` → `IX_OdsInstanceManages_Name`, - `IX_DbInstances_OdsInstanceId` → `IX_OdsInstanceManages_OdsInstanceId`). -- PostgreSQL: `ALTER TABLE adminapi."DbInstances" RENAME TO "OdsInstanceManages"`, plus - `ALTER INDEX ... RENAME TO ...` for the same two indexes. -- Idempotent, following the existing style of prior scripts in this tree (guard with existence - checks so re-running the migration is a no-op). - -EF Core mapping updates in both `AdminApiDbContext` classes (v2 and v3): -`modelBuilder.Entity().ToTable("OdsInstanceManages")...`, `DbSet OdsInstanceManages`. - -## v2 changes - -**Folder move**: `Features\DbInstances\` → `Features\OdsInstances\Manage\` (new subfolder inside -the existing `OdsInstances` feature). Old `Features\DbInstances\` folder removed entirely. - -**File renames** (namespace `EdFi.Ods.AdminApi.Features.OdsInstances.Manage`): - -| Old | New | -|---|---| -| `AddDbInstance.cs` | `AddOdsInstanceManage.cs` | -| `ReadDbInstance.cs` | `ReadOdsInstanceManage.cs` | -| `DeleteDbInstance.cs` | `DeleteOdsInstanceManage.cs` | -| `DbInstanceModel.cs` | `OdsInstanceManageModel.cs` | -| `DbInstanceMapper.cs` | `OdsInstanceManageMapper.cs` | -| `DbInstanceDatabaseNameFormatter.cs` | `OdsInstanceManageDatabaseNameFormatter.cs` | - -**Routes**: `POST/GET/DELETE /dbInstances...` → `POST/GET/DELETE /odsInstances/manage...` -(served as `/v2/odsInstances/manage`, still `BuildForVersions(AdminApiVersions.V2)`). - -**Infrastructure layer** (stays in its existing `Infrastructure\Database\Queries` / -`Commands` / `Services\Jobs` locations — renamed in place, not moved, consistent with the -existing Features/Infrastructure architectural split): - -- `IGetDbInstancesQuery`/`GetDbInstancesQuery` → `IGetOdsInstanceManagesQuery`/`GetOdsInstanceManagesQuery` -- `IGetDbInstanceByIdQuery`/`GetDbInstanceByIdQuery` → `IGetOdsInstanceManageByIdQuery`/`GetOdsInstanceManageByIdQuery` -- `AddDbInstanceCommand`/`IAddDbInstanceModel` → `AddOdsInstanceManageCommand`/`IAddOdsInstanceManageModel` -- `IDeleteDbInstanceCommand`/`DeleteDbInstanceCommand` → `IDeleteOdsInstanceManageCommand`/`DeleteOdsInstanceManageCommand` -- `CreatePendingDbInstancesDispatcherJob`/`DeletePendingDbInstancesDispatcherJob` → - `CreatePendingOdsInstanceManagesDispatcherJob`/`DeletePendingOdsInstanceManagesDispatcherJob` - -**Existing v2 `OdsInstances` files touched (not moved)**: - -- `OdsInstanceWithEducationOrganizationsModel.cs`: `DbInstanceId` property → `OdsInstanceManageId`. -- `ReadEducationOrganizations.cs`: injects `IGetOdsInstanceManagesQuery` instead of - `IGetDbInstancesQuery`; `MergeDbInstanceData` → `MergeOdsInstanceManageData`; uses - `OdsInstanceManageStatus`. - -## v3 changes - -**Folder move**: `Features\DbDataStores\` → `Features\DataStores\Manage\`. Old -`Features\DbDataStores\` folder removed entirely. - -**File renames** (namespace `EdFi.Ods.AdminApi.V3.Features.DataStores.Manage`): - -| Old | New | -|---|---| -| `AddDbDataStore.cs` | `AddDataStoreManage.cs` | -| `ReadDbDataStore.cs` | `ReadDataStoreManage.cs` | -| `DeleteDbDataStore.cs` | `DeleteDataStoreManage.cs` | -| `DbDataStoreModel.cs` | `DataStoreManageModel.cs` | -| `DbDataStoreMapper.cs` | `DataStoreManageMapper.cs` | -| `DbDataStoreDatabaseNameFormatter.cs` | `DataStoreManageDatabaseNameFormatter.cs` | - -`DbDataStoreModel`'s existing `DataStoreId`/`DataStoreName` properties carry over unchanged into -`DataStoreManageModel` — only the `DbDataStore`-named parts change. - -**Routes**: `POST/GET/DELETE /dbDataStores...` → `POST/GET/DELETE /dataStores/manage...` -(served as `/v3/dataStores/manage`, still `BuildForVersions(AdminApiVersions.V3)`). - -**Infrastructure layer** (renamed in place, stays under v3's own -`Infrastructure\Database\Queries`/`Commands`/`Services\Jobs`): - -- `IGetDbDataStoresQuery`/`GetDbDataStoresQuery` → `IGetDataStoreManagesQuery`/`GetDataStoreManagesQuery` -- `IGetDbDataStoreByIdQuery`/`GetDbDataStoreByIdQuery` → `IGetDataStoreManageByIdQuery`/`GetDataStoreManageByIdQuery` -- `AddDbDataStoreCommand`/`IAddDbDataStoreModel` → `AddDataStoreManageCommand`/`IAddDataStoreManageModel` -- `IDeleteDbDataStoreCommand`/`DeleteDbDataStoreCommand` → `IDeleteDataStoreManageCommand`/`DeleteDataStoreManageCommand` -- `CreatePendingDbInstancesDispatcherJob`/`DeletePendingDbInstancesDispatcherJob` (v3's own - copies) → `CreatePendingDataStoreManagesDispatcherJob`/`DeletePendingDataStoreManagesDispatcherJob` - -**Existing v3 `DataStores` files touched (not moved)**: - -- `ReadEducationOrganizations.cs`: injects `IGetDataStoreManagesQuery` instead of - `IGetDbDataStoresQuery`; `MergeDbDataStoreData` → `MergeDataStoreManageData`; uses - `OdsInstanceManageStatus` (shared enum). -- `DataStoreWithEducationOrganizationsModel.cs`: add a new `DataStoreManageId` field, closing - the parity gap with v2's `OdsInstanceManageId`. - -## Tests - -**Unit tests** — renamed/moved in lockstep with production files, same mapping as above: - -- v2: `Features\DbInstances\AddDbInstanceTests.cs` → `Features\OdsInstances\Manage\AddOdsInstanceManageTests.cs` - (same pattern for `ReadDbInstanceTests.cs`/`DeleteDbInstanceTests.cs`), plus - `Infrastructure\Database\Commands\AddDbInstanceCommandTests.cs` → - `AddOdsInstanceManageCommandTests.cs` (and `Delete...`), `Infrastructure\Database\Queries\GetDbInstanceByIdQueryTests.cs`/ - `GetDbInstancesQueryTests.cs` → `GetOdsInstanceManageByIdQueryTests.cs`/`GetOdsInstanceManagesQueryTests.cs`, - `Infrastructure\Services\Jobs\CreatePendingDbInstancesDispatcherJobTests.cs`/ - `DeletePendingDbInstancesDispatcherJobTests.cs` → `CreatePendingOdsInstanceManagesDispatcherJobTests.cs`/ - `DeletePendingOdsInstanceManagesDispatcherJobTests.cs`. -- v3: same pattern with `DataStoreManage` naming, e.g. `Features\DbDataStores\AddDbDataStoreTests.cs` → - `Features\DataStores\Manage\AddDataStoreManageTests.cs`. -- **New v3 unit tests added** (closing a pre-existing coverage gap — v2 has query-level unit - tests that v3 lacked): `GetDataStoreManagesQueryTests.cs` and - `GetDataStoreManageByIdQueryTests.cs` in `EdFi.Ods.AdminApi.V3.UnitTests`, mirroring v2's - existing `GetOdsInstanceManagesQueryTests.cs`/`GetOdsInstanceManageByIdQueryTests.cs`. - -**DBTests** (this repo's integration-test project) — same rename pattern: -`Database\CommandTests\AddDbInstanceCommandTests.cs` → `AddOdsInstanceManageCommandTests.cs`, -`Database\QueryTests\GetDbInstancesQueryTests.cs` → `GetOdsInstanceManagesQueryTests.cs`, and v3 -equivalents (`DataStoreManage`-named). `GetTenantEdOrgsByInstancesTests.cs`/ -`GetTenantEdOrgsByDataStoresTests.cs` keep their file names (they don't contain the renamed term) -but have internal references updated. - -**Bruno E2E collections**: - -- v2: `E2E Tests\V2\...\v2\DbInstances\` → moved under `v2\OdsInstances\Manage\`; each `.bru` - file renamed (e.g. `POST - DbInstances.bru` → `POST - OdsInstances Manage.bru`) with request - URLs updated to `/v2/odsInstances/manage...`. -- v3: `E2E Tests\Bruno Admin API E2E 3.0\v3\DbDataStores\` → moved under - `v3\DataStores\Manage\`; `.bru` files renamed similarly, URLs updated to - `/v3/dataStores/manage...`. - -**`docs/http/dbinstances.http`**: renamed to `docs/http/odsinstances-manage.http`. All -`/v2/dbinstances` → `/v2/odsinstances/manage` and `/v3/dbDataStores` → `/v3/dataStores/manage`. -This file currently has uncommitted local edits (manual testing notes) — the rename is applied -on top of those edits rather than discarding them. - -## Risk / compatibility notes - -- **Breaking change for API consumers**: old routes (`/v2/dbInstances/*`, `/v3/dbDataStores/*`) - are removed with no aliasing. Any external client or automation hitting those paths must be - updated to the new `/v2/odsInstances/manage/*` / `/v3/dataStores/manage/*` paths. -- **Breaking change for deployers**: `AppSettings` config keys change names (see Naming scheme - section) — existing `appsettings.json`/environment-variable overrides referencing the old key - names will silently stop applying (fall back to code defaults) unless updated. Call this out - in release notes. -- **Data-preserving migration**: the table rename is in-place (`sp_rename`/`ALTER TABLE...RENAME`), - so existing `DbInstances` rows in deployed databases carry over as `OdsInstanceManages` rows - with no data loss and no identity/serial reseed. From 2afeb3db9ead7eb44b33f31678b54a314ac8296d Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Tue, 28 Jul 2026 15:16:39 -0600 Subject: [PATCH 33/35] Changes on CLAUDE.md --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index dd80e3cb5..f2c8920bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,6 +29,7 @@ Concise coding conventions, nullability rules, and testing basics. * Nullability: declare variables non-nullable where possible; validate at entry points; use `is null` / `is not null`. * Testing: NUnit + Shouldly for assertions; use FakeItEasy for mocks; mirror existing test naming/style. * Run tests locally: `./build.ps1 -Command UnitTest` (see `docs/developer.md` for integration/E2E instructions). +* Bruno E2E: extract IDs from a `Location` header with `.split("/").pop()`, never a hardcoded index (`split("/")[2]`) — a route gaining/losing a path segment silently breaks index-based parsing, and failures then surface as confusing downstream assertion errors rather than at the source. ### Run & Architecture @@ -36,7 +37,7 @@ Short run/build/architecture notes — see `docs/developer.md` for full procedur * Build helper: `./build.ps1` (common commands: `build`, `UnitTest`, `IntegrationTest`, `run`). * Local run options: `build.ps1 run`, Docker compose, or Visual Studio launch profiles. -* DB migrations: scripts and artifacts under `Application/EdFi.Ods.AdminApi/Artifacts/` and `eng/run-dbup-migrations.ps1`. +* DB migrations: scripts and artifacts under `Application/EdFi.Ods.AdminApi/Artifacts/` and `eng/run-dbup-migrations.ps1`. Only this (v2) copy is actually applied by the Docker migration-runner scripts — the `EdFi.Ods.AdminApi.V3/Artifacts/` copy is a docs-only duplicate kept in sync by convention; add new migrations to both, but know that only the v2 copy has any functional effect. * Run bruno e2e tests for a particular specification (v1, v2, v3) locally, e.g: `./eng/run-e2e-bruno.ps1 -ApiVersion 2 -TenantMode multitenant -TearDown`. * Architecture: feature-based layout; `IUsersContext` handles `EdFi_Admin`, `ISecurityContext` handles `EdFi_Security` (EF Core); AutoMapper mappings in `AdminApiMappingProfile.cs`. From 4328caab779721b871098c1fd2a02b736de7d183 Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Wed, 29 Jul 2026 07:54:50 -0600 Subject: [PATCH 34/35] [ADMINAPI-1477] Fix stale DbInstance references from code review (comments 1 & 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/developer.md's "DbInstance Provisioning Jobs" section still documented the pre-rename route, table name, and config keys, plus a dead link to a design doc deleted back in 73c77cf0 (predates this rename) — repointed to design/INSTANCE-MANAGEMENT-Quartz.md, which is the actual current home for that content (job identities, retry strategy, Mermaid diagrams). TenantServiceTests.cs:284 had a leftover "DbInstance2" fixture display name (never asserted by value) — renamed to match the OdsInstanceManage naming and the sibling "Instance2" fixture pattern. Co-Authored-By: Claude Sonnet 5 --- .../Services/Tenants/TenantServiceTests.cs | 2 +- docs/developer.md | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs index ddea39813..d5b48d7ff 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs @@ -281,7 +281,7 @@ public async Task GetTenantEdOrgsByInstancesAsync_EnrichesOdsInstance_WithLinked var odsInstanceManage = new OdsInstanceManage { Id = 10, - Name = "DbInstance2", + Name = "OdsInstanceManage2", OdsInstanceId = 2, Status = OdsInstanceManageStatus.CreateInProgress.ToString(), DatabaseTemplate = "Minimal", diff --git a/docs/developer.md b/docs/developer.md index 140d58361..ca84417a6 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -15,7 +15,7 @@ * [Application Architecture](#application-architecture) * [Database Layer](#database-layer) * [Validation](#validation) - * [DbInstance Provisioning Jobs](#dbinstance-provisioning-jobs) + * [OdsInstanceManage Provisioning Jobs](#odsinstancemanage-provisioning-jobs) ## Development Pre-Requisites @@ -238,19 +238,19 @@ credentials. Validation of API requests is configured via [FluentValidation](https://docs.fluentvalidation.net/en/latest/). -### DbInstance Provisioning Jobs +### OdsInstanceManage Provisioning Jobs -The `POST /v2/dbinstances` flow is asynchronous. The endpoint persists a `Pending` `DbInstance`, schedules `CreateInstanceJob`, and returns `202 Accepted` immediately. A separate recurring `CreatePendingDbInstancesDispatcherJob` handles sweep-based recovery and capped retries for records that remain in `Pending` or move to `Error`. +The `POST /v2/odsInstances/manage` flow is asynchronous. The endpoint persists a `Pending` `OdsInstanceManage`, schedules `CreateInstanceJob`, and returns `202 Accepted` immediately. A separate recurring `CreatePendingOdsInstanceManagesDispatcherJob` handles sweep-based recovery and capped retries for records that remain in `Pending` or move to `Error`. -Use [design/DBINSTANCE-PROVISIONING-JOBS.md](design/DBINSTANCE-PROVISIONING-JOBS.md) as the durable design reference for job identities, retry strategy, reconciliation behavior, and Mermaid diagrams of the API and background-job flows. +Use [design/INSTANCE-MANAGEMENT-Quartz.md](design/INSTANCE-MANAGEMENT-Quartz.md) as the durable design reference for job identities, retry strategy, reconciliation behavior, and Mermaid diagrams of the API and background-job flows. Feature-specific prerequisites and configuration: * `AppSettings:adminApiMode` must be `v2` so startup scheduling registers the recurring dispatcher. -* Admin API DB migrations must be applied because the flow relies on `adminapi.DbInstances` and `adminapi.JobStatuses`. +* Admin API DB migrations must be applied because the flow relies on `adminapi.OdsInstanceManages` and `adminapi.JobStatuses`. * `AppSettings:EncryptionKey` must be a valid base64-encoded key. * `ConnectionStrings:EdFi_Ods` supplies the connection-string shape used to generate encrypted `OdsInstance.ConnectionString` values. * For PostgreSQL, `ConnectionStrings:EdFi_Master` should point at the maintenance database `postgres`, not an ODS database. -* `AppSettings:CreateDbInstancesSweepIntervalInMins` controls dispatcher cadence. -* `AppSettings:CreateDbInstancesMaxRetryAttempts` controls retry capping. +* `AppSettings:CreateOdsInstanceManagesSweepIntervalInMins` controls dispatcher cadence. +* `AppSettings:CreateOdsInstanceManagesMaxRetryAttempts` controls retry capping. * When `AppSettings:MultiTenancy` is enabled, the active tenant must have tenant-specific connection strings available before the worker runs. From 99805861bff9e905634eac2c8b30256d8dcbc58f Mon Sep 17 00:00:00 2001 From: Jorge David Jimenez Barrantes Date: Wed, 29 Jul 2026 08:05:23 -0600 Subject: [PATCH 35/35] deletes docs/design/adminconsole/SQL-SERVER-SUPPORT.md --- .../design/adminconsole/SQL-SERVER-SUPPORT.md | 231 ------------------ 1 file changed, 231 deletions(-) delete mode 100644 docs/design/adminconsole/SQL-SERVER-SUPPORT.md diff --git a/docs/design/adminconsole/SQL-SERVER-SUPPORT.md b/docs/design/adminconsole/SQL-SERVER-SUPPORT.md deleted file mode 100644 index 7b377c220..000000000 --- a/docs/design/adminconsole/SQL-SERVER-SUPPORT.md +++ /dev/null @@ -1,231 +0,0 @@ -# Adding MS SQL Support to Instance Management - -## Summary - -The Instance Management worker allows system administrators to manage instances of their ODS. This worker is currently configured to support PostgreSQL and needs support added for SQL Server. - -## Background - -The Instance Management worker was designed to execute ODS administration tasks without tying up a handler's main process. This asynchronous nature requires the Instance management worker to manage other aspects of its environment, including persistence and compatibility with other services. The data layer for the Instance Management worker was implemented using PostgreSQL, with the idea of using this dialect of SQL to start, verifying its implementation, then including support for SQL Server after. - -The Postgres implementation of the Instance Manager includes support for Creating, Renaming, Deleting, and Restoring ODS Instances. - -There is a Postgres DB dump of sample student data that serves as a starting point for minimal and populated templates. No such sample exists for SQL Server so this will need to be solved in order to remain in parity with the Postgres provisioning features. Additionally, SQL Server utilizes a slightly different dialect from server so these actions must be converted. Lastly, SQL Server licensing requires that images / containers including the Azure SQL Server base image not be distributed. This does not prohibit providing instructions on how to build these images. - -Azure SQL Server support is an additional consideration but will remain out of the scope of this implementation. - -## Design Overview - -Adding support for SQL Server has been divided into the following four distinct areas of focus below. - -### 1. Configuring and connecting to the database - -The first step involves adding a connection and corresponding configuration to the application. This step is to ensure the application is communicating with the desired SQL Server instance (Platform hosted, docker hosted). There is a `CreateConnection()` method in the [`SqlServerSandboxProvisioner.cs`](https://github.com/Ed-Fi-Alliance-OSS/Ed-Fi-ODS/blob/main/Application/EdFi.Ods.Sandbox/Provisioners/SqlServerSandboxProvisioner.cs) example from ODS Sandbox, which demonstrates how to connect properly. - -### 2. Translating the actions to MS SQL dialect for each of the admin functions - -The current Postgres implementation of the Instance Management Worker borrows from the provisioner patterns seen in the `EdFi.Ods.Sandbox` project. - -The `PostgresSandboxProvisioner.cs` class contains methods for creating connections, renaming, deleting, managing file paths, retrieving DB Status among others. These actions can be used to inform the implementation details for the Instance Management worker and the corresponding SQL Server actions. These actions will need to be translated to MS SQL and added to a new`SqlServerInstanceProvisioner.cs` file located in the `Provisioners/` directory. - -Note, for restoring the database, the SqlServerSandboxProvisioner.cs from the API Sandbox uses a `CopySandboxAsync` command which sets up internal functions to backup a target and restore to a new destination. An internal function in particular, `GetBackupDirectoryAsync` uses the windows registry to locate the backup directory of the server. This will need to change, as SQL Server can run on platforms outside of Windows. The design details will configures a backup location the host and the application can read. - -Other supporting configuration files, e.g. `Provisioners/DatabaseEngineProvider.cs` will also need to be updated to reflect added support for SQL Server, resulting in a seamless experience between selecting the PostgreSQL engine and SQL Server. - -### 3. Low-friction SQL Server connection setup - -The next task is providing a low-friction environment for users to spin up and connect to the desired SQL Server instance. Historically, Ed-Fi has provided guidance to users on how to provision SQL Server configurations for various environments. This is done to avoid hosting images containing the distribution itself, which would create conflict with the Apache 2.0 license that accompanies the Ed-Fi Alliance source code and tools. - -SQL Server Options that Ed-Fi provides guidance: - -1. Installation included with official binaries -2. Experimental, bare MSSQL install scripts -3. Docker compose with local sample data (Users SQL Server Express Edition) - -A quick reference for setting up SQL Server runtime can be found at the following [tech docs guide](https://docs.ed-fi.org/reference/docker/#step-2-setup-runtime-environment). Because the Instance Management worker already takes advantage of Docker compose, option three appears to provide the most benefit for little effort. This will still allow users to get up and running quickly without spending much time provisioning a host machine. - -### 4. Seeding data for restoration - -Lastly, the Instance Management worker should be able to restore an ODS instance. This by extension means that the worker must support exporting the data, creating the instance, and importing the data to a created instance. - -The SQL Server database will need to be populated in order to provide the necessary data for export. Two viable options to consider for migrating the data include: - -1. Creating the Sample data and tables directly using synthesized data. -2. Transforming Sample data from PostgreSQL backups - -It appears that the MSSQL Sandbox compose files include a populated template that connects to a predefined volume. An approach can be to connect to this instance then export the to a BACPAC file, which can be used for restoration. - -Once this is done successfully, the next step is to implement the restoration of this BACPAC template data. An action can be added to the interface that runs the necessary steps within the transaction that results in creating an instance and subsequently reading the BACPAC. - -## Design Details - -The following adds additional implementation details on the design overview provided above. - -### 1. Configuring and connecting to the Database - -This step ensure that a Database connection can be established using the provided connection string settings. - -There is an `InstanceProvisionerBase.cs` class that is responsible for retrieving the connection strings and configuration settings when the provisioner is instantiated. This class also set up abstractions for creating a DbConnection, which can be overwritten for use in the SQL Engine specific Provisioner. The following is used to create a PostgreSQL DB connection: - -```csharp -protected override DbConnection CreateConnection() => new NpgsqlConnection(ConnectionString); -``` - -The `CreateConnection()` method is then used throughout the Provisioner to create a connection object for executing queries against. - -```csharp -using (var conn = CreateConnection()) -//...remaining implementation with conn. -``` - -The _databaseNameBuilder.SandboxNameForKey(clientKey) will likely need to be modified with multi-tenancy under consideration. - -### 2. Translate actions to MS SQL Dialect - -The next lift is to translate each of the DB Provisioning methods to the appropriate MS SQL Dialect - -#### Create - -The PostgreSQL provisioner implementation of `CopyDbInstanceAsync` container includes a `CREATE` statement that uses a `TEMPLATE` database. The SQL Server implementation does not support Db templates, so this will need to user a bak file to configure the populated template. This bak file will be restored when a Create DB instance command is issued. - -The Admin API Sandbox has SqlServerSandboxProvisioner.cs which implements a method `CopySandboxAsync`. This method illustrates the restoration method used with SQL Server and the backup file. This excerpt summarizes the process by calling internal functions near the top, making it easier to follow along. - -```csharp - using (var conn = CreateConnection()) - { - try - { - await conn.OpenAsync(); - - // This points to where the template should be saved - string backupDirectory = await GetBackupDirectoryAsync() - .ConfigureAwait(false); - - _logger.Debug($"backup directory = {backupDirectory}"); - - string backup = await PathCombine(backupDirectory, originalDatabaseName + ".bak"); - _logger.Debug($"backup file = {backup}"); - - var sqlFileInfo = await GetDatabaseFilesAsync(originalDatabaseName, newDatabaseName) - .ConfigureAwait(false); - - await BackUpAndRestoreSandbox() - .ConfigureAwait(false); - //.......... - } - // ........ - } -``` - -Note that the `GetBackupDirectoryAsync` will need to use a value other than the `HKEY_LOCAL_MACHINE` for retrieving the backup directory location. This could be replaced with an AppSettings configuration and will need to be configurable when using the RESTORE functionality. - -#### Delete - -Deleting instances from the Management Worker context requires removing the Database and tables associated with the client key. For reference, the Admin API Sandbox does the following: - -```csharp - using (var conn = CreateConnection()) - { - foreach (string key in deletedClientKeys) - { - await conn.ExecuteAsync($@" - IF EXISTS (SELECT name from sys.databases WHERE (name = '{_databaseNameBuilder.SandboxNameForKey(key)}')) - BEGIN - ALTER DATABASE [{_databaseNameBuilder.SandboxNameForKey(key)}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; - DROP DATABASE [{_databaseNameBuilder.SandboxNameForKey(key)}]; - END; - ", commandTimeout: CommandTimeout) - .ConfigureAwait(false); - } - } -``` - -The above is used to iterate through DB associated with the client key to perform the drop. - -#### Rename - -Renaming an instance is a two part process. The first is renaming the instances and tables themselves. This can be done with an ALTER command similar to this: - -```csharp -await conn.ExecuteAsync($"ALTER DATABASE {oldName} MODIFY Name = {newName};") - .ConfigureAwait(false); -``` - -The next step is updating data to reference this new instance. The client credentials and secrets table will need to be updated - care must be exercised to ensure renaming a table doest not impact the effectiveness of these credentials. - -Lastly, the system will need a fallback for handling potential reads while renaming the system. One approach is to spin up the new instance while the old is still running, then switch the configuration once the new instance has been set up. This will help mitigate downtime, but coordinating this will add complexity and potentially require more resources. - -Another approach is to simple take down the existing instance, rename and wait until the system is available again. This requires less complexity to manage but does increase downtime. - -Deciding on a best approach will depending on what level of unavailability is acceptable. - -#### Restore - -The Restoration will behave similar to the create process. The main difference is the restore will target a custom .bak file provided by a user, while the create will use a predefined .bak that will scaffold necessary tables and data. - -The configuration used to set the backup path in AppSettings can be used to locate the .bak file the user would like restored. The restoration then becomes similar to the create, but with a targeted file to restore. - -### 3. Low-friction SQL Server Setup - -We will explore using the Docker Compose environment for configuring and setting up the SQL Server and connecting with the Admin API. This will require us to configure the SQL Server container and network settings in a way that works with the Admin API but also portable for easy for developers. - -#### Build SQL Server compatible image - -Ensure we can configure and run an SQL Server image with the right requirements. - -#### Start up compose network - -Reference the build file to create an image and configure compose network. This allows compose to con3figure the runtime settings of the SQL Server container. - -#### Ensure running and connect directly via Docker host - -Once the container is running using compose, confirm the services are behaving as expected by connecting via the Docker host. This might involve using the host machine to connect to the shell of the SQL Server container. - -#### Pass connection settings to connect via application - -Once the container has been verified, derive the connection string and pass to the application to ensure connection to SQL Server container is valid. - -### 4. Seeding Data for restoration - -This last section involves provisioning the Template data that will be used during the CREATE instance action. We will need the schema, tables, roles and DB set up so that the necessary configuration is provided when a user requests a new instance. - -## Test Strategy - -The following user journeys represent areas critical to instance management using SQL Server. - -An admin can connect to a SQL Server. - -* Provision the server -* Create connection string -* Ensure that the application connect to server successfully - -An admin can add a new SQL Server ODS instance - -* Connect to the SQL Server Instance -* Execute command to create a new DB Instance and tables -* Demonstrate new instance and tables are available - -An admin can rename an existing SQL Server ODS instance - -* Connect to the SQL Server Instance -* Execute command to rename ODS instance -* Ensure DB and corresponding tables and connection strings are updated -* Add additional path checking duplicate name for rename does not exist - -An admin delete a new SQL Server ODS instance - -* Connect to the SQL Server instance -* Execute command to delete instance -* Ensure instance and corresponding tables are properly marked for deletion. - -An admin can restore a new SQL Server ODS instance - -* Connect to the SQL Server instance -* Execute the create command providing a name and source of the restoration -* Ensure a new instance exists with provided restoration data (dbs, tables, and rows) - -## Outstanding Questions - -When renaming and instance, what is the expectation around down time? - -When searching for an instance to rename or delete by key, is it possible for a client key to reference more than one ODS instance?