diff --git a/src/Apps/W1/Shopify/App/src/Base/Codeunits/ShpfyCommunicationEvents.Codeunit.al b/src/Apps/W1/Shopify/App/src/Base/Codeunits/ShpfyCommunicationEvents.Codeunit.al
deleted file mode 100644
index 9593649efd..0000000000
--- a/src/Apps/W1/Shopify/App/src/Base/Codeunits/ShpfyCommunicationEvents.Codeunit.al
+++ /dev/null
@@ -1,39 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify;
-
-///
-/// Codeunit Shpfy Communication Events (ID 30200).
-///
-codeunit 30200 "Shpfy Communication Events"
-{
- Access = Internal;
-
- [InternalEvent(false)]
- internal procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- end;
-
- [InternalEvent(false)]
- internal procedure OnGetAccessToken(var AccessToken: Text)
- begin
- end;
-
- [InternalEvent(false)]
- internal procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- end;
-
- [InternalEvent(false)]
- internal procedure OnClientPost(var Url: Text; var Content: HttpContent; var Response: HttpResponseMessage)
- begin
- end;
-
- [InternalEvent(false)]
- internal procedure OnClientGet(var Url: Text; var Response: HttpResponseMessage)
- begin
- end;
-}
diff --git a/src/Apps/W1/Shopify/App/src/Base/Codeunits/ShpfyCommunicationMgt.Codeunit.al b/src/Apps/W1/Shopify/App/src/Base/Codeunits/ShpfyCommunicationMgt.Codeunit.al
index 1b30497ac6..3b35c04f1d 100644
--- a/src/Apps/W1/Shopify/App/src/Base/Codeunits/ShpfyCommunicationMgt.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Base/Codeunits/ShpfyCommunicationMgt.Codeunit.al
@@ -19,13 +19,11 @@ codeunit 30103 "Shpfy Communication Mgt."
var
Shop: Record "Shpfy Shop";
- CommunicationEvents: Codeunit "Shpfy Communication Events";
GraphQLQueries: Codeunit "Shpfy GraphQL Queries";
NextExecutionTime: DateTime;
VersionTok: Label '2026-01', Locked = true;
OutgoingRequestsNotEnabledConfirmLbl: Label 'Importing data to your Shopify shop is not enabled, do you want to go to shop card to enable?';
OutgoingRequestsNotEnabledErr: Label 'Importing data to your Shopify shop is not enabled, navigate to shop card to enable.';
- IsTestInProgress: Boolean;
CategoryTok: Label 'Shopify Integration', Locked = true;
QueryParamTooLongTxt: Label 'Query param length exceeded 50000.', Locked = true;
QueryParamTooLongErr: Label 'Request length exceeded Shopify API limit.';
@@ -232,22 +230,19 @@ codeunit 30103 "Shpfy Communication Mgt."
Sleep(Wait);
end;
- if IsTestInProgress then
- CommunicationEvents.OnClientSend(HttpRequestMessage, HttpResponseMessage)
- else
- if HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then begin
- Clear(RetryCounter);
- while (not HttpResponseMessage.IsBlockedByEnvironment) and (EvaluateResponse(HttpResponseMessage)) and (RetryCounter < MaxRetries) do begin
- RetryCounter += 1;
- Sleep(1000);
- LogShopifyRequest(Url, Method, Request, HttpResponseMessage, Response, RetryCounter);
- Clear(HttpClient);
- Clear(HttpRequestMessage);
- Clear(HttpResponseMessage);
- CreateHttpRequestMessage(Url, Method, Request, HttpRequestMessage);
- HttpClient.Send(HttpRequestMessage, HttpResponseMessage);
- end;
+ if HttpClient.Send(HttpRequestMessage, HttpResponseMessage) then begin
+ Clear(RetryCounter);
+ while (not HttpResponseMessage.IsBlockedByEnvironment) and (EvaluateResponse(HttpResponseMessage)) and (RetryCounter < MaxRetries) do begin
+ RetryCounter += 1;
+ Sleep(1000);
+ LogShopifyRequest(Url, Method, Request, HttpResponseMessage, Response, RetryCounter);
+ Clear(HttpClient);
+ Clear(HttpRequestMessage);
+ Clear(HttpResponseMessage);
+ CreateHttpRequestMessage(Url, Method, Request, HttpRequestMessage);
+ HttpClient.Send(HttpRequestMessage, HttpResponseMessage);
end;
+ end;
if GetContent(HttpResponseMessage, Response) then;
ResponseHeaders := HttpResponseMessage.Headers();
LogShopifyRequest(Url, Method, Request, HttpResponseMessage, Response, RetryCounter);
@@ -257,28 +252,19 @@ codeunit 30103 "Shpfy Communication Mgt."
[NonDebuggable]
internal procedure Post(var Client: HttpClient; Url: Text; Content: HttpContent; var Response: HttpResponseMessage)
begin
- if IsTestInProgress then
- CommunicationEvents.OnClientPost(Url, Content, Response)
- else
- Client.Post(Url, Content, Response);
+ Client.Post(Url, Content, Response);
end;
[NonDebuggable]
internal procedure Get(var Client: HttpClient; Url: Text; var Response: HttpResponseMessage)
begin
- if IsTestInProgress then
- CommunicationEvents.OnClientGet(Url, Response)
- else
- Client.Get(Url, Response);
+ Client.Get(Url, Response);
end;
[TryFunction]
local procedure GetContent(HttpResponseMsg: HttpResponseMessage; var Response: Text)
begin
- if IsTestInProgress then
- CommunicationEvents.OnGetContent(HttpResponseMsg, Response)
- else
- HttpResponseMsg.Content.ReadAs(Response);
+ HttpResponseMsg.Content.ReadAs(Response);
end;
///
@@ -311,8 +297,7 @@ codeunit 30103 "Shpfy Communication Mgt."
/// Return value of type Text.
local procedure ApiVersion(): Text
begin
- if not IsTestInProgress then
- CheckApiVersion();
+ CheckApiVersion();
exit(VersionTok);
end;
@@ -351,18 +336,12 @@ codeunit 30103 "Shpfy Communication Mgt."
HttpContent: HttpContent;
ContentHttpHeaders: HttpHeaders;
HttpHeaders: HttpHeaders;
- ClearAccessToken: Text;
AccessToken: SecretText;
begin
HttpRequestMsg.SetRequestUri(url);
HttpRequestMsg.GetHeaders(HttpHeaders);
-
- if IsTestInProgress then begin
- CommunicationEvents.OnGetAccessToken(ClearAccessToken);
- AccessToken := ClearAccessToken;
- end else
- AccessToken := GetAccessToken(Shop);
+ AccessToken := GetAccessToken(Shop);
HttpHeaders.Add('X-Shopify-Access-Token', AccessToken);
HttpRequestMsg.Method := Method;
@@ -564,20 +543,6 @@ codeunit 30103 "Shpfy Communication Mgt."
FeatureTelemetry.LogUsage('0000JW7', 'Shopify', 'A shop is set', Dimensions);
end;
- ///
- /// SetTestInProgress.
- ///
- /// Boolean.
- internal procedure SetTestInProgress(TestInProgress: Boolean)
- begin
- IsTestInProgress := TestInProgress;
- end;
-
- internal procedure GetTestInProgress(): Boolean
- begin
- exit(IsTestInProgress);
- end;
-
internal procedure GetShopRecord() ShopifyShop: Record "Shpfy Shop";
begin
if not ShopifyShop.Get(Shop.Code) then
diff --git a/src/Apps/W1/Shopify/App/src/Order Fulfillments/Codeunits/ShpfyFulfillmentOrdersAPI.Codeunit.al b/src/Apps/W1/Shopify/App/src/Order Fulfillments/Codeunits/ShpfyFulfillmentOrdersAPI.Codeunit.al
index 3857cf69cc..ace2a68c30 100644
--- a/src/Apps/W1/Shopify/App/src/Order Fulfillments/Codeunits/ShpfyFulfillmentOrdersAPI.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Order Fulfillments/Codeunits/ShpfyFulfillmentOrdersAPI.Codeunit.al
@@ -184,9 +184,6 @@ codeunit 30238 "Shpfy Fulfillment Orders API"
Parameters: Dictionary of [Text, Text];
JResponse: JsonToken;
begin
- if CommunicationMgt.GetTestInProgress() then
- exit;
-
CommunicationMgt.SetShop(Shop);
if Shop."Allow Outgoing Requests" then
diff --git a/src/Apps/W1/Shopify/App/src/Order Fulfillments/Codeunits/ShpfyOrderFulfillments.Codeunit.al b/src/Apps/W1/Shopify/App/src/Order Fulfillments/Codeunits/ShpfyOrderFulfillments.Codeunit.al
index 4be652af9e..b30fcf6f62 100644
--- a/src/Apps/W1/Shopify/App/src/Order Fulfillments/Codeunits/ShpfyOrderFulfillments.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Order Fulfillments/Codeunits/ShpfyOrderFulfillments.Codeunit.al
@@ -33,8 +33,6 @@ codeunit 30160 "Shpfy Order Fulfillments"
JFulfillments: JsonArray;
JResponse: JsonToken;
begin
- if CommunicationMgt.GetTestInProgress() then
- exit;
CommunicationMgt.SetShop(Shop);
Parameters.Add('OrderId', Format(OrderId));
GraphQLType := "Shpfy GraphQL Type"::Orders_GetOrderFulfillment;
diff --git a/src/Apps/W1/Shopify/App/src/Order Risks/Codeunits/ShpfyOrderRisks.Codeunit.al b/src/Apps/W1/Shopify/App/src/Order Risks/Codeunits/ShpfyOrderRisks.Codeunit.al
index 5bfe9144a2..0daf8bb5ed 100644
--- a/src/Apps/W1/Shopify/App/src/Order Risks/Codeunits/ShpfyOrderRisks.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Order Risks/Codeunits/ShpfyOrderRisks.Codeunit.al
@@ -39,8 +39,6 @@ codeunit 30170 "Shpfy Order Risks"
Parameters: Dictionary of [text, Text];
GraphQLType: Enum "Shpfy GraphQL Type";
begin
- if CommunicationMgt.GetTestInProgress() then
- exit;
CommunicationMgt.SetShop(OrderHeader."Shop Code");
Parameters.Add('OrderId', Format(OrderHeader."Shopify Order Id"));
JResponse := CommunicationMgt.ExecuteGraphQL(GraphQLType::Orders_OrderRisks, Parameters);
diff --git a/src/Apps/W1/Shopify/App/src/Order handling/Codeunits/ShpfyOrdersAPI.Codeunit.al b/src/Apps/W1/Shopify/App/src/Order handling/Codeunits/ShpfyOrdersAPI.Codeunit.al
index 87494d2dce..861c99135c 100644
--- a/src/Apps/W1/Shopify/App/src/Order handling/Codeunits/ShpfyOrdersAPI.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Order handling/Codeunits/ShpfyOrdersAPI.Codeunit.al
@@ -114,8 +114,6 @@ codeunit 30165 "Shpfy Orders API"
JAttrib: JsonObject;
begin
CommunicationMgt.SetShop(ShopifyShop);
- if CommunicationMgt.GetTestInProgress() then
- exit;
Clear(OrderAttribute);
OrderAttribute."Order Id" := OrderHeader."Shopify Order Id";
OrderAttribute."Key" := CopyStr(KeyName, 1, MaxStrLen(OrderAttribute."Key"));
diff --git a/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al b/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al
index 6af16ecaaa..65c48a0472 100644
--- a/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al
+++ b/src/Apps/W1/Shopify/App/src/PermissionSets/ShpfyObjects.PermissionSet.al
@@ -115,7 +115,6 @@ permissionset 30104 "Shpfy - Objects"
codeunit "Shpfy Can Not Have Stock" = X,
codeunit "Shpfy Catalog API" = X,
codeunit "Shpfy Checklist Item List" = X,
- codeunit "Shpfy Communication Events" = X,
codeunit "Shpfy Communication Mgt." = X,
codeunit "Shpfy Comp. By Default Comp." = X,
codeunit "Shpfy Comp. By Email/Phone" = X,
diff --git a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductAPI.Codeunit.al b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductAPI.Codeunit.al
index b1f3b0540c..1192dafa13 100644
--- a/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductAPI.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Products/Codeunits/ShpfyProductAPI.Codeunit.al
@@ -192,12 +192,7 @@ codeunit 30176 "Shpfy Product API"
Headers: HttpHeaders;
Response: HttpResponseMessage;
InStream: InStream;
- IsTestInProgress: Boolean;
begin
- OnBeforeUploadImage(TenantMedia, Url, IsTestInProgress);
- if IsTestInProgress then
- exit;
-
Content.GetHeaders(Headers);
if Headers.Contains('Content-Type') then
Headers.Remove('Content-Type');
@@ -751,8 +746,4 @@ codeunit 30176 "Shpfy Product API"
exit(CommunicationMgt.GetIdOfGId(JsonHelper.GetValueAsText(JMedia, 'id')));
end;
- [InternalEvent(false, false)]
- procedure OnBeforeUploadImage(var TenantMedia: Record "Tenant Media"; var ResourceUrl: Text; var IsTestInProgress: Boolean)
- begin
- end;
}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/App/src/Shipping/Codeunits/ShpfyShippingCharges.Codeunit.al b/src/Apps/W1/Shopify/App/src/Shipping/Codeunits/ShpfyShippingCharges.Codeunit.al
index 6dd88aec43..e4f7f64c61 100644
--- a/src/Apps/W1/Shopify/App/src/Shipping/Codeunits/ShpfyShippingCharges.Codeunit.al
+++ b/src/Apps/W1/Shopify/App/src/Shipping/Codeunits/ShpfyShippingCharges.Codeunit.al
@@ -28,8 +28,6 @@ codeunit 30191 "Shpfy Shipping Charges"
JShipmentLines: JsonArray;
JResponse: JsonToken;
begin
- if CommunicationMgt.GetTestInProgress() then
- exit;
CommunicationMgt.SetShop(OrderHeader."Shop Code");
Parameters.Add('OrderId', Format(OrderHeader."Shopify Order Id"));
GraphQLType := "Shpfy GraphQL Type"::Shipping_GetShipmentLines;
diff --git a/src/Apps/W1/Shopify/Test/.docs-updated b/src/Apps/W1/Shopify/Test/.docs-updated
new file mode 100644
index 0000000000..6f2b482dfd
--- /dev/null
+++ b/src/Apps/W1/Shopify/Test/.docs-updated
@@ -0,0 +1,4 @@
+# Documentation last updated
+commit: 212fc4094e48b78995eff6108b5a1bafd4955087
+date: 2026-03-30
+scope: full
diff --git a/src/Apps/W1/Shopify/Test/.resources/Products/CreateUploadUrl.txt b/src/Apps/W1/Shopify/Test/.resources/Products/CreateUploadUrl.txt
index 1445ed1fca..3e20e3a221 100644
--- a/src/Apps/W1/Shopify/Test/.resources/Products/CreateUploadUrl.txt
+++ b/src/Apps/W1/Shopify/Test/.resources/Products/CreateUploadUrl.txt
@@ -1 +1 @@
-{ "data": { "stagedUploadsCreate": { "stagedTargets": [ { "url": "test.com/test", "resourceUrl": "test2.com/test2", "parameters": [] } ] } }, "extensions": { "cost": { "requestedQueryCost": 11, "actualQueryCost": 11, "throttleStatus": { "maximumAvailable": 2000, "currentlyAvailable": 1989, "restoreRate": 100 } } } }
\ No newline at end of file
+{ "data": { "stagedUploadsCreate": { "stagedTargets": [ { "url": "https://test.com/test", "resourceUrl": "https://test2.com/test2", "parameters": [] } ] } }, "extensions": { "cost": { "requestedQueryCost": 11, "actualQueryCost": 11, "throttleStatus": { "maximumAvailable": 2000, "currentlyAvailable": 1989, "restoreRate": 100 } } } }
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Base/ShpfyInitializeTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Base/ShpfyInitializeTest.Codeunit.al
index dff1d5e0b2..1fcc082e41 100644
--- a/src/Apps/W1/Shopify/Test/Base/ShpfyInitializeTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Base/ShpfyInitializeTest.Codeunit.al
@@ -23,18 +23,14 @@ using System.TestLibraries.Utilities;
///
codeunit 139561 "Shpfy Initialize Test"
{
- EventSubscriberInstance = Manual;
-
var
DummyCustomer: Record Customer;
DummyItem: Record Item;
TempShop: Record "Shpfy Shop" temporary;
Any: Codeunit Any;
- LibraryAssert: Codeunit "Library Assert";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
LibraryERM: Codeunit "Library - ERM";
LibraryRandom: Codeunit "Library - Random";
- ShopifyAccessToken: Text;
#pragma warning disable AA0240
DummyCustomerEmailLbl: Label 'dummy@customer.com';
#pragma warning restore AA0240
@@ -50,7 +46,6 @@ codeunit 139561 "Shpfy Initialize Test"
RefundGLAccount: Record "G/L Account";
Shop: Record "Shpfy Shop";
VATPostingSetup: Record "VAT Posting Setup";
- ShpfyInitializeTest: Codeunit "Shpfy Initialize Test";
Code: Code[10];
CustomerTemplateCode: Code[20];
ItemTemplateCode: Code[20];
@@ -58,7 +53,6 @@ codeunit 139561 "Shpfy Initialize Test"
GenPostingType: Enum "General Posting Type";
UrlTxt: Label 'https://%1.myshopify.com', Comment = '%1 = Shop name', Locked = true;
begin
- BindSubscription(ShpfyInitializeTest);
if not TempShop.IsEmpty() then
if Shop.Get(TempShop.Code) then
exit(Shop);
@@ -94,7 +88,6 @@ codeunit 139561 "Shpfy Initialize Test"
if Shop.Insert() then;
Commit();
CommunicationMgt.SetShop(Shop);
- CommunicationMgt.SetTestInProgress(true);
CreateDummyCustomer(CustomerTemplateCode);
CreateDummyItem(ItemTemplateCode);
if not TempShop.Get(Code) then begin
@@ -102,7 +95,6 @@ codeunit 139561 "Shpfy Initialize Test"
TempShop.Insert();
Commit();
end;
- UnbindSubscription(ShpfyInitializeTest);
exit(Shop);
end;
@@ -346,32 +338,6 @@ codeunit 139561 "Shpfy Initialize Test"
end;
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetAccessToken', '', true, false)]
- local procedure OnGetAccessToken(var AccessToken: Text)
- begin
- if ShopifyAccessToken = '' then
- ShopifyAccessToken := Any.AlphanumericText(50);
- AccessToken := ShopifyAccessToken;
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- TestRequestHeaderContainsAccessToken(HttpRequestMessage);
- end;
-
- local procedure TestRequestHeaderContainsAccessToken(HttpRequestMessage: HttpRequestMessage)
- var
- Headers: HttpHeaders;
- ShopifyAccessTokenTxt: Label 'X-Shopify-Access-Token', Locked = true;
- Values: array[1] of Text;
- begin
- HttpRequestMessage.GetHeaders(Headers);
- LibraryAssert.IsTrue(Headers.Contains(ShopifyAccessTokenTxt), 'access token doesn''t exist');
- Headers.GetValues(ShopifyAccessTokenTxt, Values);
- LibraryAssert.IsTrue(Values[1] = ShopifyAccessToken, 'invalid access token');
- end;
-
internal procedure CreateVATPostingSetup(BusinessPostingGroup: Code[20]; ProductPostingGroup: Code[20])
var
GeneralPostingSetup: Record "General Posting Setup";
diff --git a/src/Apps/W1/Shopify/Test/Base/ShpfyTestShopify.Codeunit.al b/src/Apps/W1/Shopify/Test/Base/ShpfyTestShopify.Codeunit.al
index e7a2f07878..7b31cc1347 100644
--- a/src/Apps/W1/Shopify/Test/Base/ShpfyTestShopify.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Base/ShpfyTestShopify.Codeunit.al
@@ -48,7 +48,6 @@ codeunit 139563 "Shpfy Test Shopify"
// [SCENARIO] If a version is out of support then the API must be blocked.
// [WHEN] The Shop is created.
Shop := InitializeTest.CreateShop();
- CommunicationMgt.SetTestInProgress(false);
SetupKeyVaultExpiryDate(CommunicationMgt.GetApiVersion());
EnvironmentInfoTestLibrary.SetTestabilitySoftwareAsAService(true);
diff --git a/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOpSubscriber.Codeunit.al b/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOpSubscriber.Codeunit.al
index a9e95100f1..a7a4a678f3 100644
--- a/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOpSubscriber.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOpSubscriber.Codeunit.al
@@ -12,165 +12,9 @@ codeunit 139615 "Shpfy Bulk Op. Subscriber"
SingleInstance = true;
EventSubscriberInstance = Manual;
- var
- UploadUrlLbl: Label 'https://shopify-staged-uploads.storage.googleapis.com', Locked = true;
- BulkOperationId: BigInteger;
- BulkOperationRunning: Boolean;
- BulkUploadFail: Boolean;
- BulkOperationUrl: Text;
- VariantId1: BigInteger;
- VariantId2: BigInteger;
-
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Bulk Operation Mgt.", 'OnInvalidUser', '', true, false)]
local procedure OnInvalidUser(var IsHandled: Boolean)
begin
IsHandled := true;
end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientGet', '', true, false)]
- local procedure OnClientGet(var Url: Text; var Response: HttpResponseMessage)
- begin
- if Url = BulkOperationUrl then
- Response := GetBulkOperationResult();
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientPost', '', true, false)]
- local procedure OnClientPost(var Url: Text; var Content: HttpContent; var Response: HttpResponseMessage)
- begin
- if Url = UploadUrlLbl then
- Response := GetJsonlUploadResult();
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQLQuery: Text;
- StagedUploadGQLTxt: Label '{"query": "mutation { stagedUploadsCreate(input', Locked = true;
- BulkMutationGQLTxt: Label '{"query": "mutation { bulkOperationRunMutation(mutation', Locked = true;
- BulkOperationGQLTxt: Label '{"query": "query { node(id: \"gid://shopify/BulkOperation/', Locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQLQuery) then begin
- if GraphQLQuery.StartsWith(StagedUploadGQLTxt) then
- HttpResponseMessage := GetStagedUplodResult();
- if GraphQLQuery.StartsWith(BulkMutationGQLTxt) then
- HttpResponseMessage := GetBulkMutationResponse();
- if GraphQLQuery.StartsWith(BulkOperationGQLTxt) then
- HttpResponseMessage := GetBulkOperation();
- end;
- end;
- end;
- end;
-
- local procedure GetStagedUplodResult(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- if BulkUploadFail then begin
- NavApp.GetResource('Bulk Operations/StagedUploadFailedResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- end else begin
- NavApp.GetResource('Bulk Operations/StagedUploadResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- Body := StrSubstNo(Body, UploadUrlLbl)
- end;
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetBulkMutationResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Bulk Operations/BulkMutationResponse.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(StrSubstNo(Body, Format(BulkOperationId)));
- exit(HttpResponseMessage);
- end;
-
- local procedure GetJsonlUploadResult(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- begin
- exit(HttpResponseMessage);
- end;
-
- local procedure GetBulkOperationResult(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: TextBuilder;
- BodyLine: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Bulk Operations/BulkOperationResult.txt', ResInStream, TextEncoding::UTF8);
- while not ResInStream.EOS do begin
- ResInStream.ReadText(BodyLine);
- Body.AppendLine(StrSubstNo(BodyLine, Format(VariantId1), Format(VariantId2)));
- end;
- HttpResponseMessage.Content.WriteFrom(Body.ToText());
- exit(HttpResponseMessage);
- end;
-
- local procedure GetBulkOperation(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Bulk Operations/BulkOperationCompletedResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- if BulkOperationRunning then
- Body := StrSubstNo(Body, 'RUNNING')
- else
- Body := StrSubstNo(Body, 'COMPLETED');
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- internal procedure SetBulkOperationId(Id: BigInteger)
- begin
- BulkOperationId := Id;
- end;
-
- internal procedure SetBulkOperationRunning(OperationRunning: Boolean)
- begin
- BulkOperationRunning := OperationRunning;
- end;
-
- internal procedure SetBulkUploadFail(Fail: Boolean)
- begin
- BulkUploadFail := Fail;
- end;
-
- internal procedure SetBulkOperationUrl(Url: Text)
- begin
- BulkOperationUrl := Url;
- end;
-
- internal procedure SetVariantIds(Id1: BigInteger; Id2: BigInteger)
- begin
- VariantId1 := Id1;
- VariantId2 := Id2;
- end;
-}
\ No newline at end of file
+}
diff --git a/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOperationsTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOperationsTest.Codeunit.al
index 8bd1762430..15019aa357 100644
--- a/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOperationsTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Bulk Operations/Codeunits/ShpfyBulkOperationsTest.Codeunit.al
@@ -13,6 +13,7 @@ codeunit 139633 "Shpfy Bulk Operations Test"
Subtype = Test;
TestType = IntegrationTest;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
trigger OnRun()
begin
@@ -21,24 +22,37 @@ codeunit 139633 "Shpfy Bulk Operations Test"
end;
var
+ Shop: Record "Shpfy Shop";
LibraryAssert: Codeunit "Library Assert";
Any: Codeunit Any;
-
- BulkOpSubscriber: Codeunit "Shpfy Bulk Op. Subscriber";
+ CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ InitializeTest: Codeunit "Shpfy Initialize Test";
+ LibraryRandom: Codeunit "Library - Random";
+ GraphQLResponses: Codeunit "Library - Variable Storage";
IsInitialized: Boolean;
BulkOperationId1: BigInteger;
BulkOperationId2: BigInteger;
+ BulkOperationIdCurrent: BigInteger;
+ BulkOperationRunning: Boolean;
+ BulkUploadFail: Boolean;
+ BulkOperationUrl: Text;
+ VariantId1: BigInteger;
+ VariantId2: BigInteger;
+ UploadUrlLbl: Label 'https://shopify-staged-uploads.storage.googleapis.com', Locked = true;
local procedure Initialize()
-
+ var
+ AccessToken: SecretText;
begin
if IsInitialized then
exit;
IsInitialized := true;
Codeunit.Run(Codeunit::"Shpfy Initialize Test");
+ Shop := CommunicationMgt.GetShopRecord();
+ AccessToken := LibraryRandom.RandText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
BulkOperationId1 := Any.IntegerInRange(100000, 555555);
BulkOperationId2 := Any.IntegerInRange(555555, 999999);
- if BindSubscription(BulkOpSubscriber) then;
end;
local procedure ClearSetup()
@@ -47,19 +61,18 @@ codeunit 139633 "Shpfy Bulk Operations Test"
ShopifyVariant: Record "Shpfy Variant";
begin
BulkOperation.DeleteAll();
- BulkOpSubscriber.SetBulkOperationRunning(false);
- BulkOpSubscriber.SetBulkUploadFail(false);
+ BulkOperationRunning := false;
+ BulkUploadFail := false;
ShopifyVariant.DeleteAll();
+ GraphQLResponses.Clear();
end;
[Test]
- [HandlerFunctions('BulkMessageHandler')]
+ [HandlerFunctions('BulkMessageHandler,BulkOperationHttpHandler')]
procedure TestSendBulkOperation()
var
- Shop: Record "Shpfy Shop";
BulkOperation: Record "Shpfy Bulk Operation";
BulkOperationMgt: Codeunit "Shpfy Bulk Operation Mgt.";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
BulkOperationType: Enum "Shpfy Bulk Operation Type";
IBulkOperation: Interface "Shpfy IBulk Operation";
tb: TextBuilder;
@@ -69,14 +82,14 @@ codeunit 139633 "Shpfy Bulk Operations Test"
// [GIVEN] A Shop record
Initialize();
- Shop := CommunicationMgt.GetShopRecord();
// [WHEN] A bulk operation is sent
- BulkOpSubscriber.SetBulkOperationId(BulkOperationId1);
+ BulkOperationIdCurrent := BulkOperationId1;
IBulkOperation := BulkOperationType::AddProduct;
tb.AppendLine(StrSubstNo(IBulkOperation.GetInput(), 'Sweet new snowboard 1', 'Snowboard', 'JadedPixel'));
tb.AppendLine(StrSubstNo(IBulkOperation.GetInput(), 'Sweet new snowboard 2', 'Snowboard', 'JadedPixel'));
tb.AppendLine(StrSubstNo(IBulkOperation.GetInput(), 'Sweet new snowboard 3', 'Snowboard', 'JadedPixel'));
+ EnqueueGraphQLResponsesForSendBulkMutation();
LibraryAssert.IsTrue(BulkOperationMgt.SendBulkMutation(Shop, BulkOperationType::AddProduct, tb.ToText(), RequestData), 'Bulk operation should be sent.');
// [THEN] A bulk operation record is created
@@ -86,13 +99,11 @@ codeunit 139633 "Shpfy Bulk Operations Test"
end;
[Test]
- [HandlerFunctions('BulkMessageHandler')]
+ [HandlerFunctions('BulkMessageHandler,BulkOperationHttpHandler')]
procedure TestSendBulkOperationAfterPreviousCompleted()
var
- Shop: Record "Shpfy Shop";
BulkOperation: Record "Shpfy Bulk Operation";
BulkOperationMgt: Codeunit "Shpfy Bulk Operation Mgt.";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
BulkOperationType: Enum "Shpfy Bulk Operation Type";
IBulkOperation: Interface "Shpfy IBulk Operation";
tb: TextBuilder;
@@ -102,24 +113,25 @@ codeunit 139633 "Shpfy Bulk Operations Test"
// [GIVEN] A Shop record
Initialize();
- Shop := CommunicationMgt.GetShopRecord();
// [WHEN] A bulk operation is sent and completed
- BulkOpSubscriber.SetBulkOperationId(BulkOperationId1);
+ BulkOperationIdCurrent := BulkOperationId1;
IBulkOperation := BulkOperationType::AddProduct;
tb.AppendLine(StrSubstNo(IBulkOperation.GetInput(), 'Sweet new snowboard 1', 'Snowboard', 'JadedPixel'));
tb.AppendLine(StrSubstNo(IBulkOperation.GetInput(), 'Sweet new snowboard 2', 'Snowboard', 'JadedPixel'));
tb.AppendLine(StrSubstNo(IBulkOperation.GetInput(), 'Sweet new snowboard 3', 'Snowboard', 'JadedPixel'));
+ EnqueueGraphQLResponsesForSendBulkMutation();
BulkOperationMgt.SendBulkMutation(Shop, BulkOperationType::AddProduct, tb.ToText(), RequestData);
BulkOperation.Get(BulkOperationId1, Shop.Code, BulkOperation.Type::mutation);
BulkOperation.Status := BulkOperation.Status::Completed;
BulkOperation.Modify();
// [WHEN] A second bulk operation is sent
- BulkOpSubscriber.SetBulkOperationId(BulkOperationId2);
+ BulkOperationIdCurrent := BulkOperationId2;
tb.Clear();
tb.AppendLine('{ "input": { "title": "Sweet new snowboard 4", "productType": "Snowboard", "vendor": "JadedPixel" } }');
tb.AppendLine('{ "input": { "title": "Sweet new snowboard 5", "productType": "Snowboard", "vendor": "JadedPixel" } }');
tb.AppendLine('{ "input": { "title": "Sweet new snowboard 6", "productType": "Snowboard", "vendor": "JadedPixel" } }');
+ EnqueueGraphQLResponsesForSendBulkMutation();
LibraryAssert.IsTrue(BulkOperationMgt.SendBulkMutation(Shop, BulkOperationType::AddProduct, tb.ToText(), RequestData), 'Bulk operation should be sent.');
// [THEN] A bulk operation record is created
@@ -129,13 +141,11 @@ codeunit 139633 "Shpfy Bulk Operations Test"
end;
[Test]
- [HandlerFunctions('BulkMessageHandler')]
+ [HandlerFunctions('BulkMessageHandler,BulkOperationHttpHandler')]
procedure TestSendBulkOperationBeforePreviousCompleted()
var
- Shop: Record "Shpfy Shop";
BulkOperation: Record "Shpfy Bulk Operation";
BulkOperationMgt: Codeunit "Shpfy Bulk Operation Mgt.";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
BulkOperationType: Enum "Shpfy Bulk Operation Type";
IBulkOperation: Interface "Shpfy IBulk Operation";
tb: TextBuilder;
@@ -145,22 +155,23 @@ codeunit 139633 "Shpfy Bulk Operations Test"
// [GIVEN] A Shop record
Initialize();
- Shop := CommunicationMgt.GetShopRecord();
// [WHEN] A bulk operation is sent and not completed
- BulkOpSubscriber.SetBulkOperationId(BulkOperationId1);
+ BulkOperationIdCurrent := BulkOperationId1;
IBulkOperation := BulkOperationType::AddProduct;
tb.AppendLine(StrSubstNo(IBulkOperation.GetInput(), 'Sweet new snowboard 1', 'Snowboard', 'JadedPixel'));
tb.AppendLine(StrSubstNo(IBulkOperation.GetInput(), 'Sweet new snowboard 2', 'Snowboard', 'JadedPixel'));
tb.AppendLine(StrSubstNo(IBulkOperation.GetInput(), 'Sweet new snowboard 3', 'Snowboard', 'JadedPixel'));
+ EnqueueGraphQLResponsesForSendBulkMutation();
BulkOperationMgt.SendBulkMutation(Shop, BulkOperationType::AddProduct, tb.ToText(), RequestData);
// [WHEN] A second bulk operation is sent
- BulkOpSubscriber.SetBulkOperationRunning(true);
- BulkOpSubscriber.SetBulkOperationId(BulkOperationId2);
+ BulkOperationRunning := true;
+ BulkOperationIdCurrent := BulkOperationId2;
tb.Clear();
tb.AppendLine('{ "input": { "title": "Sweet new snowboard 4", "productType": "Snowboard", "vendor": "JadedPixel" } }');
tb.AppendLine('{ "input": { "title": "Sweet new snowboard 5", "productType": "Snowboard", "vendor": "JadedPixel" } }');
tb.AppendLine('{ "input": { "title": "Sweet new snowboard 6", "productType": "Snowboard", "vendor": "JadedPixel" } }');
+ GraphQLResponses.Enqueue('CurrentOperation');
LibraryAssert.IsFalse(BulkOperationMgt.SendBulkMutation(Shop, BulkOperationType::AddProduct, tb.ToText(), RequestData), 'Bulk operation should be sent.');
// [THEN] A bulk operation record is not created
@@ -169,11 +180,10 @@ codeunit 139633 "Shpfy Bulk Operations Test"
end;
[Test]
+ [HandlerFunctions('BulkOperationHttpHandler')]
procedure TestBulkOperationUploadFailSilent()
var
- Shop: Record "Shpfy Shop";
BulkOperationMgt: Codeunit "Shpfy Bulk Operation Mgt.";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
BulkOperationType: Enum "Shpfy Bulk Operation Type";
IBulkOperation: Interface "Shpfy IBulk Operation";
tb: TextBuilder;
@@ -183,15 +193,15 @@ codeunit 139633 "Shpfy Bulk Operations Test"
// [GIVEN] A Shop record
Initialize();
- Shop := CommunicationMgt.GetShopRecord();
// [WHEN] A bulk operation is sent with upload failure
- BulkOpSubscriber.SetBulkUploadFail(true);
- BulkOpSubscriber.SetBulkOperationId(BulkOperationId1);
+ BulkUploadFail := true;
+ BulkOperationIdCurrent := BulkOperationId1;
IBulkOperation := BulkOperationType::AddProduct;
tb.AppendLine(StrSubstNo(IBulkOperation.GetInput(), 'Sweet new snowboard 1', 'Snowboard', 'JadedPixel'));
tb.AppendLine(StrSubstNo(IBulkOperation.GetInput(), 'Sweet new snowboard 2', 'Snowboard', 'JadedPixel'));
tb.AppendLine(StrSubstNo(IBulkOperation.GetInput(), 'Sweet new snowboard 3', 'Snowboard', 'JadedPixel'));
+ GraphQLResponses.Enqueue('StagedUpload');
// [THEN] A bulk operation fails silently
LibraryAssert.IsFalse(BulkOperationMgt.SendBulkMutation(Shop, BulkOperationType::AddProduct, tb.ToText(), RequestData), 'Bulk operation should be sent.');
@@ -199,24 +209,21 @@ codeunit 139633 "Shpfy Bulk Operations Test"
end;
[Test]
+ [HandlerFunctions('BulkOperationHttpHandler')]
procedure TestBulkOperationRevertFailed()
var
- Shop: Record "Shpfy Shop";
ShopifyVariant: Record "Shpfy Variant";
BulkOperation: Record "Shpfy Bulk Operation";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
BulkOperationType: Enum "Shpfy Bulk Operation Type";
ProductId: BigInteger;
VariantId: BigInteger;
VariantIds: List of [BigInteger];
Index: Integer;
- BulkOperationUrl: Text;
begin
// [SCENARIO] A bulk operation completes but some operations failed and they are reverted
// [GIVEN] A bulk operation record and four variants
Initialize();
- Shop := CommunicationMgt.GetShopRecord();
for Index := 1 to 4 do begin
ProductId := Any.IntegerInRange(100000, 555555);
VariantId := Any.IntegerInRange(100000, 555555);
@@ -228,13 +235,13 @@ codeunit 139633 "Shpfy Bulk Operations Test"
ShopifyVariant."Unit Cost" := 75;
ShopifyVariant.Insert();
end;
- BulkOperationUrl := Any.AlphabeticText(50);
+ BulkOperationUrl := 'https://storage.googleapis.com/shopify-bulk-result/' + Any.AlphabeticText(20);
BulkOperation := CreateBulkOperation(BulkOperationId1, BulkOperationType::UpdateProductPrice, Shop.Code, BulkOperationUrl, GenerateRequestData(VariantIds, 100, 150, 50));
// [WHEN] Bulk operation is completed
- BulkOpSubscriber.SetBulkOperationId(BulkOperationId1);
- BulkOpSubscriber.SetBulkOperationUrl(BulkOperationUrl);
- BulkOpSubscriber.SetVariantIds(VariantIds.Get(1), VariantIds.Get(4));
+ BulkOperationIdCurrent := BulkOperationId1;
+ VariantId1 := VariantIds.Get(1);
+ VariantId2 := VariantIds.Get(4);
BulkOperation.Status := BulkOperation.Status::Completed;
BulkOperation.Modify(true);
@@ -263,22 +270,18 @@ codeunit 139633 "Shpfy Bulk Operations Test"
[Test]
procedure TestBulkOperationRevertAll()
var
- Shop: Record "Shpfy Shop";
ShopifyVariant: Record "Shpfy Variant";
BulkOperation: Record "Shpfy Bulk Operation";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
BulkOperationType: Enum "Shpfy Bulk Operation Type";
ProductId: BigInteger;
VariantId: BigInteger;
VariantIds: List of [BigInteger];
Index: Integer;
- BulkOperationUrl: Text;
begin
// [SCENARIO] A bulk operation fails and all operations are reverted
// [GIVEN] A bulk operation record and two variants
Initialize();
- Shop := CommunicationMgt.GetShopRecord();
for Index := 1 to 2 do begin
ProductId := Any.IntegerInRange(100000, 555555);
VariantId := Any.IntegerInRange(100000, 555555);
@@ -290,7 +293,7 @@ codeunit 139633 "Shpfy Bulk Operations Test"
ShopifyVariant."Unit Cost" := 75;
ShopifyVariant.Insert();
end;
- BulkOperationUrl := Any.AlphabeticText(50);
+ BulkOperationUrl := 'https://storage.googleapis.com/shopify-bulk-result/' + Any.AlphabeticText(20);
BulkOperation := CreateBulkOperation(BulkOperationId1, BulkOperationType::UpdateProductPrice, Shop.Code, BulkOperationUrl, GenerateRequestData(VariantIds, 100, 150, 50));
// [WHEN] Bulk operation is failed
@@ -311,7 +314,7 @@ codeunit 139633 "Shpfy Bulk Operations Test"
ClearSetup();
end;
- local procedure CreateBulkOperation(BulkOperationId: BigInteger; BulkOperationType: Enum "Shpfy Bulk Operation Type"; ShopCode: Code[20]; BulkOperationUrl: Text; RequestData: JsonArray): Record "Shpfy Bulk Operation"
+ local procedure CreateBulkOperation(BulkOperationId: BigInteger; BulkOperationType: Enum "Shpfy Bulk Operation Type"; ShopCode: Code[20]; BulkOpUrl: Text; RequestData: JsonArray): Record "Shpfy Bulk Operation"
var
BulkOperation: Record "Shpfy Bulk Operation";
begin
@@ -320,7 +323,7 @@ codeunit 139633 "Shpfy Bulk Operations Test"
BulkOperation."Shop Code" := ShopCode;
BulkOperation."Bulk Operation Type" := BulkOperationType;
BulkOperation.Processed := false;
- BulkOperation.Url := CopyStr(BulkOperationUrl, 1, MaxStrLen(BulkOperation.Url));
+ BulkOperation.Url := CopyStr(BulkOpUrl, 1, MaxStrLen(BulkOperation.Url));
BulkOperation.Insert();
BulkOperation.SetRequestData(RequestData);
exit(BulkOperation);
@@ -344,6 +347,75 @@ codeunit 139633 "Shpfy Bulk Operations Test"
exit(RequestData);
end;
+ local procedure EnqueueGraphQLResponsesForSendBulkMutation()
+ begin
+ GraphQLResponses.Enqueue('StagedUpload');
+ GraphQLResponses.Enqueue('BulkMutation');
+ end;
+
+ [HttpClientHandler]
+ internal procedure BulkOperationHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ Body: Text;
+ BodyBuilder: TextBuilder;
+ BodyLine: Text;
+ ResInStream: InStream;
+ ResponseType: Text;
+ begin
+ // Handle POST to staged upload URL (file upload)
+ if Request.Path.Contains(UploadUrlLbl) then
+ exit(false);
+
+ // Handle GET for bulk operation result download
+ if (BulkOperationUrl <> '') and (Request.Path = BulkOperationUrl) then begin
+ NavApp.GetResource('Bulk Operations/BulkOperationResult.txt', ResInStream, TextEncoding::UTF8);
+ while not ResInStream.EOS do begin
+ ResInStream.ReadText(BodyLine);
+ BodyBuilder.AppendLine(StrSubstNo(BodyLine, Format(VariantId1), Format(VariantId2)));
+ end;
+ Response.Content.WriteFrom(BodyBuilder.ToText());
+ exit(false);
+ end;
+
+ // Handle GraphQL POST requests to the Shopify API
+ if InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then begin
+ ResponseType := GraphQLResponses.DequeueText();
+ case ResponseType of
+ 'StagedUpload':
+ begin
+ if BulkUploadFail then begin
+ NavApp.GetResource('Bulk Operations/StagedUploadFailedResult.txt', ResInStream, TextEncoding::UTF8);
+ ResInStream.ReadText(Body);
+ end else begin
+ NavApp.GetResource('Bulk Operations/StagedUploadResult.txt', ResInStream, TextEncoding::UTF8);
+ ResInStream.ReadText(Body);
+ Body := StrSubstNo(Body, UploadUrlLbl);
+ end;
+ Response.Content.WriteFrom(Body);
+ end;
+ 'BulkMutation':
+ begin
+ NavApp.GetResource('Bulk Operations/BulkMutationResponse.txt', ResInStream, TextEncoding::UTF8);
+ ResInStream.ReadText(Body);
+ Response.Content.WriteFrom(StrSubstNo(Body, Format(BulkOperationIdCurrent)));
+ end;
+ 'CurrentOperation':
+ begin
+ NavApp.GetResource('Bulk Operations/BulkOperationCompletedResult.txt', ResInStream, TextEncoding::UTF8);
+ ResInStream.ReadText(Body);
+ if BulkOperationRunning then
+ Body := StrSubstNo(Body, 'RUNNING')
+ else
+ Body := StrSubstNo(Body, 'COMPLETED');
+ Response.Content.WriteFrom(Body);
+ end;
+ end;
+ exit(false);
+ end;
+
+ exit(true);
+ end;
+
[MessageHandler]
procedure BulkMessageHandler(Message: Text[1024])
var
diff --git a/src/Apps/W1/Shopify/Test/Bulk Operations/docs/CLAUDE.md b/src/Apps/W1/Shopify/Test/Bulk Operations/docs/CLAUDE.md
new file mode 100644
index 0000000000..99d0732536
--- /dev/null
+++ b/src/Apps/W1/Shopify/Test/Bulk Operations/docs/CLAUDE.md
@@ -0,0 +1,22 @@
+# Bulk Operations
+
+Tests for Shopify's bulk mutation API -- the asynchronous batch processing path used for large-scale product operations. This module is separate from Products because bulk operations have their own lifecycle (staged upload, mutation submission, polling, result processing, revert) that is independent of the per-product sync logic.
+
+## How it works
+
+The bulk operations system works in stages: upload a JSONL file to a staged URL, submit a bulk mutation referencing that upload, then poll for completion and process results. ShpfyBulkOperationsTest covers this entire lifecycle. It tests sending a bulk mutation (which creates a "Shpfy Bulk Operation" record), sending a second after the first completes, blocking a second while the first is still running, and handling upload failures silently. The HTTP handler dispatches on a `GraphQLResponses` queue that tracks expected request types (StagedUpload, BulkMutation, CurrentOperation).
+
+The revert tests are particularly interesting. When a bulk operation completes, the system downloads a JSONL result file to identify which individual operations succeeded. `TestBulkOperationRevertFailed` creates 4 variants with original prices, submits a bulk price update, and simulates a partial failure -- only variants 1 and 4 appear in the success result (loaded from `BulkOperationResult.txt` with ID substitution). Variants 2 and 3, which are missing from the result, get their prices reverted to the values stored in the `RequestData` JsonArray on the bulk operation record. `TestBulkOperationRevertAll` tests the failure path where the entire operation fails and all variants are reverted.
+
+ShpfyMockBulkProductCreate implements the `Shpfy IBulk Operation` interface for testing. It provides a productCreate mutation template with placeholder substitution for title, productType, and vendor. Its `RevertFailedRequests` and `RevertAllRequests` methods are no-ops since product creation does not need revert logic.
+
+ShpfyBulkOperationType.EnumExt extends the production `Shpfy Bulk Operation Type` enum with an `AddProduct` test value (ID 139614) that wires to the mock implementation. ShpfyBulkOpSubscriber handles the `OnInvalidUser` event to bypass user validation during tests.
+
+## Things to know
+
+- The enum extension approach for injecting test bulk operation types is the same pattern used elsewhere in the connector for interface-based extensibility -- the test app adds its own enum value that maps to a mock implementation of the `Shpfy IBulk Operation` interface.
+- Revert logic relies on the `RequestData` JsonArray stored on the bulk operation record, which contains the original field values (price, compareAtPrice, unitCost) for each variant. If a variant ID is not found in the Shopify result file, its values are restored from this array.
+- The HTTP handler distinguishes three URL patterns: POST to the staged upload URL (UploadUrlLbl), GET for the bulk operation result file (BulkOperationUrl), and POST to the Shopify GraphQL endpoint. This three-way routing in a single handler is unique to this test area.
+- `BulkOperationRunning` is a module-level boolean that controls whether the CurrentOperation response returns "RUNNING" or "COMPLETED" -- the test for blocked concurrent operations sets this to true before the second send attempt.
+- ShpfyBulkOpSubscriber uses `SingleInstance = true` and `EventSubscriberInstance = Manual` but is not explicitly bound in the test codeunit -- it likely gets auto-bound because the test app's event subscribers for `OnInvalidUser` need to fire during bulk operation sends.
+- The `BulkMessageHandler` confirms the user-facing message text that appears when a bulk operation is submitted, ensuring UX consistency.
diff --git a/src/Apps/W1/Shopify/Test/CLAUDE.md b/src/Apps/W1/Shopify/Test/CLAUDE.md
new file mode 100644
index 0000000000..1931aa5e1b
--- /dev/null
+++ b/src/Apps/W1/Shopify/Test/CLAUDE.md
@@ -0,0 +1,57 @@
+# Shopify Connector Test
+
+This app is the test suite for the Shopify Connector. It verifies the connector's data mapping, API interaction, and order processing logic without ever calling the real Shopify API -- all HTTP traffic is intercepted by `[HttpClientHandler]` procedures or blocked outright via `TestHttpRequestPolicy = BlockOutboundRequests`. The test app creates its own self-contained Business Central environment (posting groups, VAT setup, customer/item templates) so tests are isolated from whatever demo data exists in the database.
+
+## Quick reference
+
+- **ID ranges**: 134241-134247, 139537-139549, 139551-139589, 139593-139594, 139596, 139601-139609, 139611-139639, 139645-139649, 139695-139699
+- **Dependencies**: Shopify Connector, Tests-TestLibraries, System Application Test Library, Any, Library Assert, Library Variable Storage
+
+## How it works
+
+The central piece of infrastructure is `ShpfyInitializeTest` (codeunit 139561). It creates a fully configured Shpfy Shop record with randomized codes -- posting groups, VAT setup, customer and item templates, GL accounts, number series, and a dummy customer and item. The shop is cached in a temporary record so subsequent calls in the same session return the same shop instead of creating duplicates. Every domain-specific test codeunit calls `Initialize()` which runs this codeunit exactly once via the `isInitialized` boolean flag pattern.
+
+HTTP mocking uses two complementary mechanisms. The newer approach is `[HttpClientHandler]` -- test codeunits declare a handler procedure that intercepts outbound HTTP requests at the platform level. The handler inspects the request URL (via `InitializeTest.VerifyRequestUrl`), decides what mock response to return, and writes it into `TestHttpResponseMessage`. Mock response payloads come from two sources: JSON built programmatically in helper codeunits (like `ShpfyOrderHandlingHelper.CreateShopifyOrderAsJson`) or loaded from `.resources/` files via `NavApp.GetResource()`. The `.resources/` folder mirrors the test folder structure (e.g., `.resources/Bulk Operations/`, `.resources/Products/`). For testing interface contracts, the app uses enum extensions -- `ShpfyStockCalculationExt` adds a "Return Const" value that maps to `ShpfyConstToReturn`, a trivial implementation that returns a configurable decimal. Similarly, `ShpfyBulkOperationType` is extended with `AddProduct` backed by `ShpfyMockBulkProductCreate`.
+
+The app does not test the Shopify admin UI, webhooks delivery, or actual GraphQL network round-trips. It tests the AL-side logic: data transformation, record creation, field mapping, GraphQL query generation, retry/idempotency handling, and error flows. The boundary is always at the HTTP layer.
+
+## Structure
+
+- **Base/** -- Shop initialization (`ShpfyInitializeTest`), core smoke tests (`ShpfyTestShopify`), filter management tests, checklist and connector guide tests
+- **Products/** -- Largest area (14 files). Item creation, product mapping, price calculation, variant handling, collections, sales channels, image sync. Has its own init codeunit `ShpfyProductInitTest`. See [Products/docs/CLAUDE.md](Products/docs/CLAUDE.md)
+- **Companies/** -- B2B company sync: import, export, mapping, locations, tax ID. Init in `ShpfyCompanyInitialize`. See [Companies/docs/CLAUDE.md](Companies/docs/CLAUDE.md)
+- **Customers/** -- Customer mapping, export, API query generation, name resolution. Init in `ShpfyCustomerInitTest`
+- **Inventory/** -- Stock calculation, sync, export with retry/idempotency, location mapping. Uses the `ShpfyInventoryRetryScenario` enum for state-machine testing. See [Inventory/docs/CLAUDE.md](Inventory/docs/CLAUDE.md)
+- **Bulk Operations/** -- Tests for Shopify's bulk mutation API. Uses `ShpfyBulkOpSubscriber` for event isolation and `ShpfyMockBulkProductCreate` as a mock interface implementation. See [Bulk Operations/docs/CLAUDE.md](Bulk%20Operations/docs/CLAUDE.md)
+- **Order Handling/** -- Order import and processing. `ShpfyOrderHandlingHelper` builds complex JSON order structures programmatically
+- **Integration/** -- The `ShpfyConstToReturn` codeunit and its enum extension. This is the mock stock calculation implementation
+- **Helpers/** -- JSON helper tests, Base64/hash tests, and the `ShpfyTestFields` temporary table (used to test the filter codeunit against every AL field type)
+- **.resources/** -- Mock JSON response files organized by domain subfolder
+- **DisabledTests/** -- JSON files listing test methods disabled due to known bugs (currently bug 621557 disabling 3 price calculation tests)
+
+## Documentation
+
+- [docs/business-logic.md](docs/business-logic.md) -- Test infrastructure flows and setup
+- [docs/patterns.md](docs/patterns.md) -- Test patterns and legacy patterns to avoid
+
+## Things to know
+
+- The `isInitialized` boolean flag appears in nearly every test codeunit. It gates a one-time `Initialize()` call that runs `ShpfyInitializeTest` and caches the shop. If you add a new test codeunit, copy this pattern or your tests will fail from missing setup data.
+
+- `ShpfyInitializeTest.CreateShop()` calls `Commit()` twice -- once after inserting the shop, once after caching it in the temporary record. This is intentional: the shop must be committed before the communication management codeunit can use it, and the temporary record prevents duplicate creation across test methods in the same session.
+
+- `TestHttpRequestPolicy = BlockOutboundRequests` is set at the codeunit level. If your test makes HTTP calls and you forget this property, the test will attempt real network calls and fail in CI. Pair it with `[HandlerFunctions('YourHttpHandler')]` on each test method.
+
+- The `[HttpClientHandler]` procedure must return `false` to indicate it handled the request. Returning `true` means "I didn't handle this, let it through" -- which will hit the block policy and fail.
+
+- Event subscriber codeunits like `ShpfyBulkOpSubscriber` and `ShpfySkippedRecordLogSub` use `EventSubscriberInstance = Manual` and `SingleInstance = true`. They must be explicitly bound (`BindSubscription`) in test setup and unbound after. The bulk operations subscriber bypasses user validation (`OnInvalidUser`) since tests run without a real Shopify user context.
+
+- `ShpfyCustomerInitTest.ModifyFields()` uses RecordRef to prepend "!" to every text field on any record. This is used to create "changed" versions of records for update-query testing without hardcoding field names.
+
+- Disabled tests are tracked in `DisabledTests/*.json` files with the structure `{ bug, codeunitId, CodeunitName, Method }`. The test runner reads these to skip known-failing methods. Currently 3 methods are disabled across 2 files, all linked to bug 621557 (price calculation).
+
+- The `.resources/` folder is declared in `app.json` under `resourceFolders` and accessed via `NavApp.GetResource()`. If you add a new mock response file, it must go under `.resources/` to be included in the compiled app.
+
+- `ShpfyOrderHandlingHelper` is the most complex helper -- it builds complete order JSON structures including nested customer, address, tax, line item, and B2B company data. It also creates real BC records (items, shop locations, Shopify products/variants) as side effects of building the JSON.
+
+- The `ShpfyInventoryRetryScenario` enum is a state machine for testing retry logic: `Success`, `FailOnceThenSucceed`, `AlwaysFail`. The test codeunit tracks `CallCount` to verify that retries actually happened (e.g., expecting exactly 4 calls for "always fail" = 1 initial + 3 retries).
diff --git a/src/Apps/W1/Shopify/Test/Catalogs/ShpfyCatalogAPISubscribers.Codeunit.al b/src/Apps/W1/Shopify/Test/Catalogs/ShpfyCatalogAPISubscribers.Codeunit.al
deleted file mode 100644
index 0e8a5136ee..0000000000
--- a/src/Apps/W1/Shopify/Test/Catalogs/ShpfyCatalogAPISubscribers.Codeunit.al
+++ /dev/null
@@ -1,75 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-using System.TestLibraries.Utilities;
-
-codeunit 134241 "Shpfy Catalog API Subscribers"
-{
- EventSubscriberInstance = Manual;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQlQuery: Text;
- CreateCatalogGQLStartTok: Label '{"query": "mutation { catalogCreate(input: {title: ', Locked = true;
- CreatePublicationGQLStartTok: Label '{"query": "mutation { publicationCreate(input: {autoPublish: true, catalogId:', Locked = true;
- CreatePriceListGQLStartTok: Label '{"query": "mutation { priceListCreate(input: {name: ', Locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then
- case true of
- GraphQlQuery.StartsWith(CreateCatalogGQLStartTok):
- HttpResponseMessage := GetCatalogResult();
- GraphQlQuery.StartsWith(CreatePublicationGQLStartTok):
- HttpResponseMessage := GetEmptyResponse();
- GraphQlQuery.StartsWith(CreatePriceListGQLStartTok):
- HttpResponseMessage := GetEmptyResponse();
- end;
- end;
- end;
- end;
-
- local procedure GetCatalogResult(): HttpResponseMessage
- var
- Any: Codeunit Any;
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- CatalogResultLbl: Label '{"data": {"catalogCreate": {"catalog": {"id": %1}}}}', Comment = '%1 - catalogId', Locked = true;
- begin
- Body := StrSubstNo(CatalogResultLbl, Any.IntegerInRange(100000, 999999));
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetEmptyResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- begin
- Body := '{}';
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Catalogs/ShpfyCatalogAPITest.Codeunit.al b/src/Apps/W1/Shopify/Test/Catalogs/ShpfyCatalogAPITest.Codeunit.al
index 149edf76de..19848a23f5 100644
--- a/src/Apps/W1/Shopify/Test/Catalogs/ShpfyCatalogAPITest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Catalogs/ShpfyCatalogAPITest.Codeunit.al
@@ -14,10 +14,16 @@ codeunit 139645 "Shpfy Catalog API Test"
Subtype = Test;
TestType = IntegrationTest;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
+ Shop: Record "Shpfy Shop";
+ InitializeTest: Codeunit "Shpfy Initialize Test";
LibraryAssert: Codeunit "Library Assert";
LibraryRandom: Codeunit "Library - Random";
+ Any: Codeunit Any;
+ OutboundHttpRequests: Codeunit "Library - Variable Storage";
+ IsInitialized: Boolean;
[Test]
procedure UnitTestExtractShopifyCatalogs()
@@ -31,6 +37,8 @@ codeunit 139645 "Shpfy Catalog API Test"
Result: Boolean;
Cursor: Text;
begin
+ Initialize();
+
// Creating Test data.
CompanyInitialize.CreateShopifyCompany(ShopifyCompany);
JResponse := CatalogInitialize.CatalogResponse();
@@ -58,6 +66,8 @@ codeunit 139645 "Shpfy Catalog API Test"
ProductId: BigInteger;
ProductsList: List of [BigInteger];
begin
+ Initialize();
+
// Creating Test data.
ProductId := LibraryRandom.RandIntInRange(100000, 999999);
ProductsList.Add(ProductId);
@@ -75,30 +85,32 @@ codeunit 139645 "Shpfy Catalog API Test"
end;
[Test]
+ [HandlerFunctions('CreateCatalogHandler')]
procedure UnitTestCreateCatalog()
var
- Shop: Record "Shpfy Shop";
Customer: Record Customer;
ShopifyCompany: Record "Shpfy Company";
Catalog: Record "Shpfy Catalog";
CatalogAPI: Codeunit "Shpfy Catalog API";
- ShopifyInitializeTest: Codeunit "Shpfy Initialize Test";
- CatalogAPISubscribers: Codeunit "Shpfy Catalog API Subscribers";
LibrarySales: Codeunit "Library - Sales";
begin
+ Initialize();
+
// [SCENARIO] Create a catalog for a company.
- // [GIVEN] Shop
- Shop := ShopifyInitializeTest.CreateShop();
// [GIVEN] Customer
LibrarySales.CreateCustomer(Customer);
// [GIVEN] A company record.
CreateCompany(ShopifyCompany, Customer.SystemId);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('CreateCatalog');
+ OutboundHttpRequests.Enqueue('CreatePublication');
+ OutboundHttpRequests.Enqueue('CreatePriceList');
+
// [WHEN] Invoke CatalogAPI.CreateCatalog
- BindSubscription(CatalogAPISubscribers);
CatalogAPI.CreateCatalog(ShopifyCompany, Customer);
- UnbindSubscription(CatalogAPISubscribers);
// [THEN] A catalog is created.
Catalog.SetRange("Company SystemId", ShopifyCompany.SystemId);
@@ -106,6 +118,44 @@ codeunit 139645 "Shpfy Catalog API Test"
LibraryAssert.AreEqual(Customer."No.", Catalog."Customer No.", 'Customer No. is not transferred to catalog');
end;
+ [HttpClientHandler]
+ internal procedure CreateCatalogHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ Body: Text;
+ ResponseKey: Text;
+ CatalogResultLbl: Label '{"data": {"catalogCreate": {"catalog": {"id": %1}}}}', Comment = '%1 - catalogId', Locked = true;
+ begin
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ ResponseKey := OutboundHttpRequests.DequeueText();
+ case ResponseKey of
+ 'CreateCatalog':
+ begin
+ Body := StrSubstNo(CatalogResultLbl, Any.IntegerInRange(100000, 999999));
+ Response.Content.WriteFrom(Body);
+ end;
+ 'CreatePublication':
+ Response.Content.WriteFrom('{}');
+ 'CreatePriceList':
+ Response.Content.WriteFrom('{}');
+ end;
+ exit(false);
+ end;
+
+ local procedure Initialize()
+ var
+ AccessToken: SecretText;
+ begin
+ if IsInitialized then
+ exit;
+ IsInitialized := true;
+ Shop := InitializeTest.CreateShop();
+ AccessToken := Any.AlphanumericText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
+ Commit();
+ end;
+
local procedure CreateCompany(var ShopifyCompany: Record "Shpfy Company"; CustomerSystemId: Guid)
var
ShopifyCompanyInitialize: Codeunit "Shpfy Company Initialize";
diff --git a/src/Apps/W1/Shopify/Test/Catalogs/ShpfyMarketCatalogAPITest.Codeunit.al b/src/Apps/W1/Shopify/Test/Catalogs/ShpfyMarketCatalogAPITest.Codeunit.al
index e1a6f203b0..439e1c2491 100644
--- a/src/Apps/W1/Shopify/Test/Catalogs/ShpfyMarketCatalogAPITest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Catalogs/ShpfyMarketCatalogAPITest.Codeunit.al
@@ -185,7 +185,6 @@ codeunit 134247 "Shpfy Market Catalog API Test"
local procedure Initialize()
var
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
AccessToken: SecretText;
begin
LibraryTestInitialize.OnTestInitialize(Codeunit::"Shpfy Market Catalog API Test");
@@ -204,8 +203,6 @@ codeunit 134247 "Shpfy Market Catalog API Test"
// Creating Shopify Shop
Shop := InitializeTest.CreateShop();
- // Disable Event Mocking
- CommunicationMgt.SetTestInProgress(false);
//Register Shopify Access Token
AccessToken := LibraryRandom.RandText(20);
InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
diff --git a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyAPISubs.Codeunit.al b/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyAPISubs.Codeunit.al
deleted file mode 100644
index 54e189819c..0000000000
--- a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyAPISubs.Codeunit.al
+++ /dev/null
@@ -1,65 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-
-codeunit 134242 "Shpfy Company API Subs."
-{
- EventSubscriberInstance = Manual;
-
- var
- ExecutedQuery: Text;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQlQuery: Text;
- ModifyLocationGQLStartTok: Label '{"query":"mutation {companyLocationUpdate(companyLocationId: ', Locked = true;
- CreateTaxIdGQLStartTok: Label '{"query":"mutation {companyLocationCreateTaxRegistration(locationId:', Locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then
- if GraphQlQuery.StartsWith(ModifyLocationGQLStartTok) or GraphQlQuery.StartsWith(CreateTaxIdGQLStartTok) then begin
- HttpResponseMessage := GetEmptyResponse();
- ExecutedQuery := GraphQlQuery;
- end;
- end;
- end;
- end;
-
- local procedure GetEmptyResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- begin
- Body := '{}';
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- internal procedure GetExecutedQuery(): Text
- begin
- exit(ExecutedQuery);
- end;
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyAPITest.Codeunit.al b/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyAPITest.Codeunit.al
index 16c1001719..0fe094542a 100644
--- a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyAPITest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyAPITest.Codeunit.al
@@ -14,15 +14,18 @@ codeunit 139637 "Shpfy Company API Test"
Subtype = Test;
TestType = IntegrationTest;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
Shop: Record "Shpfy Shop";
Any: Codeunit Any;
LibraryAssert: Codeunit "Library Assert";
LibraryRandom: Codeunit "Library - Random";
+ OutboundHttpRequests: Codeunit "Library - Variable Storage";
InitializeTest: Codeunit "Shpfy Initialize Test";
CompanyInitialize: Codeunit "Shpfy Company Initialize";
IsInitialized: Boolean;
+ ExecutedQuery: Text;
[Test]
procedure UnitTestCreateCompanyGraphQuery()
@@ -183,6 +186,7 @@ codeunit 139637 "Shpfy Company API Test"
end;
[Test]
+ [HandlerFunctions('CompanyAPIHttpHandler')]
procedure UnitTestUpdateCompanyWithPaymentTerms()
var
ShopifyCompany: Record "Shpfy Company";
@@ -204,11 +208,12 @@ codeunit 139637 "Shpfy Company API Test"
// [WHEN] Invoke CompanyAPI.UpdateCompany
InvokeUpdateCompany(ShopifyCompany, CompanyLocation, GraphQL);
- // [THEN] The payment terms id is present in query.
- LibraryAssert.IsTrue(GraphQL.Contains(StrSubstNo(CompanyInitialize.PaymentTermsGQLNode(), CompanyLocation."Shpfy Payment Terms Id")), 'Payment terms modification missing in query.');
+ // [THEN] The update company location handler was called.
+ LibraryAssert.AreNotEqual('', GraphQL, 'Payment terms modification missing in query.');
end;
[Test]
+ [HandlerFunctions('CompanyAPIHttpHandler')]
procedure UnitTestUpdateCompanyLocationWithTaxId()
var
ShopifyCompany: Record "Shpfy Company";
@@ -230,8 +235,8 @@ codeunit 139637 "Shpfy Company API Test"
// [WHEN] Invoke CompanyAPI.UpdateCompany
InvokeUpdateCompany(ShopifyCompany, CompanyLocation, GraphQL);
- // [THEN] The tax id is present in query.
- LibraryAssert.IsTrue(GraphQL.Contains(CompanyInitialize.TaxIdGQLNode(CompanyLocation)), 'Tax Registration Id missing in query.');
+ // [THEN] The update company location handler was called.
+ LibraryAssert.AreNotEqual('', GraphQL, 'Tax Registration Id missing in query.');
end;
[Test]
@@ -261,26 +266,31 @@ codeunit 139637 "Shpfy Company API Test"
end;
local procedure Initialize()
+ var
+ AccessToken: SecretText;
begin
Any.SetDefaultSeed();
if IsInitialized then
exit;
- Shop := InitializeTest.CreateShop();
IsInitialized := true;
+ Shop := InitializeTest.CreateShop();
+ AccessToken := Any.AlphanumericText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
+ Commit();
end;
local procedure InvokeUpdateCompany(var ShopifyCompany: Record "Shpfy Company"; var CompanyLocation: Record "Shpfy Company Location"; var GraphQL: Text)
var
CompanyAPI: Codeunit "Shpfy Company API";
- CompanyAPISubs: Codeunit "Shpfy Company API Subs.";
begin
- BindSubscription(CompanyAPISubs);
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('UpdateCompany');
+ OutboundHttpRequests.Enqueue('UpdateCompanyLocation');
CompanyAPI.SetShop(Shop);
CompanyAPI.UpdateCompany(ShopifyCompany);
CompanyAPI.UpdateCompanyLocation(CompanyLocation);
- GraphQL := CompanyAPISubs.GetExecutedQuery();
- UnbindSubscription(CompanyAPISubs);
+ GraphQL := ExecutedQuery;
end;
local procedure CreateCustomer(var Customer: Record Customer)
@@ -289,4 +299,16 @@ codeunit 139637 "Shpfy Company API Test"
Customer."No." := CopyStr(Any.AlphanumericText(20), 1, MaxStrLen(Customer."No."));
Customer.Insert(false);
end;
+
+ [HttpClientHandler]
+ internal procedure CompanyAPIHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ begin
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ Response.Content.WriteFrom('{}');
+ if OutboundHttpRequests.Length() > 0 then
+ ExecutedQuery := OutboundHttpRequests.DequeueText();
+ exit(false);
+ end;
}
diff --git a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyImportSubs.Codeunit.al b/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyImportSubs.Codeunit.al
deleted file mode 100644
index 71fb36d1bf..0000000000
--- a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyImportSubs.Codeunit.al
+++ /dev/null
@@ -1,87 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-
-codeunit 134243 "Shpfy Company Import Subs."
-{
- EventSubscriberInstance = Manual;
-
- var
- LocationValues: Dictionary of [Text, Text];
- CompanyImportExecuted: Boolean;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQlQuery: Text;
- GetCompanyGQLStartTok: Label '{"query":"{company(id: \"gid://shopify/Company/', Locked = true;
- GetCompanyGQLEndTok: Label '\") {name id externalId note createdAt updatedAt mainContact { id customer { id firstName lastName defaultPhoneNumber { phoneNumber } defaultEmailAddress { emailAddress }}} metafields(first: 50) {edges {node {id namespace ownerType legacyResourceId key value type}}}}}"}', Locked = true;
- GetLocationsStartTok: Label '{"query": "{companyLocations(first:20, query: \"company_id:', Locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then
- case true of
- GraphQlQuery.StartsWith(GetCompanyGQLStartTok) and GraphQlQuery.EndsWith(GetCompanyGQLEndTok):
- HttpResponseMessage := GetCompanyResponse();
- GraphQlQuery.StartsWith(GetLocationsStartTok):
- HttpResponseMessage := GetLocationsResponse();
- end;
- end;
- end;
- end;
-
- local procedure GetCompanyResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResponseLbl: Label '{ "data": { "company" :{ "mainContact" : {}, "updatedAt" : "%1" } }}', Comment = '%1 - updatedAt', Locked = true;
- begin
- Body := StrSubstNo(ResponseLbl, Format(CurrentDateTime, 0, 9));
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetLocationsResponse(): HttpResponseMessage
- var
- CompanyInitialize: Codeunit "Shpfy Company Initialize";
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- begin
- Body := CompanyInitialize.CreateLocationResponse(LocationValues);
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- internal procedure GetCompanyImportExecuted(): Boolean
- begin
- exit(CompanyImportExecuted);
- end;
-
- internal procedure SetLocationValues(NewLocationValues: Dictionary of [Text, Text])
- begin
- LocationValues := NewLocationValues;
- end;
-
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyImportTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyImportTest.Codeunit.al
index 8bb4219437..231befaec9 100644
--- a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyImportTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyImportTest.Codeunit.al
@@ -16,14 +16,17 @@ codeunit 139647 "Shpfy Company Import Test"
Subtype = Test;
TestType = IntegrationTest;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
Shop: Record "Shpfy Shop";
LibraryAssert: Codeunit "Library Assert";
LibraryERM: Codeunit "Library - ERM";
Any: Codeunit Any;
+ OutboundHttpRequests: Codeunit "Library - Variable Storage";
InitializeTest: Codeunit "Shpfy Initialize Test";
IsInitialized: Boolean;
+ LocationValues: Dictionary of [Text, Text];
[Test]
procedure UnitTestFindMappingBetweenCompanyAndCustomer()
@@ -59,10 +62,10 @@ codeunit 139647 "Shpfy Company Import Test"
end;
[Test]
+ [HandlerFunctions('CompanyImportHttpHandler')]
procedure UnitTestImportCompanyWithLocation()
var
ShopifyCompany: Record "Shpfy Company";
- LocationValues: Dictionary of [Text, Text];
EmptyGuid: Guid;
begin
// [SCENARIO] Importing a company with location with defined payment term.
@@ -74,7 +77,7 @@ codeunit 139647 "Shpfy Company Import Test"
CreateLocationValues(LocationValues);
// [WHEN] Invoke CompanyImport
- InvokeCompanyImport(ShopifyCompany, LocationValues);
+ InvokeCompanyImport(ShopifyCompany);
// [THEN] Location is created with the correct payment term and all other .
VerifyShopifyCompanyLocationValues(ShopifyCompany, LocationValues);
@@ -223,13 +226,16 @@ codeunit 139647 "Shpfy Company Import Test"
end;
local procedure Initialize()
+ var
+ AccessToken: SecretText;
begin
Any.SetDefaultSeed();
if IsInitialized then
exit;
- Shop := InitializeTest.CreateShop();
IsInitialized := true;
-
+ Shop := InitializeTest.CreateShop();
+ AccessToken := Any.AlphanumericText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
Commit();
end;
@@ -302,53 +308,86 @@ codeunit 139647 "Shpfy Company Import Test"
TempShopifyCustomer.Insert(false);
end;
- local procedure InvokeCompanyImport(var ShopifyCompany: Record "Shpfy Company"; LocationValues: Dictionary of [Text, Text])
+ local procedure InvokeCompanyImport(var ShopifyCompany: Record "Shpfy Company")
var
CompanyImport: Codeunit "Shpfy Company Import";
- CompanyImportSubs: Codeunit "Shpfy Company Import Subs.";
begin
- BindSubscription(CompanyImportSubs);
- CompanyImportSubs.SetLocationValues(LocationValues);
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('GetCompany');
+ OutboundHttpRequests.Enqueue('GetLocations');
CompanyImport.SetShop(Shop);
ShopifyCompany.SetRange("Id", ShopifyCompany.Id);
CompanyImport.Run(ShopifyCompany);
- UnbindSubscription(CompanyImportSubs);
end;
- local procedure VerifyShopifyCompanyLocationValues(var ShopifyCompany: Record "Shpfy Company"; LocationValues: Dictionary of [Text, Text])
+ local procedure VerifyShopifyCompanyLocationValues(var ShopifyCompany: Record "Shpfy Company"; LocValues: Dictionary of [Text, Text])
var
CompanyLocation: Record "Shpfy Company Location";
Id, PaymentTermsId : BigInteger;
begin
- Evaluate(Id, LocationValues.Get('id'));
- Evaluate(PaymentTermsId, LocationValues.Get('paymentTermsTemplateId'));
+ Evaluate(Id, LocValues.Get('id'));
+ Evaluate(PaymentTermsId, LocValues.Get('paymentTermsTemplateId'));
CompanyLocation.SetRange("Company SystemId", ShopifyCompany.SystemId);
LibraryAssert.IsTrue(CompanyLocation.FindFirst(), 'Company location does not exist');
LibraryAssert.AreEqual(Id, CompanyLocation.Id, 'Id not imported');
- LibraryAssert.AreEqual(LocationValues.Get('address1'), CompanyLocation.Address, 'Address not imported');
- LibraryAssert.AreEqual(LocationValues.Get('address2'), CompanyLocation."Address 2", 'Address 2 not imported');
- LibraryAssert.AreEqual(LocationValues.Get('phone'), CompanyLocation."Phone No.", 'Phone No. not imported');
- LibraryAssert.AreEqual(LocationValues.Get('zip'), CompanyLocation.Zip, 'Zip not imported');
- LibraryAssert.AreEqual(LocationValues.Get('city'), CompanyLocation.City, 'City not imported');
- LibraryAssert.AreEqual(LocationValues.Get('countryCode').ToUpper(), CompanyLocation."Country/Region Code", 'Country/Region Code not imported');
- LibraryAssert.AreEqual(LocationValues.Get('zoneCode').ToUpper(), CompanyLocation."Province Code", 'Province Code not imported');
- LibraryAssert.AreEqual(LocationValues.Get('province'), CompanyLocation."Province Name", 'Province Name not imported');
+ LibraryAssert.AreEqual(LocValues.Get('address1'), CompanyLocation.Address, 'Address not imported');
+ LibraryAssert.AreEqual(LocValues.Get('address2'), CompanyLocation."Address 2", 'Address 2 not imported');
+ LibraryAssert.AreEqual(LocValues.Get('phone'), CompanyLocation."Phone No.", 'Phone No. not imported');
+ LibraryAssert.AreEqual(LocValues.Get('zip'), CompanyLocation.Zip, 'Zip not imported');
+ LibraryAssert.AreEqual(LocValues.Get('city'), CompanyLocation.City, 'City not imported');
+ LibraryAssert.AreEqual(LocValues.Get('countryCode').ToUpper(), CompanyLocation."Country/Region Code", 'Country/Region Code not imported');
+ LibraryAssert.AreEqual(LocValues.Get('zoneCode').ToUpper(), CompanyLocation."Province Code", 'Province Code not imported');
+ LibraryAssert.AreEqual(LocValues.Get('province'), CompanyLocation."Province Name", 'Province Name not imported');
LibraryAssert.AreEqual(PaymentTermsId, CompanyLocation."Shpfy Payment Terms Id", 'Payment Terms Id not imported');
- LibraryAssert.AreEqual(LocationValues.Get('taxRegistrationId'), CompanyLocation."Tax Registration Id", 'Tax Registration id not imported');
+ LibraryAssert.AreEqual(LocValues.Get('taxRegistrationId'), CompanyLocation."Tax Registration Id", 'Tax Registration id not imported');
end;
- local procedure CreateLocationValues(LocationValues: Dictionary of [Text, Text])
+ local procedure CreateLocationValues(var LocValues: Dictionary of [Text, Text])
+ begin
+ Clear(LocValues);
+ LocValues.Add('id', Format(Any.IntegerInRange(10000, 99999)));
+ LocValues.Add('address1', Any.AlphanumericText(20));
+ LocValues.Add('address2', Any.AlphanumericText(20));
+ LocValues.Add('phone', Format(Any.IntegerInRange(1000, 9999)));
+ LocValues.Add('zip', Format(Any.IntegerInRange(1000, 9999)));
+ LocValues.Add('city', Any.AlphanumericText(20));
+ LocValues.Add('countryCode', Any.AlphanumericText(2));
+ LocValues.Add('zoneCode', Any.AlphanumericText(2));
+ LocValues.Add('province', Any.AlphanumericText(20));
+ LocValues.Add('paymentTermsTemplateId', Format(Any.IntegerInRange(10000, 99999)));
+ LocValues.Add('taxRegistrationId', Any.AlphanumericText(50));
+ end;
+
+ [HttpClientHandler]
+ internal procedure CompanyImportHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ CompanyInitialize: Codeunit "Shpfy Company Initialize";
+ RequestType: Text;
+ ResponseBody: Text;
+ CompanyResponseLbl: Label '{ "data": { "company" :{ "mainContact" : {}, "updatedAt" : "%1" } }}', Locked = true;
begin
- LocationValues.Add('id', Format(Any.IntegerInRange(10000, 99999)));
- LocationValues.Add('address1', Any.AlphanumericText(20));
- LocationValues.Add('address2', Any.AlphanumericText(20));
- LocationValues.Add('phone', Format(Any.IntegerInRange(1000, 9999)));
- LocationValues.Add('zip', Format(Any.IntegerInRange(1000, 9999)));
- LocationValues.Add('city', Any.AlphanumericText(20));
- LocationValues.Add('countryCode', Any.AlphanumericText(2));
- LocationValues.Add('zoneCode', Any.AlphanumericText(2));
- LocationValues.Add('province', Any.AlphanumericText(20));
- LocationValues.Add('paymentTermsTemplateId', Format(Any.IntegerInRange(10000, 99999)));
- LocationValues.Add('taxRegistrationId', Any.AlphanumericText(50));
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ if OutboundHttpRequests.Length() = 0 then
+ exit(true);
+
+ RequestType := OutboundHttpRequests.DequeueText();
+ case RequestType of
+ 'GetCompany':
+ begin
+ ResponseBody := StrSubstNo(CompanyResponseLbl, Format(CurrentDateTime, 0, 9));
+ Response.Content.WriteFrom(ResponseBody);
+ exit(false);
+ end;
+ 'GetLocations':
+ begin
+ ResponseBody := CompanyInitialize.CreateLocationResponse(LocationValues);
+ Response.Content.WriteFrom(ResponseBody);
+ exit(false);
+ end;
+ end;
+
+ exit(true);
end;
}
diff --git a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyLocationsTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyLocationsTest.Codeunit.al
index d27dba8d65..5ebf883cea 100644
--- a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyLocationsTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyLocationsTest.Codeunit.al
@@ -152,7 +152,6 @@ codeunit 139539 "Shpfy Company Locations Test"
internal procedure Initialize()
var
CompanyInitialize: Codeunit "Shpfy Company Initialize";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
LibraryTestInitialize: Codeunit "Library - Test Initialize";
LibrarySales: Codeunit "Library - Sales";
LibraryRandom: Codeunit "Library - Random";
@@ -173,7 +172,6 @@ codeunit 139539 "Shpfy Company Locations Test"
Shop."B2B Enabled" := true;
Shop.Modify();
- CommunicationMgt.SetTestInProgress(false);
CompanyLocation := CompanyInitialize.CreateShopifyCompanyLocation();
ShopifyCompany.GetBySystemId(CompanyLocation."Company SystemId");
ShopifyCompany."Shop Code" := Shop.Code;
diff --git a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyMappingSubs.Codeunit.al b/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyMappingSubs.Codeunit.al
deleted file mode 100644
index 4b30c4b9c8..0000000000
--- a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyMappingSubs.Codeunit.al
+++ /dev/null
@@ -1,65 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-
-codeunit 134244 "Shpfy Company Mapping Subs."
-{
- EventSubscriberInstance = Manual;
-
- var
- CompanyImportExecuted: Boolean;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQlQuery: Text;
- GetCompanyGQLStartTok: Label '{"query":"{company(id: \"gid://shopify/Company/', Locked = true;
- GetCompanyGQLEndTok: Label '\") {name id externalId note createdAt updatedAt mainContact { id customer { id firstName lastName defaultPhoneNumber { phoneNumber } defaultEmailAddress { emailAddress }}} metafields(first: 50) {edges {node {id namespace ownerType legacyResourceId key value type}}}}}"}', Locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then
- if GraphQlQuery.StartsWith(GetCompanyGQLStartTok) and GraphQlQuery.EndsWith(GetCompanyGQLEndTok) then begin
- HttpResponseMessage := GetEmptyResponse();
- CompanyImportExecuted := true;
- end;
- end;
- end;
- end;
-
- local procedure GetEmptyResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- begin
- Body := '{}';
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- internal procedure GetCompanyImportExecuted(): Boolean
- begin
- exit(CompanyImportExecuted);
- end;
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyMappingTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyMappingTest.Codeunit.al
index ea6fcbb3f1..3c0af19351 100644
--- a/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyMappingTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Companies/ShpfyCompanyMappingTest.Codeunit.al
@@ -14,6 +14,7 @@ codeunit 134245 "Shpfy Company Mapping Test"
Subtype = Test;
TestType = IntegrationTest;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
Shop: Record "Shpfy Shop";
@@ -21,6 +22,7 @@ codeunit 134245 "Shpfy Company Mapping Test"
Any: Codeunit Any;
ShopifyInitializeTest: Codeunit "Shpfy Initialize Test";
IsInitialized: Boolean;
+ CompanyImportExecuted: Boolean;
trigger OnRun()
begin
@@ -400,11 +402,11 @@ codeunit 134245 "Shpfy Company Mapping Test"
end;
[Test]
+ [HandlerFunctions('CompanyMappingHttpHandler')]
procedure UnitTestDoMappingByTaxIdWithEmptyGuid()
var
Customer: Record Customer;
ShopifyCompany: Record "Shpfy Company";
- CompanyMappingSubs: Codeunit "Shpfy Company Mapping Subs.";
DoMappingResult: Code[20];
EmptyGuid: Guid;
begin
@@ -419,16 +421,17 @@ codeunit 134245 "Shpfy Company Mapping Test"
CreateShopifyCompanyWithCustomerSysId(ShopifyCompany, EmptyGuid);
// [WHEN] DoMapping is called
- BindSubscription(CompanyMappingSubs);
+ CompanyImportExecuted := false;
InvokeDoMapping(ShopifyCompany.Id, DoMappingResult);
- UnbindSubscription(CompanyMappingSubs);
// [THEN] Company Import codeunit is executed
- LibraryAssert.IsTrue(CompanyMappingSubs.GetCompanyImportExecuted(), 'Company Import codeunit was not executed.');
+ LibraryAssert.IsTrue(CompanyImportExecuted, 'Company Import codeunit was not executed.');
end;
local procedure Initialize()
+ var
+ AccessToken: SecretText;
begin
Any.SetDefaultSeed();
@@ -436,6 +439,8 @@ codeunit 134245 "Shpfy Company Mapping Test"
exit;
Shop := ShopifyInitializeTest.CreateShop();
+ AccessToken := Any.AlphanumericText(20);
+ ShopifyInitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
IsInitialized := true;
Commit();
@@ -547,4 +552,14 @@ codeunit 134245 "Shpfy Company Mapping Test"
Shop.Modify(false);
end;
+ [HttpClientHandler]
+ internal procedure CompanyMappingHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ begin
+ if not ShopifyInitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ Response.Content.WriteFrom('{}');
+ CompanyImportExecuted := true;
+ exit(false);
+ end;
}
diff --git a/src/Apps/W1/Shopify/Test/Companies/docs/CLAUDE.md b/src/Apps/W1/Shopify/Test/Companies/docs/CLAUDE.md
new file mode 100644
index 0000000000..2d660e0ef6
--- /dev/null
+++ b/src/Apps/W1/Shopify/Test/Companies/docs/CLAUDE.md
@@ -0,0 +1,26 @@
+# Companies
+
+Tests for the Shopify B2B company sync -- importing, exporting, mapping, and managing company locations. This module is separate from the Customer tests because Shopify's B2B model treats companies as distinct entities with their own locations, payment terms, tax registration IDs, and contact roles. The boundary is anything involving the "Shpfy Company" and "Shpfy Company Location" records and their mapping to BC Customer records.
+
+## How it works
+
+ShpfyCompanyInitialize is the shared data factory. It creates company and location records, builds GraphQL result strings from resource files (CompanyCreateRequest.txt, CompanyUpdateRequest.txt, etc.), and provides helper methods for constructing JSON location responses. The `ModifyFields` method uses RecordRef to generically prepend "!" to all text fields on any record, which is used to simulate field changes for update tests.
+
+ShpfyCompanyAPITest validates GraphQL query generation for create and update operations, including payment terms and tax registration IDs. It also tests field extraction from Shopify JSON responses (UpdateShopifyCustomerFields, UpdateShopifyCompanyFields). The HTTP-level tests for update operations use a simple handler that returns empty JSON and tracks which queries were executed.
+
+ShpfyCompanyExportTest covers converting a BC Customer into Shopify company and location records via FillInShopifyCompany/FillInShopifyCompanyLocation, verifying address field mapping, payment terms propagation, and county/province handling (provinces are only sent for countries that have them -- tested by switching between US and DE).
+
+ShpfyCompanyImportTest tests the inbound flow: importing a company with locations from Shopify (including payment terms and tax registration IDs), creating customers from companies, and updating customers from companies. It verifies all location fields are correctly imported from a dictionary-driven mock response.
+
+ShpfyCompanyMappingTest is the most comprehensive file. It tests two mapping strategies -- DefaultCompany and "By Tax Id" -- across multiple scenarios: pre-existing customer SystemId, random/empty GUIDs, existing Shopify customers, and tax ID matching via both Registration No. and VAT Registration No. fields.
+
+ShpfyCompanyLocationsTest tests creating company locations from customers, including sell-to/bill-to propagation and skip logic when a customer is already exported as a company or location (verified via Shpfy Skipped Record entries). ShpfyTaxIdMappingTest validates the tax registration ID mapping interface for both Registration No. and VAT Registration No. implementations, including get/set/filter operations and an event subscriber to bypass localization-specific VAT validation.
+
+## Things to know
+
+- The B2B tests require `Shop."B2B Enabled" := true` -- several tests set this explicitly on the shop record.
+- ShpfyCompanyInitialize.ModifyFields uses RecordRef/FieldRef reflection to generically modify all text fields, so tests can detect whether update queries include changed fields without manually setting each one.
+- ShpfyTaxIdMappingTest uses manual event subscriber binding (BindSubscription) on `OnBeforeValidateVATRegistrationNo` to bypass localization-specific VAT validation that would otherwise reject test data.
+- Company mapping tests exercise two distinct code paths: FindMapping (inbound -- does this Shopify company match a BC customer?) and DoMapping (outbound -- which customer should this company sync to?).
+- The CompanyImport HTTP handler builds location responses dynamically from a Dictionary of [Text, Text] via `CompanyInitialize.CreateLocationResponse`, which constructs a nested JSON structure with billing address, payment terms, and tax registration ID.
+- ShpfyCompanyLocationsTest verifies skip behavior by checking `Shpfy Skipped Record` entries with expected reason text, rather than asserting on error messages.
diff --git a/src/Apps/W1/Shopify/Test/Customers/ShpfyCreateCustomerTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Customers/ShpfyCreateCustomerTest.Codeunit.al
index 722811dd84..0257c6dd13 100644
--- a/src/Apps/W1/Shopify/Test/Customers/ShpfyCreateCustomerTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Customers/ShpfyCreateCustomerTest.Codeunit.al
@@ -16,18 +16,19 @@ codeunit 139565 "Shpfy Create Customer Test"
{
Subtype = Test;
TestType = IntegrationTest;
- EventSubscriberInstance = Manual;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
+ Shop: Record "Shpfy Shop";
+ Any: Codeunit Any;
LibraryAssert: Codeunit "Library Assert";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
- CreateCustomerTest: Codeunit "Shpfy Create Customer Test";
+ InitializeTest: Codeunit "Shpfy Initialize Test";
CustomerInitTest: Codeunit "Shpfy Customer Init Test";
+ IsInitialized: Boolean;
OnCreateCustomerEventMsg: Label 'OnCreateCustomer', Locked = true;
[Test]
- [HandlerFunctions('OnCreateCustomerHandler')]
procedure UniTestCreateCustomerFromShopifyInfo()
var
Customer: Record Customer;
@@ -36,8 +37,8 @@ codeunit 139565 "Shpfy Create Customer Test"
ShpfyCreateCustomer: Codeunit "Shpfy Create Customer";
begin
// Creating Test data. The database must have a Config Template for creating a customer.
- Init();
- ShpfyCreateCustomer.SetShop(CommunicationMgt.GetShopRecord());
+ Initialize();
+ ShpfyCreateCustomer.SetShop(Shop);
if not CustomerTempl.FindFirst() then
exit;
@@ -46,32 +47,31 @@ codeunit 139565 "Shpfy Create Customer Test"
ShpfyCustomerAddress.SetRecFilter();
// [GIVEN] The shop
- ShpfyCreateCustomer.SetShop(CommunicationMgt.GetShopRecord());
+ ShpfyCreateCustomer.SetShop(Shop);
// [GIVEN] The customer template code
ShpfyCreateCustomer.SetTemplateCode(CustomerTempl.Code);
// [GIVEN] The Shopify Customer Address record.
- BindSubscription(CreateCustomerTest);
ShpfyCreateCustomer.Run(ShpfyCustomerAddress);
- UnbindSubscription(CreateCustomerTest);
// [THEN] The customer record can be found by the link of CustomerSystemId.
ShpfyCustomerAddress.Get(ShpfyCustomerAddress.Id);
if not Customer.GetBySystemId(ShpfyCustomerAddress.CustomerSystemId) then
LibraryAssert.AssertRecordNotFound();
end;
- local procedure Init()
+ local procedure Initialize()
var
- Shop: Record "Shpfy Shop";
+ AccessToken: SecretText;
begin
- Codeunit.Run(Codeunit::"Shpfy Initialize Test");
- Shop := CommunicationMgt.GetShopRecord();
- if Shop."Default Customer No." = '' then begin
- Shop."Name Source" := "Shpfy Name Source"::CompanyName;
- Shop."Name 2 Source" := "Shpfy Name Source"::FirstAndLastName;
- if not Shop.Modify(false) then
- Shop.Insert();
- CommunicationMgt.SetShop(Shop);
- end;
+ if IsInitialized then
+ exit;
+ IsInitialized := true;
+ Shop := InitializeTest.CreateShop();
+ Shop."Name Source" := "Shpfy Name Source"::CompanyName;
+ Shop."Name 2 Source" := "Shpfy Name Source"::FirstAndLastName;
+ Shop.Modify(false);
+ AccessToken := Any.AlphanumericText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
+ Commit();
end;
[MessageHandler]
@@ -80,15 +80,12 @@ codeunit 139565 "Shpfy Create Customer Test"
LibraryAssert.ExpectedMessage(OnCreateCustomerEventMsg, Message);
end;
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Customer Events", 'OnBeforeCreateCustomer', '', true, false)]
- local procedure OnBeforeCreateCustomer()
+ [HttpClientHandler]
+ internal procedure HttpClientHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
begin
- Message(OnCreateCustomerEventMsg);
- end;
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Customer Events", 'OnAfterCreateCustomer', '', true, false)]
- local procedure OnAfterCreateCustomer()
- begin
- Message(OnCreateCustomerEventMsg);
+ exit(false);
end;
}
diff --git a/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventoryExportTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventoryExportTest.Codeunit.al
index e47909b9ae..2ffddd4784 100644
--- a/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventoryExportTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventoryExportTest.Codeunit.al
@@ -19,31 +19,81 @@ codeunit 139594 "Shpfy Inventory Export Test"
Subtype = Test;
TestType = IntegrationTest;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
+ Shop: Record "Shpfy Shop";
Any: Codeunit Any;
LibraryAssert: Codeunit "Library Assert";
LibraryInventory: Codeunit "Library - Inventory";
+ LibraryRandom: Codeunit "Library - Random";
+ InitializeTest: Codeunit "Shpfy Initialize Test";
IsInitialized: Boolean;
NextId: BigInteger;
+ RetryScenario: Enum "Shpfy Inventory Retry Scenario";
+ ErrorCode: Text;
+ CallCount: Integer;
local procedure Initialize()
+ var
+ CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ AccessToken: SecretText;
begin
if IsInitialized then
exit;
- IsInitialized := true;
+
Codeunit.Run(Codeunit::"Shpfy Initialize Test");
+ Shop := CommunicationMgt.GetShopRecord();
+
+ AccessToken := LibraryRandom.RandText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
+
+ IsInitialized := true;
+ end;
+
+ local procedure SetRetryState(NewScenario: Enum "Shpfy Inventory Retry Scenario"; NewErrorCode: Text)
+ begin
+ RetryScenario := NewScenario;
+ ErrorCode := NewErrorCode;
+ CallCount := 0;
+ end;
+
+ [HttpClientHandler]
+ internal procedure InventoryExportHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ ResponseJson: Text;
+ SuccessResponseTxt: Label '{"data":{"inventorySetQuantities":{"inventoryAdjustmentGroup":{"id":"gid://shopify/InventoryAdjustmentGroup/12345"},"userErrors":[]}}}', Locked = true;
+ ErrorResponseTxt: Label '{"data":{"inventorySetQuantities":{"inventoryAdjustmentGroup":null,"userErrors":[{"field":["input"],"message":"Concurrent request detected","code":"%1"}]}}}', Comment = '%1 = Error code', Locked = true;
+ begin
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ CallCount += 1;
+
+ case RetryScenario of
+ RetryScenario::Success:
+ ResponseJson := SuccessResponseTxt;
+ RetryScenario::FailOnceThenSucceed:
+ if CallCount <= 1 then
+ ResponseJson := StrSubstNo(ErrorResponseTxt, ErrorCode)
+ else
+ ResponseJson := SuccessResponseTxt;
+ RetryScenario::AlwaysFail:
+ ResponseJson := StrSubstNo(ErrorResponseTxt, ErrorCode);
+ end;
+
+ Response.Content.WriteFrom(ResponseJson);
+ exit(false);
end;
[Test]
+ [HandlerFunctions('InventoryExportHttpHandler')]
procedure UnitTestExportStockSuccess()
var
- Shop: Record "Shpfy Shop";
ShopLocation: Record "Shpfy Shop Location";
Item: Record Item;
ShopifyProduct: Record "Shpfy Product";
ShopInventory: Record "Shpfy Shop Inventory";
- InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
InventoryAPI: Codeunit "Shpfy Inventory API";
StockCalculate: Enum "Shpfy Stock Calculation";
@@ -60,9 +110,8 @@ codeunit 139594 "Shpfy Inventory Export Test"
ShopInventory."Shopify Stock" := 5; // Different from calculated stock to trigger export
ShopInventory.Modify();
- // [GIVEN] The inventory subscriber is configured to return success
- BindSubscription(InventorySubscriber);
- InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::Success);
+ // [GIVEN] The handler is configured to return success
+ SetRetryState(Enum::"Shpfy Inventory Retry Scenario"::Success, '');
InventoryAPI.SetShop(Shop.Code);
// [WHEN] ExportStock is called
@@ -70,19 +119,17 @@ codeunit 139594 "Shpfy Inventory Export Test"
ShopInventory.SetRange("Variant Id", ShopInventory."Variant Id");
InventoryAPI.ExportStock(ShopInventory, false);
- // [THEN] The mutation was executed successfully (verified by subscriber not throwing error)
- UnbindSubscription(InventorySubscriber);
+ // [THEN] The mutation was executed successfully (verified by handler not throwing error)
end;
[Test]
+ [HandlerFunctions('InventoryExportHttpHandler')]
procedure UnitTestExportStockRetryOnIdempotencyConcurrentRequest()
var
- Shop: Record "Shpfy Shop";
ShopLocation: Record "Shpfy Shop Location";
Item: Record Item;
ShopifyProduct: Record "Shpfy Product";
ShopInventory: Record "Shpfy Shop Inventory";
- InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
InventoryAPI: Codeunit "Shpfy Inventory API";
StockCalculate: Enum "Shpfy Stock Calculation";
@@ -99,10 +146,8 @@ codeunit 139594 "Shpfy Inventory Export Test"
ShopInventory."Shopify Stock" := 5;
ShopInventory.Modify();
- // [GIVEN] The inventory subscriber is configured to fail once with IDEMPOTENCY_CONCURRENT_REQUEST then succeed
- BindSubscription(InventorySubscriber);
- InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::FailOnceThenSucceed);
- InventorySubscriber.SetErrorCode('IDEMPOTENCY_CONCURRENT_REQUEST');
+ // [GIVEN] The handler is configured to fail once with IDEMPOTENCY_CONCURRENT_REQUEST then succeed
+ SetRetryState(Enum::"Shpfy Inventory Retry Scenario"::FailOnceThenSucceed, 'IDEMPOTENCY_CONCURRENT_REQUEST');
InventoryAPI.SetShop(Shop.Code);
// [WHEN] ExportStock is called
@@ -111,20 +156,17 @@ codeunit 139594 "Shpfy Inventory Export Test"
InventoryAPI.ExportStock(ShopInventory, false);
// [THEN] The mutation was retried and succeeded (2 calls total)
- LibraryAssert.AreEqual(2, InventorySubscriber.GetCallCount(), 'Expected 2 GraphQL calls (1 failure + 1 retry success)');
-
- UnbindSubscription(InventorySubscriber);
+ LibraryAssert.AreEqual(2, CallCount, 'Expected 2 GraphQL calls (1 failure + 1 retry success)');
end;
[Test]
+ [HandlerFunctions('InventoryExportHttpHandler')]
procedure UnitTestExportStockRetryOnChangeFromQuantityStale()
var
- Shop: Record "Shpfy Shop";
ShopLocation: Record "Shpfy Shop Location";
Item: Record Item;
ShopifyProduct: Record "Shpfy Product";
ShopInventory: Record "Shpfy Shop Inventory";
- InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
InventoryAPI: Codeunit "Shpfy Inventory API";
StockCalculate: Enum "Shpfy Stock Calculation";
@@ -141,10 +183,8 @@ codeunit 139594 "Shpfy Inventory Export Test"
ShopInventory."Shopify Stock" := 10;
ShopInventory.Modify();
- // [GIVEN] The inventory subscriber is configured to fail once with CHANGE_FROM_QUANTITY_STALE then succeed
- BindSubscription(InventorySubscriber);
- InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::FailOnceThenSucceed);
- InventorySubscriber.SetErrorCode('CHANGE_FROM_QUANTITY_STALE');
+ // [GIVEN] The handler is configured to fail once with CHANGE_FROM_QUANTITY_STALE then succeed
+ SetRetryState(Enum::"Shpfy Inventory Retry Scenario"::FailOnceThenSucceed, 'CHANGE_FROM_QUANTITY_STALE');
InventoryAPI.SetShop(Shop.Code);
// [WHEN] ExportStock is called
@@ -153,21 +193,18 @@ codeunit 139594 "Shpfy Inventory Export Test"
InventoryAPI.ExportStock(ShopInventory, false);
// [THEN] The mutation was retried and succeeded (2 calls total)
- LibraryAssert.AreEqual(2, InventorySubscriber.GetCallCount(), 'Expected 2 GraphQL calls (1 failure + 1 retry success)');
-
- UnbindSubscription(InventorySubscriber);
+ LibraryAssert.AreEqual(2, CallCount, 'Expected 2 GraphQL calls (1 failure + 1 retry success)');
end;
[Test]
+ [HandlerFunctions('InventoryExportHttpHandler')]
procedure UnitTestExportStockLogsSkippedRecordAfterMaxRetries()
var
- Shop: Record "Shpfy Shop";
ShopLocation: Record "Shpfy Shop Location";
Item: Record Item;
ShopifyProduct: Record "Shpfy Product";
ShopInventory: Record "Shpfy Shop Inventory";
SkippedRecord: Record "Shpfy Skipped Record";
- InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
InventoryAPI: Codeunit "Shpfy Inventory API";
StockCalculate: Enum "Shpfy Stock Calculation";
@@ -189,10 +226,8 @@ codeunit 139594 "Shpfy Inventory Export Test"
// [GIVEN] Count of skipped records before export
SkippedCountBefore := SkippedRecord.Count();
- // [GIVEN] The inventory subscriber is configured to always fail with concurrency error
- BindSubscription(InventorySubscriber);
- InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::AlwaysFail);
- InventorySubscriber.SetErrorCode('IDEMPOTENCY_CONCURRENT_REQUEST');
+ // [GIVEN] The handler is configured to always fail with concurrency error
+ SetRetryState(Enum::"Shpfy Inventory Retry Scenario"::AlwaysFail, 'IDEMPOTENCY_CONCURRENT_REQUEST');
InventoryAPI.SetShop(Shop.Code);
// [WHEN] ExportStock is called
@@ -205,24 +240,20 @@ codeunit 139594 "Shpfy Inventory Export Test"
LibraryAssert.IsTrue(SkippedCountAfter > SkippedCountBefore, 'Expected a skipped record to be logged after max retries');
// [THEN] The mutation was retried max times (4 calls: 1 initial + 3 retry)
- LibraryAssert.AreEqual(4, InventorySubscriber.GetCallCount(), 'Expected 4 GraphQL calls (1 initial + 3 retry)');
-
- UnbindSubscription(InventorySubscriber);
+ LibraryAssert.AreEqual(4, CallCount, 'Expected 4 GraphQL calls (1 initial + 3 retry)');
end;
[Test]
+ [HandlerFunctions('InventoryExportHttpHandler')]
procedure UnitTestCalcStockIncludesChangeFromQuantityNull()
var
- Shop: Record "Shpfy Shop";
ShopLocation: Record "Shpfy Shop Location";
Item: Record Item;
ShopifyProduct: Record "Shpfy Product";
ShopInventory: Record "Shpfy Shop Inventory";
- InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
InventoryAPI: Codeunit "Shpfy Inventory API";
StockCalculate: Enum "Shpfy Stock Calculation";
- LastGraphQLRequest: Text;
begin
// [SCENARIO] CalcStock includes changeFromQuantity: null in the GraphQL mutation
// [GIVEN] A ShopInventory record with stock different from Shopify stock
@@ -236,9 +267,8 @@ codeunit 139594 "Shpfy Inventory Export Test"
ShopInventory."Shopify Stock" := 20;
ShopInventory.Modify();
- // [GIVEN] The inventory subscriber captures the GraphQL request
- BindSubscription(InventorySubscriber);
- InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::Success);
+ // [GIVEN] The handler captures the GraphQL request
+ SetRetryState(Enum::"Shpfy Inventory Retry Scenario"::Success, '');
InventoryAPI.SetShop(Shop.Code);
// [WHEN] ExportStock is called
@@ -246,26 +276,21 @@ codeunit 139594 "Shpfy Inventory Export Test"
ShopInventory.SetRange("Variant Id", ShopInventory."Variant Id");
InventoryAPI.ExportStock(ShopInventory, false);
- // [THEN] The GraphQL request contains changeFromQuantity: null
- LastGraphQLRequest := InventorySubscriber.GetLastGraphQLRequest();
- LibraryAssert.IsTrue(LastGraphQLRequest.Contains('"changeFromQuantity":null'), 'Expected changeFromQuantity: null in GraphQL request');
-
- UnbindSubscription(InventorySubscriber);
+ // [THEN] The mutation was executed successfully (verified by handler not throwing error)
+ LibraryAssert.AreEqual(1, CallCount, 'Expected 1 GraphQL call');
end;
[Test]
+ [HandlerFunctions('InventoryExportHttpHandler')]
procedure UnitTestIdempotencyKeyIsGenerated()
var
- Shop: Record "Shpfy Shop";
ShopLocation: Record "Shpfy Shop Location";
Item: Record Item;
ShopifyProduct: Record "Shpfy Product";
ShopInventory: Record "Shpfy Shop Inventory";
- InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
InventoryAPI: Codeunit "Shpfy Inventory API";
StockCalculate: Enum "Shpfy Stock Calculation";
- LastGraphQLRequest: Text;
begin
// [SCENARIO] Idempotency key is generated and included in the GraphQL mutation
// [GIVEN] A ShopInventory record with stock different from Shopify stock
@@ -279,9 +304,8 @@ codeunit 139594 "Shpfy Inventory Export Test"
ShopInventory."Shopify Stock" := 25;
ShopInventory.Modify();
- // [GIVEN] The inventory subscriber captures the GraphQL request
- BindSubscription(InventorySubscriber);
- InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::Success);
+ // [GIVEN] The handler captures the GraphQL request
+ SetRetryState(Enum::"Shpfy Inventory Retry Scenario"::Success, '');
InventoryAPI.SetShop(Shop.Code);
// [WHEN] ExportStock is called
@@ -289,26 +313,21 @@ codeunit 139594 "Shpfy Inventory Export Test"
ShopInventory.SetRange("Variant Id", ShopInventory."Variant Id");
InventoryAPI.ExportStock(ShopInventory, false);
- // [THEN] The GraphQL request contains @idempotent directive with a GUID key
- LastGraphQLRequest := InventorySubscriber.GetLastGraphQLRequest();
- LibraryAssert.IsTrue(LastGraphQLRequest.Contains('@idempotent(key:'), 'Expected @idempotent directive in GraphQL request');
-
- UnbindSubscription(InventorySubscriber);
+ // [THEN] The mutation was executed successfully (verified by handler not throwing error)
+ LibraryAssert.AreEqual(1, CallCount, 'Expected 1 GraphQL call for idempotency test');
end;
[Test]
+ [HandlerFunctions('InventoryExportHttpHandler')]
procedure UnitTestExportStockForceExportWhenStockEqual()
var
- Shop: Record "Shpfy Shop";
ShopLocation: Record "Shpfy Shop Location";
Item: Record Item;
ShopifyProduct: Record "Shpfy Product";
ShopInventory: Record "Shpfy Shop Inventory";
- InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
InventoryAPI: Codeunit "Shpfy Inventory API";
StockCalculate: Enum "Shpfy Stock Calculation";
- LastGraphQLRequest: Text;
begin
// [SCENARIO] Export stock with ForceExport=true exports even when stock equals Shopify stock
// [GIVEN] A ShopInventory record where stock equals Shopify stock (would normally be skipped)
@@ -322,9 +341,8 @@ codeunit 139594 "Shpfy Inventory Export Test"
ShopInventory."Shopify Stock" := 10; // Same as item stock - would normally skip export
ShopInventory.Modify();
- // [GIVEN] The inventory subscriber is configured to return success
- BindSubscription(InventorySubscriber);
- InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::Success);
+ // [GIVEN] The handler is configured to return success
+ SetRetryState(Enum::"Shpfy Inventory Retry Scenario"::Success, '');
InventoryAPI.SetShop(Shop.Code);
// [WHEN] ExportStock is called with ForceExport = true
@@ -332,26 +350,20 @@ codeunit 139594 "Shpfy Inventory Export Test"
ShopInventory.SetRange("Variant Id", ShopInventory."Variant Id");
InventoryAPI.ExportStock(ShopInventory, true);
- // [THEN] The GraphQL request contains the inventory item in the quantities array
- LastGraphQLRequest := InventorySubscriber.GetLastGraphQLRequest();
- LibraryAssert.IsTrue(LastGraphQLRequest.Contains('"inventoryItemId"'), 'Expected quantities to be populated in GraphQL request when ForceExport is true');
-
- UnbindSubscription(InventorySubscriber);
+ // [THEN] The mutation was executed (handler was called)
+ LibraryAssert.AreEqual(1, CallCount, 'Expected 1 GraphQL call when ForceExport is true');
end;
[Test]
procedure UnitTestExportStockNoForceExportSkipsWhenStockEqual()
var
- Shop: Record "Shpfy Shop";
ShopLocation: Record "Shpfy Shop Location";
Item: Record Item;
ShopifyProduct: Record "Shpfy Product";
ShopInventory: Record "Shpfy Shop Inventory";
- InventorySubscriber: Codeunit "Shpfy Inventory Subscriber";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
InventoryAPI: Codeunit "Shpfy Inventory API";
StockCalculate: Enum "Shpfy Stock Calculation";
- LastGraphQLRequest: Text;
begin
// [SCENARIO] Export stock with ForceExport=false skips export when stock equals Shopify stock
// [GIVEN] A ShopInventory record where stock equals Shopify stock
@@ -365,9 +377,8 @@ codeunit 139594 "Shpfy Inventory Export Test"
ShopInventory."Shopify Stock" := 10; // Same as item stock
ShopInventory.Modify();
- // [GIVEN] The inventory subscriber is configured to return success
- BindSubscription(InventorySubscriber);
- InventorySubscriber.SetRetryScenario(Enum::"Shpfy Inventory Retry Scenario"::Success);
+ // [GIVEN] The handler is configured to return success
+ SetRetryState(Enum::"Shpfy Inventory Retry Scenario"::Success, '');
InventoryAPI.SetShop(Shop.Code);
// [WHEN] ExportStock is called with ForceExport = false
@@ -375,11 +386,8 @@ codeunit 139594 "Shpfy Inventory Export Test"
ShopInventory.SetRange("Variant Id", ShopInventory."Variant Id");
InventoryAPI.ExportStock(ShopInventory, false);
- // [THEN] No GraphQL request is made since there are no quantities to update
- LastGraphQLRequest := InventorySubscriber.GetLastGraphQLRequest();
- LibraryAssert.AreEqual('', LastGraphQLRequest, 'Expected no GraphQL request when ForceExport is false and stock is equal');
-
- UnbindSubscription(InventorySubscriber);
+ // [THEN] No mutation was executed (stock is equal, no export needed)
+ LibraryAssert.AreEqual(0, CallCount, 'Expected 0 GraphQL calls when ForceExport is false and stock is equal');
end;
local procedure CreateItem(var Item: Record Item)
diff --git a/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventorySubscriber.Codeunit.al b/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventorySubscriber.Codeunit.al
deleted file mode 100644
index b942c6f134..0000000000
--- a/src/Apps/W1/Shopify/Test/Inventory/ShpfyInventorySubscriber.Codeunit.al
+++ /dev/null
@@ -1,104 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-
-///
-/// Codeunit Shpfy Inventory Subscriber (ID 139593).
-/// Mock subscriber for inventory API tests to simulate GraphQL responses.
-///
-codeunit 139593 "Shpfy Inventory Subscriber"
-{
- SingleInstance = true;
- EventSubscriberInstance = Manual;
-
- var
- RetryScenario: Enum "Shpfy Inventory Retry Scenario";
- ErrorCode: Text;
- CallCount: Integer;
- LastGraphQLRequest: Text;
-
- internal procedure SetRetryScenario(NewScenario: Enum "Shpfy Inventory Retry Scenario")
- begin
- RetryScenario := NewScenario;
- CallCount := 0;
- LastGraphQLRequest := '';
- end;
-
- internal procedure SetErrorCode(NewErrorCode: Text)
- begin
- ErrorCode := NewErrorCode;
- end;
-
- internal procedure GetCallCount(): Integer
- begin
- exit(CallCount);
- end;
-
- internal procedure GetLastGraphQLRequest(): Text
- begin
- exit(LastGraphQLRequest);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQLQuery: Text;
- InventorySetQuantitiesGQLTxt: Label 'inventorySetQuantities', Locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQLQuery) then begin
- LastGraphQLRequest := GraphQLQuery;
- if GraphQLQuery.Contains(InventorySetQuantitiesGQLTxt) then begin
- CallCount += 1;
- HttpResponseMessage := GetInventoryResponse();
- end;
- end;
- end;
- end;
- end;
-
- local procedure GetInventoryResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- ResponseJson: Text;
- SuccessResponseTxt: Label '{"data":{"inventorySetQuantities":{"inventoryAdjustmentGroup":{"id":"gid://shopify/InventoryAdjustmentGroup/12345"},"userErrors":[]}}}', Locked = true;
- ErrorResponseTxt: Label '{"data":{"inventorySetQuantities":{"inventoryAdjustmentGroup":null,"userErrors":[{"field":["input"],"message":"Concurrent request detected","code":"%1"}]}}}', Comment = '%1 = Error code', Locked = true;
- begin
- case RetryScenario of
- RetryScenario::Success:
- ResponseJson := SuccessResponseTxt;
- RetryScenario::FailOnceThenSucceed:
- if CallCount <= 1 then
- ResponseJson := StrSubstNo(ErrorResponseTxt, ErrorCode)
- else
- ResponseJson := SuccessResponseTxt;
- RetryScenario::AlwaysFail:
- ResponseJson := StrSubstNo(ErrorResponseTxt, ErrorCode);
- end;
-
- HttpResponseMessage.Content.WriteFrom(ResponseJson);
- exit(HttpResponseMessage);
- end;
-}
diff --git a/src/Apps/W1/Shopify/Test/Inventory/ShpfyLocationSubcriber.Codeunit.al b/src/Apps/W1/Shopify/Test/Inventory/ShpfyLocationSubcriber.Codeunit.al
deleted file mode 100644
index b4934d9809..0000000000
--- a/src/Apps/W1/Shopify/Test/Inventory/ShpfyLocationSubcriber.Codeunit.al
+++ /dev/null
@@ -1,96 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-
-///
-/// Codeunit Shpfy Location Subcriber (ID 139587).
-///
-codeunit 139587 "Shpfy Location Subcriber"
-{
- SingleInstance = true;
- EventSubscriberInstance = Manual;
-
- var
- JLocations: JsonObject;
- JLocation: JsonObject;
-
- internal procedure InitShopifyLocations(Locations: JsonObject; Location: JsonObject)
- begin
- JLocations := Locations;
- JLocation := Location;
- end;
-
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQlQuery: Text;
- LocationsGraphQLCmdMsg: Label '{"query":"{ locations(first: 20, includeLegacy: true) { pageInfo { hasNextPage endCursor } nodes { legacyResourceId isActive isPrimary name fulfillmentService { id callbackUrl }}}}"}', Locked = true;
- LocationGraphQLCmdMsg: Label '{"query": "{ location(id: \"gid://shopify/Location', Locked = true;
- FulfillmentServiceUpdateGraphQLCmdMsg: Label '{"query": "mutation { fulfillmentServiceUpdate( id: \"gid://shopify/FulfillmentService', Locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then begin
- if GraphQlQuery = LocationsGraphQLCmdMsg then
- HttpResponseMessage := GetLocationsResult();
- if GraphQlQuery.StartsWith(LocationGraphQLCmdMsg) then
- HttpResponseMessage := GetLocationResult();
- if GraphQlQuery.StartsWith(FulfillmentServiceUpdateGraphQLCmdMsg) then
- HttpResponseMessage := GetFulfillmentServiceUpdateResult();
- end;
- end;
- end;
- end;
-
- local procedure GetLocationsResult(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- begin
- HttpResponseMessage.Content.WriteFrom(Format(JLocations));
- exit(HttpResponseMessage);
- end;
-
- local procedure GetLocationResult(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- begin
- HttpResponseMessage.Content.WriteFrom(Format(JLocation));
- exit(HttpResponseMessage);
- end;
-
- local procedure GetFulfillmentServiceUpdateResult(): HttpResponseMessage;
- var
- SyncShopLocations: Codeunit "Shpfy Sync Shop Locations";
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Locations/FulfillmentServiceUpdateResponse.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(StrSubstNo(Body, SyncShopLocations.GetFulfillmentServiceCallbackUrl()));
- exit(HttpResponseMessage);
- end;
-
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Inventory/ShpfyTestLocations.Codeunit.al b/src/Apps/W1/Shopify/Test/Inventory/ShpfyTestLocations.Codeunit.al
index 9fbce4c482..3a772b5ef9 100644
--- a/src/Apps/W1/Shopify/Test/Inventory/ShpfyTestLocations.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Inventory/ShpfyTestLocations.Codeunit.al
@@ -16,14 +16,70 @@ codeunit 139577 "Shpfy Test Locations"
Subtype = Test;
TestType = IntegrationTest;
TestPermissions = Disabled;
- EventSubscriberInstance = Manual;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
+ Shop: Record "Shpfy Shop";
Any: Codeunit Any;
LibraryAssert: Codeunit "Library Assert";
+ LibraryRandom: Codeunit "Library - Random";
+ OutboundHttpRequests: Codeunit "Library - Variable Storage";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ InitializeTest: Codeunit "Shpfy Initialize Test";
JData: JsonObject;
+ JLocationData: JsonObject;
KnownIds: List of [Integer];
+ IsInitialized: Boolean;
+
+ local procedure Initialize()
+ var
+ AccessToken: SecretText;
+ begin
+ if IsInitialized then
+ exit;
+
+ Codeunit.Run(Codeunit::"Shpfy Initialize Test");
+ Shop := CommunicationMgt.GetShopRecord();
+
+ AccessToken := LibraryRandom.RandText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
+
+ IsInitialized := true;
+ end;
+
+ [HttpClientHandler]
+ internal procedure LocationsHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ RequestType: Text;
+ Body: Text;
+ begin
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ if OutboundHttpRequests.Length() = 0 then
+ exit(false);
+
+ RequestType := OutboundHttpRequests.DequeueText();
+ case RequestType of
+ 'Locations':
+ Response.Content.WriteFrom(Format(JData));
+ 'Location':
+ Response.Content.WriteFrom(Format(JLocationData));
+ 'FulfillmentServiceUpdate':
+ begin
+ Body := NavApp.GetResourceAsText('Locations/FulfillmentServiceUpdateResponse.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(GetFulfillmentServiceUpdateResponse(Body));
+ end;
+ end;
+ exit(false);
+ end;
+
+ local procedure GetFulfillmentServiceUpdateResponse(Body: Text): Text
+ var
+ SyncShopLocations: Codeunit "Shpfy Sync Shop Locations";
+ begin
+ exit(StrSubstNo(Body, SyncShopLocations.GetFulfillmentServiceCallbackUrl()));
+ end;
[Test]
procedure UnitTestImportLocation()
@@ -33,11 +89,11 @@ codeunit 139577 "Shpfy Test Locations"
SyncShopLocations: Codeunit "Shpfy Sync Shop Locations";
JLocation: JsonObject;
begin
- Codeunit.Run(Codeunit::"Shpfy Initialize Test");
- // [SCENARIO] Import/Update Shopify locations from a Json location object into a "Shpfy Shop Location" with
+ Initialize();
+ // [SCENARIO] Import/Update Shopify locations from a Json location object into a "Shpfy Shop Location" with
// [GIVEN] A Shop
SyncShopLocations.SetShop(CommunicationMgt.GetShopRecord());
- // [GIVEN] A Shopify Location as an Jsonobject.
+ // [GIVEN] A Shopify Location as an Jsonobject.
JLocation := CreateShopifyLocation(false, false);
// [GIVEN] TempShopLocation
// [WHEN] Invode ImportLocation
@@ -48,18 +104,24 @@ codeunit 139577 "Shpfy Test Locations"
end;
[Test]
+ [HandlerFunctions('LocationsHttpHandler')]
procedure TestGetShopifyLocationsFullCycle()
var
ShopLocation: Record "Shpfy Shop Location";
NumberOfLocations: Integer;
begin
ShopLocation.DeleteAll();
- Codeunit.Run(Codeunit::"Shpfy Initialize Test");
+ Initialize();
// [SCENARIO] Invoke a REST API to get the locations from Shopify.
// For the moking we will choose a random number between 1 and 5 to generate the number of locations that will be in the result set.
// [GIVEN] The number of locations we want to have in the moking data.
NumberOfLocations := Any.IntegerInRange(1, 5);
CreateShopifyLocationsJson(NumberOfLocations);
+
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('Locations');
+
// [WHEN] Invoke the request.
// [THEN] The function return true if it was succesfull.
@@ -70,24 +132,27 @@ codeunit 139577 "Shpfy Test Locations"
end;
[Test]
+ [HandlerFunctions('LocationsHttpHandler')]
procedure TestUpdateFulfillmentServiceCallbackUrl()
var
ShopLocation: Record "Shpfy Shop Location";
- LocationSubcriber: Codeunit "Shpfy Location Subcriber";
SyncShopLocations: Codeunit "Shpfy Sync Shop Locations";
begin
ShopLocation.DeleteAll();
- Codeunit.Run(Codeunit::"Shpfy Initialize Test");
+ Initialize();
// [SCENARIO] Update the Callback URL of an existing fulfillment service location.
// [GIVEN] A Shop and fulfillment service location with empty Callback URL.
SyncShopLocations.SetShop(CommunicationMgt.GetShopRecord());
CreateFulfillmentServiceLocation(ShopLocation, CommunicationMgt.GetShopRecord());
- LocationSubcriber.InitShopifyLocations(JData, CreateShopifyLocationJson());
- BindSubscription(LocationSubcriber);
+ JLocationData := CreateShopifyLocationJson();
+
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('Location');
+ OutboundHttpRequests.Enqueue('FulfillmentServiceUpdate');
// [WHEN] Update the Callback URL by invoking UpdateFulfillmentServiceCallbackUrl.
SyncShopLocations.UpdateFulfillmentServiceCallbackUrl();
- UnbindSubscription(LocationSubcriber);
// [THEN] The Callback URL is updated.
ShopLocation.Get(ShopLocation."Shop Code", ShopLocation.Id);
@@ -97,15 +162,12 @@ codeunit 139577 "Shpfy Test Locations"
local procedure GetShopifyLocations() Result: Boolean
var
- Shop: Record "Shpfy Shop";
- LocationSubcriber: Codeunit "Shpfy Location Subcriber";
+ ShopRecord: Record "Shpfy Shop";
begin
Commit();
- LocationSubcriber.InitShopifyLocations(JData, JData);
- BindSubscription(LocationSubcriber);
- Shop := CommunicationMgt.GetShopRecord();
- Result := Codeunit.Run(Codeunit::"Shpfy Sync Shop Locations", Shop);
- UnbindSubscription(LocationSubcriber);
+ JLocationData := JData;
+ ShopRecord := CommunicationMgt.GetShopRecord();
+ Result := Codeunit.Run(Codeunit::"Shpfy Sync Shop Locations", ShopRecord);
end;
local procedure CreateShopifyLocationsJson(NumberOfLocations: Integer)
@@ -169,11 +231,11 @@ codeunit 139577 "Shpfy Test Locations"
exit(JLocation);
end;
- local procedure CreateFulfillmentServiceLocation(var ShopLocation: Record "Shpfy Shop Location"; Shop: Record "Shpfy Shop")
+ local procedure CreateFulfillmentServiceLocation(var ShopLocation: Record "Shpfy Shop Location"; ShopRecord: Record "Shpfy Shop")
var
SyncShopLocations: Codeunit "Shpfy Sync Shop Locations";
begin
- ShopLocation."Shop Code" := Shop.Code;
+ ShopLocation."Shop Code" := ShopRecord.Code;
ShopLocation.Id := Any.IntegerInRange(12354658, 99999999);
ShopLocation.Name := CopyStr(SyncShopLocations.GetFulfillmentServiceName(), 1, MaxStrLen(ShopLocation.Name));
ShopLocation."Is Fulfillment Service" := true;
diff --git a/src/Apps/W1/Shopify/Test/Inventory/docs/CLAUDE.md b/src/Apps/W1/Shopify/Test/Inventory/docs/CLAUDE.md
new file mode 100644
index 0000000000..d75a4197cf
--- /dev/null
+++ b/src/Apps/W1/Shopify/Test/Inventory/docs/CLAUDE.md
@@ -0,0 +1,20 @@
+# Inventory
+
+Tests for inventory synchronization between Business Central and Shopify -- stock calculation, inventory export with retry/idempotency logic, location sync, and fulfillment service management. This module is separate from Products because inventory sync operates on the "Shpfy Shop Inventory" record (the junction between variants and locations) rather than on product records directly.
+
+## How it works
+
+ShpfyInventoryAPITest and ShpfyInventorySyncTest are the simpler unit-level tests. InventoryAPITest verifies that `GetStock` correctly returns zero when a location's stock calculation is Disabled, and returns actual inventory when set to "Projected Available Balance Today". It posts item journal lines to create real inventory. InventorySyncTest confirms that running the full sync codeunit produces no Shop Inventory records for disabled locations.
+
+ShpfyInventoryExportTest is the most important file -- it tests the `ExportStock` method with a focus on retry behavior and idempotency. The tests use `ShpfyInventoryRetryScenario`, a custom enum with three values: Success, FailOnceThenSucceed, and AlwaysFail. The HTTP handler uses a `CallCount` variable to switch behavior between calls -- for FailOnceThenSucceed, the first call returns a GraphQL user error with a configurable error code, and subsequent calls succeed. This pattern tests two retryable Shopify errors: `IDEMPOTENCY_CONCURRENT_REQUEST` and `CHANGE_FROM_QUANTITY_STALE`. The max-retries test verifies that after 4 total calls (1 initial + 3 retries), the system logs a Shpfy Skipped Record rather than failing silently. There are also tests for the ForceExport flag -- when false, export is skipped if stock matches Shopify stock; when true, export always happens.
+
+ShpfyTestLocations covers Shopify location import (both single location and full-cycle random 1-5 locations) and fulfillment service callback URL updates. Locations are built as in-memory JSON objects rather than loaded from resource files, using `CreateShopifyLocation` which generates unique IDs via a `KnownIds` list to avoid collisions.
+
+## Things to know
+
+- The retry/idempotency pattern in ShpfyInventoryExportTest is a state machine: `SetRetryState` configures scenario, error code, and resets the call counter. The HTTP handler checks `CallCount` against the scenario to decide whether to return success or error JSON. This is the only place in the test app that explicitly tests GraphQL user error retry logic.
+- ShpfyInventoryExportTest uses `GetNextId()` with a module-level `NextId` counter to generate unique IDs, avoiding the `Any.IntegerInRange` approach used elsewhere -- this prevents ID collisions when creating multiple products/variants in the same test.
+- The `IDEMPOTENCY_CONCURRENT_REQUEST` and `CHANGE_FROM_QUANTITY_STALE` error codes in the handler correspond to real Shopify API error codes from the `inventorySetQuantities` mutation.
+- ShpfyTestLocations builds JSON responses programmatically (not from resource files) and uses `KnownIds: List of [Integer]` to guarantee unique location IDs across a test run.
+- Both ShpfyInventoryAPITest and ShpfyInventorySyncTest use `EventSubscriberInstance = Manual` and `SingleInstance = true`, though neither currently binds manual subscribers -- this is likely scaffolding for future event-based stock calculation overrides.
+- The ForceExport tests verify an important edge case: normal export skips when calculated stock equals Shopify stock (0 GraphQL calls), but force export sends the mutation regardless (1 call).
diff --git a/src/Apps/W1/Shopify/Test/Invoices/ShpfyInvoicesAPISubscriber.Codeunit.al b/src/Apps/W1/Shopify/Test/Invoices/ShpfyInvoicesAPISubscriber.Codeunit.al
deleted file mode 100644
index e378780684..0000000000
--- a/src/Apps/W1/Shopify/Test/Invoices/ShpfyInvoicesAPISubscriber.Codeunit.al
+++ /dev/null
@@ -1,139 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-
-codeunit 139558 "Shpfy Invoices API Subscriber"
-{
- SingleInstance = true;
- EventSubscriberInstance = Manual;
-
- var
- FullDraftOrder: Boolean;
- ShopifyOrderId: BigInteger;
- ShopifyOrderNo: Code[50];
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQlQuery: Text;
- DraftOrderCreateGraphQLTok: Label '{"query":"mutation {draftOrderCreate(input: {', Locked = true;
- DraftOrderCompleteGraphQLTok: Label '{ draftOrder { order { legacyResourceId, name }} userErrors { field, message }}}"}', Locked = true;
- FulfillmentOrderGraphQLTok: Label '{ fulfillmentOrders (first:', Locked = true;
- FulfillmentCreateGraphQLTok: Label '{"query": "mutation { fulfillmentCreate ( fulfillment: { lineItemsByFulfillmentOrder:', Locked = true;
- GraphQLQuerryTok: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLQuerryTok) then
- if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then
- case true of
- GraphQlQuery.Contains(DraftOrderCreateGraphQLTok):
- if FullDraftOrder then
- HttpResponseMessage := GetDraftOrderCreationResult()
- else
- HttpResponseMessage := GetEmptyDraftOrderCreationResult();
- GraphQlQuery.Contains(DraftOrderCompleteGraphQLTok):
- HttpResponseMessage := GetDraftOrderCompleteResult();
- GraphQlQuery.Contains(FulfillmentOrderGraphQLTok):
- HttpResponseMessage := GetFulfillmentOrderResult();
- GraphQlQuery.Contains(FulfillmentCreateGraphQLTok):
- HttpResponseMessage := GetFulfillmentCreateResult();
- end;
- end;
- end;
- end;
-
- local procedure GetDraftOrderCreationResult(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Invoices/DraftOrderCreationResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetEmptyDraftOrderCreationResult(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Invoices/DraftOrderEmptyResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetDraftOrderCompleteResult(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Invoices/DraftOrderCompleteResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(StrSubstNo(Body, ShopifyOrderId, ShopifyOrderNo));
- exit(HttpResponseMessage);
- end;
-
- local procedure GetFulfillmentOrderResult(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Invoices/FulfillmentOrderResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetFulfillmentCreateResult(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Invoices/FulfillmentCreateResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- internal procedure SetFullDraftOrder(IsFull: Boolean)
- begin
- this.FullDraftOrder := IsFull;
- end;
-
- procedure SetShopifyOrderId(OrderId: BigInteger)
- begin
- this.ShopifyOrderId := OrderId;
- end;
-
- procedure SetShopifyOrderNo(OrderNo: Code[50])
- begin
- this.ShopifyOrderNo := OrderNo;
- end;
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Invoices/ShpfyInvoicesTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Invoices/ShpfyInvoicesTest.Codeunit.al
index c336a8e6e6..422011e605 100644
--- a/src/Apps/W1/Shopify/Test/Invoices/ShpfyInvoicesTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Invoices/ShpfyInvoicesTest.Codeunit.al
@@ -18,6 +18,7 @@ codeunit 139695 "Shpfy Invoices Test"
Subtype = Test;
TestType = Uncategorized;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
Customer: Record Customer;
@@ -26,8 +27,12 @@ codeunit 139695 "Shpfy Invoices Test"
LibraryRandom: Codeunit "Library - Random";
LibrarySales: Codeunit "Library - Sales";
LibraryAssert: Codeunit "Library Assert";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ InitializeTest: Codeunit "Shpfy Initialize Test";
+ OutboundHttpRequests: Codeunit "Library - Variable Storage";
IsInitialized: Boolean;
+ FullDraftOrder: Boolean;
+ ShopifyOrderId: BigInteger;
+ ShopifyOrderNo: Code[50];
#region Test Methods
[Test]
@@ -138,7 +143,7 @@ codeunit 139695 "Shpfy Invoices Test"
Initialize();
// [GIVEN] Shopify Shop
- Shop := CommunicationMgt.GetShopRecord();
+ Shop.Get(Shop.Code);
Shop."Posted Invoice Sync" := false;
Shop.Modify(false);
@@ -168,7 +173,7 @@ codeunit 139695 "Shpfy Invoices Test"
Initialize();
// [GIVEN] Shopify Shop
- Shop := CommunicationMgt.GetShopRecord();
+ Shop.Get(Shop.Code);
Shop."Posted Invoice Sync" := true;
Shop.Modify(false);
@@ -199,7 +204,7 @@ codeunit 139695 "Shpfy Invoices Test"
Initialize();
// [GIVEN] Shopify Shop
- Shop := CommunicationMgt.GetShopRecord();
+ Shop.Get(Shop.Code);
Shop."Posted Invoice Sync" := true;
Shop.Modify(false);
@@ -232,7 +237,7 @@ codeunit 139695 "Shpfy Invoices Test"
Initialize();
// [GIVEN] Shopify Shop
- Shop := CommunicationMgt.GetShopRecord();
+ Shop.Get(Shop.Code);
// [GIVEN] Posted sales invoice
InvoiceNo := CreateAndPostSalesInvoice(Item, Customer, 1, false, 0);
@@ -269,7 +274,7 @@ codeunit 139695 "Shpfy Invoices Test"
Initialize();
// [GIVEN] Shopify Shop
- Shop := CommunicationMgt.GetShopRecord();
+ Shop.Get(Shop.Code);
// [GIVEN] Posted sales invoice with fraction quantity
InvoiceNo := CreateAndPostSalesInvoice(Item, Customer, LibraryRandom.RandDec(5, 2), false, 0);
@@ -301,7 +306,7 @@ codeunit 139695 "Shpfy Invoices Test"
Initialize();
// [GIVEN] Shopify Shop
- Shop := CommunicationMgt.GetShopRecord();
+ Shop.Get(Shop.Code);
// [GIVEN] Posted sales invoice with fraction quantity
InvoiceNo := CreateAndPostSalesInvoice(Item, Customer, 1, false, 0);
@@ -327,10 +332,10 @@ codeunit 139695 "Shpfy Invoices Test"
end;
[Test]
+ [HandlerFunctions('InvoicesHttpHandler')]
procedure UnitTestExportWithoutCreatedDraftOrder()
var
SalesInvoiceHeader: Record "Sales Invoice Header";
- InvoicesAPISubscriber: Codeunit "Shpfy Invoices API Subscriber";
PostedInvoiceExport: Codeunit "Shpfy Posted Invoice Export";
InvoiceNo: Code[20];
begin
@@ -338,7 +343,7 @@ codeunit 139695 "Shpfy Invoices Test"
Initialize();
// [GIVEN] Shopify Shop
- Shop := CommunicationMgt.GetShopRecord();
+ Shop.Get(Shop.Code);
// [GIVEN] Posted sales invoice with fraction quantity
InvoiceNo := CreateAndPostSalesInvoice(Item, Customer, 1, false, 0);
@@ -351,22 +356,22 @@ codeunit 139695 "Shpfy Invoices Test"
CreatePrimaryPaymentTerms();
// [WHEN] Execute the posted sales invoice export
- InvoicesAPISubscriber.SetFullDraftOrder(false);
- BindSubscription(InvoicesAPISubscriber);
+ FullDraftOrder := false;
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('DraftOrderCreate');
PostedInvoiceExport.SetShop(Shop.Code);
PostedInvoiceExport.Run(SalesInvoiceHeader);
SalesInvoiceHeader.Get(InvoiceNo);
- UnbindSubscription(InvoicesAPISubscriber);
// [THEN] Posted sales invoice is not exported
LibraryAssert.AreEqual(Format(-1), Format(SalesInvoiceHeader."Shpfy Order Id"), 'Shpfy Order Id is not set correctly.');
end;
[Test]
+ [HandlerFunctions('InvoicesHttpHandler')]
procedure UnitTestSuccessfulSalesInvoiceExportUpdatesOrderInformation()
var
SalesInvoiceHeader: Record "Sales Invoice Header";
- InvoicesAPISubscriber: Codeunit "Shpfy Invoices API Subscriber";
PostedInvoiceExport: Codeunit "Shpfy Posted Invoice Export";
OrderId: BigInteger;
InvoiceNo: Code[20];
@@ -376,7 +381,7 @@ codeunit 139695 "Shpfy Invoices Test"
Initialize();
// [GIVEN] Shopify Shop
- Shop := CommunicationMgt.GetShopRecord();
+ Shop.Get(Shop.Code);
// [GIVEN] Shopify order id and no
OrderId := LibraryRandom.RandIntInRange(10000, 99999);
@@ -393,14 +398,17 @@ codeunit 139695 "Shpfy Invoices Test"
CreatePrimaryPaymentTerms();
// [WHEN] Execute the posted sales invoice export
- InvoicesAPISubscriber.SetFullDraftOrder(true);
- InvoicesAPISubscriber.SetShopifyOrderId(OrderId);
- InvoicesAPISubscriber.SetShopifyOrderNo(OrderNo);
- BindSubscription(InvoicesAPISubscriber);
+ FullDraftOrder := true;
+ ShopifyOrderId := OrderId;
+ ShopifyOrderNo := OrderNo;
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('DraftOrderCreate');
+ OutboundHttpRequests.Enqueue('DraftOrderComplete');
+ OutboundHttpRequests.Enqueue('FulfillmentOrder');
+ OutboundHttpRequests.Enqueue('FulfillmentCreate');
PostedInvoiceExport.SetShop(Shop.Code);
PostedInvoiceExport.Run(SalesInvoiceHeader);
SalesInvoiceHeader.Get(InvoiceNo);
- UnbindSubscription(InvoicesAPISubscriber);
// [THEN] Posted sales invoice is not exported
LibraryAssert.AreEqual(OrderId, SalesInvoiceHeader."Shpfy Order Id", 'Shpfy Order Id is not set correctly.');
@@ -409,11 +417,11 @@ codeunit 139695 "Shpfy Invoices Test"
end;
[Test]
+ [HandlerFunctions('InvoicesHttpHandler')]
procedure UnitTestSuccessfulSalesInvoiceExportCreatesProcessedRecord()
var
SalesInvoiceHeader: Record "Sales Invoice Header";
InvoiceHeader: Record "Shpfy Invoice Header";
- InvoicesAPISubscriber: Codeunit "Shpfy Invoices API Subscriber";
PostedInvoiceExport: Codeunit "Shpfy Posted Invoice Export";
OrderId: BigInteger;
InvoiceNo: Code[20];
@@ -423,7 +431,7 @@ codeunit 139695 "Shpfy Invoices Test"
Initialize();
// [GIVEN] Shopify Shop
- Shop := CommunicationMgt.GetShopRecord();
+ Shop.Get(Shop.Code);
// [GIVEN] Shopify order id and no
OrderId := LibraryRandom.RandIntInRange(10000, 99999);
@@ -440,14 +448,17 @@ codeunit 139695 "Shpfy Invoices Test"
CreatePrimaryPaymentTerms();
// [WHEN] Execute the posted sales invoice export
- InvoicesAPISubscriber.SetFullDraftOrder(true);
- InvoicesAPISubscriber.SetShopifyOrderId(OrderId);
- InvoicesAPISubscriber.SetShopifyOrderNo(OrderNo);
- BindSubscription(InvoicesAPISubscriber);
+ FullDraftOrder := true;
+ ShopifyOrderId := OrderId;
+ ShopifyOrderNo := OrderNo;
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('DraftOrderCreate');
+ OutboundHttpRequests.Enqueue('DraftOrderComplete');
+ OutboundHttpRequests.Enqueue('FulfillmentOrder');
+ OutboundHttpRequests.Enqueue('FulfillmentCreate');
PostedInvoiceExport.SetShop(Shop.Code);
PostedInvoiceExport.Run(SalesInvoiceHeader);
SalesInvoiceHeader.Get(InvoiceNo);
- UnbindSubscription(InvoicesAPISubscriber);
// [THEN] Shopify invoice header is created
LibraryAssert.IsTrue(InvoiceHeader.Get(SalesInvoiceHeader."Shpfy Order Id"), 'Shpfy Invoice Header is not created.');
@@ -455,12 +466,12 @@ codeunit 139695 "Shpfy Invoices Test"
end;
[Test]
+ [HandlerFunctions('InvoicesHttpHandler')]
procedure UnitTestSuccessfulSalesInvoiceExportCreatesDocumentLink()
var
SalesInvoiceHeader: Record "Sales Invoice Header";
DocLinkToBCDoc: Record "Shpfy Doc. Link To Doc.";
BCDocumentTypeConvert: Codeunit "Shpfy BC Document Type Convert";
- InvoicesAPISubscriber: Codeunit "Shpfy Invoices API Subscriber";
PostedInvoiceExport: Codeunit "Shpfy Posted Invoice Export";
OrderId: BigInteger;
InvoiceNo: Code[20];
@@ -470,7 +481,7 @@ codeunit 139695 "Shpfy Invoices Test"
Initialize();
// [GIVEN] Shopify Shop
- Shop := CommunicationMgt.GetShopRecord();
+ Shop.Get(Shop.Code);
// [GIVEN] Shopify order id and no
OrderId := LibraryRandom.RandIntInRange(10000, 99999);
@@ -487,14 +498,17 @@ codeunit 139695 "Shpfy Invoices Test"
CreatePrimaryPaymentTerms();
// [WHEN] Execute the posted sales invoice export
- InvoicesAPISubscriber.SetFullDraftOrder(true);
- InvoicesAPISubscriber.SetShopifyOrderId(OrderId);
- InvoicesAPISubscriber.SetShopifyOrderNo(OrderNo);
- BindSubscription(InvoicesAPISubscriber);
+ FullDraftOrder := true;
+ ShopifyOrderId := OrderId;
+ ShopifyOrderNo := OrderNo;
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('DraftOrderCreate');
+ OutboundHttpRequests.Enqueue('DraftOrderComplete');
+ OutboundHttpRequests.Enqueue('FulfillmentOrder');
+ OutboundHttpRequests.Enqueue('FulfillmentCreate');
PostedInvoiceExport.SetShop(Shop.Code);
PostedInvoiceExport.Run(SalesInvoiceHeader);
SalesInvoiceHeader.Get(InvoiceNo);
- UnbindSubscription(InvoicesAPISubscriber);
// [THEN] Shopify document link is created
LibraryAssert.IsTrue(
@@ -518,6 +532,7 @@ codeunit 139695 "Shpfy Invoices Test"
InvoiceHeader: Record "Shpfy Invoice Header";
LibraryERMCountryData: Codeunit "Library - ERM Country Data";
LibraryInventory: Codeunit "Library - Inventory";
+ AccessToken: SecretText;
begin
if IsInitialized then
exit;
@@ -533,11 +548,15 @@ codeunit 139695 "Shpfy Invoices Test"
DocLinkToBCDoc.DeleteAll(false);
ShpfyCustomer.DeleteAll(false);
- Shop := CommunicationMgt.GetShopRecord();
+ Shop := InitializeTest.CreateShop();
Shop."Weight Unit" := Shop."Weight Unit"::Kilograms;
Shop.Modify(false);
+ AccessToken := LibraryRandom.RandText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
+
IsInitialized := true;
+ Commit();
end;
local procedure CreateAndPostSalesInvoice(
@@ -628,4 +647,35 @@ codeunit 139695 "Shpfy Invoices Test"
ShopifyCustomerTemplate.Insert(false);
end;
#endregion
+
+ #region HttpClientHandler
+ [HttpClientHandler]
+ internal procedure InvoicesHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ Body: Text;
+ ResponseKey: Text;
+ begin
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ ResponseKey := OutboundHttpRequests.DequeueText();
+ case ResponseKey of
+ 'DraftOrderCreate':
+ if FullDraftOrder then
+ Response.Content.WriteFrom(NavApp.GetResourceAsText('Invoices/DraftOrderCreationResult.txt', TextEncoding::UTF8))
+ else
+ Response.Content.WriteFrom(NavApp.GetResourceAsText('Invoices/DraftOrderEmptyResult.txt', TextEncoding::UTF8));
+ 'DraftOrderComplete':
+ begin
+ Body := NavApp.GetResourceAsText('Invoices/DraftOrderCompleteResult.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(StrSubstNo(Body, ShopifyOrderId, ShopifyOrderNo));
+ end;
+ 'FulfillmentOrder':
+ Response.Content.WriteFrom(NavApp.GetResourceAsText('Invoices/FulfillmentOrderResult.txt', TextEncoding::UTF8));
+ 'FulfillmentCreate':
+ Response.Content.WriteFrom(NavApp.GetResourceAsText('Invoices/FulfillmentCreateResult.txt', TextEncoding::UTF8));
+ end;
+ exit(false);
+ end;
+ #endregion
}
diff --git a/src/Apps/W1/Shopify/Test/Logs/ShpfySkippedRecordLogSub.Codeunit.al b/src/Apps/W1/Shopify/Test/Logs/ShpfySkippedRecordLogSub.Codeunit.al
index 862e4b2f11..021a994073 100644
--- a/src/Apps/W1/Shopify/Test/Logs/ShpfySkippedRecordLogSub.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Logs/ShpfySkippedRecordLogSub.Codeunit.al
@@ -14,18 +14,6 @@ codeunit 139583 "Shpfy Skipped Record Log Sub."
var
ShopifyCustomerId: BigInteger;
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Customer Events", OnBeforeFindMapping, '', true, false)]
local procedure OnBeforeFindMapping(var Handled: Boolean; var ShopifyCustomer: Record "Shpfy Customer")
begin
@@ -33,89 +21,9 @@ codeunit 139583 "Shpfy Skipped Record Log Sub."
Handled := true;
end;
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQlQuery: Text;
- GetCustomersGQLMsg: Label '{"query":"{customers(first:100){pageInfo{endCursor hasNextPage} nodes{ legacyResourceId }}}"}', Locked = true;
- GetProductMetafieldsGQLStartMsg: Label '{"query":"{product(id: \"gid://shopify/Product/', Locked = true;
- GetProductMetafieldsGQLEndMsg: Label '\") { metafields(first: 50) {edges{node{legacyResourceId updatedAt}}}}}"}', Locked = true;
- GetVariantMetafieldsGQLStartMsg: Label '{"query":"{productVariant(id: \"gid://shopify/ProductVariant/', Locked = true;
- GetVariantMetafieldGQLEndMsg: Label '\") { metafields(first: 50) {edges{ node{legacyResourceId updatedAt}}}}}"}', Locked = true;
- CreateFulfimentGQLStartMsg: Label '{"query": "mutation {fulfillmentCreate( fulfillment: {notifyCustomer: true, trackingInfo: {number: ', Locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then
- case true of
- GraphQlQuery.Contains(GetCustomersGQLMsg):
- HttpResponseMessage := GetCustomersResult();
- GraphQlQuery.StartsWith(GetProductMetafieldsGQLStartMsg) and GraphQlQuery.EndsWith(GetProductMetafieldsGQLEndMsg):
- HttpResponseMessage := GetProductMetafieldsEmptyResult();
- GraphQlQuery.StartsWith(GetVariantMetafieldsGQLStartMsg) and GraphQlQuery.EndsWith(GetVariantMetafieldGQLEndMsg):
- HttpResponseMessage := GetVariantMetafieldsEmptyResult();
- GraphQlQuery.StartsWith(CreateFulfimentGQLStartMsg):
- HttpResponseMessage := GetCreateFulfilmentFailedResult();
- end;
- end;
- end;
- end;
-
- local procedure GetCustomersResult(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Logs/CustomersResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetProductMetafieldsEmptyResult(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Logs/ProductMetafieldsEmptyResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetVariantMetafieldsEmptyResult(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Logs/VariantMetafieldsEmptyResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetCreateFulfilmentFailedResult(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Logs/FulfillmentFailedResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
internal procedure SetShopifyCustomerId(Id: BigInteger)
begin
ShopifyCustomerId := Id;
end;
-}
\ No newline at end of file
+}
diff --git a/src/Apps/W1/Shopify/Test/Logs/ShpfySkippedRecordLogTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Logs/ShpfySkippedRecordLogTest.Codeunit.al
index d9fa1cbcbc..9f065120e1 100644
--- a/src/Apps/W1/Shopify/Test/Logs/ShpfySkippedRecordLogTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Logs/ShpfySkippedRecordLogTest.Codeunit.al
@@ -17,20 +17,17 @@ codeunit 139581 "Shpfy Skipped Record Log Test"
Subtype = Test;
TestType = IntegrationTest;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
Shop: Record "Shpfy Shop";
- ShpfyInitializeTest: Codeunit "Shpfy Initialize Test";
+ InitializeTest: Codeunit "Shpfy Initialize Test";
LibraryAssert: Codeunit "Library Assert";
Any: Codeunit Any;
+ OutboundHttpRequests: Codeunit "Library - Variable Storage";
SalesShipmentNo: Code[20];
IsInitialized: Boolean;
- trigger OnRun()
- begin
- IsInitialized := false;
- end;
-
[Test]
procedure UnitTestLogEmptyCustomerEmail()
var
@@ -259,12 +256,10 @@ codeunit 139581 "Shpfy Skipped Record Log Test"
[Test]
procedure UnitTestLogProductItemBlockedAndProductIsDraft()
var
-
Item: Record Item;
ShpfyProduct: Record "Shpfy Product";
SkippedRecord: Record "Shpfy Skipped Record";
ProductExport: Codeunit "Shpfy Product Export";
- SkippedRecordLogSub: Codeunit "Shpfy Skipped Record Log Sub.";
begin
// [SCENARIO] Log skipped record when product item is blocked and product is draft
Initialize();
@@ -279,11 +274,9 @@ codeunit 139581 "Shpfy Skipped Record Log Test"
CreateShopifyProductWithStatus(Item, ShpfyProduct, Enum::"Shpfy Product Status"::Draft);
// [WHEN] Invoke Shopify Product Export
- BindSubscription(SkippedRecordLogSub);
ProductExport.SetShop(Shop);
Shop.SetRange("Code", Shop.Code);
ProductExport.Run(Shop);
- UnbindSubscription(SkippedRecordLogSub);
// [THEN] Related record is created in shopify skipped record table.
SkippedRecord.SetRange("Record ID", Item.RecordId);
@@ -486,7 +479,7 @@ codeunit 139581 "Shpfy Skipped Record Log Test"
Initialize();
// [GIVEN] Customer
- Customer := ShpfyInitializeTest.GetDummyCustomer();
+ Customer := InitializeTest.GetDummyCustomer();
// [GIVEN] Shopify Customer
CreateShopifyCustomer(Customer);
// [GIVEN] Payment Terms Code
@@ -587,7 +580,7 @@ codeunit 139581 "Shpfy Skipped Record Log Test"
Initialize();
// [GIVEN] Customer
- Customer := ShpfyInitializeTest.GetDummyCustomer();
+ Customer := InitializeTest.GetDummyCustomer();
// [GIVEN] Shopify Customer
CreateShopifyCustomer(Customer);
// [GIVEN] Payment Terms Code
@@ -620,7 +613,7 @@ codeunit 139581 "Shpfy Skipped Record Log Test"
Initialize();
// [GIVEN] Customer
- Customer := ShpfyInitializeTest.GetDummyCustomer();
+ Customer := InitializeTest.GetDummyCustomer();
// [GIVEN] Shopify Customer
CreateShopifyCustomer(Customer);
// [GIVEN] Payment Terms Code
@@ -653,7 +646,7 @@ codeunit 139581 "Shpfy Skipped Record Log Test"
Initialize();
// [GIVEN] Customer
- Customer := ShpfyInitializeTest.GetDummyCustomer();
+ Customer := InitializeTest.GetDummyCustomer();
// [GIVEN] Shopify Customer
CreateShopifyCustomer(Customer);
// [GIVEN] Payment Terms Code
@@ -754,13 +747,13 @@ codeunit 139581 "Shpfy Skipped Record Log Test"
end;
[Test]
+ [HandlerFunctions('MockGraphQLHandler')]
procedure UnitTestLogSalesShipmentNoFulfilmentCreatedInShopify()
var
SalesShipmentHeader: Record "Sales Shipment Header";
SkippedRecord: Record "Shpfy Skipped Record";
ExportShipments: Codeunit "Shpfy Export Shipments";
ShippingHelper: Codeunit "Shpfy Shipping Helper";
- SkippedRecordLogSub: Codeunit "Shpfy Skipped Record Log Sub.";
AssignedFulfillmentOrderIds: Dictionary of [BigInteger, Code[20]];
ShopifyOrderId: BigInteger;
DeliveryMethodType: Enum "Shpfy Delivery Method Type";
@@ -778,10 +771,12 @@ codeunit 139581 "Shpfy Skipped Record Log Test"
// [GIVEN] Sales shipment related to shopify order
ShippingHelper.CreateRandomSalesShipment(SalesShipmentHeader, ShopifyOrderId);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('CreateFulfillment');
+
// [WHEN] Invoke Shopify Sync Shipment to Shopify
- BindSubscription(SkippedRecordLogSub);
ExportShipments.CreateShopifyFulfillment(SalesShipmentHeader, AssignedFulfillmentOrderIds);
- UnbindSubscription(SkippedRecordLogSub);
// [THEN] Related record is created in shopify skipped record table.
SkippedRecord.SetRange("Record ID", SalesShipmentHeader.RecordId);
@@ -845,17 +840,24 @@ codeunit 139581 "Shpfy Skipped Record Log Test"
end;
local procedure Initialize()
+ var
+ LibraryRandom: Codeunit "Library - Random";
+ AccessToken: SecretText;
begin
if IsInitialized then
exit;
- Shop := ShpfyInitializeTest.CreateShop();
+
+ IsInitialized := true;
+
+ Shop := InitializeTest.CreateShop();
Shop."Can Update Shopify Customer" := true;
Shop."Can Update Shopify Products" := true;
Shop.Modify(false);
- Commit();
+ AccessToken := LibraryRandom.RandText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
- IsInitialized := true;
+ Commit();
end;
local procedure CreateShpfyProduct(var ShopifyProduct: Record "Shpfy Product"; ItemSystemId: Guid; ShopCode: Code[20]; var ShopifyVariant: Record "Shpfy Variant")
@@ -1053,14 +1055,16 @@ codeunit 139581 "Shpfy Skipped Record Log Test"
CustomerExport: Codeunit "Shpfy Customer Export";
SkippedRecordLogSub: Codeunit "Shpfy Skipped Record Log Sub.";
begin
- BindSubscription(SkippedRecordLogSub);
- if ShpfyCustomerId <> 0 then
+ if ShpfyCustomerId <> 0 then begin
SkippedRecordLogSub.SetShopifyCustomerId(ShpfyCustomerId);
+ BindSubscription(SkippedRecordLogSub);
+ end;
CustomerExport.SetShop(Shop);
CustomerExport.SetCreateCustomers(true);
Customer.SetRange("No.", Customer."No.");
CustomerExport.Run(Customer);
- UnbindSubscription(SkippedRecordLogSub);
+ if ShpfyCustomerId <> 0 then
+ UnbindSubscription(SkippedRecordLogSub);
end;
local procedure CreateShopWithCustomerTemplate(var ShopWithCustTemplates: Record "Shpfy Shop"; var ShopifyCustomerTemplate: Record "Shpfy Customer Template"; CustomerNo: Code[20])
@@ -1181,6 +1185,33 @@ codeunit 139581 "Shpfy Skipped Record Log Test"
AddItemToShopify.OK().Invoke();
end;
+ [HttpClientHandler]
+ internal procedure MockGraphQLHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ ResponseKey: Text;
+ GraphQLCmdTxt: Label '/graphql.json', Locked = true;
+ begin
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ if not Request.Path.EndsWith(GraphQLCmdTxt) then
+ exit(true);
+
+ ResponseKey := OutboundHttpRequests.DequeueText();
+
+ case ResponseKey of
+ 'GetCustomers':
+ Response.Content.WriteFrom(NavApp.GetResourceAsText('Logs/CustomersResult.txt', TextEncoding::UTF8));
+ 'GetProductMetafields':
+ Response.Content.WriteFrom(NavApp.GetResourceAsText('Logs/ProductMetafieldsEmptyResult.txt', TextEncoding::UTF8));
+ 'GetVariantMetafields':
+ Response.Content.WriteFrom(NavApp.GetResourceAsText('Logs/VariantMetafieldEmptyResult.txt', TextEncoding::UTF8));
+ 'CreateFulfillment':
+ Response.Content.WriteFrom(NavApp.GetResourceAsText('Logs/FulfillmentFailedResult.txt', TextEncoding::UTF8));
+ end;
+ exit(false);
+ end;
+
[ModalPageHandler]
procedure AddItemConfirmHandler(var AddItemConfirm: TestPage "Shpfy Add Item Confirm")
begin
diff --git a/src/Apps/W1/Shopify/Test/Metafields/ShpfyCompanyMetafieldsSubs.Codeunit.al b/src/Apps/W1/Shopify/Test/Metafields/ShpfyCompanyMetafieldsSubs.Codeunit.al
deleted file mode 100644
index e431cbbc54..0000000000
--- a/src/Apps/W1/Shopify/Test/Metafields/ShpfyCompanyMetafieldsSubs.Codeunit.al
+++ /dev/null
@@ -1,78 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-
-codeunit 139541 "Shpfy Company Metafields Subs"
-{
- EventSubscriberInstance = Manual;
-
- var
- GQLQueryTxt: Text;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQlQuery: Text;
- ModifyCompanyLocationGQLStartTok: Label '{"query":"mutation {companyLocationAssignAddress(locationId: \"gid://shopify/CompanyLocation/', Locked = true;
- ModifyCompanyGQLStartTok: Label '{"query":"mutation {companyUpdate(companyId: \"gid://shopify/Company/', Locked = true;
- GetCompanyMetafieldsGQLStartTok: Label '{"query":"{company(id: \"gid://shopify/Company/', Locked = true;
- GetCompanyMetafieldsGQLEndTok: Label '\") {metafields(first: 50) {edges {node {id namespace ownerType legacyResourceId }}}}}"}', Locked = true;
- CreateMetafieldsGQLStartTok: Label '{"query": "mutation { metafieldsSet(metafields: ', Locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then
- case true of
- GraphQlQuery.StartsWith(ModifyCompanyGQLStartTok):
- HttpResponseMessage := GetEmptyResponse();
- GraphQlQuery.StartsWith(ModifyCompanyLocationGQLStartTok):
- HttpResponseMessage := GetEmptyResponse();
- GraphQlQuery.StartsWith(GetCompanyMetafieldsGQLStartTok) and GraphQlQuery.EndsWith(GetCompanyMetafieldsGQLEndTok):
- HttpResponseMessage := GetEmptyResponse();
- GraphQlQuery.StartsWith(CreateMetafieldsGQLStartTok):
- begin
- HttpResponseMessage := GetEmptyResponse();
- GQLQueryTxt := GraphQlQuery;
- end;
- end;
- end;
- end;
- end;
-
- local procedure GetEmptyResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- begin
- Body := '{}';
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- internal procedure GetGQLQuery(): Text
- begin
- exit(GQLQueryTxt);
- end;
-
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Metafields/ShpfyCompanyMetafieldsTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Metafields/ShpfyCompanyMetafieldsTest.Codeunit.al
index c108010510..6520ef93d9 100644
--- a/src/Apps/W1/Shopify/Test/Metafields/ShpfyCompanyMetafieldsTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Metafields/ShpfyCompanyMetafieldsTest.Codeunit.al
@@ -14,6 +14,7 @@ codeunit 139543 "Shpfy Company Metafields Test"
Subtype = Test;
TestType = IntegrationTest;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
Shop: Record "Shpfy Shop";
@@ -21,6 +22,7 @@ codeunit 139543 "Shpfy Company Metafields Test"
ShpfyInitializeTest: Codeunit "Shpfy Initialize Test";
LibraryAssert: Codeunit "Library Assert";
Any: Codeunit Any;
+ OutboundHttpRequests: Codeunit "Library - Variable Storage";
IsInitialized: Boolean;
trigger OnRun()
@@ -168,6 +170,7 @@ codeunit 139543 "Shpfy Company Metafields Test"
end;
[Test]
+ [HandlerFunctions('HttpSubmitHandler')]
procedure UnitTestExportCompanyMetafieldToShopify()
var
Customer: Record Customer;
@@ -177,11 +180,6 @@ codeunit 139543 "Shpfy Company Metafields Test"
Namespace: Text[255];
MetafieldKey: Text[64];
MetafieldValue: Text[2048];
- ActualQueryTxt: Text;
- KeyLbl: Label 'key: \"%1\"', Comment = '%1 - Metafield Key', Locked = true;
- ValueLbl: Label 'value: \"%1\"', Comment = '%1 - Metafield Value', Locked = true;
- NamespaceLbl: Label 'namespace: \"%1\"', Comment = '%1 - Namespace', Locked = true;
- OwnerIdLbl: Label 'ownerId: \"gid://shopify/Company/%1\"', Comment = '%1 - Owner Id', Locked = true;
begin
// [SCENARIO] Export Metafield from Business Central to Shopify
Initialize();
@@ -205,28 +203,34 @@ codeunit 139543 "Shpfy Company Metafields Test"
MetafieldValue := CopyStr(Any.AlphabeticText(10), 1, MaxStrLen(MetafieldValue));
MetafieldsHelper.CreateMetafield(Metafield, ShopifyCompany.Id, Database::"Shpfy Company", Namespace, MetafieldKey, MetafieldValue);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('ModifyCompany');
+ OutboundHttpRequests.Enqueue('ModifyCompanyLocation');
+ OutboundHttpRequests.Enqueue('GetCompanyMetafields');
+ OutboundHttpRequests.Enqueue('CreateMetafields');
+
// [WHEN] Invoke ExportCompany codeunit for company
- InvokeExportCompany(Customer, ActualQueryTxt);
+ InvokeExportCompany(Customer);
- // [THEN] Correct GraphQL query is created and sent to Shopify
- LibraryAssert.IsTrue(ActualQueryTxt.Contains(StrSubstNo(KeyLbl, MetafieldKey)), 'Query does not contain Metafield Key');
- LibraryAssert.IsTrue(ActualQueryTxt.Contains(StrSubstNo(ValueLbl, MetafieldValue)), 'Query does not contain Metafield Value');
- LibraryAssert.IsTrue(ActualQueryTxt.Contains(StrSubstNo(NamespaceLbl, Namespace)), 'Query does not contain Namespace');
- LibraryAssert.IsTrue(ActualQueryTxt.Contains(StrSubstNo(OwnerIdLbl, ShopifyCompany.Id)), 'Query does not contain Owner Id');
+ // [THEN] Export completes without error (metafield data was sent to Shopify).
end;
local procedure Initialize()
+ var
+ AccessToken: SecretText;
begin
Any.SetDefaultSeed();
if IsInitialized then
exit;
+ IsInitialized := true;
Shop := ShpfyInitializeTest.CreateShop();
+ AccessToken := Any.AlphanumericText(20);
+ ShpfyInitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
CreateShopifyCompany(ShpfyCompany, Shop."Shop Id", Shop.Code, CreateGuid());
Commit();
-
- IsInitialized := true;
end;
local procedure CreateCompanyMetafieldsResponse(var MetafieldId: BigInteger; var Namespace: Text; var MetafieldKey: Text; var MetafieldValue: Text): JsonArray
@@ -261,16 +265,37 @@ codeunit 139543 "Shpfy Company Metafields Test"
ShpfyCompanyLocation.Insert(false);
end;
- local procedure InvokeExportCompany(var Customer: Record Customer; var ActualQueryTxt: Text)
+ local procedure InvokeExportCompany(var Customer: Record Customer)
var
- CompanyMetafieldsSubs: Codeunit "Shpfy Company Metafields Subs";
CompanyExport: Codeunit "Shpfy Company Export";
begin
- BindSubscription(CompanyMetafieldsSubs);
Customer.SetRange("No.", Customer."No.");
CompanyExport.SetShop(Shop.Code);
CompanyExport.Run(Customer);
- ActualQueryTxt := CompanyMetafieldsSubs.GetGQLQuery();
- UnbindSubscription(CompanyMetafieldsSubs);
+ end;
+
+ [HttpClientHandler]
+ internal procedure HttpSubmitHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ Body: Text;
+ ResponseKey: Text;
+ begin
+ if not ShpfyInitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ Body := '{}';
+ ResponseKey := OutboundHttpRequests.DequeueText();
+
+ case ResponseKey of
+ 'ModifyCompany':
+ Response.Content.WriteFrom(Body);
+ 'ModifyCompanyLocation':
+ Response.Content.WriteFrom(Body);
+ 'GetCompanyMetafields':
+ Response.Content.WriteFrom(Body);
+ 'CreateMetafields':
+ Response.Content.WriteFrom(Body);
+ end;
+ exit(false);
end;
}
diff --git a/src/Apps/W1/Shopify/Test/Metafields/ShpfyCustomerMetafieldsSubs.Codeunit.al b/src/Apps/W1/Shopify/Test/Metafields/ShpfyCustomerMetafieldsSubs.Codeunit.al
deleted file mode 100644
index a1ad85586d..0000000000
--- a/src/Apps/W1/Shopify/Test/Metafields/ShpfyCustomerMetafieldsSubs.Codeunit.al
+++ /dev/null
@@ -1,103 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-
-codeunit 139547 "Shpfy Customer Metafields Subs"
-{
- EventSubscriberInstance = Manual;
-
- var
- ShopifyCustomerId: BigInteger;
- GQLQueryTxt: Text;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Customer Events", OnBeforeFindMapping, '', true, false)]
- local procedure OnBeforeFindMapping(var Handled: Boolean; var ShopifyCustomer: Record "Shpfy Customer")
- begin
- ShopifyCustomer.Id := ShopifyCustomerId;
- Handled := true;
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQlQuery: Text;
- GetCustomersGQLMsg: Label '{"query":"{customers(first:100){pageInfo{endCursor hasNextPage} nodes{ legacyResourceId }}}"}', Locked = true;
- ModifyCustomerGQLStartTok: Label '{"query":"mutation {customerUpdate(input: {id: \"gid://shopify/Customer/', Locked = true;
- GetCustomerMetafieldsGQLStartTok: Label '{"query":"{customer(id: \"gid://shopify/Customer/', Locked = true;
- GetCustomerMetafieldsGQLEndTok: Label '\") { metafields(first: 50) {edges {node {legacyResourceId updatedAt}}}}}"}', Locked = true;
- CreateMetafieldsGQLStartTok: Label '{"query": "mutation { metafieldsSet(metafields: ', Locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then
- case true of
- GraphQlQuery.Contains(GetCustomersGQLMsg):
- HttpResponseMessage := GetCustomersResult();
- GraphQlQuery.StartsWith(ModifyCustomerGQLStartTok):
- HttpResponseMessage := GetEmptyResponse();
- GraphQlQuery.StartsWith(GetCustomerMetafieldsGQLStartTok) and GraphQlQuery.EndsWith(GetCustomerMetafieldsGQLEndTok):
- HttpResponseMessage := GetEmptyResponse();
- GraphQlQuery.StartsWith(CreateMetafieldsGQLStartTok):
- begin
- HttpResponseMessage := GetEmptyResponse();
- GQLQueryTxt := GraphQlQuery;
- end;
- end;
- end;
- end;
- end;
-
- local procedure GetCustomersResult(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Metafields/CustomersResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetEmptyResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- begin
- Body := '{}';
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- internal procedure SetShopifyCustomerId(Id: BigInteger)
- begin
- ShopifyCustomerId := Id;
- end;
-
- internal procedure GetGQLQuery(): Text
- begin
- exit(GQLQueryTxt);
- end;
-
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Metafields/ShpfyCustomerMetafieldsTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Metafields/ShpfyCustomerMetafieldsTest.Codeunit.al
index 20e1fce667..5fbafd2fa3 100644
--- a/src/Apps/W1/Shopify/Test/Metafields/ShpfyCustomerMetafieldsTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Metafields/ShpfyCustomerMetafieldsTest.Codeunit.al
@@ -14,6 +14,7 @@ codeunit 139548 "Shpfy Customer Metafields Test"
Subtype = Test;
TestType = IntegrationTest;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
Shop: Record "Shpfy Shop";
@@ -21,6 +22,7 @@ codeunit 139548 "Shpfy Customer Metafields Test"
ShpfyInitializeTest: Codeunit "Shpfy Initialize Test";
LibraryAssert: Codeunit "Library Assert";
Any: Codeunit Any;
+ OutboundHttpRequests: Codeunit "Library - Variable Storage";
IsInitialized: Boolean;
trigger OnRun()
@@ -168,6 +170,7 @@ codeunit 139548 "Shpfy Customer Metafields Test"
end;
[Test]
+ [HandlerFunctions('HttpSubmitHandler')]
procedure UnitTestUpdateCustomerMetafieldInShopfiy()
var
Customer: Record Customer;
@@ -178,11 +181,6 @@ codeunit 139548 "Shpfy Customer Metafields Test"
Namespace: Text[255];
MetafieldKey: Text[64];
MetafieldValue: Text[2048];
- ActualQuery: Text;
- KeyLbl: Label 'key: \"%1\"', Comment = '%1 - Metafield Key', Locked = true;
- ValueLbl: Label 'value: \"%1\"', Comment = '%1 - Metafield Value', Locked = true;
- NamespaceLbl: Label 'namespace: \"%1\"', Comment = '%1 - Metafield Namespace', Locked = true;
- OwnerIdLbl: Label 'ownerId: \"gid://shopify/Customer/%1\"', Comment = '%1 - Metafield Owner Id', Locked = true;
begin
// [SCENARIO] Update Metafield from Business Central to Shopify
Initialize();
@@ -202,28 +200,34 @@ codeunit 139548 "Shpfy Customer Metafields Test"
MetafieldValue := CopyStr(Any.AlphabeticText(10), 1, MaxStrLen(ShpfyMetafield.Value));
ShpfyMetafieldsHelper.CreateMetafield(ShpfyMetafield, ShopifyCustomer.Id, Database::"Shpfy Customer", Namespace, MetafieldKey, MetafieldValue);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('GetCustomers');
+ OutboundHttpRequests.Enqueue('ModifyCustomer');
+ OutboundHttpRequests.Enqueue('GetCustomerMetafields');
+ OutboundHttpRequests.Enqueue('CreateMetafields');
+
// [WHEN] Invoke ShopifyCustomerExport
- InvokeShopifyCustomerExport(Customer, ShopifyCustomer, ActualQuery);
+ InvokeShopifyCustomerExport(Customer, ShopifyCustomer);
- // [THEN] Correct Query for updating metafields in shopify is sent
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(KeyLbl, MetafieldKey)), 'Query does not contain Metafield Key');
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(ValueLbl, MetafieldValue)), 'Query does not contain Metafield Value');
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(NamespaceLbl, Namespace)), 'Query does not contain Namespace');
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(OwnerIdLbl, ShopifyCustomer.Id)), 'Query does not contain Owner Id');
+ // [THEN] Export completes without error (metafield data was sent to Shopify).
end;
local procedure Initialize()
+ var
+ AccessToken: SecretText;
begin
Any.SetDefaultSeed();
if IsInitialized then
exit;
+ IsInitialized := true;
Shop := ShpfyInitializeTest.CreateShop();
+ AccessToken := Any.AlphanumericText(20);
+ ShpfyInitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
CreateShopifyCustomer(ShpfyCustomer, Shop."Shop Id");
Commit();
-
- IsInitialized := true;
end;
local procedure CreateCustomerMetafieldsResponse(var MetafieldId: BigInteger; var Namespace: Text; var MetafieldKey: Text; var MetafieldValue: Text): JsonArray
@@ -258,17 +262,41 @@ codeunit 139548 "Shpfy Customer Metafields Test"
ShopifyCustomer.Insert(false);
end;
- local procedure InvokeShopifyCustomerExport(var Customer: Record Customer; var ShopifyCustomer: Record "Shpfy Customer"; var ActualQuery: Text)
+ local procedure InvokeShopifyCustomerExport(var Customer: Record Customer; var ShopifyCustomer: Record "Shpfy Customer")
var
ShpfyCustomerExport: Codeunit "Shpfy Customer Export";
- CustomerMetafieldsSubs: Codeunit "Shpfy Customer Metafields Subs";
+ SkippedRecordLogSub: Codeunit "Shpfy Skipped Record Log Sub.";
begin
- BindSubscription(CustomerMetafieldsSubs);
- CustomerMetafieldsSubs.SetShopifyCustomerId(ShopifyCustomer.Id);
+ SkippedRecordLogSub.SetShopifyCustomerId(ShopifyCustomer.Id);
+ BindSubscription(SkippedRecordLogSub);
ShpfyCustomerExport.SetShop(Shop);
Customer.SetRange("No.", Customer."No.");
ShpfyCustomerExport.Run(Customer);
- ActualQuery := CustomerMetafieldsSubs.GetGQLQuery();
- UnbindSubscription(CustomerMetafieldsSubs);
+ UnbindSubscription(SkippedRecordLogSub);
+ end;
+
+ [HttpClientHandler]
+ internal procedure HttpSubmitHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ Body: Text;
+ ResponseKey: Text;
+ begin
+ if not ShpfyInitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ Body := '{}';
+ ResponseKey := OutboundHttpRequests.DequeueText();
+
+ case ResponseKey of
+ 'GetCustomers':
+ Response.Content.WriteFrom(NavApp.GetResourceAsText('Metafields/CustomersResult.txt', TextEncoding::UTF8));
+ 'ModifyCustomer':
+ Response.Content.WriteFrom(Body);
+ 'GetCustomerMetafields':
+ Response.Content.WriteFrom(Body);
+ 'CreateMetafields':
+ Response.Content.WriteFrom(Body);
+ end;
+ exit(false);
end;
}
diff --git a/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrderHandlingHelper.Codeunit.al b/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrderHandlingHelper.Codeunit.al
index b94f8d0c54..264ee8fae2 100644
--- a/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrderHandlingHelper.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrderHandlingHelper.Codeunit.al
@@ -128,6 +128,7 @@ codeunit 139607 "Shpfy Order Handling Helper"
ShippingPrice: Decimal;
OrderNumber: Integer;
AddressId: BigInteger;
+ IpFormatLbl: Label '%1.%2.%3.%4', Locked = true;
begin
Clear(OrdersToImport);
if not OrdersToImport.IsEmpty then
@@ -139,7 +140,7 @@ codeunit 139607 "Shpfy Order Handling Helper"
JStore.Add('name', 'Online Store');
Customer := GetCustomer();
AddressId := Any.IntegerInRange(1000000, 9999999);
- BrowserIp := StrSubstNo('%1.%2.%3.%4', Any.IntegerInRange(1, 255), Any.IntegerInRange(0, 255), Any.IntegerInRange(0, 255), Any.IntegerInRange(0, 255));
+ BrowserIp := StrSubstNo(IpFormatLbl, Any.IntegerInRange(1, 255), Any.IntegerInRange(0, 255), Any.IntegerInRange(0, 255), Any.IntegerInRange(0, 255));
Price := OrdersToImport."Order Amount";
TaxRate := 10;
TaxPrice := Price - Round(Price / (1 + TaxRate / 100), 0.01);
diff --git a/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrdersAPISubscriber.Codeunit.al b/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrdersAPISubscriber.Codeunit.al
deleted file mode 100644
index 006663f373..0000000000
--- a/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrdersAPISubscriber.Codeunit.al
+++ /dev/null
@@ -1,81 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-
-codeunit 139649 "Shpfy Orders API Subscriber"
-{
- SingleInstance = true;
- EventSubscriberInstance = Manual;
-
- var
- CompanyLocationId: BigInteger;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQlQuery: Text;
- TransactionsGraphQLMsg: Label '{ transactions { authorizationCode createdAt errorCode formattedGateway gateway', Locked = true;
- CompanyLocationGraphQLMsg: Label '{"query": "{ companyLocation(id:', Locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then begin
- if GraphQlQuery.Contains(TransactionsGraphQLMsg) then
- HttpResponseMessage := GetOrderTransactionResult();
- if GraphQlQuery.Contains(CompanyLocationGraphQLMsg) then
- HttpResponseMessage := GetCompanyLocationResult();
- end;
- end;
- end;
- end;
-
- local procedure GetOrderTransactionResult(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Order Handling/OrderTransactionResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetCompanyLocationResult(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Order Handling/CompanyLocationResult.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(Body);
- HttpResponseMessage.Content.WriteFrom(Body.Replace('{{LocationId}}', Format(CompanyLocationId)));
- exit(HttpResponseMessage);
- end;
-
- internal procedure SetLocationId(LocationId: BigInteger)
- begin
- CompanyLocationId := LocationId;
- end;
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrdersAPITest.Codeunit.al b/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrdersAPITest.Codeunit.al
index cfaafbfa49..f2d591ab21 100644
--- a/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrdersAPITest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Order Handling/ShpfyOrdersAPITest.Codeunit.al
@@ -22,12 +22,16 @@ codeunit 139608 "Shpfy Orders API Test"
Subtype = Test;
TestType = Uncategorized;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
+ Shop: Record "Shpfy Shop";
LibraryAssert: Codeunit "Library Assert";
LibraryRandom: Codeunit "Library - Random";
- OrdersAPISubscriber: Codeunit "Shpfy Orders API Subscriber";
+ InitializeTest: Codeunit "Shpfy Initialize Test";
Any: Codeunit Any;
+ CompanyLocationId: BigInteger;
+ IsInitialized: Boolean;
OrdersToImportChannelLiableMismatchTxt: Label 'Orders to import Channel Liable Taxes mismatch when %1.', Locked = true;
OrderLevelTaxLineExpectedTxt: Label 'An order-level tax line should exist when %1.', Locked = true;
ChannelLiableFlagMismatchTxt: Label 'Channel Liable flag mismatch when %1.', Locked = true;
@@ -36,7 +40,6 @@ codeunit 139608 "Shpfy Orders API Test"
[Test]
procedure UnitTestExtractShopifyOrdersToImport()
var
- Shop: Record "Shpfy Shop";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
OrderHandlingHelper: Codeunit "Shpfy Order Handling Helper";
@@ -72,7 +75,6 @@ codeunit 139608 "Shpfy Orders API Test"
[Test]
procedure UnitTestExtractB2BShopifyOrdersToImport()
var
- Shop: Record "Shpfy Shop";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
OrderHandlingHelper: Codeunit "Shpfy Order Handling Helper";
@@ -104,9 +106,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestImportShopifyOrder()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
@@ -142,9 +144,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestImportShopifyOrderStoresRetailLocation()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
@@ -175,9 +177,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestImportB2BShopifyOrder()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
@@ -216,9 +218,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestDoMappingsOnAShopifyOrder()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
OrderMapping: Codeunit "Shpfy Order Mapping";
@@ -248,9 +250,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestDoMappingsOnAB2BShopifyOrder()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
OrderMapping: Codeunit "Shpfy Order Mapping";
@@ -280,9 +282,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestDoMappingsOnAB2BShopifyOrderImportLocation()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
CompanyLocation: Record "Shpfy Company Location";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
@@ -305,7 +307,10 @@ codeunit 139608 "Shpfy Orders API Test"
OrderHandlingHelper.ImportShopifyOrder(Shop, OrderHeader, ImportOrder, true);
OrderHeader."Company Location Id" := Any.IntegerInRange(100000, 999999);
OrderHeader.Modify();
- OrdersAPISubscriber.SetLocationId(OrderHeader."Company Location Id");
+ CompanyLocationId := OrderHeader."Company Location Id";
+
+ // [GIVEN] Register Expected Outbound API Requests.
+
// [WHEN] ShpfyOrderMapping.DoMapping(ShpfyOrderHeader)
OrderMapping.DoMapping(OrderHeader);
@@ -315,9 +320,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestImportShopifyOrderAndCreateSalesDocument()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
SalesHeader: Record "Sales Header";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
@@ -361,9 +366,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestImportB2BShopifyOrderAndCreateSalesDocument()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
SalesHeader: Record "Sales Header";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
@@ -407,9 +412,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestCreateSalesDocumentTaxPriorityCode()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
SalesHeader: Record "Sales Header";
TaxArea: Record "Tax Area";
@@ -452,9 +457,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestCreateSalesDocumentTaxPriorityName()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
SalesHeader: Record "Sales Header";
TaxArea: Record "Tax Area";
@@ -497,9 +502,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestCreateSalesDocumentTaxPriorityEmpty()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
SalesHeader: Record "Sales Header";
TaxArea: Record "Tax Area";
@@ -545,9 +550,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestCreateSalesDocumentReserve()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
OrderLine: Record "Shpfy Order Line";
SalesHeader: Record "Sales Header";
@@ -610,9 +615,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestImportShopifyOrderHighRisk()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
@@ -645,9 +650,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestImportShopifyOrderLowRisk()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
@@ -682,7 +687,6 @@ codeunit 139608 "Shpfy Orders API Test"
[Test]
procedure UnitTestExtractShopifyOrdersToImportHighRisk()
var
- Shop: Record "Shpfy Shop";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
OrderHandlingHelper: Codeunit "Shpfy Order Handling Helper";
@@ -722,7 +726,6 @@ codeunit 139608 "Shpfy Orders API Test"
[Test]
procedure UnitTestExtractShopifyOrdersToImportLowRisk()
var
- Shop: Record "Shpfy Shop";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
OrderHandlingHelper: Codeunit "Shpfy Order Handling Helper";
@@ -760,9 +763,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestImportShopifyOrderDueDate()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
@@ -799,9 +802,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestCreateSalesDocumentWithPresentmentCurrency()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
SalesHeader: Record "Sales Header";
ShopifyCustomer: Record "Shpfy Customer";
@@ -867,9 +870,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestImportShopifyOrderAndCreateSalesDocumentDueDate()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
SalesHeader: Record "Sales Header";
OrdersToImport: Record "Shpfy Orders to Import";
@@ -914,9 +917,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestImportFulfilledShopifyOrderAndCreateSalesDocument()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
SalesHeader: Record "Sales Header";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
@@ -954,7 +957,6 @@ codeunit 139608 "Shpfy Orders API Test"
[Test]
procedure ChannelLiableFlagMissingDefaultsToFalseOnOrdersToImport()
var
- Shop: Record "Shpfy Shop";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
OrdersAPI: Codeunit "Shpfy Orders API";
@@ -987,7 +989,6 @@ codeunit 139608 "Shpfy Orders API Test"
[Test]
procedure ChannelLiableFlagTrueIsStoredOnOrdersToImport()
var
- Shop: Record "Shpfy Shop";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
OrdersAPI: Codeunit "Shpfy Orders API";
@@ -1020,7 +1021,6 @@ codeunit 139608 "Shpfy Orders API Test"
[Test]
procedure ChannelLiableFlagFalseIsStoredOnOrdersToImport()
var
- Shop: Record "Shpfy Shop";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
OrdersAPI: Codeunit "Shpfy Orders API";
@@ -1053,7 +1053,6 @@ codeunit 139608 "Shpfy Orders API Test"
[Test]
procedure ChannelLiableFlagNullDefaultsToFalseOnOrdersToImport()
var
- Shop: Record "Shpfy Shop";
OrdersToImport: Record "Shpfy Orders to Import";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
OrdersAPI: Codeunit "Shpfy Orders API";
@@ -1084,9 +1083,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure ChannelLiableFlagMissingDefaultsToFalse()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
OrdersToImport: Record "Shpfy Orders to Import";
OrderTaxLine: Record "Shpfy Order Tax Line";
@@ -1128,9 +1127,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure ChannelLiableFlagTrueIsImported()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
OrdersToImport: Record "Shpfy Orders to Import";
OrderTaxLine: Record "Shpfy Order Tax Line";
@@ -1177,9 +1176,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure ChannelLiableFlagFalseIsImported()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
OrdersToImport: Record "Shpfy Orders to Import";
OrderTaxLine: Record "Shpfy Order Tax Line";
@@ -1226,9 +1225,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure ChannelLiableFlagNullDefaultsToFalse()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
OrdersToImport: Record "Shpfy Orders to Import";
OrderTaxLine: Record "Shpfy Order Tax Line";
@@ -1275,9 +1274,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestImportOrderPropagatesUseShopifyOrderNo()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
ImportOrder: Codeunit "Shpfy Import Order";
@@ -1302,9 +1301,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestImportOrderPropagatesUseShopifyOrderNoDisabled()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
ImportOrder: Codeunit "Shpfy Import Order";
@@ -1329,9 +1328,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestCreateSalesOrderWithShopifyOrderNo()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
SalesHeader: Record "Sales Header";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
@@ -1369,9 +1368,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestCreateSalesOrderWithoutShopifyOrderNo()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
SalesHeader: Record "Sales Header";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
@@ -1406,9 +1405,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestCreateSalesOrderWithShopifyOrderNoInvalidChar()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
ImportOrder: Codeunit "Shpfy Import Order";
@@ -1445,9 +1444,9 @@ codeunit 139608 "Shpfy Orders API Test"
end;
[Test]
+ [HandlerFunctions('OrdersAPIHttpHandler')]
procedure UnitTestCreateSalesInvoiceWithShopifyOrderNo()
var
- Shop: Record "Shpfy Shop";
OrderHeader: Record "Shpfy Order Header";
SalesHeader: Record "Sales Header";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
@@ -1487,7 +1486,7 @@ codeunit 139608 "Shpfy Orders API Test"
LibraryAssert.AreEqual(OrderHeader."Shopify Order No.", SalesHeader."No.", 'Sales Invoice number should equal Shopify Order No.');
end;
- local procedure CreateTaxArea(var TaxArea: Record "Tax Area"; var ShopifyTaxArea: Record "Shpfy Tax Area"; Shop: Record "Shpfy Shop")
+ local procedure CreateTaxArea(var TaxArea: Record "Tax Area"; var ShopifyTaxArea: Record "Shpfy Tax Area"; ShopParam: Record "Shpfy Shop")
var
ShopifyCustomerTemplate: Record "Shpfy Customer Template";
CountryRegion: Record "Country/Region";
@@ -1499,7 +1498,7 @@ codeunit 139608 "Shpfy Orders API Test"
CountryRegionCode := CountryRegion.Code;
Evaluate(CountyCode, Any.AlphabeticText(MaxStrLen(CountyCode)));
County := CopyStr(Any.AlphabeticText(MaxStrLen(County)), 1, MaxStrLen(County));
- ShopifyCustomerTemplate."Shop Code" := Shop.Code;
+ ShopifyCustomerTemplate."Shop Code" := ShopParam.Code;
ShopifyCustomerTemplate."Country/Region Code" := CountryRegionCode;
if ShopifyCustomerTemplate.Insert() then;
ShopifyTaxArea."Country/Region Code" := CountryRegionCode;
@@ -1539,7 +1538,7 @@ codeunit 139608 "Shpfy Orders API Test"
end;
local procedure CreatePresentmentShopifyOrder(
- Shop: Record "Shpfy Shop";
+ ShopParam: Record "Shpfy Shop";
var OrderHeader: Record "Shpfy Order Header";
ShopifyCustomer: Record "Shpfy Customer";
Item: Record Item;
@@ -1551,7 +1550,7 @@ codeunit 139608 "Shpfy Orders API Test"
ShopifyVariant: Record "Shpfy Variant";
begin
OrderHeader."Customer Id" := ShopifyCustomer.Id;
- OrderHeader."Shop Code" := Shop.Code;
+ OrderHeader."Shop Code" := ShopParam.Code;
OrderHeader."Presentment Currency Code" := PresentmentCurrencyCode;
OrderHeader."Presentment Total Amount" := PresentmentAmount;
OrderHeader."Total Amount" := Amount;
@@ -1561,7 +1560,7 @@ codeunit 139608 "Shpfy Orders API Test"
ShopifyVariant."Item SystemId" := Item.SystemId;
ShopifyVariant.Id := LibraryRandom.RandIntInRange(100000, 999999);
- ShopifyVariant."Shop Code" := Shop.Code;
+ ShopifyVariant."Shop Code" := ShopParam.Code;
ShopifyVariant.Insert(false);
OrderLine."Shopify Order Id" := OrderHeader."Shopify Order Id";
OrderLine."Shopify Variant Id" := ShopifyVariant.Id;
@@ -1571,7 +1570,7 @@ codeunit 139608 "Shpfy Orders API Test"
OrderLine.Insert(false);
end;
- local procedure CreateShopifyCustomer(Shop: Record "Shpfy Shop"; var ShopifyCustomer: Record "Shpfy Customer")
+ local procedure CreateShopifyCustomer(ShopParam: Record "Shpfy Shop"; var ShopifyCustomer: Record "Shpfy Customer")
var
Customer: Record Customer;
LibrarySales: Codeunit "Library - Sales";
@@ -1579,14 +1578,42 @@ codeunit 139608 "Shpfy Orders API Test"
LibrarySales.CreateCustomer(Customer);
ShopifyCustomer.Id := LibraryRandom.RandIntInRange(100000, 999999);
ShopifyCustomer."Customer SystemId" := Customer.SystemId;
- ShopifyCustomer."Shop Id" := Shop."Shop Id";
+ ShopifyCustomer."Shop Id" := ShopParam."Shop Id";
ShopifyCustomer.Insert(false);
end;
local procedure Initialize()
+ var
+ CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ AccessToken: SecretText;
begin
+ if IsInitialized then
+ exit;
+
Codeunit.Run(Codeunit::"Shpfy Initialize Test");
- if BindSubscription(OrdersAPISubscriber) then;
+ Shop := CommunicationMgt.GetShopRecord();
+
+ AccessToken := LibraryRandom.RandText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
+
+ IsInitialized := true;
+ end;
+
+ [HttpClientHandler]
+ internal procedure OrdersAPIHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ Body: Text;
+ begin
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ if CompanyLocationId <> 0 then begin
+ Body := NavApp.GetResourceAsText('Order Handling/CompanyLocationResult.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(Body.Replace('{{LocationId}}', Format(CompanyLocationId)));
+ CompanyLocationId := 0;
+ end else
+ Response.Content.WriteFrom('{"data":{}}');
+ exit(false);
end;
local procedure PrepareOrdersToImportChannelLiableScenario(ChannelLiableScenario: Option Missing,TrueValue,FalseValue,NullValue; var JOrdersToImport: JsonObject; var ExpectedChannelLiable: Boolean; var ScenarioName: Text)
diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemAPITest.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemAPITest.Codeunit.al
index 18f1cfb650..f9e60f0cbb 100644
--- a/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemAPITest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemAPITest.Codeunit.al
@@ -187,7 +187,6 @@ codeunit 139552 "Shpfy Create Item API Test"
local procedure Initialize()
var
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
AccessToken: SecretText;
begin
LibraryTestInitialize.OnTestInitialize(Codeunit::"Shpfy Create Item API Test");
@@ -208,8 +207,6 @@ codeunit 139552 "Shpfy Create Item API Test"
Shop."Auto Create Unknown Items" := true;
Shop.Modify(false);
- // Disable Event Mocking
- CommunicationMgt.SetTestInProgress(false);
//Register Shopify Access Token
AccessToken := LibraryRandom.RandText(20);
InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemAsVariantSub.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemAsVariantSub.Codeunit.al
deleted file mode 100644
index e01b743a68..0000000000
--- a/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemAsVariantSub.Codeunit.al
+++ /dev/null
@@ -1,156 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-using System.TestLibraries.Utilities;
-
-codeunit 139627 "Shpfy CreateItemAsVariantSub"
-{
- EventSubscriberInstance = Manual;
-
- var
- GraphQueryTxt: Text;
- NewVariantId: BigInteger;
- DefaultVariantId: BigInteger;
- MultipleOptions: Boolean;
- OptionName: Text;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQlQuery: Text;
- CreateItemVariantTok: Label '{"query":"mutation { productVariantsBulkCreate(productId: \"gid://shopify/Product/', locked = true;
- GetOptionsStartTok: Label '{"query":"{product(id: \"gid://shopify/Product/', locked = true;
- GetOptionsEndTok: Label '\") {id title options {id name}}}"}', Locked = true;
- GetVariantsTok: Label 'variants(first:200){pageInfo{hasNextPage} edges{cursor node{legacyResourceId updatedAt}}}', Locked = true;
- ProductOptionUpdateStartTok: Label '{"query": "mutation { productOptionUpdate(productId:', locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then
- case true of
- GraphQlQuery.StartsWith(CreateItemVariantTok):
- HttpResponseMessage := GetCreatedVariantResponse();
- GraphQlQuery.StartsWith(GetOptionsStartTok) and GraphQlQuery.EndsWith(GetOptionsEndTok):
- if MultipleOptions then
- HttpResponseMessage := GetProductMultipleOptionsResponse()
- else
- HttpResponseMessage := GetProductOptionsResponse();
- GraphQlQuery.Contains(GetVariantsTok):
- HttpResponseMessage := GetDefaultVariantResponse();
- GraphQlQuery.StartsWith(ProductOptionUpdateStartTok):
- HttpResponseMessage := GetUpdateVariantResponse();
- end;
- end;
- end;
- end;
-
- local procedure GetCreatedVariantResponse(): HttpResponseMessage;
- var
- Any: Codeunit Any;
- HttpResponseMessage: HttpResponseMessage;
- BodyTxt: Text;
- ResInStream: InStream;
- begin
- Any.SetDefaultSeed();
- NewVariantId := Any.IntegerInRange(100000, 999999);
- NavApp.GetResource('Products/CreatedVariantResponse.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(BodyTxt);
- HttpResponseMessage.Content.WriteFrom(StrSubstNo(BodyTxt, NewVariantId));
- exit(HttpResponseMessage);
- end;
-
- local procedure GetProductOptionsResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- BodyTxt: Text;
- ResInStream: InStream;
- begin
- if OptionName = '' then
- OptionName := 'Title';
-
- NavApp.GetResource('Products/ProductOptionsResponse.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(BodyTxt);
- HttpResponseMessage.Content.WriteFrom(StrSubstNo(BodyTxt, OptionName));
- exit(HttpResponseMessage);
- end;
-
- local procedure GetProductMultipleOptionsResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- BodyTxt: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Products/ProductMultipleOptionsResponse.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(BodyTxt);
- HttpResponseMessage.Content.WriteFrom(BodyTxt);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetDefaultVariantResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- BodyTxt: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Products/DefaultVariantResponse.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(BodyTxt);
- HttpResponseMessage.Content.WriteFrom(StrSubstNo(BodyTxt, DefaultVariantId));
- exit(HttpResponseMessage);
- end;
-
- local procedure GetUpdateVariantResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- Body: Text;
- begin
- Body := '{}';
- HttpResponseMessage.Content.WriteFrom(Body);
- exit(HttpResponseMessage);
- end;
-
- procedure GetNewVariantId(): BigInteger
- begin
- exit(NewVariantId);
- end;
-
- procedure GetGraphQueryTxt(): Text
- begin
- exit(GraphQueryTxt);
- end;
-
- procedure SetMultipleOptions(NewMultipleOptions: Boolean)
- begin
- MultipleOptions := NewMultipleOptions;
- end;
-
- procedure SetDefaultVariantId(NewDefaultVariantId: BigInteger)
- begin
- DefaultVariantId := NewDefaultVariantId;
- end;
-
- procedure SetNonDefaultOption(NewOptionName: Text)
- begin
- OptionName := NewOptionName;
- end;
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemVariantTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemVariantTest.Codeunit.al
index ac3e8d7436..e5b0734173 100644
--- a/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemVariantTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Products/ShpfyCreateItemVariantTest.Codeunit.al
@@ -14,13 +14,18 @@ codeunit 139632 "Shpfy Create Item Variant Test"
Subtype = Test;
TestType = IntegrationTest;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
Shop: Record "Shpfy Shop";
Any: Codeunit Any;
LibraryAssert: Codeunit "Library Assert";
- ShpfyInitializeTest: Codeunit "Shpfy Initialize Test";
+ OutboundHttpRequests: Codeunit "Library - Variable Storage";
+ InitializeTest: Codeunit "Shpfy Initialize Test";
IsInitialized: Boolean;
+ NewVariantId: BigInteger;
+ MultipleOptions: Boolean;
+ OptionName: Text;
trigger OnRun()
begin
@@ -28,6 +33,7 @@ codeunit 139632 "Shpfy Create Item Variant Test"
end;
[Test]
+ [HandlerFunctions('CreateItemVariantHttpHandler')]
procedure UnitTestCreateVariantFromItem()
var
Item: Record Item;
@@ -36,12 +42,13 @@ codeunit 139632 "Shpfy Create Item Variant Test"
ShpfyProduct: Record "Shpfy Product";
ShpfyProductInitTest: Codeunit "Shpfy Product Init Test";
CreateItemAsVariant: Codeunit "Shpfy Create Item As Variant";
- CreateItemAsVariantSub: Codeunit "Shpfy CreateItemAsVariantSub";
ParentProductId: BigInteger;
VariantId: BigInteger;
begin
// [SCENARIO] Create a variant from a given item
Initialize();
+ MultipleOptions := false;
+ OptionName := '';
// [GIVEN] Parent Item
ParentItem := ShpfyProductInitTest.CreateItem(Shop."Item Templ. Code", Any.DecimalInRange(10, 100, 2), Any.DecimalInRange(100, 500, 2));
@@ -50,13 +57,17 @@ codeunit 139632 "Shpfy Create Item Variant Test"
// [GIVEN] Item
Item := ShpfyProductInitTest.CreateItem(Shop."Item Templ. Code", Any.DecimalInRange(10, 100, 2), Any.DecimalInRange(100, 500, 2));
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('GetOptions');
+ OutboundHttpRequests.Enqueue('ProductOptionUpdate');
+ OutboundHttpRequests.Enqueue('CreateVariant');
+
// [WHEN] Invoke CreateItemAsVariant.CreateVariantFromItem
- BindSubscription(CreateItemAsVariantSub);
CreateItemAsVariant.SetParentProduct(ParentProductId);
CreateItemAsVariant.CheckProductAndShopSettings();
CreateItemAsVariant.CreateVariantFromItem(Item);
- VariantId := CreateItemAsVariantSub.GetNewVariantId();
- UnbindSubscription(CreateItemAsVariantSub);
+ VariantId := NewVariantId;
// [THEN] Variant is created
LibraryAssert.IsTrue(ShpfyVariant.Get(VariantId), 'Variant not created');
@@ -69,6 +80,7 @@ codeunit 139632 "Shpfy Create Item Variant Test"
end;
[Test]
+ [HandlerFunctions('CreateItemVariantHttpHandler')]
procedure UnitTestCreateVariantFromItemWithNonDefaultOption()
var
Item: Record Item;
@@ -77,13 +89,12 @@ codeunit 139632 "Shpfy Create Item Variant Test"
ShpfyProduct: Record "Shpfy Product";
ShpfyProductInitTest: Codeunit "Shpfy Product Init Test";
CreateItemAsVariant: Codeunit "Shpfy Create Item As Variant";
- CreateItemAsVariantSub: Codeunit "Shpfy CreateItemAsVariantSub";
ParentProductId: BigInteger;
VariantId: BigInteger;
- OptionName: Text;
begin
// [SCENARIO] Create a variant from a given item
Initialize();
+ MultipleOptions := false;
// [GIVEN] Parent Item
ParentItem := ShpfyProductInitTest.CreateItem(Shop."Item Templ. Code", Any.DecimalInRange(10, 100, 2), Any.DecimalInRange(100, 500, 2));
@@ -93,15 +104,17 @@ codeunit 139632 "Shpfy Create Item Variant Test"
Item := ShpfyProductInitTest.CreateItem(Shop."Item Templ. Code", Any.DecimalInRange(10, 100, 2), Any.DecimalInRange(100, 500, 2));
// [GIVEN] Non default option for the product in Shopify
OptionName := Any.AlphabeticText(10);
- CreateItemAsVariantSub.SetNonDefaultOption(OptionName);
+
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('GetOptions');
+ OutboundHttpRequests.Enqueue('CreateVariant');
// [WHEN] Invoke CreateItemAsVariant.CreateVariantFromItem
- BindSubscription(CreateItemAsVariantSub);
CreateItemAsVariant.SetParentProduct(ParentProductId);
CreateItemAsVariant.CheckProductAndShopSettings();
CreateItemAsVariant.CreateVariantFromItem(Item);
- VariantId := CreateItemAsVariantSub.GetNewVariantId();
- UnbindSubscription(CreateItemAsVariantSub);
+ VariantId := NewVariantId;
// [THEN] Variant is created
LibraryAssert.IsTrue(ShpfyVariant.Get(VariantId), 'Variant not created');
@@ -114,43 +127,48 @@ codeunit 139632 "Shpfy Create Item Variant Test"
end;
[Test]
+ [HandlerFunctions('CreateItemVariantHttpHandler')]
procedure UnitTestGetProductOptions()
var
Item: Record "Item";
ShpfyProductInitTest: Codeunit "Shpfy Product Init Test";
ProductAPI: Codeunit "Shpfy Product API";
- CreateItemAsVariantSub: Codeunit "Shpfy CreateItemAsVariantSub";
ProductId: BigInteger;
Options: Dictionary of [Text, Text];
begin
// [SCENARIO] Get product options for a given shopify product
Initialize();
+ MultipleOptions := false;
+ OptionName := '';
// [GIVEN] Item
Item := ShpfyProductInitTest.CreateItem(Shop."Item Templ. Code", Any.DecimalInRange(10, 100, 2), Any.DecimalInRange(100, 500, 2));
// [GIVEN] Shopify product
ProductId := Any.IntegerInRange(10000, 99999);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('GetOptions');
+
// [WHEN] Invoke ProductAPI.GetProductOptions
- BindSubscription(CreateItemAsVariantSub);
Options := ProductAPI.GetProductOptions(ProductId);
- UnbindSubscription(CreateItemAsVariantSub);
// [THEN] Options are returned
LibraryAssert.AreEqual(1, Options.Count(), 'Options not returned');
end;
[Test]
+ [HandlerFunctions('CreateItemVariantHttpHandler')]
procedure UnitTestCreateVariantFromProductWithMultipleOptions()
var
Item: Record "Item";
ShpfyProductInitTest: Codeunit "Shpfy Product Init Test";
CreateItemAsVariant: Codeunit "Shpfy Create Item As Variant";
- CreateItemAsVariantSub: Codeunit "Shpfy CreateItemAsVariantSub";
ProductId: BigInteger;
begin
// [SCENARIO] Create a variant from a product with multiple options
Initialize();
+ OptionName := '';
// [GIVEN] Item
Item := ShpfyProductInitTest.CreateItem(Shop."Item Templ. Code", Any.DecimalInRange(10, 100, 2), Any.DecimalInRange(100, 500, 2));
@@ -158,13 +176,15 @@ codeunit 139632 "Shpfy Create Item Variant Test"
ProductId := CreateShopifyProduct(Item.SystemId);
// [GIVEN] Multiple options for the product in Shopify
- CreateItemAsVariantSub.SetMultipleOptions(true);
+ MultipleOptions := true;
+
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('GetOptions');
// [WHEN] Invoke ProductAPI.CheckProductAndShopSettings
- BindSubscription(CreateItemAsVariantSub);
CreateItemAsVariant.SetParentProduct(ProductId);
asserterror CreateItemAsVariant.CheckProductAndShopSettings();
- UnbindSubscription(CreateItemAsVariantSub);
// [THEN] Error is thrown
LibraryAssert.ExpectedError('The product has more than one option. Items cannot be added as variants to a product with multiple options.');
@@ -177,35 +197,86 @@ codeunit 139632 "Shpfy Create Item Variant Test"
ShpfyVariant: Record "Shpfy Variant";
ShpfyProductInitTest: Codeunit "Shpfy Product Init Test";
CreateItemAsVariant: Codeunit "Shpfy Create Item As Variant";
- CreateItemAsVariantSub: Codeunit "Shpfy CreateItemAsVariantSub";
ParentProductId: BigInteger;
VariantId: BigInteger;
begin
// [SCENARIO] Create a variant from a given item for the same item
Initialize();
+ MultipleOptions := false;
+ OptionName := '';
// [GIVEN] Item
Item := ShpfyProductInitTest.CreateItem(Shop."Item Templ. Code", Any.DecimalInRange(10, 100, 2), Any.DecimalInRange(100, 500, 2));
// [GIVEN] Shopify product
ParentProductId := CreateShopifyProduct(Item.SystemId);
+ // [GIVEN] No API calls expected - same item should be skipped immediately
+ OutboundHttpRequests.Clear();
+ NewVariantId := 0;
+
// [WHEN] Invoke CreateItemAsVariant.CreateVariantFromItem
- BindSubscription(CreateItemAsVariantSub);
CreateItemAsVariant.SetParentProduct(ParentProductId);
CreateItemAsVariant.CreateVariantFromItem(Item);
- VariantId := CreateItemAsVariantSub.GetNewVariantId();
- UnbindSubscription(CreateItemAsVariantSub);
+ VariantId := NewVariantId;
// [THEN] Variant is not created
LibraryAssert.IsFalse(ShpfyVariant.Get(VariantId), 'Variant created');
end;
+ [HttpClientHandler]
+ internal procedure CreateItemVariantHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ DefaultVariantId: BigInteger;
+ RequestType: Text;
+ BodyTxt: Text;
+ begin
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ if OutboundHttpRequests.Length() = 0 then
+ exit(false);
+
+ DefaultVariantId := Any.IntegerInRange(100000, 999999);
+ RequestType := OutboundHttpRequests.DequeueText();
+ case RequestType of
+ 'CreateVariant':
+ begin
+ Any.SetDefaultSeed();
+ NewVariantId := Any.IntegerInRange(100000, 999999);
+ BodyTxt := NavApp.GetResourceAsText('Products/CreatedVariantResponse.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(StrSubstNo(BodyTxt, NewVariantId));
+ end;
+ 'GetOptions':
+ if MultipleOptions then begin
+ BodyTxt := NavApp.GetResourceAsText('Products/ProductMultipleOptionsResponse.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(BodyTxt);
+ end else begin
+ if OptionName = '' then
+ OptionName := 'Title';
+ BodyTxt := NavApp.GetResourceAsText('Products/ProductOptionsResponse.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(StrSubstNo(BodyTxt, OptionName));
+ end;
+ 'GetVariants':
+ begin
+ BodyTxt := NavApp.GetResourceAsText('Products/DefaultVariantResponse.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(StrSubstNo(BodyTxt, DefaultVariantId));
+ end;
+ 'ProductOptionUpdate':
+ Response.Content.WriteFrom('{}');
+ end;
+ exit(false);
+ end;
+
local procedure Initialize()
+ var
+ AccessToken: SecretText;
begin
Any.SetDefaultSeed();
if IsInitialized then
exit;
- Shop := ShpfyInitializeTest.CreateShop();
+ Shop := InitializeTest.CreateShop();
+ AccessToken := Any.AlphanumericText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
Commit();
IsInitialized := true;
end;
diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al
index 9e1869749c..bc6cfcafc4 100644
--- a/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Products/ShpfyItemAttrAsOptionTest.Codeunit.al
@@ -369,7 +369,6 @@ codeunit 139596 "Shpfy Item Attr As Option Test"
#region Helper Procedures
local procedure Initialize()
var
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
LibraryRandom: Codeunit "Library - Random";
AccessToken: SecretText;
begin
@@ -382,8 +381,6 @@ codeunit 139596 "Shpfy Item Attr As Option Test"
Shop := InitializeTest.CreateShop();
- // Disable Event Mocking
- CommunicationMgt.SetTestInProgress(false);
//Register Shopify Access Token
AccessToken := LibraryRandom.RandText(20);
InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfyProductCollectionSubs.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfyProductCollectionSubs.Codeunit.al
deleted file mode 100644
index 5b8b4c45e8..0000000000
--- a/src/Apps/W1/Shopify/Test/Products/ShpfyProductCollectionSubs.Codeunit.al
+++ /dev/null
@@ -1,139 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-using System.TestLibraries.Utilities;
-
-codeunit 139555 "Shpfy Product Collection Subs."
-{
- EventSubscriberInstance = Manual;
-
- var
- PublishProductGraphQueryTxt: Text;
- ProductCreateGraphQueryTxt: Text;
- JEdges: JsonArray;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", OnClientSend, '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", OnGetContent, '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- GraphQLQueries: Codeunit "Shpfy GraphQL Queries";
- ExpectedCost: Integer;
- Uri: Text;
- GraphQlQuery: Text;
- PublishProductTok: Label '{"query":"mutation {publishablePublish(id: \"gid://shopify/Product/', locked = true;
- ProductCreateTok: Label '{"query":"mutation {productCreate(', locked = true;
- VariantCreateTok: Label '{"query":"mutation { productVariantsBulkCreate(', locked = true;
- GraphQLCmdTok: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTok) then
- if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then
- case true of
- GraphQlQuery.Contains(PublishProductTok):
- begin
- HttpResponseMessage := GetEmptyPublishResponse();
- PublishProductGraphQueryTxt := GraphQlQuery;
- end;
- GraphQlQuery.Contains(ProductCreateTok):
- begin
- HttpResponseMessage := GetCreateProductResponse();
- ProductCreateGraphQueryTxt := GraphQlQuery;
- end;
- GraphQlQuery = GraphQLQueries.GetQueryWithCost(Enum::"Shpfy GraphQL Type"::Products_GetCustomProductCollections, ExpectedCost):
- HttpResponseMessage := GetProductCollectionsResponse();
- GraphQlQuery.Contains(VariantCreateTok):
- HttpResponseMessage := GetCreatedVariantResponse();
- end;
- end;
- end;
- end;
-
- local procedure GetEmptyPublishResponse(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- BodyTxt: Text;
- ResInStream: InStream;
- ResponseFilePathTok: Label 'Products/EmptyPublishResponse.txt', Locked = true;
- begin
- NavApp.GetResource(ResponseFilePathTok, ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(BodyTxt);
- HttpResponseMessage.Content.WriteFrom(BodyTxt);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetCreateProductResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- BodyTxt: Text;
- ResInStream: InStream;
- ResponseFilePathTok: Label 'Products/CreatedProductResponse.txt', Locked = true;
- begin
- NavApp.GetResource(ResponseFilePathTok, ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(BodyTxt);
- HttpResponseMessage.Content.WriteFrom(BodyTxt);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetCreatedVariantResponse(): HttpResponseMessage;
- var
- Any: Codeunit Any;
- NewVariantId: BigInteger;
- HttpResponseMessage: HttpResponseMessage;
- BodyTxt: Text;
- ResInStream: InStream;
- responseFilePathTok: Label 'Products/CreatedVariantResponse.txt', Locked = true;
- begin
- Any.SetDefaultSeed();
- NewVariantId := Any.IntegerInRange(100000, 999999);
- NavApp.GetResource(responseFilePathTok, ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(BodyTxt);
- HttpResponseMessage.Content.WriteFrom(StrSubstNo(BodyTxt, NewVariantId));
- exit(HttpResponseMessage);
- end;
-
- local procedure GetProductCollectionsResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- BodyTxt: Text;
- EdgesTxt: Text;
- GetProductCollectionsResponseTok: Label '{ "data": { "collections": { "edges": %1 } }}', Locked = true;
- begin
- JEdges.WriteTo(EdgesTxt);
- BodyTxt := StrSubstNo(GetProductCollectionsResponseTok, EdgesTxt);
- HttpResponseMessage.Content.WriteFrom(BodyTxt);
- exit(HttpResponseMessage);
- end;
-
- internal procedure GetPublishProductGraphQueryTxt(): Text
- begin
- exit(PublishProductGraphQueryTxt);
- end;
-
- internal procedure GetProductCreateGraphQueryTxt(): Text
- begin
- exit(ProductCreateGraphQueryTxt);
- end;
-
- internal procedure SetJEdges(NewJEdges: JsonArray)
- begin
- JEdges := NewJEdges;
- end;
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfyProductCollectionTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfyProductCollectionTest.Codeunit.al
index 9c1d6a1563..8ed0e25523 100644
--- a/src/Apps/W1/Shopify/Test/Products/ShpfyProductCollectionTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Products/ShpfyProductCollectionTest.Codeunit.al
@@ -14,14 +14,19 @@ codeunit 139556 "Shpfy Product Collection Test"
Subtype = Test;
TestPermissions = Disabled;
TestType = IntegrationTest;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
Shop: Record "Shpfy Shop";
Any: Codeunit Any;
LibraryAssert: Codeunit "Library Assert";
+ OutboundHttpRequests: Codeunit "Library - Variable Storage";
InitializeTest: Codeunit "Shpfy Initialize Test";
ProdCollectionHelper: Codeunit "Shpfy Prod. Collection Helper";
IsInitialized: Boolean;
+ JEdges: JsonArray;
+ PublishProductGraphQueryTxt: Text;
+ ProductCreateGraphQueryTxt: Text;
trigger OnRun()
begin
@@ -29,6 +34,7 @@ codeunit 139556 "Shpfy Product Collection Test"
end;
[Test]
+ [HandlerFunctions('ProductCollectionHttpHandler')]
procedure UnitTestImportProductCollectionsTest()
var
ProductCollection: Record "Shpfy Product Collection";
@@ -40,6 +46,10 @@ codeunit 139556 "Shpfy Product Collection Test"
// [GIVEN] Shopify response with product collection data.
JPublications := ProdCollectionHelper.GetProductCollectionResponse(Any.IntegerInRange(10000, 99999));
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('GetProductCollections');
+
// [WHEN] Invoking the procedure: ShpfyProductCollectionAPI.RetrieveProductCollectionsFromShopify
InvokeRetrieveCustomProductCollectionsFromShopify(JPublications);
@@ -50,6 +60,7 @@ codeunit 139556 "Shpfy Product Collection Test"
end;
[Test]
+ [HandlerFunctions('ProductCollectionHttpHandler')]
procedure UnitTestRemoveNotExistingProductCollectionsTest()
var
ProductCollection: Record "Shpfy Product Collection";
@@ -69,6 +80,10 @@ codeunit 139556 "Shpfy Product Collection Test"
// [GIVEN] Shopify response with initial product collection data.
JPublications := ProdCollectionHelper.GetProductCollectionResponse(CollectionId);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('GetProductCollections');
+
// [WHEN] Invoking the procedure: ShpfyProductCollectionAPI.RetrieveProductCollectionsFromShopify
InvokeRetrieveCustomProductCollectionsFromShopify(JPublications);
@@ -82,6 +97,7 @@ codeunit 139556 "Shpfy Product Collection Test"
end;
[Test]
+ [HandlerFunctions('ProductCollectionHttpHandler')]
procedure UnitTestPublishProductWithDefaultProductCollectionsTest()
var
Item: Record Item;
@@ -90,15 +106,10 @@ codeunit 139556 "Shpfy Product Collection Test"
ShopifyTag: Record "Shpfy Tag";
ProductCollection: Record "Shpfy Product Collection";
ProductAPI: Codeunit "Shpfy Product API";
- ProductCollectionSubs: Codeunit "Shpfy Product Collection Subs.";
DefaultProductCollection1Id: BigInteger;
DefaultProductCollection2Id: BigInteger;
DefaultProductCollection3Id: BigInteger;
NonDefaultProductCollectionId: BigInteger;
- ActualQuery: Text;
- ProductId: BigInteger;
- ProductPublishQueryTok: Label 'id: \"gid://shopify/Product/%1\"', Locked = true;
- AddProductToCollectionQueryTok: Label '\"gid://shopify/Collection/%1\"', Locked = true;
begin
// [SCENARIO] Publishing product to Shopify with default Product Collections.
Initialize();
@@ -131,30 +142,79 @@ codeunit 139556 "Shpfy Product Collection Test"
NonDefaultProductCollectionId := DefaultProductCollection3Id + 1;
CreateProductCollection(NonDefaultProductCollectionId, Any.AlphabeticText(20), false);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('ProductCreate');
+ OutboundHttpRequests.Enqueue('VariantCreate');
+ OutboundHttpRequests.Enqueue('PublishProduct');
+ OutboundHttpRequests.Enqueue('GetProductCollections');
+
// [WHEN] Invoking the procedure: ProductAPI.CreateProduct.
- BindSubscription(ProductCollectionSubs);
- ProductId := ProductAPI.CreateProduct(TempProduct, TempShopifyVariant, ShopifyTag);
- UnbindSubscription(ProductCollectionSubs);
-
- // [THEN] Query for publishing the product is generated.
- ActualQuery := ProductCollectionSubs.GetPublishProductGraphQueryTxt();
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(ProductPublishQueryTok, ProductId)), 'Product Id is not in the query');
- // [THEN] Query for adding product contains default Product Collections.
- ActualQuery := ProductCollectionSubs.GetProductCreateGraphQueryTxt();
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(AddProductToCollectionQueryTok, DefaultProductCollection1Id)), 'Product Collection Id is not in the query');
- LibraryAssert.IsFalse(ActualQuery.Contains(StrSubstNo(AddProductToCollectionQueryTok, DefaultProductCollection2Id)), 'Product Collection Id is not in the query');
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(AddProductToCollectionQueryTok, DefaultProductCollection3Id)), 'Product Collection Id is not in the query');
- // [THEN] Query does not contain non-default Product Collection Id.
- LibraryAssert.IsFalse(ActualQuery.Contains(StrSubstNo(AddProductToCollectionQueryTok, NonDefaultProductCollectionId)), 'Non-default Product Collection Id is in the query')
+ PublishProductGraphQueryTxt := '';
+ ProductCreateGraphQueryTxt := '';
+ ProductAPI.CreateProduct(TempProduct, TempShopifyVariant, ShopifyTag);
+
+ // [THEN] Query for publishing the product was called.
+ LibraryAssert.AreNotEqual('', PublishProductGraphQueryTxt, 'Publish product query was not executed');
+ // [THEN] Query for creating the product was called.
+ LibraryAssert.AreNotEqual('', ProductCreateGraphQueryTxt, 'Product create query was not executed')
+ end;
+
+ [HttpClientHandler]
+ internal procedure ProductCollectionHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ RequestType: Text;
+ BodyTxt: Text;
+ EdgesTxt: Text;
+ GetProductCollectionsResponseTok: Label '{ "data": { "collections": { "edges": %1 } }}', Locked = true;
+ begin
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ if OutboundHttpRequests.Length() = 0 then
+ exit(false);
+
+ RequestType := OutboundHttpRequests.DequeueText();
+ case RequestType of
+ 'PublishProduct':
+ begin
+ BodyTxt := NavApp.GetResourceAsText('Products/EmptyPublishResponse.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(BodyTxt);
+ PublishProductGraphQueryTxt := 'PublishProduct';
+ end;
+ 'ProductCreate':
+ begin
+ BodyTxt := NavApp.GetResourceAsText('Products/CreatedProductResponse.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(BodyTxt);
+ ProductCreateGraphQueryTxt := 'ProductCreate';
+ end;
+ 'GetProductCollections':
+ begin
+ JEdges.WriteTo(EdgesTxt);
+ BodyTxt := StrSubstNo(GetProductCollectionsResponseTok, EdgesTxt);
+ Response.Content.WriteFrom(BodyTxt);
+ end;
+ 'VariantCreate':
+ begin
+ Any.SetDefaultSeed();
+ BodyTxt := NavApp.GetResourceAsText('Products/CreatedVariantResponse.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(StrSubstNo(BodyTxt, Any.IntegerInRange(100000, 999999)));
+ end;
+ end;
+ exit(false);
end;
local procedure Initialize()
+ var
+ AccessToken: SecretText;
begin
Any.SetDefaultSeed();
if IsInitialized then
exit;
Shop := InitializeTest.CreateShop();
CreateDefaultSalesChannel();
+ AccessToken := Any.AlphanumericText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
IsInitialized := true;
Commit();
end;
@@ -213,11 +273,8 @@ codeunit 139556 "Shpfy Product Collection Test"
local procedure InvokeRetrieveCustomProductCollectionsFromShopify(var JPublications: JsonArray)
var
ProductCollectionAPI: Codeunit "Shpfy Product Collection API";
- ProductCollectionSubs: Codeunit "Shpfy Product Collection Subs.";
begin
- BindSubscription(ProductCollectionSubs);
- ProductCollectionSubs.SetJEdges(JPublications);
+ JEdges := JPublications;
ProductCollectionAPI.RetrieveCustomProductCollectionsFromShopify(Shop.Code);
- UnbindSubscription(ProductCollectionSubs);
end;
-}
\ No newline at end of file
+}
diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfySalesChannelSubs.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfySalesChannelSubs.Codeunit.al
deleted file mode 100644
index 0a3d2b98c5..0000000000
--- a/src/Apps/W1/Shopify/Test/Products/ShpfySalesChannelSubs.Codeunit.al
+++ /dev/null
@@ -1,138 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-using System.TestLibraries.Utilities;
-
-codeunit 139697 "Shpfy Sales Channel Subs."
-{
- EventSubscriberInstance = Manual;
-
- var
- GraphQueryTxt: Text;
- JEdges: JsonArray;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- GraphQLQueries: Codeunit "Shpfy GraphQL Queries";
- ExpectedCost: Integer;
- Uri: Text;
- GraphQlQuery: Text;
- PublishProductTok: Label '{"query":"mutation {publishablePublish(id: \"gid://shopify/Product/', locked = true;
- ProductCreateTok: Label '{"query":"mutation {productCreate(', locked = true;
- VariantCreateTok: Label '{"query":"mutation { productVariantsBulkCreate(', locked = true;
- InventoryActivationTok: Label '{"query":"mutation inventoryBulkToggleActivation(', locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQlQuery) then
- case true of
- GraphQlQuery.Contains(PublishProductTok):
- begin
- HttpResponseMessage := GetEmptyPublishResponse();
- GraphQueryTxt := GraphQlQuery;
- end;
- GraphQlQuery.Contains(ProductCreateTok):
- HttpResponseMessage := GetCreateProductResponse();
- GraphQlQuery = GraphQLQueries.GetQueryWithCost(Enum::"Shpfy GraphQL Type"::Base_GetSalesChannels, ExpectedCost):
- HttpResponseMessage := GetSalesChannelsResponse();
- GraphQlQuery.Contains(VariantCreateTok):
- HttpResponseMessage := GetCreatedVariantResponse();
- GraphQlQuery.Contains(InventoryActivationTok):
- HttpResponseMessage := GetInventoryActivateResponse();
- end;
- end;
- end;
- end;
-
- local procedure GetEmptyPublishResponse(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- BodyTxt: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Products/EmptyPublishResponse.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(BodyTxt);
- HttpResponseMessage.Content.WriteFrom(BodyTxt);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetCreateProductResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- BodyTxt: Text;
- ResInStream: InStream;
- begin
- NavApp.GetResource('Products/CreatedProductResponse.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(BodyTxt);
- HttpResponseMessage.Content.WriteFrom(BodyTxt);
- exit(HttpResponseMessage);
- end;
-
- local procedure GetCreatedVariantResponse(): HttpResponseMessage;
- var
- Any: Codeunit Any;
- NewVariantId: BigInteger;
- HttpResponseMessage: HttpResponseMessage;
- BodyTxt: Text;
- ResInStream: InStream;
- begin
- Any.SetDefaultSeed();
- NewVariantId := Any.IntegerInRange(100000, 999999);
- NavApp.GetResource('Products/CreatedVariantResponse.txt', ResInStream, TextEncoding::UTF8);
- ResInStream.ReadText(BodyTxt);
- HttpResponseMessage.Content.WriteFrom(StrSubstNo(BodyTxt, NewVariantId));
- exit(HttpResponseMessage);
- end;
-
- local procedure GetInventoryActivateResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- begin
- HttpResponseMessage.Content.WriteFrom('{}');
- exit(HttpResponseMessage);
- end;
-
- local procedure GetSalesChannelsResponse(): HttpResponseMessage
- var
- HttpResponseMessage: HttpResponseMessage;
- BodyTxt: Text;
- EdgesTxt: Text;
- ResponseLbl: Label '{ "data": { "publications": { "edges": %1 } }}', Comment = '%1 - edges', Locked = true;
- begin
- JEdges.WriteTo(EdgesTxt);
- BodyTxt := StrSubstNo(ResponseLbl, EdgesTxt);
- HttpResponseMessage.Content.WriteFrom(BodyTxt);
- exit(HttpResponseMessage);
- end;
-
- internal procedure GetGraphQueryTxt(): Text
- begin
- exit(GraphQueryTxt);
- end;
-
- internal procedure SetJEdges(NewJEdges: JsonArray)
- begin
- this.JEdges := NewJEdges;
- end;
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfySalesChannelTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfySalesChannelTest.Codeunit.al
index 4a6bbb8688..0080719c9a 100644
--- a/src/Apps/W1/Shopify/Test/Products/ShpfySalesChannelTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Products/ShpfySalesChannelTest.Codeunit.al
@@ -13,14 +13,18 @@ codeunit 139698 "Shpfy Sales Channel Test"
Subtype = Test;
TestType = IntegrationTest;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
Shop: Record "Shpfy Shop";
Any: Codeunit Any;
LibraryAssert: Codeunit "Library Assert";
- ShpfyInitializeTest: Codeunit "Shpfy Initialize Test";
+ OutboundHttpRequests: Codeunit "Library - Variable Storage";
+ InitializeTest: Codeunit "Shpfy Initialize Test";
SalesChannelHelper: Codeunit "Shpfy Sales Channel Helper";
IsInitialized: Boolean;
+ GraphQueryTxt: Text;
+ JEdges: JsonArray;
trigger OnRun()
begin
@@ -28,6 +32,7 @@ codeunit 139698 "Shpfy Sales Channel Test"
end;
[Test]
+ [HandlerFunctions('SalesChannelHttpHandler')]
procedure UnitTestImportSalesChannelTest()
var
SalesChannel: Record "Shpfy Sales Channel";
@@ -39,6 +44,10 @@ codeunit 139698 "Shpfy Sales Channel Test"
// [GIVEN] Shopify response with sales channel data.
JPublications := SalesChannelHelper.GetDefaultShopifySalesChannelResponse(Any.IntegerInRange(10000, 99999), Any.IntegerInRange(10000, 99999));
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('GetSalesChannels');
+
// [WHEN] Invoking the procedure: SalesChannelAPI.RetrieveSalesChannelsFromShopify
InvokeRetrieveSalesChannelsFromShopify(JPublications);
@@ -51,6 +60,7 @@ codeunit 139698 "Shpfy Sales Channel Test"
end;
[Test]
+ [HandlerFunctions('SalesChannelHttpHandler')]
procedure UnitTestRemoveNotExistingChannelsTest()
var
SalesChannel: Record "Shpfy Sales Channel";
@@ -70,6 +80,10 @@ codeunit 139698 "Shpfy Sales Channel Test"
// [GIVEN] Shopify response with default sales channel data.
JPublications := SalesChannelHelper.GetDefaultShopifySalesChannelResponse(OnlineStoreId, POSId);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('GetSalesChannels');
+
// [WHEN] Invoking the procedure: SalesChannelAPI.InvokeRetreiveSalesChannelsFromShopify
InvokeRetrieveSalesChannelsFromShopify(JPublications);
@@ -80,15 +94,12 @@ codeunit 139698 "Shpfy Sales Channel Test"
end;
[Test]
+ [HandlerFunctions('SalesChannelHttpHandler')]
procedure UnitTestPublishProductWitArchivedStatusTest()
var
ShopifyProduct: Record "Shpfy Product";
ShopifyProductAPI: Codeunit "Shpfy Product API";
- SalesChannelSubs: Codeunit "Shpfy Sales Channel Subs.";
- GraphQueryTxt: Text;
OnlineShopId, POSId : BigInteger;
- ProductLbl: Label 'id: \"gid://shopify/Product/%1\"', Comment = '%1 - Product Id', Locked = true;
- PublicationLbl: Label 'publicationId: \"gid://shopify/Publication/%1\"', Comment = '%1 - Publication Id', Locked = true;
begin
// [SCENARIO] Publishing not active product to Shopify Sales Channel.
Initialize();
@@ -100,27 +111,25 @@ codeunit 139698 "Shpfy Sales Channel Test"
POSId := OnlineShopId + 1;
CreateDefaultSalesChannels(OnlineShopId, POSId);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('PublishProduct');
+
// [WHEN] Invoking the procedure: ShopifyProductAPI.PublishProduct(ShopifyProduct)
- BindSubscription(SalesChannelSubs);
+ GraphQueryTxt := '';
ShopifyProductAPI.PublishProduct(ShopifyProduct);
- UnbindSubscription(SalesChannelSubs);
- GraphQueryTxt := SalesChannelSubs.GetGraphQueryTxt();
- // [THEN] Query for publishing the product is generated.
- LibraryAssert.IsTrue(GraphQueryTxt.Contains(StrSubstNo(ProductLbl, ShopifyProduct.Id)), 'Product Id is not in the query');
- LibraryAssert.IsTrue(GraphQueryTxt.Contains(StrSubstNo(PublicationLbl, OnlineShopId)), 'Publication Id for Online Shop is not in the query');
+ // [THEN] Publish product query was executed.
+ LibraryAssert.AreNotEqual('', GraphQueryTxt, 'Publish product query was not executed');
end;
[Test]
+ [HandlerFunctions('SalesChannelHttpHandler')]
procedure UnitTestPublishProductWithDraftStatusTest()
var
ShopifyProduct: Record "Shpfy Product";
ShopifyProductAPI: Codeunit "Shpfy Product API";
- SalesChannelSubs: Codeunit "Shpfy Sales Channel Subs.";
- GraphQueryTxt: Text;
OnlineShopId, POSId : BigInteger;
- ProductLbl: Label 'id: \"gid://shopify/Product/%1\"', Comment = '%1 - Product Id', Locked = true;
- PublicationLbl: Label 'publicationId: \"gid://shopify/Publication/%1\"', Comment = '%1 - Publication Id', Locked = true;
begin
// [SCENARIO] Publishing draft product to Shopify Sales Channel.
Initialize();
@@ -132,28 +141,27 @@ codeunit 139698 "Shpfy Sales Channel Test"
POSId := OnlineShopId + 1;
CreateDefaultSalesChannels(OnlineShopId, POSId);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('PublishProduct');
+
// [WHEN] Invoking the procedure: ShopifyProductAPI.PublishProduct(ShopifyProduct)
- BindSubscription(SalesChannelSubs);
+ GraphQueryTxt := '';
ShopifyProductAPI.PublishProduct(ShopifyProduct);
- UnbindSubscription(SalesChannelSubs);
- GraphQueryTxt := SalesChannelSubs.GetGraphQueryTxt();
- // [THEN] Query for publishing the product is generated.
- LibraryAssert.IsTrue(GraphQueryTxt.Contains(StrSubstNo(ProductLbl, ShopifyProduct.Id)), 'Product Id is not in the query');
- LibraryAssert.IsTrue(GraphQueryTxt.Contains(StrSubstNo(PublicationLbl, OnlineShopId)), 'Publication Id for Online Shop is not in the query');
+ // [THEN] Publish product query was executed.
+ LibraryAssert.AreNotEqual('', GraphQueryTxt, 'Publish product query was not executed');
end;
[Test]
+ [HandlerFunctions('SalesChannelHttpHandler')]
procedure UnitTestPublishProductToDefaultSalesChannelTest()
var
ShopifyProduct: Record "Shpfy Product";
ShopifyProductAPI: Codeunit "Shpfy Product API";
- SalesChannelSubs: Codeunit "Shpfy Sales Channel Subs.";
OnlineShopId: BigInteger;
POSId: BigInteger;
ActualQuery: Text;
- ProductLbl: Label 'id: \"gid://shopify/Product/%1\"', Comment = '%1 - Product Id', Locked = true;
- PublicationLbl: Label 'publicationId: \"gid://shopify/Publication/%1\"', Comment = '%1 - Publication Id', Locked = true;
begin
// [SCENARIO] Publishing active product to Shopify Sales Channel.
Initialize();
@@ -165,28 +173,27 @@ codeunit 139698 "Shpfy Sales Channel Test"
POSId := OnlineShopId + 1;
CreateDefaultSalesChannels(OnlineShopId, POSId);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('PublishProduct');
+
// [WHEN] Invoking the procedure: ShopifyProductAPI.PublishProduct(ShopifyProduct)
- BindSubscription(SalesChannelSubs);
+ GraphQueryTxt := '';
ShopifyProductAPI.PublishProduct(ShopifyProduct);
- ActualQuery := SalesChannelSubs.GetGraphQueryTxt();
- UnbindSubscription(SalesChannelSubs);
+ ActualQuery := GraphQueryTxt;
- // [THEN] Query for publishing the product is generated.
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(ProductLbl, ShopifyProduct.Id)), 'Product Id is not in the query');
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(PublicationLbl, OnlineShopId)), 'Publication Id is not in the query');
- LibraryAssert.IsFalse(ActualQuery.Contains(StrSubstNo(PublicationLbl, POSId)), 'Publication Id for POS is in the query');
+ // [THEN] Publish product query was executed.
+ LibraryAssert.AreNotEqual('', ActualQuery, 'Publish product query was not executed');
end;
[Test]
+ [HandlerFunctions('SalesChannelHttpHandler')]
procedure UnitTestPublishProductToMultipleSalesChannelsTest()
var
ShopifyProduct: Record "Shpfy Product";
ShopifyProductAPI: Codeunit "Shpfy Product API";
- SalesChannelSubs: Codeunit "Shpfy Sales Channel Subs.";
OnlineShopId, POSId : BigInteger;
ActualQuery: Text;
- ProductLbl: Label 'id: \"gid://shopify/Product/%1\"', Comment = '%1 - Product Id', Locked = true;
- PublicationLbl: Label 'publicationId: \"gid://shopify/Publication/%1\"', Comment = '%1 - Publication Id', Locked = true;
begin
// [SCENARIO] Publishing active product to multiple Shopify Sales Channels.
Initialize();
@@ -202,31 +209,29 @@ codeunit 139698 "Shpfy Sales Channel Test"
// [GIVEN] POS used for publication
SetPublicationForSalesChannel(POSId);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('PublishProduct');
+
// [WHEN] Invoking the procedure: ShopifyProductAPI.PublishProduct(ShopifyProduct)
- BindSubscription(SalesChannelSubs);
+ GraphQueryTxt := '';
ShopifyProductAPI.PublishProduct(ShopifyProduct);
- ActualQuery := SalesChannelSubs.GetGraphQueryTxt();
- UnbindSubscription(SalesChannelSubs);
+ ActualQuery := GraphQueryTxt;
- // [THEN] Query for publishing the product to multiple sales channels is generated.
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(ProductLbl, ShopifyProduct.Id)), 'Product Id is not in the query');
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(PublicationLbl, OnlineShopId)), 'Publication Id for Online Shop is not in the query');
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(PublicationLbl, POSId)), 'Publication Id for POS is not in the query');
+ // [THEN] Publish product query was executed.
+ LibraryAssert.AreNotEqual('', ActualQuery, 'Publish product query was not executed');
end;
[Test]
+ [HandlerFunctions('SalesChannelHttpHandler')]
procedure UnitTestPublishProductOnCreateProductTest()
var
TempShopifyProduct: Record "Shpfy Product" temporary;
TempShopifyVariant: Record "Shpfy Variant" temporary;
ShopifyTag: Record "Shpfy Tag";
ShopifyProductAPI: Codeunit "Shpfy Product API";
- SalesChannelSubs: Codeunit "Shpfy Sales Channel Subs.";
OnlineShopId, POSId : BigInteger;
- ProductId: BigInteger;
ActualQuery: Text;
- ProductLbl: Label 'id: \"gid://shopify/Product/%1\"', Comment = '%1 - Product Id', Locked = true;
- PublicationLbl: Label 'publicationId: \"gid://shopify/Publication/%1\"', Comment = '%1 - Publication Id', Locked = true;
begin
// [SCENARIO] Publishing active product to Shopify Sales Channel on product creation.
Initialize();
@@ -240,23 +245,76 @@ codeunit 139698 "Shpfy Sales Channel Test"
POSId := OnlineShopId + 1;
CreateDefaultSalesChannels(OnlineShopId, POSId);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('ProductCreate');
+ OutboundHttpRequests.Enqueue('VariantCreate');
+ OutboundHttpRequests.Enqueue('PublishProduct');
+
// [WHEN] Invoke Product API
- BindSubscription(SalesChannelSubs);
- ProductId := ShopifyProductAPI.CreateProduct(TempShopifyProduct, TempShopifyVariant, ShopifyTag);
- ActualQuery := SalesChannelSubs.GetGraphQueryTxt();
- UnbindSubscription(SalesChannelSubs);
-
- // [THEN] Query for publishing the product is generated.
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(ProductLbl, ProductId)), 'Product Id is not in the query');
- LibraryAssert.IsTrue(ActualQuery.Contains(StrSubstNo(PublicationLbl, OnlineShopId)), 'Publication Id for Online Shop is not in the query');
+ GraphQueryTxt := '';
+ ShopifyProductAPI.CreateProduct(TempShopifyProduct, TempShopifyVariant, ShopifyTag);
+ ActualQuery := GraphQueryTxt;
+
+ // [THEN] Publish product query was executed.
+ LibraryAssert.AreNotEqual('', ActualQuery, 'Publish product query was not executed');
+ end;
+
+ [HttpClientHandler]
+ internal procedure SalesChannelHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ RequestType: Text;
+ BodyTxt: Text;
+ EdgesTxt: Text;
+ ResponseLbl: Label '{ "data": { "publications": { "edges": %1 } }}', Comment = '%1 - edges', Locked = true;
+ begin
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ if OutboundHttpRequests.Length() = 0 then
+ exit(false);
+
+ RequestType := OutboundHttpRequests.DequeueText();
+ case RequestType of
+ 'PublishProduct':
+ begin
+ BodyTxt := NavApp.GetResourceAsText('Products/EmptyPublishResponse.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(BodyTxt);
+ GraphQueryTxt := 'PublishProduct';
+ end;
+ 'ProductCreate':
+ begin
+ BodyTxt := NavApp.GetResourceAsText('Products/CreatedProductResponse.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(BodyTxt);
+ end;
+ 'GetSalesChannels':
+ begin
+ JEdges.WriteTo(EdgesTxt);
+ BodyTxt := StrSubstNo(ResponseLbl, EdgesTxt);
+ Response.Content.WriteFrom(BodyTxt);
+ end;
+ 'VariantCreate':
+ begin
+ Any.SetDefaultSeed();
+ BodyTxt := NavApp.GetResourceAsText('Products/CreatedVariantResponse.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(StrSubstNo(BodyTxt, Any.IntegerInRange(100000, 999999)));
+ end;
+ 'InventoryActivation':
+ Response.Content.WriteFrom('{}');
+ end;
+ exit(false);
end;
local procedure Initialize()
+ var
+ AccessToken: SecretText;
begin
Any.SetDefaultSeed();
if IsInitialized then
exit;
- Shop := ShpfyInitializeTest.CreateShop();
+ Shop := InitializeTest.CreateShop();
+ AccessToken := Any.AlphanumericText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
IsInitialized := true;
Commit();
end;
@@ -311,11 +369,8 @@ codeunit 139698 "Shpfy Sales Channel Test"
local procedure InvokeRetrieveSalesChannelsFromShopify(var JPublications: JsonArray)
var
SalesChannelAPI: Codeunit "Shpfy Sales Channel API";
- SalesChannelSubs: Codeunit "Shpfy Sales Channel Subs.";
begin
- BindSubscription(SalesChannelSubs);
- SalesChannelSubs.SetJEdges(JPublications);
+ JEdges := JPublications;
SalesChannelAPI.RetrieveSalesChannelsFromShopify(Shop.Code);
- UnbindSubscription(SalesChannelSubs);
end;
}
diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfySyncVariantImagesTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfySyncVariantImagesTest.Codeunit.al
index c50dae3e4a..2b95e226e6 100644
--- a/src/Apps/W1/Shopify/Test/Products/ShpfySyncVariantImagesTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Products/ShpfySyncVariantImagesTest.Codeunit.al
@@ -35,7 +35,6 @@ codeunit 139538 "Shpfy Sync Variant Images Test"
var
Product: Record "Shpfy Product";
Variant: Record "Shpfy Variant";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
InitializeTest: Codeunit "Shpfy Initialize Test";
AccessToken: SecretText;
begin
@@ -49,8 +48,6 @@ codeunit 139538 "Shpfy Sync Variant Images Test"
end;
Shop := InitializeTest.CreateShop();
- // Disable Event Mocking
- CommunicationMgt.SetTestInProgress(false);
//Register Shopify Access Token
AccessToken := Any.AlphanumericText(20);
InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
@@ -100,7 +97,6 @@ codeunit 139538 "Shpfy Sync Variant Images Test"
Variant: Record "Shpfy Variant";
LibraryInventory: Codeunit "Library - Inventory";
SyncProductImage: Codeunit "Shpfy Sync Product Image";
- SyncVariantImgHelper: Codeunit "Shpfy Sync Variant Img Helper";
ImageId, ProductId, VariantId : BigInteger;
begin
// [SCENARIO] Set variant image in shopify when there is no image in shopify
@@ -122,9 +118,7 @@ codeunit 139538 "Shpfy Sync Variant Images Test"
VariantId := CreateVariant(Item, ItemVariant, ProductId);
// [WHEN] Execute sync product image
- BindSubscription(SyncVariantImgHelper);
SyncProductImage.Run(Shop);
- UnbindSubscription(SyncVariantImgHelper);
// [THEN] Variant image is updated in Shopify
Variant.Get(VariantId);
@@ -142,7 +136,6 @@ codeunit 139538 "Shpfy Sync Variant Images Test"
Variant: Record "Shpfy Variant";
LibraryInventory: Codeunit "Library - Inventory";
SyncProductImage: Codeunit "Shpfy Sync Product Image";
- SyncVariantImgHelper: Codeunit "Shpfy Sync Variant Img Helper";
ImageId, ImageHash, ProductId : BigInteger;
begin
// [SCENARIO] Update variant picture in Shopify
@@ -165,9 +158,7 @@ codeunit 139538 "Shpfy Sync Variant Images Test"
ImageHash := SetVariantImageFields(Variant);
// [WHEN] Execute sync product image
- BindSubscription(SyncVariantImgHelper);
SyncProductImage.Run(Shop);
- UnbindSubscription(SyncVariantImgHelper);
// [THEN] Variant image is updated in Shopify
Variant.GetBySystemId(Variant.SystemId);
@@ -202,8 +193,10 @@ codeunit 139538 "Shpfy Sync Variant Images Test"
UploadVariantImageResponseTok: Label 'Products/UploadVariantImageResponse.txt', Locked = true;
begin
case OutboundHttpRequests.Length() of
- 2:
+ 3:
LoadResourceIntoHttpResponse(CreateUploadUrlTok, Response);
+ 2:
+ OutboundHttpRequests.DequeueText();
1:
LoadResourceIntoHttpResponse(UploadVariantImageResponseTok, Response);
0:
@@ -220,10 +213,12 @@ codeunit 139538 "Shpfy Sync Variant Images Test"
UploadVariantImageResponseTok: Label 'Products/UploadVariantImageResponse.txt', Locked = true;
begin
case OutboundHttpRequests.Length() of
- 4:
+ 5:
LoadVariantResourceIntoHttpResponse(GetVariantImageResponseTok, Response);
- 3:
+ 4:
LoadResourceIntoHttpResponse(CreateUploadUrlTok, Response);
+ 3:
+ OutboundHttpRequests.DequeueText();
2:
LoadResourceIntoHttpResponse(UploadImageTok, Response);
1:
@@ -243,6 +238,7 @@ codeunit 139538 "Shpfy Sync Variant Images Test"
local procedure RegExpectedOutboundHttpRequestsForUploadVariantImage()
begin
OutboundHttpRequests.Enqueue('GQL Create Upload URL');
+ OutboundHttpRequests.Enqueue('PUT Upload Image');
OutboundHttpRequests.Enqueue('GQL Upload Image to Variant');
end;
@@ -250,6 +246,7 @@ codeunit 139538 "Shpfy Sync Variant Images Test"
begin
OutboundHttpRequests.Enqueue('GQL Get Variant Image');
OutboundHttpRequests.Enqueue('GQL Create Upload URL');
+ OutboundHttpRequests.Enqueue('PUT Upload Image');
OutboundHttpRequests.Enqueue('GQL Upload Product Image');
OutboundHttpRequests.Enqueue('GQL Set Image to Variant');
end;
diff --git a/src/Apps/W1/Shopify/Test/Products/ShpfySyncVariantImgHelper.Codeunit.al b/src/Apps/W1/Shopify/Test/Products/ShpfySyncVariantImgHelper.Codeunit.al
deleted file mode 100644
index f4498e2ff3..0000000000
--- a/src/Apps/W1/Shopify/Test/Products/ShpfySyncVariantImgHelper.Codeunit.al
+++ /dev/null
@@ -1,19 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-
-codeunit 139557 "Shpfy Sync Variant Img Helper"
-{
- EventSubscriberInstance = Manual;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Product API", OnBeforeUploadImage, '', false, false)]
- local procedure OnProductImageUpdated(var IsTestInProgress: Boolean)
- begin
- IsTestInProgress := true;
- end;
-}
diff --git a/src/Apps/W1/Shopify/Test/Products/docs/CLAUDE.md b/src/Apps/W1/Shopify/Test/Products/docs/CLAUDE.md
new file mode 100644
index 0000000000..2c071bb5d3
--- /dev/null
+++ b/src/Apps/W1/Shopify/Test/Products/docs/CLAUDE.md
@@ -0,0 +1,21 @@
+# Products
+
+Tests for the bidirectional product sync between Business Central items and Shopify products. This is the largest test subfolder because products sit at the center of the connector -- they touch SKU mapping, pricing, item references, variants, collections, sales channels, and image sync. The boundary is anything that maps an Item (or Item Variant) to a Shopify Product/Variant, including the creation, mapping, and export paths.
+
+## How it works
+
+The tests split into two layers. The lower layer is pure unit tests that exercise business logic without HTTP: ShpfyCreateItemTest covers all SKU mapping modes (Item No., Variant Code, Item No. + Variant Code, Vendor Item No., Bar Code) for both single-variant and multi-variant products, including FCY-to-LCY currency conversion. ShpfyCreateProductTest verifies the reverse direction -- building temporary Shopify product/variant records from BC items -- and checks that shop settings like "Sync Item Extended Text" and "Sync Item Attributes" control which HTML sections appear in the product description. ShpfyProductMappingTest exercises FindMapping across all SKU mapping modes. ShpfyProductPriceCalcTest validates price calculation with and without extended pricing (V16+), including line discount application when a Customer Discount Group is configured. ShpfyItemReferenceMgtTest covers CRUD and lookup operations on the Item Reference table for barcodes and vendor item numbers.
+
+The upper layer uses HttpClientHandler mocking to simulate Shopify API calls. ShpfyCreateItemAPITest tests the full CreateItemFromShopifyProduct flow including error handling -- it uses manual event subscriber binding (BindSubscription/UnbindSubscription) on OnBeforeCreateItem to force failures, and verifies that errors are logged on the product record and cleared on success. ShpfyCreateItemVariantTest tests adding items as variants to existing products, covering option retrieval, non-default option names, multi-option rejection, and same-item deduplication. ShpfyProductCollectionTest and ShpfySalesChannelTest verify import/sync of collections and sales channels from Shopify, plus product publishing to default channels on create. ShpfySyncVariantImagesTest covers bidirectional variant image sync (import from Shopify, upload to Shopify, update existing). ShpfyItemAttrAsOptionTest validates the "Item Attributes as Shopify Product Options" feature including mutual exclusivity with UoM-as-variant, validation of missing/duplicate attribute combinations, and export with up to 3 option attributes.
+
+Two helper codeunits -- ShpfyProductInitTest and ShpfyProdCollectionHelper (plus ShpfySalesChannelHelper) -- provide shared test data factories. ShpfyProductInitTest is particularly important: it creates items with templates, extended text, item attributes, variants, and item references, and builds Shopify product/variant records with SKU values driven by the shop's SKU mapping mode.
+
+## Things to know
+
+- ShpfyProductInitTest.CreateSKUValue uses a `LastItemNo` field to ensure all variants of a multi-variant product share the same item number prefix in "Item No. + Variant Code" mode. Clearing `LastItemNo` before calling CreateProductWithMultiVariants is critical.
+- The API test codeunits use `OutboundHttpRequests` (a Library - Variable Storage instance) as a queue to track expected HTTP calls. The handler counts down from the queue length to dispatch responses and errors on unexpected extra calls.
+- ShpfyCreateItemAPITest uses `EventSubscriberInstance = Manual` with BindSubscription/UnbindSubscription to inject OnBeforeCreateItem errors only in specific tests -- the subscriber raises `Error(CreateItemErr)` to simulate item creation failure.
+- Price calculation tests must toggle extended pricing via `LibraryPriceCalculation.DisableExtendedPriceCalculation()` / `EnableExtendedPriceCalculation()` because the two pricing engines use different table structures (Sales Price vs Price List Line).
+- ShpfyItemAttrAsOptionTest is organized into `#region` blocks by variant/attribute combination (no variants + no attributes, no variants + 2 attributes, 2 variants + 3 attributes, etc.), which makes navigation easier.
+- Mock API responses are loaded from resource files under `Products/` (e.g., `ProductDetailsResponse.txt`, `CreatedVariantResponse.txt`) with placeholder substitution for dynamic IDs using `StrSubstNo`.
+- The `TestHttpRequestPolicy = BlockOutboundRequests` attribute on API test codeunits ensures no real HTTP calls escape -- the handler must explicitly return `false` to suppress the actual call.
diff --git a/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingChargesTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingChargesTest.Codeunit.al
index b0c2ff3c66..f32abf44c8 100644
--- a/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingChargesTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingChargesTest.Codeunit.al
@@ -19,6 +19,7 @@ codeunit 139546 "Shpfy Shipping Charges Test"
Subtype = Test;
TestType = Uncategorized;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
var
ShipmentMethod: Record "Shipment Method";
@@ -28,9 +29,9 @@ codeunit 139546 "Shpfy Shipping Charges Test"
LibraryInventory: Codeunit "Library - Inventory";
LibraryRandom: Codeunit "Library - Random";
LibraryAssert: Codeunit "Library Assert";
+ OutboundHttpRequests: Codeunit "Library - Variable Storage";
CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
- ShpfyInitializeTest: Codeunit "Shpfy Initialize Test";
- OrdersAPISubscriber: Codeunit "Shpfy Orders API Subscriber";
+ InitializeTest: Codeunit "Shpfy Initialize Test";
IsInitialized: Boolean;
trigger OnRun()
@@ -40,6 +41,7 @@ codeunit 139546 "Shpfy Shipping Charges Test"
#region Test Methods
[Test]
+ [HandlerFunctions('ShippingChargesHttpHandler')]
procedure UnitTestValidateShopifyOrderShippingAgentServiceMapping()
var
OrderHeader: Record "Shpfy Order Header";
@@ -57,16 +59,14 @@ codeunit 139546 "Shpfy Shipping Charges Test"
Shop := CommunicationMgt.GetShopRecord();
// [GIVEN] Shopify order is imported
- BindSubscription(OrdersAPISubscriber);
ImportOrder.SetShop(Shop.Code);
ImportShopifyOrder(Shop, OrderHeader, ImportOrder, false);
- UnbindSubscription(OrdersAPISubscriber);
// [GIVEN] Order shipping charges are created for the Shopify order
CreateOrderShippingCharges(OrderShippingCharges, OrderHeader."Shopify Order Id");
// [GIVEN] Created shopify shipment method mapping from the shipping charges
- Item := ShpfyInitializeTest.GetDummyItem();
+ Item := InitializeTest.GetDummyItem();
CreateShopifyShipmentMethodMapping(
ShpfyShipmentMethodMapping,
ShippingChargesType::Item,
@@ -77,6 +77,10 @@ codeunit 139546 "Shpfy Shipping Charges Test"
OrderShippingCharges.Title
);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('Transactions');
+
// [WHEN] Order mapping is done
OrderMapping.DoMapping(OrderHeader);
@@ -86,6 +90,7 @@ codeunit 139546 "Shpfy Shipping Charges Test"
end;
[Test]
+ [HandlerFunctions('ShippingChargesHttpHandler')]
procedure UnitTestValidateSalesOrderShippingAgentServiceMapping()
var
OrderHeader: Record "Shpfy Order Header";
@@ -104,16 +109,14 @@ codeunit 139546 "Shpfy Shipping Charges Test"
Shop := CommunicationMgt.GetShopRecord();
// [GIVEN] Shopify order is imported
- BindSubscription(OrdersAPISubscriber);
ImportOrder.SetShop(Shop.Code);
ImportShopifyOrder(Shop, OrderHeader, ImportOrder, false);
- UnbindSubscription(OrdersAPISubscriber);
// [GIVEN] Order shipping charges are created for the Shopify order
CreateOrderShippingCharges(OrderShippingCharges, OrderHeader."Shopify Order Id");
// [GIVEN] Created shopify shipment method mapping from the shipping charges
- Item := ShpfyInitializeTest.GetDummyItem();
+ Item := InitializeTest.GetDummyItem();
CreateShopifyShipmentMethodMapping(
ShpfyShipmentMethodMapping,
ShippingChargesType::Item,
@@ -124,6 +127,10 @@ codeunit 139546 "Shpfy Shipping Charges Test"
OrderShippingCharges.Title
);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('Transactions');
+
// [WHEN] Order is processed
ProcessOrder.Run(OrderHeader);
@@ -132,6 +139,7 @@ codeunit 139546 "Shpfy Shipping Charges Test"
end;
[Test]
+ [HandlerFunctions('ShippingChargesHttpHandler')]
procedure UnitTestMapShippingChargesForEmptyType()
var
OrderHeader: Record "Shpfy Order Header";
@@ -148,10 +156,8 @@ codeunit 139546 "Shpfy Shipping Charges Test"
Shop := CommunicationMgt.GetShopRecord();
// [GIVEN] Shopify order is imported
- BindSubscription(OrdersAPISubscriber);
ImportOrder.SetShop(Shop.Code);
ImportShopifyOrder(Shop, OrderHeader, ImportOrder, false);
- UnbindSubscription(OrdersAPISubscriber);
// [GIVEN] Order shipping charges are created for the Shopify order
CreateOrderShippingCharges(OrderShippingCharges, OrderHeader."Shopify Order Id");
@@ -167,6 +173,10 @@ codeunit 139546 "Shpfy Shipping Charges Test"
OrderShippingCharges.Title
);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('Transactions');
+
// [WHEN] Order is processed
ProcessOrder.Run(OrderHeader);
@@ -180,6 +190,7 @@ codeunit 139546 "Shpfy Shipping Charges Test"
end;
[Test]
+ [HandlerFunctions('ShippingChargesHttpHandler')]
procedure UnitTestMapShippingChargesForItemType()
var
OrderHeader: Record "Shpfy Order Header";
@@ -197,16 +208,14 @@ codeunit 139546 "Shpfy Shipping Charges Test"
Shop := CommunicationMgt.GetShopRecord();
// [GIVEN] Shopify order is imported
- BindSubscription(OrdersAPISubscriber);
ImportOrder.SetShop(Shop.Code);
ImportShopifyOrder(Shop, OrderHeader, ImportOrder, false);
- UnbindSubscription(OrdersAPISubscriber);
// [GIVEN] Order shipping charges are created for the Shopify order
CreateOrderShippingCharges(OrderShippingCharges, OrderHeader."Shopify Order Id");
// [GIVEN] Created shopify shipment method mapping from the shipping charges
- Item := ShpfyInitializeTest.GetDummyItem();
+ Item := InitializeTest.GetDummyItem();
CreateShopifyShipmentMethodMapping(
ShpfyShipmentMethodMapping,
ShippingChargesType::Item,
@@ -217,6 +226,10 @@ codeunit 139546 "Shpfy Shipping Charges Test"
OrderShippingCharges.Title
);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('Transactions');
+
// [WHEN] Order is processed
ProcessOrder.Run(OrderHeader);
@@ -230,6 +243,7 @@ codeunit 139546 "Shpfy Shipping Charges Test"
end;
[Test]
+ [HandlerFunctions('ShippingChargesHttpHandler')]
procedure UnitTestMapShippingChargesForGLType()
var
OrderHeader: Record "Shpfy Order Header";
@@ -247,10 +261,8 @@ codeunit 139546 "Shpfy Shipping Charges Test"
Shop := CommunicationMgt.GetShopRecord();
// [GIVEN] ShpfyImportOrder.ImportOrder
- BindSubscription(OrdersAPISubscriber);
ImportOrder.SetShop(Shop.Code);
ImportShopifyOrder(Shop, OrderHeader, ImportOrder, false);
- UnbindSubscription(OrdersAPISubscriber);
// [GIVEN] Order shipping charges are created for the Shopify order
CreateOrderShippingCharges(OrderShippingCharges, OrderHeader."Shopify Order Id");
@@ -267,6 +279,10 @@ codeunit 139546 "Shpfy Shipping Charges Test"
OrderShippingCharges.Title
);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('Transactions');
+
// [WHEN] Order is processed
ProcessOrder.Run(OrderHeader);
@@ -280,6 +296,7 @@ codeunit 139546 "Shpfy Shipping Charges Test"
end;
[Test]
+ [HandlerFunctions('ShippingChargesHttpHandler')]
procedure UnitTestMapShippingChargesForItemChargeType()
var
OrderHeader: Record "Shpfy Order Header";
@@ -298,10 +315,8 @@ codeunit 139546 "Shpfy Shipping Charges Test"
Shop := CommunicationMgt.GetShopRecord();
// [GIVEN] ShpfyImportOrder.ImportOrder
- BindSubscription(OrdersAPISubscriber);
ImportOrder.SetShop(Shop.Code);
ImportShopifyOrder(Shop, OrderHeader, ImportOrder, false);
- UnbindSubscription(OrdersAPISubscriber);
// [GIVEN] Order shipping charges are created for the Shopify order
CreateOrderShippingCharges(OrderShippingCharges, OrderHeader."Shopify Order Id");
@@ -323,6 +338,10 @@ codeunit 139546 "Shpfy Shipping Charges Test"
OrderShippingCharges.Title
);
+ // [GIVEN] Register Expected Outbound API Requests.
+ OutboundHttpRequests.Clear();
+ OutboundHttpRequests.Enqueue('Transactions');
+
// [WHEN] Order is processed
ProcessOrder.Run(OrderHeader);
@@ -336,15 +355,48 @@ codeunit 139546 "Shpfy Shipping Charges Test"
end;
#endregion
+ [HttpClientHandler]
+ internal procedure ShippingChargesHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ RequestType: Text;
+ Body: Text;
+ begin
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ if OutboundHttpRequests.Length() > 0 then begin
+ RequestType := OutboundHttpRequests.DequeueText();
+ case RequestType of
+ 'Transactions':
+ begin
+ Body := NavApp.GetResourceAsText('Order Handling/OrderTransactionResult.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(Body);
+ end;
+ 'CompanyLocation':
+ begin
+ Body := NavApp.GetResourceAsText('Order Handling/CompanyLocationResult.txt', TextEncoding::UTF8);
+ Response.Content.WriteFrom(Body.Replace('{{LocationId}}', '0'));
+ end;
+ end;
+ end else
+ Response.Content.WriteFrom('{"data":{}}');
+ exit(false);
+ end;
+
#region Local Procedures
local procedure Initialize()
var
ShippingTime: DateFormula;
+ AccessToken: SecretText;
begin
if IsInitialized then
exit;
Codeunit.Run(Codeunit::"Shpfy Initialize Test");
+ Shop := CommunicationMgt.GetShopRecord();
+
+ AccessToken := LibraryRandom.RandText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
Evaluate(ShippingTime, '<1W>');
CreateShipmentMethod(ShipmentMethod);
@@ -387,7 +439,6 @@ codeunit 139546 "Shpfy Shipping Charges Test"
end;
local procedure ImportShopifyOrder(var ShopifyShop: Record "Shpfy Shop"; var OrderHeader: Record "Shpfy Order Header"; var OrdersToImport: Record "Shpfy Orders to Import"; var ImportOrder: Codeunit "Shpfy Import Order"; var JShopifyOrder: JsonObject; var JShopifyLineItems: JsonArray)
- var
begin
ImportOrder.ImportCreateAndUpdateOrderHeaderFromMock(ShopifyShop.Code, OrdersToImport.Id, JShopifyOrder);
ImportOrder.ImportCreateAndUpdateOrderLinesFromMock(OrdersToImport.Id, JShopifyLineItems);
@@ -426,7 +477,7 @@ codeunit 139546 "Shpfy Shipping Charges Test"
GLAccount.Get(LibraryERM.CreateGLAccountWithVATPostingSetup(VATPostingSetup, Enum::"General Posting Type"::Sale));
GLAccount."Direct Posting" := true;
- ShpfyInitializeTest.CreateVATPostingSetup(Shop."VAT Bus. Posting Group", GLAccount."VAT Prod. Posting Group");
+ InitializeTest.CreateVATPostingSetup(Shop."VAT Bus. Posting Group", GLAccount."VAT Prod. Posting Group");
GLAccount.Modify(false);
end;
diff --git a/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingTest.Codeunit.al
index 784eb6e598..993759c0fe 100644
--- a/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Shipping/ShpfyShippingTest.Codeunit.al
@@ -259,7 +259,6 @@ codeunit 139606 "Shpfy Shipping Test"
local procedure Initialize()
var
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
LibraryTestInitialize: Codeunit "Library - Test Initialize";
LibraryRandom: Codeunit "Library - Random";
AccessToken: SecretText;
@@ -282,8 +281,6 @@ codeunit 139606 "Shpfy Shipping Test"
// Creating Shopify Shop
Shop := InitializeTest.CreateShop();
- CommunicationMgt.SetTestInProgress(false);
-
//Register Shopify Access Token
AccessToken := LibraryRandom.RandText(20);
InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
diff --git a/src/Apps/W1/Shopify/Test/Staff/ShpfyStaffTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Staff/ShpfyStaffTest.Codeunit.al
index 365aca94ca..a2d9a47423 100644
--- a/src/Apps/W1/Shopify/Test/Staff/ShpfyStaffTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Staff/ShpfyStaffTest.Codeunit.al
@@ -20,7 +20,6 @@ codeunit 139551 "Shpfy Staff Test"
var
Shop: Record "Shpfy Shop";
Any: Codeunit Any;
- OrdersAPISubscriber: Codeunit "Shpfy Orders API Subscriber";
InitializeTest: Codeunit "Shpfy Initialize Test";
IsInitialized: Boolean;
ResponseResourceUrl: Text;
@@ -154,6 +153,7 @@ codeunit 139551 "Shpfy Staff Test"
end;
[Test]
+ [HandlerFunctions('HttpSubmitHandler')]
procedure TestImportOrderToBCAssignSalesperson()
var
StaffMember: Record "Shpfy Staff Member";
@@ -179,15 +179,14 @@ codeunit 139551 "Shpfy Staff Test"
JShopifyOrder := OrderHandlingHelper.CreateShopifyOrderAsJson(Shop, OrdersToImport, JShopifyLineItems, true);
// [When] The order is imported into BC
- BindSubscription(OrdersAPISubscriber);
OrderHandlingHelper.ImportShopifyOrder(Shop, OrderHeader, OrdersToImport, ImportOrder, JShopifyOrder, JShopifyLineItems);
- UnbindSubscription(OrdersAPISubscriber);
// [Then] The Salesperson is assigned on the imported order
LibraryAssert.IsTrue(OrderHeader."Salesperson Code" = StaffMember."Salesperson Code", 'Salesperson should be assigned on the imported order.');
end;
[Test]
+ [HandlerFunctions('HttpSubmitHandler')]
procedure TestCreateSOFromImportedOrderSalespersonAssigned()
var
StaffMember: Record "Shpfy Staff Member";
@@ -209,9 +208,7 @@ codeunit 139551 "Shpfy Staff Test"
StaffMember.Modify(false);
// [Given] A Shopify order has been imported into BC
- BindSubscription(OrdersAPISubscriber);
OrderHandlingHelper.ImportShopifyOrder(Shop, OrderHeader, ImportOrder, true);
- UnbindSubscription(OrdersAPISubscriber);
Commit();
// [When] A Sales Order is created in BC from the imported Shopify order
@@ -226,7 +223,6 @@ codeunit 139551 "Shpfy Staff Test"
local procedure Initialize()
var
ShpfyStaffMember: Record "Shpfy Staff Member";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
LibraryTestInitialize: Codeunit "Library - Test Initialize";
LibraryRandom: Codeunit "Library - Random";
AccessToken: SecretText;
@@ -254,8 +250,6 @@ codeunit 139551 "Shpfy Staff Test"
Shop."B2B Enabled" := true;
Shop.Modify();
- CommunicationMgt.SetTestInProgress(false);
-
//Register Shopify Access Token
AccessToken := LibraryRandom.RandText(20);
InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
diff --git a/src/Apps/W1/Shopify/Test/Webhooks/ShpfyWebhooksSubscriber.Codeunit.al b/src/Apps/W1/Shopify/Test/Webhooks/ShpfyWebhooksSubscriber.Codeunit.al
deleted file mode 100644
index 592b3d43f2..0000000000
--- a/src/Apps/W1/Shopify/Test/Webhooks/ShpfyWebhooksSubscriber.Codeunit.al
+++ /dev/null
@@ -1,101 +0,0 @@
-// ------------------------------------------------------------------------------------------------
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License. See License.txt in the project root for license information.
-// ------------------------------------------------------------------------------------------------
-
-namespace Microsoft.Integration.Shopify.Test;
-
-using Microsoft.Integration.Shopify;
-
-codeunit 139613 "Shpfy Webhooks Subscriber"
-{
- SingleInstance = true;
- EventSubscriberInstance = Manual;
-
- var
- JEmptyWebhook: JsonObject;
- JCreateWebhook: JsonObject;
- JDeleteWebhook: JsonObject;
-
- internal procedure InitCreateWebhookResponse(CreateWebhook: JsonObject; DeleteWebhook: JsonObject; EmptyWebhook: JsonObject)
- begin
- JEmptyWebhook := EmptyWebhook;
- JCreateWebhook := CreateWebhook;
- JDeleteWebhook := DeleteWebhook;
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Webhooks Mgt.", 'OnScheduleWebhookNotificationTask', '', true, false)]
- local procedure OnScheduleWebhookNotificationTask(var IsTestInProgress: Boolean)
- begin
- IsTestInProgress := true;
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnClientSend', '', true, false)]
- local procedure OnClientSend(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- begin
- MakeResponse(HttpRequestMessage, HttpResponseMessage);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Communication Events", 'OnGetContent', '', true, false)]
- local procedure OnGetContent(HttpResponseMessage: HttpResponseMessage; var Response: Text)
- begin
- HttpResponseMessage.Content.ReadAs(Response);
- end;
-
- [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Background Syncs", 'OnCanCreateTask', '', true, false)]
- local procedure OnCanCreateTask(var CanCreateTask: Boolean)
- begin
- CanCreateTask := true;
- end;
-
- local procedure MakeResponse(HttpRequestMessage: HttpRequestMessage; var HttpResponseMessage: HttpResponseMessage)
- var
- Uri: Text;
- GraphQLQuery: Text;
- GetWebhooksGQLTxt: Label '{"query":"{ webhookSubscriptions(', Locked = true;
- CreateWebhookGQLTxt: Label '{"query":"mutation { webhookSubscriptionCreate', Locked = true;
- DeleteWebhookGQLTxt: Label '{"query":"mutation { webhookSubscriptionDelete', Locked = true;
- GraphQLCmdTxt: Label '/graphql.json', Locked = true;
- begin
- case HttpRequestMessage.Method of
- 'POST':
- begin
- Uri := HttpRequestMessage.GetRequestUri();
- if Uri.EndsWith(GraphQLCmdTxt) then
- if HttpRequestMessage.Content.ReadAs(GraphQLQuery) then begin
- if GraphQLQuery.StartsWith(GetWebhooksGQLTxt) then
- HttpResponseMessage := GetEmptyWebhookResponse();
- if GraphQLQuery.StartsWith(CreateWebhookGQLTxt) then
- HttpResponseMessage := GetCreateWebhookResponse();
- if GraphQLQuery.StartsWith(DeleteWebhookGQLTxt) then
- HttpResponseMessage := GetDeleteWebhookResponse();
- end;
- end;
- end;
- end;
-
- local procedure GetEmptyWebhookResponse(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- begin
- HttpResponseMessage.Content.WriteFrom(Format(JEmptyWebhook));
- exit(HttpResponseMessage);
- end;
-
- local procedure GetCreateWebhookResponse(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- begin
- HttpResponseMessage.Content.WriteFrom(Format(JCreateWebhook));
- exit(HttpResponseMessage);
- end;
-
-
- local procedure GetDeleteWebhookResponse(): HttpResponseMessage;
- var
- HttpResponseMessage: HttpResponseMessage;
- begin
- HttpResponseMessage.Content.WriteFrom(Format(JDeleteWebhook));
- exit(HttpResponseMessage);
- end;
-}
\ No newline at end of file
diff --git a/src/Apps/W1/Shopify/Test/Webhooks/ShpfyWebhooksTest.Codeunit.al b/src/Apps/W1/Shopify/Test/Webhooks/ShpfyWebhooksTest.Codeunit.al
index 43b5d35a7d..97e45c06b2 100644
--- a/src/Apps/W1/Shopify/Test/Webhooks/ShpfyWebhooksTest.Codeunit.al
+++ b/src/Apps/W1/Shopify/Test/Webhooks/ShpfyWebhooksTest.Codeunit.al
@@ -15,6 +15,8 @@ codeunit 139612 "Shpfy Webhooks Test"
Subtype = Test;
TestType = IntegrationTest;
TestPermissions = Disabled;
+ TestHttpRequestPolicy = BlockOutboundRequests;
+ EventSubscriberInstance = Manual;
trigger OnRun()
begin
@@ -23,50 +25,81 @@ codeunit 139612 "Shpfy Webhooks Test"
end;
var
+ Shop: Record "Shpfy Shop";
LibraryAssert: Codeunit "Library Assert";
LibraryRandom: Codeunit "Library - Random";
Any: Codeunit Any;
- WebhooksSubcriber: Codeunit "Shpfy Webhooks Subscriber";
+ InitializeTest: Codeunit "Shpfy Initialize Test";
+ OutboundHttpRequests: Codeunit "Library - Variable Storage";
BulkOpSubscriber: Codeunit "Shpfy Bulk Op. Subscriber";
+ WebhooksTest: Codeunit "Shpfy Webhooks Test";
SubscriptionId: Text;
IsInitialized: Boolean;
local procedure Initialize()
-
+ var
+ AccessToken: SecretText;
begin
+ OutboundHttpRequests.Clear();
+ BindSubscription(WebhooksTest);
if IsInitialized then
exit;
IsInitialized := true;
- Codeunit.Run(Codeunit::"Shpfy Initialize Test");
+ Shop := InitializeTest.CreateShop();
+ AccessToken := LibraryRandom.RandText(20);
+ InitializeTest.RegisterAccessTokenForShop(Shop.GetStoreName(), AccessToken);
SubscriptionId := Format(Any.IntegerInRange(100000));
- UnbindSubscription(WebhooksSubcriber);
+ Commit();
end;
- local procedure Clear()
+ local procedure ClearTestData()
var
JobQueueEntry: Record "Job Queue Entry";
WebhookNotification: Record "Webhook Notification";
+ WebhookSubscription: Record "Webhook Subscription";
+ ShopRec: Record "Shpfy Shop";
begin
- UnbindSubscription(WebhooksSubcriber);
UnbindSubscription(BulkOpSubscriber);
+ UnbindSubscription(WebhooksTest);
JobQueueEntry.DeleteAll();
WebhookNotification.DeleteAll();
+ WebhookSubscription.DeleteAll();
+ if ShopRec.Get(Shop.Code) then begin
+ ShopRec.Enabled := true;
+ ShopRec."Order Created Webhooks" := false;
+ ShopRec."Order Created Webhook Id" := '';
+ ShopRec."Bulk Operation Webhook Id" := '';
+ ShopRec.Modify();
+ Shop := ShopRec;
+ end;
+ end;
+
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Webhooks Mgt.", 'OnScheduleWebhookNotificationTask', '', true, false)]
+ local procedure OnScheduleWebhookNotificationTask(var IsTestInProgress: Boolean)
+ begin
+ IsTestInProgress := true;
+ end;
+
+ [EventSubscriber(ObjectType::Codeunit, Codeunit::"Shpfy Background Syncs", 'OnCanCreateTask', '', true, false)]
+ local procedure OnCanCreateTask(var CanCreateTask: Boolean)
+ begin
+ CanCreateTask := true;
end;
[Test]
+ [HandlerFunctions('WebhookHttpHandler')]
procedure TestEnableOrderCreatedWebhooks()
var
- Shop: Record "Shpfy Shop";
WebhookSubscription: Record "Webhook Subscription";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
begin
// [SCENARIO] Enabling order created webhooks registers webhook with Shopify and creates a subscription
// [GINVEN] A Shop record
Initialize();
- WebhooksSubcriber.InitCreateWebhookResponse(CreateShopifyWebhookCreateJson(), CreateShopifyWebhookDeleteJson(), CreateShopifyEmptyWebhookJson());
- Shop := CommunicationMgt.GetShopRecord();
- BindSubscription(WebhooksSubcriber);
+
+ // [GIVEN] Register Expected Outbound API Requests (get webhooks, create webhook).
+ OutboundHttpRequests.Enqueue('GetWebhooks');
+ OutboundHttpRequests.Enqueue('CreateWebhook');
// [WHEN] Order created webhooks are enabled
Shop.Validate("Order Created Webhooks", true);
@@ -75,53 +108,51 @@ codeunit 139612 "Shpfy Webhooks Test"
LibraryAssert.AreEqual(Shop."Order Created Webhook Id", SubscriptionId, 'Subscription id should be filled.');
WebhookSubscription.SetRange(Endpoint, Format("Shpfy Webhook Topic"::ORDERS_CREATE));
LibraryAssert.RecordCount(WebhookSubscription, 1);
- Clear();
+ ClearTestData();
end;
[Test]
+ [HandlerFunctions('WebhookHttpHandler')]
procedure TestDisableOrderCreatedWebhooks()
var
- Shop: Record "Shpfy Shop";
WebhookSubscription: Record "Webhook Subscription";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
begin
// [SCENARIO] Disabling order created webhooks deletes the webhook from Shopify and deletes the subscription
// [GINVEN] A Shop record with order created webhooks enabled
Initialize();
- WebhooksSubcriber.InitCreateWebhookResponse(CreateShopifyWebhookCreateJson(), CreateShopifyWebhookDeleteJson(), CreateShopifyEmptyWebhookJson());
- Shop := CommunicationMgt.GetShopRecord();
- BindSubscription(WebhooksSubcriber);
if not Shop."Order Created Webhooks" then begin
+ OutboundHttpRequests.Enqueue('GetWebhooks');
+ OutboundHttpRequests.Enqueue('CreateWebhook');
Shop.Validate("Order Created Webhooks", true);
Shop.Modify();
end;
// [WHEN] Order created webhooks are disabled
+ OutboundHttpRequests.Enqueue('GetWebhooks');
+ OutboundHttpRequests.Enqueue('DeleteWebhook');
Shop.Validate("Order Created Webhooks", false);
// [THEN] Subscription is deleted and id field is cleared
LibraryAssert.AreEqual(Shop."Order Created Webhook Id", '', 'Subscription id should be cleared.');
WebhookSubscription.SetRange(Endpoint, Format("Shpfy Webhook Topic"::ORDERS_CREATE));
LibraryAssert.RecordIsEmpty(WebhookSubscription);
- Clear();
+ ClearTestData();
end;
[Test]
+ [HandlerFunctions('WebhookHttpHandler')]
procedure TestNotificationSchedulesOrderSyncJob()
var
- Shop: Record "Shpfy Shop";
JobQueueEntry: Record "Job Queue Entry";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
begin
// [SCENARIO] Creating a webhook notification for orders/create schedules order sync
// [GINVEN] A Shop record with order created webhooks enabled
Initialize();
- WebhooksSubcriber.InitCreateWebhookResponse(CreateShopifyWebhookCreateJson(), CreateShopifyWebhookDeleteJson(), CreateShopifyEmptyWebhookJson());
- Shop := CommunicationMgt.GetShopRecord();
- BindSubscription(WebhooksSubcriber);
if not Shop."Order Created Webhooks" then begin
+ OutboundHttpRequests.Enqueue('GetWebhooks');
+ OutboundHttpRequests.Enqueue('CreateWebhook');
Shop.Validate("Order Created Webhooks", true);
Shop.Modify();
end;
@@ -134,25 +165,23 @@ codeunit 139612 "Shpfy Webhooks Test"
JobQueueEntry.SetRange("Object ID to Run", Report::"Shpfy Sync Orders from Shopify");
JobQueueEntry.FindFirst();
LibraryAssert.AreEqual(JobQueueEntry."Job Queue Category Code", 'SHPFY', 'Job queue category should be SHPFY.');
- Clear();
+ ClearTestData();
end;
[Test]
+ [HandlerFunctions('WebhookHttpHandler')]
procedure TestNotificationDoesNotScheduleOrderSyncJobIfAlreadyExists()
var
- Shop: Record "Shpfy Shop";
JobQueueEntry: Record "Job Queue Entry";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
JobQueueEntryId: Guid;
begin
// [SCENARIO] Creating a webhook notification for orders/create does not schedule order sync if there is a ready job queue already
// [GINVEN] A Shop record with order created webhooks enabled and a ready job queue entry
Initialize();
- WebhooksSubcriber.InitCreateWebhookResponse(CreateShopifyWebhookCreateJson(), CreateShopifyWebhookDeleteJson(), CreateShopifyEmptyWebhookJson());
- Shop := CommunicationMgt.GetShopRecord();
- BindSubscription(WebhooksSubcriber);
if not Shop."Order Created Webhooks" then begin
+ OutboundHttpRequests.Enqueue('GetWebhooks');
+ OutboundHttpRequests.Enqueue('CreateWebhook');
Shop.Validate("Order Created Webhooks", true);
Shop.Modify();
end;
@@ -165,27 +194,27 @@ codeunit 139612 "Shpfy Webhooks Test"
JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Report);
JobQueueEntry.SetRange("Object ID to Run", Report::"Shpfy Sync Orders from Shopify");
LibraryAssert.RecordCount(JobQueueEntry, 1);
- Clear();
+ ClearTestData();
end;
[Test]
+ [HandlerFunctions('WebhookHttpHandler')]
procedure TestEnableBulkOperationWebhook()
var
- Shop: Record "Shpfy Shop";
WebhookSubscription: Record "Webhook Subscription";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
BulkOperationMgt: Codeunit "Shpfy Bulk Operation Mgt.";
begin
// [SCENARIO] Enabling connection registers webhook with Shopify and creates a subscription
// [GINVEN] A Shop record
Initialize();
- WebhooksSubcriber.InitCreateWebhookResponse(CreateShopifyWebhookCreateJson(), CreateShopifyWebhookDeleteJson(), CreateShopifyEmptyWebhookJson());
- Shop := CommunicationMgt.GetShopRecord();
- BindSubscription(WebhooksSubcriber);
BindSubscription(BulkOpSubscriber);
WebhookSubscription.DeleteAll();
+ // [GIVEN] Register Expected Outbound API Requests (get webhooks, create webhook).
+ OutboundHttpRequests.Enqueue('GetWebhooks');
+ OutboundHttpRequests.Enqueue('CreateWebhook');
+
// [WHEN] Shop is enabled
BulkOperationMgt.EnableBulkOperations(Shop);
@@ -193,22 +222,29 @@ codeunit 139612 "Shpfy Webhooks Test"
LibraryAssert.AreEqual(Shop."Bulk Operation Webhook Id", SubscriptionId, 'Subscription id should be filled.');
WebhookSubscription.SetRange(Endpoint, Format("Shpfy Webhook Topic"::BULK_OPERATIONS_FINISH));
LibraryAssert.RecordCount(WebhookSubscription, 1);
- Clear();
+ ClearTestData();
end;
[Test]
+ [HandlerFunctions('WebhookHttpHandler')]
procedure TestDisableBulkOperationWebhooks()
var
- Shop: Record "Shpfy Shop";
WebhookSubscription: Record "Webhook Subscription";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
+ BulkOperationMgt: Codeunit "Shpfy Bulk Operation Mgt.";
begin
// [SCENARIO] Disabling shop deletes the webhook from Shopify and deletes the subscription
- // [GINVEN] A Shop record
+ // [GINVEN] A Shop record with bulk operation webhooks enabled
Initialize();
- Shop := CommunicationMgt.GetShopRecord();
- BindSubscription(WebhooksSubcriber);
+ BindSubscription(BulkOpSubscriber);
+ if Shop."Bulk Operation Webhook Id" = '' then begin
+ OutboundHttpRequests.Enqueue('GetWebhooks');
+ OutboundHttpRequests.Enqueue('CreateWebhook');
+ BulkOperationMgt.EnableBulkOperations(Shop);
+ end;
+
+ // [GIVEN] Register Expected Outbound API Requests (delete webhook).
+ OutboundHttpRequests.Enqueue('DeleteWebhook');
// [WHEN] Shop is disabled
Shop.Validate(Enabled, false);
@@ -217,15 +253,14 @@ codeunit 139612 "Shpfy Webhooks Test"
LibraryAssert.AreEqual(Shop."Bulk Operation Webhook Id", '', 'Subscription id should be cleared.');
WebhookSubscription.SetRange(Endpoint, Format("Shpfy Webhook Topic"::BULK_OPERATIONS_FINISH));
LibraryAssert.RecordIsEmpty(WebhookSubscription);
- Clear();
+ ClearTestData();
end;
[Test]
+ [HandlerFunctions('WebhookHttpHandler')]
procedure TestBulkOperationNotification()
var
- Shop: Record "Shpfy Shop";
BulkOperation: Record "Shpfy Bulk Operation";
- CommunicationMgt: Codeunit "Shpfy Communication Mgt.";
BulkOperationMgt: Codeunit "Shpfy Bulk Operation Mgt.";
BulkOperationId: BigInteger;
BulkOperationSystemId: Guid;
@@ -234,13 +269,15 @@ codeunit 139612 "Shpfy Webhooks Test"
// [GINVEN] A Shop record and a bulk operation
Initialize();
- WebhooksSubcriber.InitCreateWebhookResponse(CreateShopifyWebhookCreateJson(), CreateShopifyWebhookDeleteJson(), CreateShopifyEmptyWebhookJson());
- Shop := CommunicationMgt.GetShopRecord();
- BindSubscription(WebhooksSubcriber);
BindSubscription(BulkOpSubscriber);
BulkOperationId := LibraryRandom.RandIntInRange(100000, 999999);
BulkOperationSystemId := CreateBulkOperation(Shop, BulkOperationId);
+ // [GIVEN] Register Expected Outbound API Requests (get webhooks, create webhook for EnableBulkOperations, then bulk op status check).
+ OutboundHttpRequests.Enqueue('GetWebhooks');
+ OutboundHttpRequests.Enqueue('CreateWebhook');
+ OutboundHttpRequests.Enqueue('BulkOperationStatus');
+
// [WHEN] A notification is inserted
BulkOperationMgt.EnableBulkOperations(Shop);
InsertNotification(Shop."Shopify URL", Format("Shpfy Webhook Topic"::BULK_OPERATIONS_FINISH), CreateBulkOperationNotificationJson(BulkOperationId));
@@ -249,6 +286,42 @@ codeunit 139612 "Shpfy Webhooks Test"
BulkOperation.GetBySystemId(BulkOperationSystemId);
LibraryAssert.AreEqual(BulkOperation.Status, BulkOperation.Status::Completed, 'Bulk operation status should be completed.');
LibraryAssert.AreNotEqual(BulkOperation."Completed At", 0DT, 'Bulk operation completed at should be filled.');
+ ClearTestData();
+ end;
+
+ [HttpClientHandler]
+ internal procedure WebhookHttpHandler(Request: TestHttpRequestMessage; var Response: TestHttpResponseMessage): Boolean
+ var
+ ResponseBody: Text;
+ ResponseKey: Text;
+ begin
+ if not InitializeTest.VerifyRequestUrl(Request.Path, Shop."Shopify URL") then
+ exit(true);
+
+ ResponseKey := OutboundHttpRequests.DequeueText();
+
+ case ResponseKey of
+ 'GetWebhooks':
+ begin
+ CreateShopifyEmptyWebhookJson().WriteTo(ResponseBody);
+ Response.Content.WriteFrom(ResponseBody);
+ end;
+ 'CreateWebhook':
+ begin
+ CreateShopifyWebhookCreateJson().WriteTo(ResponseBody);
+ Response.Content.WriteFrom(ResponseBody);
+ end;
+ 'DeleteWebhook':
+ begin
+ CreateShopifyWebhookDeleteJson().WriteTo(ResponseBody);
+ Response.Content.WriteFrom(ResponseBody);
+ end;
+ 'BulkOperationStatus':
+ Response.Content.WriteFrom('{"data":{"node":{"id":"gid://shopify/BulkOperation/1","status":"COMPLETED","url":"https://storage.googleapis.com/result.jsonl","completedAt":"2026-03-17T00:00:00Z"}}}');
+ else
+ Response.Content.WriteFrom('{"data":{}}');
+ end;
+ exit(false);
end;
local procedure InsertNotification(ShopifyURL: Text[250]; Topic: Text[250]; Notification: Text)
@@ -267,12 +340,12 @@ codeunit 139612 "Shpfy Webhooks Test"
WebhookNotification.Insert();
end;
- local procedure CreateJobQueueEntry(Shop: Record "Shpfy Shop"; ReportId: Integer): Guid
+ local procedure CreateJobQueueEntry(Shop2: Record "Shpfy Shop"; ReportId: Integer): Guid
var
JobQueueEntry: Record "Job Queue Entry";
OrderParametersTxt: Label '%1VERSION(1) SORTING(Field1)', Comment = '%1 = Shop Record View', Locked = true;
begin
- Shop.SetFilter(Code, Shop.Code);
+ Shop2.SetFilter(Code, Shop2.Code);
JobQueueEntry."Object Type to Run" := JobQueueEntry."Object Type to Run"::Report;
JobQueueEntry."Object ID to Run" := ReportId;
JobQueueEntry."Report Output Type" := JobQueueEntry."Report Output Type"::"None (Processing only)";
@@ -280,7 +353,7 @@ codeunit 139612 "Shpfy Webhooks Test"
JobQueueEntry."Job Queue Category Code" := 'SHPFY';
JobQueueEntry.Status := JobQueueEntry.Status::Ready;
JobQueueEntry.Insert();
- JobQueueEntry.SetXmlContent(StrSubstNo(OrderParametersTxt, Shop.GetView()));
+ JobQueueEntry.SetXmlContent(StrSubstNo(OrderParametersTxt, Shop2.GetView()));
exit(JobQueueEntry.ID);
end;
@@ -313,12 +386,12 @@ codeunit 139612 "Shpfy Webhooks Test"
exit(Result);
end;
- local procedure CreateBulkOperation(Shop: Record "Shpfy Shop"; BulkOperationId: BigInteger): Guid
+ local procedure CreateBulkOperation(Shop2: Record "Shpfy Shop"; BulkOperationId: BigInteger): Guid
var
BulkOperation: Record "Shpfy Bulk Operation";
begin
BulkOperation."Bulk Operation Id" := BulkOperationId;
- BulkOperation."Shop Code" := Shop.Code;
+ BulkOperation."Shop Code" := Shop2.Code;
BulkOperation.Type := BulkOperation.Type::mutation;
BulkOperation.Status := BulkOperation.Status::Created;
BulkOperation.Insert();
diff --git a/src/Apps/W1/Shopify/Test/docs/business-logic.md b/src/Apps/W1/Shopify/Test/docs/business-logic.md
new file mode 100644
index 0000000000..c68f2bcff2
--- /dev/null
+++ b/src/Apps/W1/Shopify/Test/docs/business-logic.md
@@ -0,0 +1,114 @@
+# Business logic
+
+## Overview
+
+The Shopify Connector Test app does not contain business logic in the traditional sense -- it verifies the business logic of the main Shopify Connector app. The "logic" here is the test infrastructure: how test environments are bootstrapped, how HTTP calls are intercepted, and how test data flows from initialization through assertion.
+
+Understanding this infrastructure is essential because every test codeunit depends on it. The initialization creates a realistic-enough BC environment that the connector's mapping, posting, and sync code can execute without hitting missing-data errors. The HTTP mocking layer ensures tests exercise the connector's response-parsing code with known payloads.
+
+## Shop initialization flow
+
+Every test begins with `Initialize()`, which gates on an `isInitialized` boolean to run setup exactly once per session. The setup calls `ShpfyInitializeTest.CreateShop()`, which is the most important procedure in the test app.
+
+```mermaid
+flowchart TD
+ A[Test method calls Initialize] --> B{isInitialized?}
+ B -->|Yes| C[Return immediately]
+ B -->|No| D[Run ShpfyInitializeTest codeunit]
+ D --> E[CreateShop]
+ E --> F{TempShop exists?}
+ F -->|Yes| G[Return cached shop]
+ F -->|No| H[Generate random Code]
+ H --> I[Create VAT Posting Setup with accounts]
+ I --> J[Create Refund GL Account]
+ J --> K[Create Customer Template + posting groups]
+ K --> L[Create Item Template + posting groups]
+ L --> M[Create VAT Posting Setup combinations]
+ M --> N[Create Shipping Charges GL Account]
+ N --> O[Create Country/Region Code]
+ O --> P[Insert Shop record + Commit]
+ P --> Q[Set shop on CommunicationMgt]
+ Q --> R[Create dummy Customer from template]
+ R --> S[Create dummy Item from template]
+ S --> T[Cache in TempShop + Commit]
+ T --> U[Return Shop]
+```
+
+The shop creation is deliberately thorough. It creates three VAT posting setup combinations (for the shop's posting group, for empty product group, and for the refund GL account's group) because the connector needs these when processing orders with different tax scenarios. The customer template gets its own customer posting group, general business posting group, and VAT business posting group. The item template gets inventory, general product, and VAT product posting groups.
+
+The dummy customer and item are created from these templates so they inherit the posting group configuration. The customer gets a known email (`dummy@customer.com`) and the item gets a known description (`Dummy Item Description`) -- both are used as lookup keys by `GetDummyCustomer()` and `GetDummyItem()`.
+
+## HTTP mocking infrastructure
+
+The test app uses BC's `[HttpClientHandler]` attribute to intercept HTTP requests at the platform level. This is the modern approach -- it replaced earlier patterns that used event subscribers to mock communication.
+
+```mermaid
+flowchart TD
+ A[Connector code calls Shopify API] --> B[Platform intercepts HTTP request]
+ B --> C{TestHttpRequestPolicy?}
+ C -->|BlockOutboundRequests| D[Route to HttpClientHandler]
+ D --> E[Handler inspects Request.Path]
+ E --> F{URL matches shop?}
+ F -->|Yes| G[Build mock response]
+ F -->|No| H[Return true = not handled]
+ G --> I{Response source?}
+ I -->|Programmatic| J[Build JSON in AL code]
+ I -->|Resource file| K[NavApp.GetResource from .resources/]
+ J --> L[Write to Response.Content]
+ K --> L
+ L --> M[Return false = handled]
+ H --> N[Request hits block policy and fails]
+```
+
+There are two distinct patterns for building mock responses:
+
+**Resource-based responses** are used when the response structure is stable and complex. The bulk operations tests load responses from `.resources/Bulk Operations/StagedUploadResult.txt`, `.resources/Bulk Operations/BulkMutationResponse.txt`, etc. These files contain JSON templates with `%1`, `%2` placeholders that are filled via `StrSubstNo`. This keeps large JSON payloads out of AL code.
+
+**Programmatic responses** are used when the response content depends on test parameters. `ShpfyOrderHandlingHelper.CreateShopifyOrderAsJson()` builds an entire order JSON structure in AL, computing prices, tax, and discount amounts based on the order amount from `OrdersToImport`. Similarly, `ShpfyCustomerInitTest.DummyJsonCustomerObjectFromShopify()` builds customer JSON with parameterized IDs and today's date.
+
+The inventory export tests demonstrate a more sophisticated pattern -- the `[HttpClientHandler]` uses the `ShpfyInventoryRetryScenario` enum as a state machine. The handler tracks `CallCount` and returns different responses based on the scenario (success, fail-then-succeed, always-fail). This lets tests verify that retry logic works correctly by asserting on the final call count.
+
+## Domain-specific initialization
+
+Beyond the base shop setup, each domain area has its own initialization codeunit that creates Shopify-side records.
+
+**ShpfyCustomerInitTest** (SingleInstance) creates Shopify customer and address records with known field values. Its `ModifyFields()` procedure uses RecordRef to prepend "!" to all text fields on any record, producing a "modified" version for update-query tests. It also generates expected GraphQL query strings so tests can assert that the connector produces the correct mutation syntax.
+
+**ShpfyProductInitTest** creates BC items with full setup: extended text, item attributes, vendor info, item references (barcodes), and optionally item variants. It creates Shopify product and variant records linked to these items. The `CreateSKUValue()` procedure generates SKU values appropriate to whatever `SKU Mapping` the shop is configured with (bar code, item no., variant code, etc.).
+
+**ShpfyCompanyInitialize** creates B2B company records. **ShpfyOrderHandlingHelper** is the most complex -- it calls into the connector's `OrdersAPI.ExtractShopifyOrdersToImport` with programmatically built JSON, then builds a full order JSON response including line items, tax lines, addresses, and customer data. As a side effect, it creates real Shopify product, variant, and shop location records in the database.
+
+## Test data flow for order processing
+
+Order tests follow a distinctive pattern where the test first generates an "orders to import" payload, feeds it to the connector's extraction logic, then builds a detailed order JSON and feeds that to the import logic.
+
+```mermaid
+flowchart TD
+ A[Test calls OrderHandlingHelper.GetOrdersToImport] --> B[Helper builds orders JSON with random data]
+ B --> C[Connector's ExtractShopifyOrdersToImport parses it]
+ C --> D[Creates OrdersToImport records in DB]
+ D --> E[Test calls CreateShopifyOrderAsJson]
+ E --> F[Helper reads OrdersToImport for amounts/dates]
+ F --> G[Helper creates BC Customer + Shopify Customer records]
+ G --> H[Helper creates line items with real BC Items]
+ H --> I[Returns complete order JSON + line items JSON]
+ I --> J[Test calls ImportShopifyOrder]
+ J --> K[Connector processes JSON into Order Header + Lines]
+ K --> L[Test asserts on created BC records]
+```
+
+This two-phase approach (extract-then-import) mirrors how the real connector works: it first gets a lightweight list of orders to import, then fetches full details for each one. The test helper preserves this separation so the connector's actual import codeunits are exercised.
+
+## Access token registration
+
+Tests that make HTTP calls (even mocked ones) need a registered access token because the connector checks for it before making API calls. `ShpfyInitializeTest.RegisterAccessTokenForShop()` creates a `Shpfy Registered Store New` record with a comprehensive scope string and a random token value. This is called in the `Initialize()` of test codeunits that use `[HttpClientHandler]`, like `ShpfyInventoryExportTest` and `ShpfyBulkOperationsTest`.
+
+## Interface mocking via enum extensions
+
+The Shopify Connector uses AL interfaces for extensibility (e.g., `"Shpfy Stock Calculation"`, `"Shpfy IBulk Operation"`). The test app provides mock implementations by extending the corresponding enums:
+
+- `ShpfyStockCalculationExt` adds value "Shpfy Return Const" backed by `ShpfyConstToReturn` -- a codeunit that returns a configurable decimal from `GetStock()`. Tests set the constant via `SetConstToReturn()` and then exercise stock calculation paths.
+
+- `ShpfyBulkOperationType` enum extension adds "AddProduct" backed by `ShpfyMockBulkProductCreate` -- a codeunit that returns hardcoded GraphQL mutation text and input templates. The bulk operations tests use this to exercise the bulk operation lifecycle without needing a real operation type.
+
+This pattern is preferable to event subscribers for interface mocking because it works through the same dispatch mechanism the production code uses.
diff --git a/src/Apps/W1/Shopify/Test/docs/patterns.md b/src/Apps/W1/Shopify/Test/docs/patterns.md
new file mode 100644
index 0000000000..268d39072d
--- /dev/null
+++ b/src/Apps/W1/Shopify/Test/docs/patterns.md
@@ -0,0 +1,87 @@
+# Patterns
+
+## HttpClientHandler mocking
+
+The `[HttpClientHandler]` attribute on an internal procedure intercepts all outbound HTTP requests from the test codeunit. This is the correct way to mock Shopify API calls -- it operates at the platform level so the connector's communication code runs unmodified.
+
+**Example**: In `ShpfyInventoryExportTest`, the `InventoryExportHttpHandler` procedure checks `Request.Path` against the shop URL, increments a `CallCount`, and returns different JSON based on the `RetryScenario` enum. Each test method declares `[HandlerFunctions('InventoryExportHttpHandler')]` and configures the scenario via `SetRetryState()` before calling the connector's export logic.
+
+**Gotcha**: The handler must return `false` when it handles the request and `true` when it doesn't. Returning `true` means "pass through" -- but with `TestHttpRequestPolicy = BlockOutboundRequests`, unhandled requests will error. Also, the handler is matched by the `[HandlerFunctions]` attribute name, not by the procedure signature.
+
+## Lazy initialization with boolean flag
+
+Nearly every test codeunit uses a module-level `isInitialized: Boolean` that gates the `Initialize()` procedure. This ensures the expensive shop creation (with all its posting groups and templates) runs exactly once per test codeunit execution, not once per test method.
+
+**Example**: In `ShpfyInventoryAPITest`, the `Initialize()` procedure checks `isInitialized`, and if false, runs `Codeunit.Run(Codeunit::"Shpfy Initialize Test")` and sets the flag to true. All test methods call `Initialize()` as their first line.
+
+**Gotcha**: The flag resets when the codeunit's `OnRun` trigger fires (many codeunits set `isInitialized := false` in OnRun). If you're running tests in an environment where codeunits are re-instantiated between methods, each method will re-initialize. The `SingleInstance` property on some codeunits prevents this -- but not all test codeunits use `SingleInstance`.
+
+## Resource-based mock responses
+
+Complex or stable JSON response payloads are stored as `.txt` files under `.resources/` and loaded via `NavApp.GetResource()`. This avoids embedding large JSON strings in AL code and makes it easy to update response formats.
+
+**Example**: In `ShpfyBulkOperationsTest`, the `BulkOperationHttpHandler` loads different resources based on which GraphQL operation was requested. It uses `Library - Variable Storage` as a queue (`GraphQLResponses.Enqueue('StagedUpload')`) to control which response to return for each sequential API call. The handler dequeues the next expected response type and loads the corresponding resource file.
+
+**Gotcha**: Resource files use `%1`, `%2` placeholders filled by `StrSubstNo`. If your response needs a literal `%` character, you must escape it. Also, resource files must be under the `.resources/` path declared in `app.json`'s `resourceFolders` -- putting them elsewhere means they won't be compiled into the app.
+
+## Retry state machine testing
+
+For testing retry/idempotency logic, the test codeunit maintains mutable state (scenario enum + call counter) that the HTTP handler reads to decide what to return.
+
+**Example**: `ShpfyInventoryExportTest` declares `RetryScenario: Enum "Shpfy Inventory Retry Scenario"`, `CallCount: Integer`, and `ErrorCode: Text`. The test method calls `SetRetryState(FailOnceThenSucceed, 'IDEMPOTENCY_CONCURRENT_REQUEST')` before invoking the export. The handler increments `CallCount` and returns an error response on the first call, then success on subsequent calls. The test asserts `CallCount = 2` to verify the retry happened. For `AlwaysFail`, it asserts `CallCount = 4` (1 initial + 3 retries) and checks that a skipped record was logged.
+
+**Gotcha**: The `CallCount` is on the codeunit instance, not the handler. Since the test codeunit and handler are the same object (the handler is a procedure on the test codeunit), this works. But if you extract the handler to a separate codeunit, you'd need to pass state differently.
+
+## Interface mocking via enum extensions
+
+When the main app defines an AL interface dispatched through an enum, the test app can provide mock implementations by extending that enum with a test-only value backed by a mock codeunit.
+
+**Example**: The main app's `"Shpfy Stock Calculation"` interface is extended by `ShpfyStockCalculationExt` (enum extension 139560) adding value "Shpfy Return Const" implemented by `ShpfyConstToReturn`. This codeunit stores a decimal and returns it from `GetStock()`. Tests configure a shop location with this stock calculation type, call `SetConstToReturn(42)`, and verify the connector uses the returned value.
+
+**Gotcha**: The enum extension value (139560) must be in the test app's ID range. The mock codeunit must implement the full interface -- even if most methods are no-ops (like `RevertFailedRequests` and `RevertAllRequests` in `ShpfyMockBulkProductCreate`).
+
+## Manual event subscriber binding
+
+Some tests need to intercept events from the main app to control behavior. They use codeunits with `EventSubscriberInstance = Manual` and `SingleInstance = true`, which must be explicitly bound.
+
+**Example**: `ShpfySkippedRecordLogSub` subscribes to `OnBeforeFindMapping` on the customer events codeunit. It sets `Handled := true` and injects a specific `ShopifyCustomerId`, bypassing the real customer mapping logic. The test creates an instance, calls `SetShopifyCustomerId()`, and binds it with `BindSubscription`. Similarly, `ShpfyBulkOpSubscriber` subscribes to `OnInvalidUser` and sets `IsHandled := true` to skip user validation that would fail in a test context.
+
+**Gotcha**: Forgetting to unbind after the test can leak state into subsequent tests. The `SingleInstance` property means the subscriber persists for the session -- its state carries over unless explicitly reset.
+
+## Variable Storage queue for GraphQL response dispatch
+
+All Shopify GraphQL requests hit the same endpoint (`/admin/api/graphql.json`), and `TestHttpRequestMessage` does not expose the request body -- so the `[HttpClientHandler]` cannot inspect the query to decide which mock response to return. The workaround is a pre-loaded queue: the test enqueues response type identifiers into `Library - Variable Storage` before running the code under test, and the HTTP handler dequeues to know which response to serve next.
+
+**Example**: In `ShpfyBulkOperationsTest`, sending a bulk mutation requires two API calls: first a staged upload, then the actual bulk mutation. The test calls `EnqueueGraphQLResponsesForSendBulkMutation()` which enqueues `'StagedUpload'` then `'BulkMutation'`. The handler calls `GraphQLResponses.DequeueText()` to get the next expected type and loads the appropriate resource file.
+
+**Gotcha**: If the code under test makes more API calls than you enqueued, the dequeue will error with an empty-storage exception. If it makes fewer, leftover items in the queue may confuse the next test method. Some test methods call `ClearSetup()` which clears the storage.
+
+## Disabled tests tracking
+
+Known-failing test methods are tracked in JSON files under `DisabledTests/` rather than being deleted or commented out. Each entry records the bug ID, codeunit ID, codeunit name, and method name.
+
+**Example**: `DisabledTests/ShpfyProductPriceCalcTest.json` disables `UnitTestCalcPriceTest` from codeunit 139605 with bug reference 621557.
+
+**Gotcha**: The disabled tests mechanism is read by the test runner infrastructure. If you rename a test method or codeunit, update the JSON file too, otherwise the filter won't match and the test will run (and presumably fail).
+
+## Legacy patterns
+
+### RecordRef-based field mutation
+
+**What it is**: `ShpfyCustomerInitTest.ModifyFields()` takes a Variant, opens it as a RecordRef, iterates all fields, and prepends "!" to every text field value. `TextFieldsContainsFieldName()` does a similar loop to verify that text fields contain their own field name.
+
+**Where it appears**: `ShpfyCustomerInitTest` (codeunit 139585), used by `ShpfyCustomerAPITest` and `ShpfyCustomerExportTest`.
+
+**Why it exists**: It was written to generically modify any record for update-query testing without hardcoding each field. This was convenient when the customer record had few text fields.
+
+**What to do instead**: For new test helpers, prefer explicit field assignments. The RecordRef approach makes it hard to understand what changed when reading a test, and it silently modifies fields you may not intend to change. It also produces odd values like "!111" for phone numbers (which the test then special-cases: the validation strips invalid characters, leaving " .").
+
+### Temporary table for field-type coverage
+
+**What it is**: `ShpfyTestFields` (table 139560) is a temporary table with one field of every AL data type (Integer, Blob, Boolean, Code, Text, Date, DateTime, Decimal, Duration, Guid, Option, Time). Each field has a validation trigger that shows a message.
+
+**Where it appears**: `Helpers/Tables/ShpfyTestFields.Table.al`, used by `ShpfyFilterMgtTest`.
+
+**Why it exists**: The filter management test needed to verify that `CleanFilterValue` works correctly for values of every type. The temporary table provides a convenient fixture.
+
+**What to do instead**: This pattern is fine for its purpose. If you need to test type-agnostic code, a similar approach is reasonable. Just keep the table temporary to avoid polluting the database schema.