diff --git a/.AL-Go/settings.json b/.AL-Go/settings.json index b0480f6..51eeccf 100644 --- a/.AL-Go/settings.json +++ b/.AL-Go/settings.json @@ -1,7 +1,9 @@ { "$schema": "https://raw.githubusercontent.com/Freddy-D4P/AL-Go/main/Actions/.Modules/settings.schema.json", "appFolders": [], - "testFolders": [], + "testFolders": [ + "CCMSTests" + ], "bcptTestFolders": [], "enableCodeCop": true, "enableUICop": true, diff --git a/CCMS/app.json b/CCMS/app.json index 9089fa9..837c0e5 100644 --- a/CCMS/app.json +++ b/CCMS/app.json @@ -29,5 +29,12 @@ "features": [ "NoImplicitWith", "TranslationFile" + ], + "internalsVisibleTo": [ + { + "id": "f10bf877-caa5-49c6-882e-22922fca331c", + "name": "Cloud Customer Management Solution Tests", + "publisher": "Directions for partners" + } ] } diff --git a/CCMS/src/Environment/D4PBCEnvironmentCard.page.al b/CCMS/src/Environment/D4PBCEnvironmentCard.page.al index 82e53e5..9d6a21a 100644 --- a/CCMS/src/Environment/D4PBCEnvironmentCard.page.al +++ b/CCMS/src/Environment/D4PBCEnvironmentCard.page.al @@ -364,20 +364,19 @@ page 62004 "D4P BC Environment Card" SelectedDate: Date; ExpectedMonth: Integer; ExpectedYear: Integer; + IgnoreUpdateWindow: Boolean; NoUpdatesAvailableErr: Label 'No updates available for the environment %1.', Comment = '%1 = Environment Name'; TargetVersion: Text[100]; begin - // Get available updates from API EnvironmentManagement.GetAvailableUpdates(Rec, TempAvailableUpdate); if TempAvailableUpdate.IsEmpty() then Error(NoUpdatesAvailableErr, Rec.Name); - // Pass data to selection dialog and show it UpdateSelectionDialog.SetData(TempAvailableUpdate); if UpdateSelectionDialog.RunModal() = Action::OK then begin - UpdateSelectionDialog.GetSelectedVersion(TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear); - EnvironmentManagement.SelectTargetVersion(Rec, TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear); + UpdateSelectionDialog.GetSelectedVersion(TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear, IgnoreUpdateWindow); + EnvironmentManagement.SelectTargetVersion(Rec, TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear, IgnoreUpdateWindow); CurrPage.Update(false); end; end; diff --git a/CCMS/src/Environment/D4PBCEnvironmentMgt.codeunit.al b/CCMS/src/Environment/D4PBCEnvironmentMgt.codeunit.al index a6261c3..c908642 100644 --- a/CCMS/src/Environment/D4PBCEnvironmentMgt.codeunit.al +++ b/CCMS/src/Environment/D4PBCEnvironmentMgt.codeunit.al @@ -24,9 +24,8 @@ codeunit 62000 "D4P BC Environment Mgt" JsonResponse: JsonObject; JsonVersionDetails: JsonObject; JsonToken: JsonToken; - JsonTokenField: JsonToken; JsonTokenLoop: JsonToken; - JsonValue: JsonValue; + TextValue: Text; FailedToFetchErr: Label 'Failed to fetch data from Endpoint: %1', Comment = '%1 = Error message'; ResponseText: Text; begin @@ -41,110 +40,64 @@ codeunit 62000 "D4P BC Environment Mgt" if JsonResponse.Get('value', JsonToken) then begin JsonArray := JsonToken.AsArray(); - BCEnvironment.Init(); - BCEnvironment."Customer No." := BCTenant."Customer No."; - BCEnvironment."Tenant ID" := BCTenant."Tenant ID"; foreach JsonTokenLoop in JsonArray do begin + BCEnvironment.Init(); + BCEnvironment."Customer No." := BCTenant."Customer No."; + BCEnvironment."Tenant ID" := BCTenant."Tenant ID"; JsonObjectLoop := JsonTokenLoop.AsObject(); - if JsonObjectLoop.Get('name', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment.Name := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('applicationFamily', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."Application Family" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('type', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment.Type := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('status', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment.State := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('countryCode', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."Country/Region" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('applicationVersion', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."Current Version" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('friendlyName', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."Friendly Name" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('aadTenantId', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - if not Evaluate(BCEnvironment."AAD Tenant ID", JsonValue.AsText()) then - BCEnvironment."AAD Tenant ID" := CreateGuid(); // Fallback if evaluation fails - end; - if JsonObjectLoop.Get('webClientLoginUrl', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."Web Client Login URL" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('webServiceUrl', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."Web Service URL" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('locationName', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."Location Name" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('geoName', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."Geo Name" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('ringName', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."Ring Name" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('appInsightsKey', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."Application Insights String" := JsonValue.AsText(); + if GetJsonText(JsonObjectLoop, 'name', TextValue) then + BCEnvironment.Name := TextValue; + if GetJsonText(JsonObjectLoop, 'applicationFamily', TextValue) then + BCEnvironment."Application Family" := TextValue; + if GetJsonText(JsonObjectLoop, 'type', TextValue) then + BCEnvironment.Type := TextValue; + if GetJsonText(JsonObjectLoop, 'status', TextValue) then + BCEnvironment.State := TextValue; + if GetJsonText(JsonObjectLoop, 'countryCode', TextValue) then + BCEnvironment."Country/Region" := TextValue; + if GetJsonText(JsonObjectLoop, 'applicationVersion', TextValue) then + BCEnvironment."Current Version" := TextValue; + if GetJsonText(JsonObjectLoop, 'friendlyName', TextValue) then + BCEnvironment."Friendly Name" := TextValue; + if GetJsonText(JsonObjectLoop, 'aadTenantId', TextValue) then + if not Evaluate(BCEnvironment."AAD Tenant ID", TextValue) then + BCEnvironment."AAD Tenant ID" := CreateGuid(); + if GetJsonText(JsonObjectLoop, 'webClientLoginUrl', TextValue) then + BCEnvironment."Web Client Login URL" := TextValue; + if GetJsonText(JsonObjectLoop, 'webServiceUrl', TextValue) then + BCEnvironment."Web Service URL" := TextValue; + if GetJsonText(JsonObjectLoop, 'locationName', TextValue) then + BCEnvironment."Location Name" := TextValue; + if GetJsonText(JsonObjectLoop, 'geoName', TextValue) then + BCEnvironment."Geo Name" := TextValue; + if GetJsonText(JsonObjectLoop, 'ringName', TextValue) then + BCEnvironment."Ring Name" := TextValue; + if GetJsonText(JsonObjectLoop, 'appInsightsKey', TextValue) then + BCEnvironment."Application Insights String" := TextValue; + if GetJsonText(JsonObjectLoop, 'SoftDeletedOn', TextValue) then + if not Evaluate(BCEnvironment."Soft Deleted On", TextValue) then + BCEnvironment."Soft Deleted On" := 0DT; + if GetJsonText(JsonObjectLoop, 'HardDeletePendingOn', TextValue) then + if not Evaluate(BCEnvironment."Hard Delete Pending On", TextValue) then + BCEnvironment."Hard Delete Pending On" := 0DT; + if GetJsonText(JsonObjectLoop, 'DeleteReason', TextValue) then + BCEnvironment."Delete Reason" := TextValue; + if GetJsonText(JsonObjectLoop, 'appSourceAppsUpdateCadence', TextValue) then + BCEnvironment."AppSource Apps Update Cadence" := TextValue; + if GetJsonText(JsonObjectLoop, 'platformVersion', TextValue) then + BCEnvironment."Platform Version" := TextValue; + if GetJsonText(JsonObjectLoop, 'linkedPowerPlatformEnvironmentId', TextValue) then + BCEnvironment."Linked PowerPlatform Env ID" := TextValue; + + if GetJsonObject(JsonObjectLoop, 'versionDetails', JsonVersionDetails) then begin + if GetJsonText(JsonVersionDetails, 'gracePeriodStartDate', TextValue) then + if not Evaluate(BCEnvironment."Grace Period Start Date", TextValue) then + BCEnvironment."Grace Period Start Date" := 0DT; + if GetJsonText(JsonVersionDetails, 'enforcedUpdatePeriodStartDate', TextValue) then + if not Evaluate(BCEnvironment."Enforced Update Period Start", TextValue) then + BCEnvironment."Enforced Update Period Start" := 0DT; end; - if JsonObjectLoop.Get('SoftDeletedOn', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - if not Evaluate(BCEnvironment."Soft Deleted On", JsonValue.AsText()) then - BCEnvironment."Soft Deleted On" := 0DT; // Clear if evaluation fails - end; - if JsonObjectLoop.Get('HardDeletePendingOn', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - if not Evaluate(BCEnvironment."Hard Delete Pending On", JsonValue.AsText()) then - BCEnvironment."Hard Delete Pending On" := 0DT; // Clear if evaluation fails - end; - if JsonObjectLoop.Get('DeleteReason', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."Delete Reason" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('appSourceAppsUpdateCadence', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."AppSource Apps Update Cadence" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('platformVersion', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."Platform Version" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('linkedPowerPlatformEnvironmentId', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - BCEnvironment."Linked PowerPlatform Env ID" := JsonValue.AsText(); - end; - // Handle nested versionDetails object - if JsonObjectLoop.Get('versionDetails', JsonTokenField) then - if JsonTokenField.IsObject() then begin - JsonVersionDetails := JsonTokenField.AsObject(); - if JsonVersionDetails.Get('gracePeriodStartDate', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - if not Evaluate(BCEnvironment."Grace Period Start Date", JsonValue.AsText()) then - BCEnvironment."Grace Period Start Date" := 0DT; // Clear if evaluation fails - end; - if JsonVersionDetails.Get('enforcedUpdatePeriodStartDate', JsonTokenField) then begin - JsonValue := JsonTokenField.AsValue(); - if not Evaluate(BCEnvironment."Enforced Update Period Start", JsonValue.AsText()) then - BCEnvironment."Enforced Update Period Start" := 0DT; // Clear if evaluation fails - end; - end; BCEnvironment.Insert(); end; @@ -190,7 +143,7 @@ codeunit 62000 "D4P BC Environment Mgt" JsonResponse: JsonObject; JsonToken: JsonToken; JsonTokenLoop: JsonToken; - JsonValue: JsonValue; + TextValue: Text; FailedToFetchErr: Label 'Failed to fetch data from Endpoint: %1', Comment = '%1 = Error message'; ResponseText: Text; begin @@ -210,32 +163,22 @@ codeunit 62000 "D4P BC Environment Mgt" if JsonResponse.Get('value', JsonToken) then begin JsonArray := JsonToken.AsArray(); - InstalledApp.Init(); - InstalledApp."Customer No." := BCTenant."Customer No."; - InstalledApp."Tenant ID" := BCTenant."Tenant ID"; - InstalledApp."Environment Name" := BCEnvironment.Name; - foreach JsonTokenLoop in JsonArray do begin + InstalledApp.Init(); + InstalledApp."Customer No." := BCTenant."Customer No."; + InstalledApp."Tenant ID" := BCTenant."Tenant ID"; + InstalledApp."Environment Name" := BCEnvironment.Name; JsonObjectLoop := JsonTokenLoop.AsObject(); - if JsonObjectLoop.Get('id', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - InstalledApp."App ID" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('name', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - InstalledApp."App Name" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('publisher', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - InstalledApp."App Publisher" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('version', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - InstalledApp."App Version" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('state', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - case JsonValue.AsText() of + if GetJsonText(JsonObjectLoop, 'id', TextValue) then + InstalledApp."App ID" := TextValue; + if GetJsonText(JsonObjectLoop, 'name', TextValue) then + InstalledApp."App Name" := TextValue; + if GetJsonText(JsonObjectLoop, 'publisher', TextValue) then + InstalledApp."App Publisher" := TextValue; + if GetJsonText(JsonObjectLoop, 'version', TextValue) then + InstalledApp."App Version" := TextValue; + if GetJsonText(JsonObjectLoop, 'state', TextValue) then + case TextValue of 'Installed': InstalledApp.State := Enum::"D4P App State"::Installed; 'UpdatePending': @@ -245,10 +188,8 @@ codeunit 62000 "D4P BC Environment Mgt" else InstalledApp.State := Enum::"D4P App State"::Installed; end; - end; - if JsonObjectLoop.Get('appType', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - case LowerCase(JsonValue.AsText()) of + if GetJsonText(JsonObjectLoop, 'appType', TextValue) then + case LowerCase(TextValue) of 'global': InstalledApp."App Type" := Enum::"D4P App Type"::Global; 'pte', 'tenant': @@ -258,14 +199,10 @@ codeunit 62000 "D4P BC Environment Mgt" else InstalledApp."App Type" := Enum::"D4P App Type"::" "; end; - end; - if JsonObjectLoop.Get('lastOperationId', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - InstalledApp."Last Operation Id" := JsonValue.AsText(); - end; - if JsonObjectLoop.Get('lastUpdateAttemptResult', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - case JsonValue.AsText() of + if GetJsonText(JsonObjectLoop, 'lastOperationId', TextValue) then + InstalledApp."Last Operation Id" := TextValue; + if GetJsonText(JsonObjectLoop, 'lastUpdateAttemptResult', TextValue) then + case TextValue of 'Succeeded': InstalledApp."Last Update Attempt Result" := Enum::"D4P Update Attempt Result"::Succeeded; 'Failed': @@ -277,7 +214,6 @@ codeunit 62000 "D4P BC Environment Mgt" else InstalledApp."Last Update Attempt Result" := Enum::"D4P Update Attempt Result"::Succeeded; end; - end; InstalledApp."Available Update Version" := ''; InstalledApp.Insert(); end; @@ -292,6 +228,8 @@ codeunit 62000 "D4P BC Environment Mgt" selected: Boolean; latestSelectableDate: DateTime; selectedDateTime: DateTime; + ParsedDate: Date; + ParsedDateTime: DateTime; month: Integer; year: Integer; JsonArray: JsonArray; @@ -301,7 +239,6 @@ codeunit 62000 "D4P BC Environment Mgt" JsonScheduleDetails: JsonObject; JsonToken: JsonToken; JsonTokenLoop: JsonToken; - JsonValue: JsonValue; FailedToFetchErr: Label 'Failed to fetch environment updates: %1', Comment = '%1 = Error message'; NoAvailableUpdatesMsg: Label 'No available updates found for the selected environment.'; NoSelectedUpdateMsg: Label 'No selected update found for the selected environment.'; @@ -315,7 +252,6 @@ codeunit 62000 "D4P BC Environment Mgt" begin BCTenant.Get(BCEnvironment."Customer No.", BCEnvironment."Tenant ID"); - // Call Admin API to get environment updates Endpoint := '/applications/' + BCEnvironment."Application Family" + '/environments/' + BCEnvironment.Name + '/updates'; if APIHelper.SendAdminAPIRequest(BCTenant, 'GET', Endpoint, '', ResponseText) then begin JsonResponse.ReadFrom(ResponseText); @@ -327,88 +263,44 @@ codeunit 62000 "D4P BC Environment Mgt" foreach JsonTokenLoop in JsonArray do begin JsonObjectLoop := JsonTokenLoop.AsObject(); - // Check if this version is selected selected := false; - if JsonObjectLoop.Get('selected', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - selected := JsonValue.AsBoolean(); - end; + GetJsonBoolean(JsonObjectLoop, 'selected', selected); if selected then begin - // Get the target version - if JsonObjectLoop.Get('targetVersion', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - targetVersion := JsonValue.AsText(); - end; - - // Get availability status + GetJsonText(JsonObjectLoop, 'targetVersion', targetVersion); available := false; - if JsonObjectLoop.Get('available', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - available := JsonValue.AsBoolean(); - end; - - // Get target version type + GetJsonBoolean(JsonObjectLoop, 'available', available); targetVersionType := ''; - if JsonObjectLoop.Get('targetVersionType', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - targetVersionType := JsonValue.AsText(); - end; + GetJsonText(JsonObjectLoop, 'targetVersionType', targetVersionType); - // Get schedule details if available (for released versions) - if JsonObjectLoop.Get('scheduleDetails', JsonTokenLoop) then begin - JsonScheduleDetails := JsonTokenLoop.AsObject(); + if GetJsonObject(JsonObjectLoop, 'scheduleDetails', JsonScheduleDetails) then begin + TryGetJsonDateTime(JsonScheduleDetails, 'selectedDateTime', selectedDateTime); - // Get selected date time - if JsonScheduleDetails.Get('selectedDateTime', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - if not JsonValue.IsNull() then - selectedDateTime := JsonValue.AsDateTime(); + ParsedDateTime := 0DT; + if not TryGetJsonDateTime(JsonScheduleDetails, 'latestSelectableDateTime', ParsedDateTime) then begin + ParsedDate := 0D; + if TryGetJsonDate(JsonScheduleDetails, 'latestSelectableDate', ParsedDate) then + ParsedDateTime := CreateDateTime(ParsedDate, 0T); end; + latestSelectableDate := ParsedDateTime; - // Get latest selectable date - if JsonScheduleDetails.Get('latestSelectableDate', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - if not JsonValue.IsNull() then - latestSelectableDate := JsonValue.AsDateTime(); - end; - - // Get ignore update window ignoreUpdateWindow := false; - if JsonScheduleDetails.Get('ignoreUpdateWindow', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - ignoreUpdateWindow := JsonValue.AsBoolean(); - end; - - // Get rollout status + GetJsonBoolean(JsonScheduleDetails, 'ignoreUpdateWindow', ignoreUpdateWindow); rolloutStatus := ''; - if JsonScheduleDetails.Get('rolloutStatus', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - rolloutStatus := JsonValue.AsText(); - end; + GetJsonText(JsonScheduleDetails, 'rolloutStatus', rolloutStatus); end; - // Get expected availability if available (for unreleased versions) expectedAvailability := ''; - if JsonObjectLoop.Get('expectedAvailability', JsonTokenLoop) then begin - JsonExpectedAvailability := JsonTokenLoop.AsObject(); - - if JsonExpectedAvailability.Get('month', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - month := JsonValue.AsInteger(); - end; - - if JsonExpectedAvailability.Get('year', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - year := JsonValue.AsInteger(); - end; - - expectedAvailability := Format(year) + '/' + PadStr('', 2 - StrLen(Format(month)), '0') + Format(month); + if GetJsonObject(JsonObjectLoop, 'expectedAvailability', JsonExpectedAvailability) then begin + month := 0; + GetJsonInteger(JsonExpectedAvailability, 'month', month); + year := 0; + GetJsonInteger(JsonExpectedAvailability, 'year', year); + expectedAvailability := FormatExpectedAvailability(year, month); end; end; end; - // Update the environment with the selected update information if targetVersion <> '' then begin BCEnvironment."Target Version" := targetVersion; BCEnvironment."Available" := available; @@ -473,7 +365,7 @@ codeunit 62000 "D4P BC Environment Mgt" JsonResponse: JsonObject; JsonToken: JsonToken; JsonTokenLoop: JsonToken; - JsonValue: JsonValue; + TextValue: Text; AvailableUpdatesFetchedMsg: Label 'Available updates for the selected environment have been fetched successfully.'; FailedToFetchErr: Label 'Failed to fetch data from Endpoint: %1', Comment = '%1 = Error message'; NoAvailableUpdatesMsg: Label 'No available updates found for the selected environment.'; @@ -493,26 +385,15 @@ codeunit 62000 "D4P BC Environment Mgt" foreach JsonTokenLoop in JsonArray do begin JsonObjectLoop := JsonTokenLoop.AsObject(); - if JsonObjectLoop.Get('appId', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - appId := JsonValue.AsText(); - end; - // if JsonObjectLoop.Get('name', JsonTokenLoop) then begin - // JsonValue := JsonTokenLoop.AsValue(); - // appName := JsonValue.AsText(); - // end; - // if JsonObjectLoop.Get('publisher', JsonTokenLoop) then begin - // JsonValue := JsonTokenLoop.AsValue(); - // appPublisher := JsonValue.AsText(); - // end; - if JsonObjectLoop.Get('version', JsonTokenLoop) then begin - JsonValue := JsonTokenLoop.AsValue(); - appVersion := JsonValue.AsText(); - end; - //Update the app entry - if InstalledApp.Get(BCTenant."Customer No.", BCTenant."Tenant ID", BCEnvironment.Name, appId) then begin - InstalledApp."Available Update Version" := appVersion; - InstalledApp.Modify(); + Clear(appId); + Clear(appVersion); + if GetJsonText(JsonObjectLoop, 'appId', TextValue) then begin + appId := TextValue; + if GetJsonText(JsonObjectLoop, 'version', appVersion) then + if InstalledApp.Get(BCTenant."Customer No.", BCTenant."Tenant ID", BCEnvironment.Name, appId) then begin + InstalledApp."Available Update Version" := appVersion; + InstalledApp.Modify(); + end; end; end; if ShowMessage and GuiAllowed then @@ -634,6 +515,8 @@ codeunit 62000 "D4P BC Environment Mgt" CurrentUpdate: Integer; EntryNo: Integer; TotalUpdates: Integer; + ParsedDate: Date; + ParsedDateTime: DateTime; JsonArray: JsonArray; JsonExpectedAvailability: JsonObject; JsonObjectLoop: JsonObject; @@ -641,7 +524,7 @@ codeunit 62000 "D4P BC Environment Mgt" JsonScheduleDetails: JsonObject; JsonToken: JsonToken; JsonTokenLoop: JsonToken; - JsonValue: JsonValue; + TextValue: Text; FailedToFetchErr: Label 'Failed to fetch available updates: %1', Comment = '%1 = Error message'; FetchingUpdatesMsg: Label 'Fetching available updates...'; NoUpdatesFoundMsg: Label 'No updates found in API response for environment %1.', Comment = '%1 = Environment Name'; @@ -654,17 +537,14 @@ codeunit 62000 "D4P BC Environment Mgt" TempAvailableUpdate.Reset(); TempAvailableUpdate.DeleteAll(); - // Show progress dialog ProgressDialog.Open(FetchingUpdatesMsg); - // Call Admin API to get available updates Endpoint := '/applications/' + BCEnvironment."Application Family" + '/environments/' + BCEnvironment.Name + '/updates'; if not APIHelper.SendAdminAPIRequest(BCTenant, 'GET', Endpoint, '', ResponseText) then begin ProgressDialog.Close(); Error(FailedToFetchErr, ResponseText); end; - // Debug mode: Show API response if BCSetup."Debug Mode" then Message('DEBUG - Get Available Updates:\%1', ResponseText); @@ -692,84 +572,39 @@ codeunit 62000 "D4P BC Environment Mgt" TempAvailableUpdate.Init(); TempAvailableUpdate."Entry No." := EntryNo; - // Update progress dialog ProgressDialog.Update(1, CurrentUpdate); ProgressDialog.Update(2, TotalUpdates); - // Get target version - if JsonObjectLoop.Get('targetVersion', JsonToken) then begin - JsonValue := JsonToken.AsValue(); - TempAvailableUpdate."Target Version" := CopyStr(JsonValue.AsText(), 1, MaxStrLen(TempAvailableUpdate."Target Version")); + if GetJsonText(JsonObjectLoop, 'targetVersion', TextValue) then begin + TempAvailableUpdate."Target Version" := CopyStr(TextValue, 1, MaxStrLen(TempAvailableUpdate."Target Version")); ProgressDialog.Update(3, TempAvailableUpdate."Target Version"); end; - // Get availability status - if JsonObjectLoop.Get('available', JsonToken) then begin - JsonValue := JsonToken.AsValue(); - TempAvailableUpdate.Available := JsonValue.AsBoolean(); - end; + GetJsonBoolean(JsonObjectLoop, 'available', TempAvailableUpdate.Available); + GetJsonBoolean(JsonObjectLoop, 'selected', TempAvailableUpdate.Selected); - // Get selected status - if JsonObjectLoop.Get('selected', JsonToken) then begin - JsonValue := JsonToken.AsValue(); - TempAvailableUpdate.Selected := JsonValue.AsBoolean(); - end; + if GetJsonText(JsonObjectLoop, 'targetVersionType', TextValue) then + TempAvailableUpdate."Target Version Type" := CopyStr(TextValue, 1, MaxStrLen(TempAvailableUpdate."Target Version Type")); - // Get target version type - if JsonObjectLoop.Get('targetVersionType', JsonToken) then begin - JsonValue := JsonToken.AsValue(); - TempAvailableUpdate."Target Version Type" := CopyStr(JsonValue.AsText(), 1, MaxStrLen(TempAvailableUpdate."Target Version Type")); - end; - - // Get schedule details if available (for released versions) - if JsonObjectLoop.Get('scheduleDetails', JsonToken) then begin - JsonScheduleDetails := JsonToken.AsObject(); - - // Get selected date time - if JsonScheduleDetails.Get('selectedDateTime', JsonToken) then begin - JsonValue := JsonToken.AsValue(); - if not JsonValue.IsNull() then - TempAvailableUpdate."Selected DateTime" := DT2Date(JsonValue.AsDateTime()); - end; + if GetJsonObject(JsonObjectLoop, 'scheduleDetails', JsonScheduleDetails) then begin + ParsedDateTime := 0DT; + if TryGetJsonDateTime(JsonScheduleDetails, 'selectedDateTime', ParsedDateTime) then + TempAvailableUpdate."Selected DateTime" := DT2Date(ParsedDateTime); - // Get latest selectable date - try both field names (API inconsistency) - if JsonScheduleDetails.Get('latestSelectableDateTime', JsonToken) then begin - JsonValue := JsonToken.AsValue(); - if not JsonValue.IsNull() then - TempAvailableUpdate."Latest Selectable Date" := DT2Date(JsonValue.AsDateTime()); - end else - if JsonScheduleDetails.Get('latestSelectableDate', JsonToken) then begin - JsonValue := JsonToken.AsValue(); - if not JsonValue.IsNull() then - TempAvailableUpdate."Latest Selectable Date" := DT2Date(JsonValue.AsDateTime()); - end; + ParsedDate := 0D; + if not TryGetJsonDate(JsonScheduleDetails, 'latestSelectableDateTime', ParsedDate) then + TryGetJsonDate(JsonScheduleDetails, 'latestSelectableDate', ParsedDate); + TempAvailableUpdate."Latest Selectable Date" := ParsedDate; - // Get ignore update window - if JsonScheduleDetails.Get('ignoreUpdateWindow', JsonToken) then begin - JsonValue := JsonToken.AsValue(); - TempAvailableUpdate."Ignore Update Window" := JsonValue.AsBoolean(); - end; + GetJsonBoolean(JsonScheduleDetails, 'ignoreUpdateWindow', TempAvailableUpdate."Ignore Update Window"); - // Get rollout status - if JsonScheduleDetails.Get('rolloutStatus', JsonToken) then begin - JsonValue := JsonToken.AsValue(); - TempAvailableUpdate."Rollout Status" := CopyStr(JsonValue.AsText(), 1, MaxStrLen(TempAvailableUpdate."Rollout Status")); - end; + if GetJsonText(JsonScheduleDetails, 'rolloutStatus', TextValue) then + TempAvailableUpdate."Rollout Status" := CopyStr(TextValue, 1, MaxStrLen(TempAvailableUpdate."Rollout Status")); end; - // Get expected availability if available (for unreleased versions) - if JsonObjectLoop.Get('expectedAvailability', JsonToken) then begin - JsonExpectedAvailability := JsonToken.AsObject(); - - if JsonExpectedAvailability.Get('month', JsonToken) then begin - JsonValue := JsonToken.AsValue(); - TempAvailableUpdate."Expected Month" := JsonValue.AsInteger(); - end; - - if JsonExpectedAvailability.Get('year', JsonToken) then begin - JsonValue := JsonToken.AsValue(); - TempAvailableUpdate."Expected Year" := JsonValue.AsInteger(); - end; + if GetJsonObject(JsonObjectLoop, 'expectedAvailability', JsonExpectedAvailability) then begin + GetJsonInteger(JsonExpectedAvailability, 'month', TempAvailableUpdate."Expected Month"); + GetJsonInteger(JsonExpectedAvailability, 'year', TempAvailableUpdate."Expected Year"); end; TempAvailableUpdate.Insert(); @@ -782,7 +617,7 @@ codeunit 62000 "D4P BC Environment Mgt" end; end; - procedure SelectTargetVersion(var BCEnvironment: Record "D4P BC Environment"; TargetVersion: Text[100]; SelectedDate: Date; ExpectedMonth: Integer; ExpectedYear: Integer) + internal procedure SelectTargetVersion(var BCEnvironment: Record "D4P BC Environment"; TargetVersion: Text[100]; SelectedDate: Date; ExpectedMonth: Integer; ExpectedYear: Integer; IgnoreUpdateWindow: Boolean) var BCSetup: Record "D4P BC Setup"; BCTenant: Record "D4P BC Tenant"; @@ -800,45 +635,38 @@ codeunit 62000 "D4P BC Environment Mgt" BCTenant.Get(BCEnvironment."Customer No.", BCEnvironment."Tenant ID"); BCSetup.Get(); - // Determine if the version is available (has a date) or not (has month/year) IsAvailable := (SelectedDate <> 0D); - // Build JSON request body JsonObject.Add('selected', true); if IsAvailable then begin - // Convert Date to DateTime (at midnight) SelectedDateTime := CreateDateTime(SelectedDate, 0T); - // For available versions, include schedule details JsonScheduleDetails.Add('selectedDateTime', SelectedDateTime); - JsonScheduleDetails.Add('ignoreUpdateWindow', false); + JsonScheduleDetails.Add('ignoreUpdateWindow', IgnoreUpdateWindow); JsonObject.Add('scheduleDetails', JsonScheduleDetails); end; JsonObject.WriteTo(RequestBody); - // Debug mode: Show request body if BCSetup."Debug Mode" then Message('DEBUG - Select Target Version Request:\Target Version: %1\Request Body: %2', TargetVersion, RequestBody); - // Call Admin API to select target version Endpoint := '/applications/' + BCEnvironment."Application Family" + '/environments/' + BCEnvironment.Name + '/updates/' + TargetVersion; if not APIHelper.SendAdminAPIRequest(BCTenant, 'PATCH', Endpoint, RequestBody, ResponseText) then Error(FailedToSelectErr, ResponseText); - // Debug mode: Show API response if BCSetup."Debug Mode" then Message('DEBUG - Select Target Version Response:\%1', ResponseText); - // Update environment record BCEnvironment."Target Version" := TargetVersion; + BCEnvironment."Ignore Update Window" := IgnoreUpdateWindow; if IsAvailable then begin BCEnvironment."Selected DateTime" := SelectedDateTime; BCEnvironment."Expected Availability" := ''; Message(UpdateScheduledMsg, TargetVersion, SelectedDate); end else begin BCEnvironment."Selected DateTime" := 0DT; - BCEnvironment."Expected Availability" := Format(ExpectedYear) + '/' + PadStr('', 2 - StrLen(Format(ExpectedMonth)), '0') + Format(ExpectedMonth); + BCEnvironment."Expected Availability" := FormatExpectedAvailability(ExpectedYear, ExpectedMonth); Message(UpdateSelectedMsg, TargetVersion, ExpectedMonth, ExpectedYear); end; BCEnvironment.Modify(); @@ -890,4 +718,103 @@ codeunit 62000 "D4P BC Environment Mgt" end else Error(FailedToSetKeyErr, ResponseText); end; + + local procedure GetJsonText(JsonObj: JsonObject; FieldName: Text; var Value: Text): Boolean + var + JsonToken: JsonToken; + JsonValue: JsonValue; + begin + if not JsonObj.Get(FieldName, JsonToken) then + exit(false); + JsonValue := JsonToken.AsValue(); + if JsonValue.IsNull() then + exit(false); + Value := JsonValue.AsText(); + exit(true); + end; + + local procedure GetJsonBoolean(JsonObj: JsonObject; FieldName: Text; var Value: Boolean): Boolean + var + JsonToken: JsonToken; + JsonValue: JsonValue; + begin + if not JsonObj.Get(FieldName, JsonToken) then + exit(false); + JsonValue := JsonToken.AsValue(); + if JsonValue.IsNull() then + exit(false); + Value := JsonValue.AsBoolean(); + exit(true); + end; + + local procedure GetJsonInteger(JsonObj: JsonObject; FieldName: Text; var Value: Integer): Boolean + var + JsonToken: JsonToken; + JsonValue: JsonValue; + begin + if not JsonObj.Get(FieldName, JsonToken) then + exit(false); + JsonValue := JsonToken.AsValue(); + if JsonValue.IsNull() then + exit(false); + Value := JsonValue.AsInteger(); + exit(true); + end; + + local procedure GetJsonObject(JsonObj: JsonObject; FieldName: Text; var ChildObj: JsonObject): Boolean + var + JsonToken: JsonToken; + begin + if not JsonObj.Get(FieldName, JsonToken) then + exit(false); + if not JsonToken.IsObject() then + exit(false); + ChildObj := JsonToken.AsObject(); + exit(true); + end; + + local procedure FormatExpectedAvailability(Year: Integer; Month: Integer): Text + begin + exit(Format(Year) + '/' + PadStr('', 2 - StrLen(Format(Month)), '0') + Format(Month)); + end; + + internal procedure TryGetJsonDate(JsonObj: JsonObject; FieldName: Text; var ResultDate: Date): Boolean + var + JsonToken: JsonToken; + JsonValue: JsonValue; + ResultDateTime: DateTime; + TextValue: Text; + begin + ResultDate := 0D; + if not JsonObj.Get(FieldName, JsonToken) then + exit(false); + JsonValue := JsonToken.AsValue(); + if JsonValue.IsNull() then + exit(false); + TextValue := JsonValue.AsText(); + if Evaluate(ResultDate, TextValue, 9) then + exit(true); + ResultDateTime := 0DT; + if Evaluate(ResultDateTime, TextValue, 9) then begin + ResultDate := DT2Date(ResultDateTime); + exit(true); + end; + exit(false); + end; + + internal procedure TryGetJsonDateTime(JsonObj: JsonObject; FieldName: Text; var ResultDateTime: DateTime): Boolean + var + JsonToken: JsonToken; + JsonValue: JsonValue; + TextValue: Text; + begin + ResultDateTime := 0DT; + if not JsonObj.Get(FieldName, JsonToken) then + exit(false); + JsonValue := JsonToken.AsValue(); + if JsonValue.IsNull() then + exit(false); + TextValue := JsonValue.AsText(); + exit(Evaluate(ResultDateTime, TextValue, 9)); + end; } \ No newline at end of file diff --git a/CCMS/src/Environment/D4PUpdateSelectionDialog.page.al b/CCMS/src/Environment/D4PUpdateSelectionDialog.page.al index 84f37de..d33a055 100644 --- a/CCMS/src/Environment/D4PUpdateSelectionDialog.page.al +++ b/CCMS/src/Environment/D4PUpdateSelectionDialog.page.al @@ -46,12 +46,21 @@ page 62025 "D4P Update Selection Dialog" Editable = DateFieldEditable; Visible = DateFieldsVisible; StyleExpr = RowStyleExpr; + ToolTip = 'Specifies the date for the update. Select a date between today and the latest selectable date.'; trigger OnValidate() begin ValidateSelectedDate(); end; } + field("Ignore Update Window"; Rec."Ignore Update Window") + { + Caption = 'Ignore Update Window'; + Editable = DateFieldEditable; + Visible = DateFieldsVisible; + StyleExpr = RowStyleExpr; + ToolTip = 'Specifies whether to bypass the configured update window and schedule the update for any time on the selected date.'; + } field("Rollout Status"; Rec."Rollout Status") { Editable = false; @@ -71,11 +80,6 @@ page 62025 "D4P Update Selection Dialog" StyleExpr = RowStyleExpr; } } - group(SelectionGroup) - { - Caption = 'Schedule Details'; - Visible = false; // Hidden since Selected Date is now in repeater - } } } @@ -114,19 +118,18 @@ page 62025 "D4P Update Selection Dialog" local procedure UpdateFieldVisibility() begin - // Show date fields if version is available DateFieldsVisible := Rec.Available; - DateFieldEditable := Rec.Available and (Rec."Latest Selectable Date" <> 0D); - - // Show expected fields if version is not available + DateFieldEditable := Rec.Available; ExpectedFieldsVisible := not Rec.Available; end; local procedure SetDefaultDateTime() begin - // Set default date to Latest Selectable Date if available if (Rec."User Selected Date" = 0D) and DateFieldEditable then - Rec."User Selected Date" := Rec."Latest Selectable Date"; + if Rec."Latest Selectable Date" <> 0D then + Rec."User Selected Date" := Rec."Latest Selectable Date" + else + Rec."User Selected Date" := Today(); end; local procedure ValidateSelectedDate() @@ -134,11 +137,9 @@ page 62025 "D4P Update Selection Dialog" if Rec."User Selected Date" = 0D then exit; - // Validate that selected date is not in the past if Rec."User Selected Date" < Today() then Error(DateTooEarlyErr); - // Validate that selected date is not later than Latest Selectable Date if (Rec."Latest Selectable Date" <> 0D) and (Rec."User Selected Date" > Rec."Latest Selectable Date") then Error(DateTooLateErr, Rec."Latest Selectable Date"); end; @@ -150,7 +151,6 @@ page 62025 "D4P Update Selection Dialog" begin CurrentEntryNo := Rec."Entry No."; - // If this record is now selected, unselect all others if Rec.Selected then begin TempUpdate.Copy(Rec, true); TempUpdate.Reset(); @@ -164,17 +164,17 @@ page 62025 "D4P Update Selection Dialog" end; end; - procedure GetSelectedVersion(var TargetVersion: Text[100]; var SelectedDate: Date; var ExpectedMonth: Integer; var ExpectedYear: Integer) + procedure GetSelectedVersion(var TargetVersion: Text[100]; var SelectedDate: Date; var ExpectedMonth: Integer; var ExpectedYear: Integer; var IgnoreUpdateWindow: Boolean) begin TargetVersion := Rec."Target Version"; SelectedDate := Rec."User Selected Date"; ExpectedMonth := Rec."Expected Month"; ExpectedYear := Rec."Expected Year"; + IgnoreUpdateWindow := Rec."Ignore Update Window"; end; procedure SetData(var TempSourceUpdate: Record "D4P BC Available Update" temporary) begin - // Copy all records from source temporary table to page's temporary table TempSourceUpdate.Reset(); if TempSourceUpdate.FindSet() then repeat diff --git a/CCMSTests/app.json b/CCMSTests/app.json new file mode 100644 index 0000000..6a92e2c --- /dev/null +++ b/CCMSTests/app.json @@ -0,0 +1,40 @@ +{ + "id": "f10bf877-caa5-49c6-882e-22922fca331c", + "name": "Cloud Customer Management Solution Tests", + "publisher": "Directions for partners", + "version": "0.0.2.0", + "brief": "Test app for CCMS.", + "description": "Automated tests for the Cloud Customer Management Solution.", + "privacyStatement": "", + "EULA": "", + "help": "", + "url": "", + "logo": "", + "dependencies": [ + { + "id": "e33746ef-a796-46ca-8a1a-fb3840a42c48", + "name": "Cloud Customer Management Solution", + "publisher": "Directions for partners", + "version": "0.0.2.0" + } + ], + "screenshots": [], + "platform": "1.0.0.0", + "application": "27.0.0.0", + "idRanges": [ + { + "from": 62050, + "to": 62099 + } + ], + "resourceExposurePolicy": { + "allowDebugging": true, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "runtime": "16.0", + "features": [ + "NoImplicitWith", + "TranslationFile" + ] +} diff --git a/CCMSTests/src/D4PJsonHelperTests.codeunit.al b/CCMSTests/src/D4PJsonHelperTests.codeunit.al new file mode 100644 index 0000000..1ec598f --- /dev/null +++ b/CCMSTests/src/D4PJsonHelperTests.codeunit.al @@ -0,0 +1,153 @@ +namespace D4P.CCMS.Environment.Tests; + +using D4P.CCMS.Environment; + +codeunit 62051 "D4P JSON Helper Tests" +{ + Subtype = Test; + + var + EnvironmentMgt: Codeunit "D4P BC Environment Mgt"; + + [Test] + procedure TryGetJsonDate_DateOnlyString_ParsesCorrectly() + var + JsonObj: JsonObject; + ResultDate: Date; + Found: Boolean; + begin + // Arrange: date-only ISO string (root cause of Bug 1) + JsonObj.Add('latestSelectableDate', '2025-06-15'); + + // Act + Found := EnvironmentMgt.TryGetJsonDate(JsonObj, 'latestSelectableDate', ResultDate); + + // Assert + VerifyIsTrue(Found, 'TryGetJsonDate should return true for date-only string'); + VerifyDateEqual(ResultDate, DMY2Date(15, 6, 2025), 'Parsed date'); + end; + + [Test] + procedure TryGetJsonDate_DateTimeString_ParsesDatePortion() + var + JsonObj: JsonObject; + ResultDate: Date; + Found: Boolean; + begin + // Arrange: datetime string — the root cause scenario (AsDateTime() returned 0DT for this) + JsonObj.Add('latestSelectableDate', '2025-06-15T00:00:00Z'); + + // Act + Found := EnvironmentMgt.TryGetJsonDate(JsonObj, 'latestSelectableDate', ResultDate); + + // Assert + VerifyIsTrue(Found, 'TryGetJsonDate should return true for datetime string'); + VerifyDateEqual(ResultDate, DMY2Date(15, 6, 2025), 'Parsed date from datetime string'); + end; + + [Test] + procedure TryGetJsonDate_NullValue_ReturnsFalse() + var + JsonObj: JsonObject; + NullToken: JsonToken; + ResultDate: Date; + Found: Boolean; + begin + // Arrange: JSON null value + NullToken.ReadFrom('null'); + JsonObj.Add('latestSelectableDate', NullToken); + + // Act + Found := EnvironmentMgt.TryGetJsonDate(JsonObj, 'latestSelectableDate', ResultDate); + + // Assert + VerifyIsFalse(Found, 'TryGetJsonDate should return false for null'); + VerifyDateEqual(ResultDate, 0D, 'ResultDate should be 0D for null'); + end; + + [Test] + procedure TryGetJsonDate_MissingField_ReturnsFalse() + var + JsonObj: JsonObject; + ResultDate: Date; + Found: Boolean; + begin + // Arrange: empty object + // Act + Found := EnvironmentMgt.TryGetJsonDate(JsonObj, 'latestSelectableDate', ResultDate); + + // Assert + VerifyIsFalse(Found, 'TryGetJsonDate should return false for missing field'); + VerifyDateEqual(ResultDate, 0D, 'ResultDate should be 0D for missing field'); + end; + + [Test] + procedure TryGetJsonDateTime_ValidDateTimeString_ParsesCorrectly() + var + JsonObj: JsonObject; + ResultDateTime: DateTime; + Found: Boolean; + begin + // Arrange + JsonObj.Add('selectedDateTime', '2025-06-15T10:30:00'); + + // Act + Found := EnvironmentMgt.TryGetJsonDateTime(JsonObj, 'selectedDateTime', ResultDateTime); + + // Assert + VerifyIsTrue(Found, 'TryGetJsonDateTime should return true for valid string'); + VerifyIsTrue(ResultDateTime <> 0DT, 'ResultDateTime should not be 0DT'); + end; + + [Test] + procedure TryGetJsonDateTime_NullValue_ReturnsFalse() + var + JsonObj: JsonObject; + NullToken: JsonToken; + ResultDateTime: DateTime; + Found: Boolean; + begin + // Arrange + NullToken.ReadFrom('null'); + JsonObj.Add('selectedDateTime', NullToken); + + // Act + Found := EnvironmentMgt.TryGetJsonDateTime(JsonObj, 'selectedDateTime', ResultDateTime); + + // Assert + VerifyIsFalse(Found, 'TryGetJsonDateTime should return false for null'); + VerifyIsTrue(ResultDateTime = 0DT, 'ResultDateTime should be 0DT for null'); + end; + + [Test] + procedure TryGetJsonDateTime_MissingField_ReturnsFalse() + var + JsonObj: JsonObject; + ResultDateTime: DateTime; + Found: Boolean; + begin + // Act + Found := EnvironmentMgt.TryGetJsonDateTime(JsonObj, 'selectedDateTime', ResultDateTime); + + // Assert + VerifyIsFalse(Found, 'TryGetJsonDateTime should return false for missing field'); + end; + + local procedure VerifyIsTrue(Value: Boolean; Msg: Text) + begin + if not Value then + Error('Assert failed (expected true): %1', Msg); + end; + + local procedure VerifyIsFalse(Value: Boolean; Msg: Text) + begin + if Value then + Error('Assert failed (expected false): %1', Msg); + end; + + local procedure VerifyDateEqual(Actual: Date; Expected: Date; FieldName: Text) + begin + if Actual <> Expected then + Error('Assert failed (%1): expected %2, got %3', FieldName, Expected, Actual); + end; +} diff --git a/CCMSTests/src/D4PUpdateSelectionDlgTests.codeunit.al b/CCMSTests/src/D4PUpdateSelectionDlgTests.codeunit.al new file mode 100644 index 0000000..ff75181 --- /dev/null +++ b/CCMSTests/src/D4PUpdateSelectionDlgTests.codeunit.al @@ -0,0 +1,216 @@ +namespace D4P.CCMS.Environment.Tests; + +using D4P.CCMS.Environment; + +codeunit 62050 "D4P Update Selection Dlg Tests" +{ + Subtype = Test; + + [Test] + procedure IgnoreUpdateWindowTrue_RoundTripsCorrectly() + var + TempUpdate: Record "D4P BC Available Update" temporary; + UpdateSelectionDialog: Page "D4P Update Selection Dialog"; + TargetVersion: Text[100]; + SelectedDate: Date; + ExpectedMonth: Integer; + ExpectedYear: Integer; + IgnoreUpdateWindow: Boolean; + begin + // Arrange: available update with IgnoreUpdateWindow = true + InsertAvailableUpdate(TempUpdate, 1, '25.1.0.0', true, Today(), true); + + // Act + UpdateSelectionDialog.SetData(TempUpdate); + TempUpdate.FindFirst(); + UpdateSelectionDialog.GetSelectedVersion(TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear, IgnoreUpdateWindow); + + // Assert + VerifyIsTrue(IgnoreUpdateWindow, 'IgnoreUpdateWindow should be true'); + end; + + [Test] + procedure IgnoreUpdateWindowFalse_RoundTripsCorrectly() + var + TempUpdate: Record "D4P BC Available Update" temporary; + UpdateSelectionDialog: Page "D4P Update Selection Dialog"; + TargetVersion: Text[100]; + SelectedDate: Date; + ExpectedMonth: Integer; + ExpectedYear: Integer; + IgnoreUpdateWindow: Boolean; + begin + // Arrange: available update with IgnoreUpdateWindow = false + InsertAvailableUpdate(TempUpdate, 1, '25.1.0.0', true, Today(), false); + + // Act + UpdateSelectionDialog.SetData(TempUpdate); + TempUpdate.FindFirst(); + UpdateSelectionDialog.GetSelectedVersion(TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear, IgnoreUpdateWindow); + + // Assert + VerifyIsFalse(IgnoreUpdateWindow, 'IgnoreUpdateWindow should be false'); + end; + + [Test] + procedure UserSelectedDateToday_NoLatestSelectableDate_RoundTripsCorrectly() + var + TempUpdate: Record "D4P BC Available Update" temporary; + UpdateSelectionDialog: Page "D4P Update Selection Dialog"; + TargetVersion: Text[100]; + SelectedDate: Date; + ExpectedMonth: Integer; + ExpectedYear: Integer; + IgnoreUpdateWindow: Boolean; + begin + // Arrange: no LatestSelectableDate (0D) — regression for Bug 1 + InsertAvailableUpdate(TempUpdate, 1, '25.1.0.0', true, Today(), false); + TempUpdate.FindFirst(); + TempUpdate."Latest Selectable Date" := 0D; + TempUpdate.Modify(); + + // Act + UpdateSelectionDialog.SetData(TempUpdate); + TempUpdate.FindFirst(); + UpdateSelectionDialog.GetSelectedVersion(TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear, IgnoreUpdateWindow); + + // Assert: User Selected Date passes through correctly + VerifyDateEqual(SelectedDate, Today(), 'SelectedDate should equal Today when LatestSelectableDate is 0D'); + end; + + [Test] + procedure UserSelectedDate_WithLatestSelectableDate_RoundTripsCorrectly() + var + TempUpdate: Record "D4P BC Available Update" temporary; + UpdateSelectionDialog: Page "D4P Update Selection Dialog"; + TargetVersion: Text[100]; + SelectedDate: Date; + ExpectedDate: Date; + ExpectedMonth: Integer; + ExpectedYear: Integer; + IgnoreUpdateWindow: Boolean; + begin + // Arrange: LatestSelectableDate = Today + 30 + ExpectedDate := CalcDate('<+30D>', Today()); + InsertAvailableUpdate(TempUpdate, 1, '25.1.0.0', true, ExpectedDate, false); + + // Act + UpdateSelectionDialog.SetData(TempUpdate); + TempUpdate.FindFirst(); + UpdateSelectionDialog.GetSelectedVersion(TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear, IgnoreUpdateWindow); + + // Assert + VerifyDateEqual(SelectedDate, ExpectedDate, 'SelectedDate should equal the inserted UserSelectedDate'); + end; + + [Test] + procedure AllParameters_RoundTripCorrectly() + var + TempUpdate: Record "D4P BC Available Update" temporary; + UpdateSelectionDialog: Page "D4P Update Selection Dialog"; + TargetVersion: Text[100]; + SelectedDate: Date; + ExpectedMonth: Integer; + ExpectedYear: Integer; + IgnoreUpdateWindow: Boolean; + ExpectedUpdateMonth: Integer; + ExpectedUpdateYear: Integer; + begin + // Arrange + InsertAvailableUpdate(TempUpdate, 1, '25.2.0.0', true, CalcDate('<+14D>', Today()), true); + ExpectedUpdateMonth := Date2DMY(Today(), 2); + ExpectedUpdateYear := Date2DMY(Today(), 3); + TempUpdate.FindFirst(); + TempUpdate."Expected Month" := ExpectedUpdateMonth; + TempUpdate."Expected Year" := ExpectedUpdateYear; + TempUpdate.Modify(); + + // Act + UpdateSelectionDialog.SetData(TempUpdate); + TempUpdate.FindFirst(); + UpdateSelectionDialog.GetSelectedVersion(TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear, IgnoreUpdateWindow); + + // Assert all 5 output params + VerifyTextEqual(TargetVersion, '25.2.0.0', 'TargetVersion'); + VerifyDateEqual(SelectedDate, CalcDate('<+14D>', Today()), 'SelectedDate'); + VerifyIsTrue(IgnoreUpdateWindow, 'IgnoreUpdateWindow'); + VerifyIntEqual(ExpectedMonth, ExpectedUpdateMonth, 'ExpectedMonth'); + VerifyIntEqual(ExpectedYear, ExpectedUpdateYear, 'ExpectedYear'); + end; + + [Test] + procedure UnavailableUpdate_ExpectedMonthYear_RoundTripCorrectly() + var + TempUpdate: Record "D4P BC Available Update" temporary; + UpdateSelectionDialog: Page "D4P Update Selection Dialog"; + TargetVersion: Text[100]; + SelectedDate: Date; + ExpectedMonth: Integer; + ExpectedYear: Integer; + IgnoreUpdateWindow: Boolean; + CurrentMonth: Integer; + CurrentYear: Integer; + begin + // Arrange: unavailable (future) update + CurrentMonth := Date2DMY(Today(), 2); + CurrentYear := Date2DMY(Today(), 3); + TempUpdate.Init(); + TempUpdate."Entry No." := 1; + TempUpdate."Target Version" := '26.0.0.0'; + TempUpdate.Available := false; + TempUpdate."Expected Month" := CurrentMonth; + TempUpdate."Expected Year" := CurrentYear; + TempUpdate.Insert(); + + // Act + UpdateSelectionDialog.SetData(TempUpdate); + TempUpdate.FindFirst(); + UpdateSelectionDialog.GetSelectedVersion(TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear, IgnoreUpdateWindow); + + // Assert + VerifyTextEqual(TargetVersion, '26.0.0.0', 'TargetVersion for unavailable update'); + VerifyIntEqual(ExpectedMonth, CurrentMonth, 'ExpectedMonth'); + VerifyIntEqual(ExpectedYear, CurrentYear, 'ExpectedYear'); + end; + + local procedure InsertAvailableUpdate(var TempUpdate: Record "D4P BC Available Update" temporary; EntryNo: Integer; Version: Text[100]; IsAvailable: Boolean; UserSelectedDate: Date; IgnoreWindow: Boolean) + begin + TempUpdate.Init(); + TempUpdate."Entry No." := EntryNo; + TempUpdate."Target Version" := Version; + TempUpdate.Available := IsAvailable; + TempUpdate."User Selected Date" := UserSelectedDate; + TempUpdate."Ignore Update Window" := IgnoreWindow; + TempUpdate.Insert(); + end; + + local procedure VerifyIsTrue(Value: Boolean; Message: Text) + begin + if not Value then + Error('Assert failed (expected true): %1', Message); + end; + + local procedure VerifyIsFalse(Value: Boolean; Message: Text) + begin + if Value then + Error('Assert failed (expected false): %1', Message); + end; + + local procedure VerifyDateEqual(Actual: Date; Expected: Date; FieldName: Text) + begin + if Actual <> Expected then + Error('Assert failed (%1): expected %2, got %3', FieldName, Expected, Actual); + end; + + local procedure VerifyTextEqual(Actual: Text; Expected: Text; FieldName: Text) + begin + if Actual <> Expected then + Error('Assert failed (%1): expected "%2", got "%3"', FieldName, Expected, Actual); + end; + + local procedure VerifyIntEqual(Actual: Integer; Expected: Integer; FieldName: Text) + begin + if Actual <> Expected then + Error('Assert failed (%1): expected %2, got %3', FieldName, Expected, Actual); + end; +}