diff --git a/.github/AL-Go-Settings.json b/.github/AL-Go-Settings.json index de73e4e206..3f5065d27d 100644 --- a/.github/AL-Go-Settings.json +++ b/.github/AL-Go-Settings.json @@ -42,5 +42,6 @@ "main" ], "rulesetFile": "..\\Build\\Rulesets\\module.ruleset.json", - "PullRequestTrigger": "pull_request" + "PullRequestTrigger": "pull_request", + "alwaysBuildAllProjects": true } diff --git a/Modules/System Tests/Rest Client/app.json b/Modules/System Tests/Rest Client/app.json new file mode 100644 index 0000000000..8df2fbd440 --- /dev/null +++ b/Modules/System Tests/Rest Client/app.json @@ -0,0 +1,49 @@ +{ + "id": "ae153cbb-ad55-447c-9226-5af3ef57280f", + "name": "Rest Client Tests", + "publisher": "Microsoft", + "version": "23.0.0.0", + "brief": "Tests for the Rest Client module", + "description": "Tests for the Rest Client module", + "privacyStatement": "https://go.microsoft.com/fwlink/?LinkId=724009", + "EULA": "https://go.microsoft.com/fwlink/?linkid=2182906", + "help": "https://go.microsoft.com/fwlink/?linkid=2206603", + "url": "https://go.microsoft.com/fwlink/?LinkId=72401", + "dependencies": [ + { + "id": "812b339d-a9db-4a6e-84e4-fe35cbef0c44", + "name": "Rest Client", + "publisher": "Microsoft", + "version": "23.0.0.0" + }, + { + "id": "dd0be2ea-f733-4d65-bb34-a28f4624fb14", + "name": "Library Assert", + "publisher": "Microsoft", + "version": "23.0.0.0" + }, + { + "id": "e31ad830-3d46-472e-afeb-1d3d35247943", + "name": "BLOB Storage", + "publisher": "Microsoft", + "version": "23.0.0.0" + } + ], + "screenshots": [], + "platform": "23.0.0.0", + "target": "OnPrem", + "idRanges": [ + { + "from": 134970, + "to": 134974 + } + ], + "resourceExposurePolicy": { + "allowDebugging": true, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "features": [ + "NoImplicitWith" + ] +} diff --git a/Modules/System Tests/Rest Client/src/HttpAuthenticationTests.Codeunit.al b/Modules/System Tests/Rest Client/src/HttpAuthenticationTests.Codeunit.al new file mode 100644 index 0000000000..7b5e65284f --- /dev/null +++ b/Modules/System Tests/Rest Client/src/HttpAuthenticationTests.Codeunit.al @@ -0,0 +1,58 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Test.RestClient; + +using System.RestClient; +using System.TestLibraries.Utilities; + +codeunit 134973 "Http Authentication Tests" +{ + Subtype = Test; + + var + Assert: Codeunit "Library Assert"; + + [Test] + procedure TestAnonymousAuthentication() + var + HttpAuthenticationAnonymous: Codeunit "Http Authentication Anonymous"; + begin + // [GIVEN] An anonymous authentication object + + // [WHEN] The authentication object is asked if authentication is required + // [THEN] The authentication object should return false + Assert.IsFalse(HttpAuthenticationAnonymous.IsAuthenticationRequired(), 'Anonymous authentication should report that authentication is not required'); + + // [WHEN] The authentication object is asked to return the authorization header + // [THEN] The authentication object should return an empty list + Assert.AreEqual(HttpAuthenticationAnonymous.GetAuthorizationHeaders().Count, 0, 'Anonymous authentication should not return an authorization header'); + end; + + [NonDebuggable] + [Test] + procedure TestBasicAuthentication() + var + HttpAuthenticationBasic: Codeunit "Http Authentication Basic"; + AuthHeader: Dictionary of [Text, SecretText]; + BasicAuthHeaderValue: SecretText; + begin + // [GIVEN] A basic authentication object + HttpAuthenticationBasic.Initialize('USER01', SecretText.SecretStrSubstNo('Password123!')); + + // [WHEN] The authentication object is asked if authentication is required + // [THEN] The authentication object should return true + Assert.IsTrue(HttpAuthenticationBasic.IsAuthenticationRequired(), 'Basic authentication should report that authentication is required'); + + // [WHEN] The authentication object is asked to return the authorization header + // [THEN] THe authentication object should return a dictionary with one element that is a base64 encoded string + AuthHeader := HttpAuthenticationBasic.GetAuthorizationHeaders(); + Assert.AreEqual(AuthHeader.Count, 1, 'Basic authentication should return one authorization header'); + Assert.AreEqual(AuthHeader.ContainsKey('Authorization'), true, 'Basic authentication should return an authorization header'); + + BasicAuthHeaderValue := AuthHeader.Get('Authorization'); + Assert.AreEqual(BasicAuthHeaderValue.Unwrap(), 'Basic VVNFUjAxOlBhc3N3b3JkMTIzIQ==', 'Basic authentication should return a base64 encoded string'); + end; +} \ No newline at end of file diff --git a/Modules/System Tests/Rest Client/src/HttpContentTests.Codeunit.al b/Modules/System Tests/Rest Client/src/HttpContentTests.Codeunit.al new file mode 100644 index 0000000000..b7d63f86f3 --- /dev/null +++ b/Modules/System Tests/Rest Client/src/HttpContentTests.Codeunit.al @@ -0,0 +1,458 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Test.RestClient; + +using System.RestClient; +using System.TestLibraries.Utilities; +using System.Utilities; + +codeunit 134970 "Http Content Tests" +{ + Subtype = Test; + + var + Assert: Codeunit "Library Assert"; + + [Test] + procedure TestCreateWithText() + var + ALHttpContent: Codeunit "Http Content"; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + HeaderValues: List of [Text]; + TextContent: Text; + begin + // [SCENARIO] Create AL Http Content object with text + + // [GIVEN] AL Http Content created with text + ALHttpContent := ALHttpContent.Create('Hello World'); + + // [WHEN] Content is retrieved as Text + TextContent := ALHttpContent.AsText(); + + // [THEN] Content is equal to Hello World + Assert.AreEqual('Hello World', TextContent, 'The content must be equal to Hello World'); + + // [THEN] Header Content-Type is text/plain + HttpContent := ALHttpContent.GetHttpContent(); + HttpContent.GetHeaders(HttpHeaders); + HttpHeaders.GetValues('Content-Type', HeaderValues); + Assert.AreEqual('text/plain', HeaderValues.Get(1), 'The Content-Type header must be text/plain'); + end; + + [Test] + procedure TestCreateWithTextAndContentType() + var + ALHttpContent: Codeunit "Http Content"; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + HeaderValues: List of [Text]; + TextContent: Text; + begin + // [SCENARIO] Create AL Http Content object with text and content type + + // [GIVEN] AL Http Content created with text and content type + ALHttpContent := ALHttpContent.Create('Hello World', 'text/html'); + + // [WHEN] Content is retrieved as Text + TextContent := ALHttpContent.AsText(); + + // [THEN] Content is equal to Hello World + Assert.AreEqual('Hello World', TextContent, 'The content must be equal to Hello World'); + + // [THEN] Header Content-Type is text/html + HttpContent := ALHttpContent.GetHttpContent(); + HttpContent.GetHeaders(HttpHeaders); + HttpHeaders.GetValues('Content-Type', HeaderValues); + Assert.AreEqual('text/html', HeaderValues.Get(1), 'The Content-Type header must be text/html'); + end; + + [Test] + procedure TestCreateWithJsonObject() + var + ALHttpContent: Codeunit "Http Content"; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + HeaderValues: List of [Text]; + JsonObject1: JsonObject; + JsonObject2: JsonObject; + JsonText1: Text; + JsonText2: Text; + begin + // [SCENARIO] Create AL Http Content object with json object + + // [GIVEN] AL Http Content created with json object + JsonObject1.Add('name', 'John'); + JsonObject1.Add('age', 30); + ALHttpContent := ALHttpContent.Create(JsonObject1); + + // [WHEN] Http Content is retrieved as JsonObject + JsonObject2 := ALHttpContent.AsJson().AsObject(); + + // [THEN] JsonObject is equal to JsonObject2 + JsonObject1.WriteTo(JsonText1); + JsonObject2.WriteTo(JsonText2); + Assert.AreEqual(JsonText1, JsonText2, 'The json objects must be equal'); + + // [THEN] Header Content-Type is application/json + HttpContent := ALHttpContent.GetHttpContent(); + HttpContent.GetHeaders(HttpHeaders); + HttpHeaders.GetValues('Content-Type', HeaderValues); + Assert.AreEqual('application/json', HeaderValues.Get(1), 'The Content-Type header must be application/json'); + end; + + [Test] + procedure TestCreateWithJsonArray() + var + ALHttpContent: Codeunit "Http Content"; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + HeaderValues: List of [Text]; + JsonArray1: JsonArray; + JsonArray2: JsonArray; + JsonText1: Text; + JsonText2: Text; + begin + // [SCENARIO] Create AL Http Content object with json array + + // [GIVEN] AL Http Content created with json array + JsonArray1.Add('John'); + JsonArray1.Add('Doe'); + ALHttpContent := ALHttpContent.Create(JsonArray1); + + // [WHEN] Http Content is retrieved as JsonArray + JsonArray2 := ALHttpContent.AsJson().AsArray(); + + // [THEN] JsonArray is equal to JsonArray2 + JsonArray1.WriteTo(JsonText1); + JsonArray2.WriteTo(JsonText2); + Assert.AreEqual(JsonText1, JsonText2, 'The json arrays must be equal'); + + // [THEN] Header Content-Type is application/json + HttpContent := ALHttpContent.GetHttpContent(); + HttpContent.GetHeaders(HttpHeaders); + HttpHeaders.GetValues('Content-Type', HeaderValues); + Assert.AreEqual('application/json', HeaderValues.Get(1), 'The Content-Type header must be application/json'); + end; + + [Test] + procedure TestCreateWithJsonToken() + var + ALHttpContent: Codeunit "Http Content"; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + HeaderValues: List of [Text]; + JsonToken1: JsonToken; + JsonToken2: JsonToken; + JsonText1: Text; + JsonText2: Text; + begin + // [SCENARIO] Create AL Http Content object with json token + + // [GIVEN] AL Http Content created with json token + JsonToken1.ReadFrom('{"name":"John","age":30}'); + ALHttpContent := ALHttpContent.Create(JsonToken1); + + // [WHEN] Http Content is retrieved as JsonToken + JsonToken2 := ALHttpContent.AsJson(); + + // [THEN] JsonToken is equal to JsonToken2 + JsonToken1.WriteTo(JsonText1); + JsonToken2.WriteTo(JsonText2); + Assert.AreEqual(JsonText1, JsonText2, 'The json tokens must be equal'); + + // [THEN] Header Content-Type is application/json + HttpContent := ALHttpContent.GetHttpContent(); + HttpContent.GetHeaders(HttpHeaders); + HttpHeaders.GetValues('Content-Type', HeaderValues); + Assert.AreEqual('application/json', HeaderValues.Get(1), 'The Content-Type header must be application/json'); + end; + + [Test] + procedure TestCreateWithXmlDocument() + var + ALHttpContent: Codeunit "Http Content"; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + HeaderValues: List of [Text]; + XmlDoc1: XmlDocument; + XmlDoc2: XmlDocument; + XmlReadOptions: XmlReadOptions; + XmlWriteOptions: XmlWriteOptions; + Xml1: Text; + Xml2: Text; + begin + // [SCENARIO] Create AL Http Content object with xml document + + // [GIVEN] AL Http Content created with xml document + XmlReadOptions.PreserveWhitespace(false); + XmlDocument.ReadFrom('John30', XmlReadOptions, XmlDoc1); + ALHttpContent := ALHttpContent.Create(XmlDoc1); + + // [WHEN] Http Content is retrieved as XmlDocument + XmlDoc2 := ALHttpContent.AsXmlDocument(); + + // [THEN] Xml is equal to XmlDoc + XmlWriteOptions.PreserveWhitespace(false); + XmlDoc1.WriteTo(XmlWriteOptions, Xml1); + + Clear(XmlWriteOptions); // PreserveWhitespace setting is not preserved between two calls to WriteTo, needed to reset it + XmlWriteOptions.PreserveWhitespace(false); + XmlDoc2.WriteTo(XmlWriteOptions, Xml2); + Assert.AreEqual(Xml1, Xml2, 'The xml documents must be equal'); + + // [THEN] Header Content-Type is text/xml + HttpContent := ALHttpContent.GetHttpContent(); + HttpContent.GetHeaders(HttpHeaders); + HttpHeaders.GetValues('Content-Type', HeaderValues); + Assert.AreEqual('text/xml', HeaderValues.Get(1), 'The Content-Type header must be application/xml'); + end; + + [Test] + procedure TestCreateWithInStream() + var + ALHttpContent: Codeunit "Http Content"; + TempBlob: Codeunit "Temp Blob"; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + HeaderValues: List of [Text]; + OutStream: OutStream; + InStream1: InStream; + InStream2: InStream; + TextContent: Text; + begin + // [SCENARIO] Create AL Http Content object with InStream + + // [GIVEN] AL Http Content created with InStream + TempBlob.CreateOutStream(OutStream); + OutStream.WriteText('Hello World'); + TempBlob.CreateInStream(InStream1); + ALHttpContent := ALHttpContent.Create(InStream1); + + // [WHEN] Http Content is retrieved as InStream + InStream2 := ALHttpContent.AsInStream(); + + // [THEN] Content is equal to Hello World + InStream2.ReadText(TextContent); + Assert.AreEqual('Hello World', TextContent, 'The content must be equal to Hello World'); + + // [THEN] Header Content-Type is application/octet-stream + HttpContent := ALHttpContent.GetHttpContent(); + HttpContent.GetHeaders(HttpHeaders); + HttpHeaders.GetValues('Content-Type', HeaderValues); + Assert.AreEqual('application/octet-stream', HeaderValues.Get(1), 'The Content-Type header must be application/octet-stream'); + end; + + [Test] + procedure TestCreateWithInStreamWithContentType() + var + ALHttpContent: Codeunit "Http Content"; + TempBlob: Codeunit "Temp Blob"; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + HeaderValues: List of [Text]; + OutStream: OutStream; + InStream1: InStream; + InStream2: InStream; + TextContent: Text; + begin + // [SCENARIO] Create AL Http Content object with InStream + + // [GIVEN] AL Http Content created with InStream + TempBlob.CreateOutStream(OutStream); + OutStream.WriteText('Hello World'); + TempBlob.CreateInStream(InStream1); + ALHttpContent := ALHttpContent.Create(InStream1, 'text/plain'); + + // [WHEN] Http Content is retrieved as InStream + InStream2 := ALHttpContent.AsInStream(); + + // [THEN] Content is equal to Hello World + InStream2.ReadText(TextContent); + Assert.AreEqual('Hello World', TextContent, 'The content must be equal to Hello World'); + + // [THEN] Header Content-Type is application/octet-stream + HttpContent := ALHttpContent.GetHttpContent(); + HttpContent.GetHeaders(HttpHeaders); + HttpHeaders.GetValues('Content-Type', HeaderValues); + Assert.AreEqual('text/plain', HeaderValues.Get(1), 'The Content-Type header must be text/plain'); + end; + + [Test] + procedure TestCreateWithTempBlob() + var + ALHttpContent: Codeunit "Http Content"; + TempBlob1: Codeunit "Temp Blob"; + TempBlob2: Codeunit "Temp Blob"; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + HeaderValues: List of [Text]; + OutStream: OutStream; + InStream: InStream; + TextContent: Text; + begin + // [SCENARIO] Create AL Http Content object with Temp Blob + + // [GIVEN] AL Http Content created with Temp Blob + TempBlob1.CreateOutStream(OutStream); + OutStream.WriteText('Hello World'); + ALHttpContent := ALHttpContent.Create(TempBlob1); + + // [WHEN] Http Content is retrieved as Temp Blob + TempBlob2 := ALHttpContent.AsBlob(); + + // [THEN] Content is equal to Hello World + TempBlob2.CreateInStream(InStream); + InStream.ReadText(TextContent); + Assert.AreEqual('Hello World', TextContent, 'The content must be equal to Hello World'); + + // [THEN] Header Content-Type is application/octet-stream + HttpContent := ALHttpContent.GetHttpContent(); + HttpContent.GetHeaders(HttpHeaders); + HttpHeaders.GetValues('Content-Type', HeaderValues); + Assert.AreEqual('application/octet-stream', HeaderValues.Get(1), 'The Content-Type header must be application/octet-stream'); + end; + + [Test] + procedure TestCreateWithTempBlobWithContentType() + var + ALHttpContent: Codeunit "Http Content"; + TempBlob1: Codeunit "Temp Blob"; + TempBlob2: Codeunit "Temp Blob"; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + HeaderValues: List of [Text]; + OutStream: OutStream; + InStream: InStream; + TextContent: Text; + begin + // [SCENARIO] Create AL Http Content object with Temp Blob + + // [GIVEN] AL Http Content created with Temp Blob + TempBlob1.CreateOutStream(OutStream); + OutStream.WriteText('Hello World'); + ALHttpContent := ALHttpContent.Create(TempBlob1, 'text/plain'); + + // [WHEN] Http Content is retrieved as Temp Blob + TempBlob2 := ALHttpContent.AsBlob(); + + // [THEN] Content is equal to Hello World + TempBlob2.CreateInStream(InStream); + InStream.ReadText(TextContent); + Assert.AreEqual('Hello World', TextContent, 'The content must be equal to Hello World'); + + // [THEN] Header Content-Type is application/octet-stream + HttpContent := ALHttpContent.GetHttpContent(); + HttpContent.GetHeaders(HttpHeaders); + HttpHeaders.GetValues('Content-Type', HeaderValues); + Assert.AreEqual('text/plain', HeaderValues.Get(1), 'The Content-Type header must be text/plain'); + end; + + [Test] + procedure TestCreateWithHttpContent() + var + ALHttpContent: Codeunit "Http Content"; + HttpContent: HttpContent; + HttpContentHeaders: HttpHeaders; + HeaderValues: List of [Text]; + Json1: Text; + Json2: Text; + begin + // [SCENARIO] Create AL Http Content object with HttpContent + + // [GIVEN] AL Http Content is created with HttpContent + Json1 := '{"name":"John","age":30}'; + HttpContent.WriteFrom(Json1); + HttpContent.GetHeaders(HttpContentHeaders); + if HttpContentHeaders.Contains('Content-Type') then + HttpContentHeaders.Remove('Content-Type'); + HttpContentHeaders.Add('Content-Type', 'application/json'); + ALHttpContent := ALHttpContent.Create(HttpContent); + + // [WHEN] HttpContent object is retrieved + HttpContent := ALHttpContent.GetHttpContent(); + + // [THEN] HttpContent is equal to json + HttpContent.ReadAs(Json2); + Assert.AreEqual(Json1, Json2, 'The http content must be equal to json'); + + // [THEN] Header Content-Type is application/json + HttpContent.GetHeaders(HttpContentHeaders); + HttpContentHeaders.GetValues('Content-Type', HeaderValues); + Assert.AreEqual('application/json', HeaderValues.Get(1), 'The Content-Type header must be application/json'); + end; + + [Test] + procedure TestSetContentTypeHeader() + var + ALHttpContent: Codeunit "Http Content"; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + HeaderValues: List of [Text]; + begin + // [SCENARIO] Set Content-Type header + + // [GIVEN] AL Http Content created with text + ALHttpContent := ALHttpContent.Create('{ "name": "John", "age": 30 }'); + + // [WHEN] Content-Type header is set + ALHttpContent.SetContentTypeHeader('application/json'); + + // [THEN] Header Content-Type is application/json + HttpContent := ALHttpContent.GetHttpContent(); + HttpContent.GetHeaders(HttpHeaders); + HttpHeaders.GetValues('Content-Type', HeaderValues); + Assert.AreEqual('application/json', HeaderValues.Get(1), 'The Content-Type header must be application/json'); + end; + + [Test] + procedure TestSetContentEncoding() + var + ALHttpContent: Codeunit "Http Content"; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + HeaderValues: List of [Text]; + begin + // [SCENARIO] Set Content-Encoding header + + // [GIVEN] AL Http Content created with text + ALHttpContent := ALHttpContent.Create('{ "name": "John", "age": 30 }'); + + // [WHEN] Content-Encoding header is set + ALHttpContent.AddContentEncoding('deflate'); + ALHttpContent.AddContentEncoding('br'); + + // [THEN] Header Content-Encoding is deflate, br + HttpContent := ALHttpContent.GetHttpContent(); + HttpContent.GetHeaders(HttpHeaders); + HttpHeaders.GetValues('Content-Encoding', HeaderValues); + Assert.AreEqual('deflate', HeaderValues.Get(1), 'The Content-Encoding header must be deflate'); + Assert.AreEqual('br', HeaderValues.Get(2), 'The Content-Encoding header must be br'); + end; + + [Test] + procedure TestSetHeader() + var + ALHttpContent: Codeunit "Http Content"; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + HeaderValues: List of [Text]; + begin + // [SCENARIO] Set header + + // [GIVEN] AL Http Content created with text + ALHttpContent := ALHttpContent.Create('{ "name": "John", "age": 30 }'); + + // [WHEN] Header is set + ALHttpContent.SetHeader('X-My-Custom-Header', 'BC Rest Client'); + + // [THEN] Header X-My-Custom-Header is BC Rest Client + HttpContent := ALHttpContent.GetHttpContent(); + HttpContent.GetHeaders(HttpHeaders); + HttpHeaders.GetValues('X-My-Custom-Header', HeaderValues); + Assert.AreEqual('BC Rest Client', HeaderValues.Get(1), 'The X-Test header must be BC Rest Client'); + end; +} \ No newline at end of file diff --git a/Modules/System Tests/Rest Client/src/RequestMessageTests.Codeunit.al b/Modules/System Tests/Rest Client/src/RequestMessageTests.Codeunit.al new file mode 100644 index 0000000000..10d54896a4 --- /dev/null +++ b/Modules/System Tests/Rest Client/src/RequestMessageTests.Codeunit.al @@ -0,0 +1,158 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Test.RestClient; + +using System.RestClient; +using System.TestLibraries.Utilities; + +codeunit 134972 "Request Message Tests" +{ + Subtype = Test; + + var + Assert: Codeunit "Library Assert"; + + [Test] + procedure TestSetHttpMethod() + var + ALHttpRequestMessage: Codeunit "Http Request Message"; + HttpRequestMessage: HttpRequestMessage; + begin + // [SCENARIO] The request message is initialized with an empty content + + // [GIVEN] An initialized Http Request Message + ALHttpRequestMessage.SetHttpMethod(Enum::"Http Method"::PATCH); + + // [WHEN] The request message is read + HttpRequestMessage := ALHttpRequestMessage.GetHttpRequestMessage(); + + // [THEN] The request message is initialized correctly + Assert.AreEqual(HttpRequestMessage.Method(), 'PATCH', 'The request message method is not correct.'); + end; + + [Test] + procedure TestRequestMessageWithoutContent() + var + ALHttpRequestMessage: Codeunit "Http Request Message"; + HttpRequestMessage: HttpRequestMessage; + begin + // [SCENARIO] The request message is initialized without content + + // [GIVEN] An initialized Http Request Message + ALHttpRequestMessage.SetHttpMethod('GET'); + ALHttpRequestMessage.SetRequestUri('https://www.microsoft.com/'); + + // [WHEN] The request message is read + HttpRequestMessage := ALHttpRequestMessage.GetHttpRequestMessage(); + + // [THEN] The request message is initialized correctly + Assert.AreEqual(HttpRequestMessage.Method(), 'GET', 'The request message method is not correct.'); + Assert.AreEqual(HttpRequestMessage.GetRequestUri(), 'https://www.microsoft.com/', 'The request message request URI is not correct.'); + end; + + [Test] + procedure TestRequestMessageWithTextContent() + var + ALHttpRequestMessage: Codeunit "Http Request Message"; + HttpContent: Codeunit "Http Content"; + HttpRequestMessage: HttpRequestMessage; + ContentHeaders: HttpHeaders; + ContentHeaderValues: List of [Text]; + ContentText: Text; + begin + // [GIVEN] An initialized Http Request Message + ALHttpRequestMessage.SetHttpMethod('POST'); + ALHttpRequestMessage.SetRequestUri('https://www.microsoft.com/'); + + // [GIVEN] The request message content is a text + ALHttpRequestMessage.SetContent(HttpContent.Create('Hello World!')); + + // [WHEN] The request message is read + HttpRequestMessage := ALHttpRequestMessage.GetHttpRequestMessage(); + + // [THEN] The request message is initialized correctly + Assert.AreEqual(HttpRequestMessage.Method(), 'POST', 'The request message method is not correct.'); + Assert.AreEqual(HttpRequestMessage.GetRequestUri(), 'https://www.microsoft.com/', 'The request message request URI is not correct.'); + + HttpRequestMessage.Content().ReadAs(ContentText); + Assert.AreEqual(ContentText, 'Hello World!', 'The request message content is not correct.'); + + HttpRequestMessage.Content.GetHeaders(ContentHeaders); + Assert.AreEqual(ContentHeaders.Contains('Content-Type'), true, 'The content type header is missing.'); + + ContentHeaders.GetValues('Content-Type', ContentHeaderValues); + Assert.AreEqual(ContentHeaderValues.Get(1), 'text/plain', 'The request message content type is not correct.'); + end; + + [Test] + procedure TestRequestMessageWithJsonContent() + var + ALHttpRequestMessage: Codeunit "Http Request Message"; + HttpContent: Codeunit "Http Content"; + HttpRequestMessage: HttpRequestMessage; + ContentHeaders: HttpHeaders; + ContentHeaderValues: List of [Text]; + ContentJson: JsonObject; + ContentText: Text; + begin + // [GIVEN] An initialized Http Request Message + ALHttpRequestMessage.SetHttpMethod('POST'); + ALHttpRequestMessage.SetRequestUri('https://www.microsoft.com/'); + + // [GIVEN] The request message content is a JSON object + ContentJson.Add('value', 'Hello World!'); + ALHttpRequestMessage.SetContent(HttpContent.Create(ContentJson)); + + // [WHEN] The request message is read + HttpRequestMessage := ALHttpRequestMessage.GetHttpRequestMessage(); + + // [THEN] The request message is initialized correctly + Assert.AreEqual(HttpRequestMessage.Method(), 'POST', 'The request message method is not correct.'); + Assert.AreEqual(HttpRequestMessage.GetRequestUri(), 'https://www.microsoft.com/', 'The request message request URI is not correct.'); + + HttpRequestMessage.Content().ReadAs(ContentText); + Assert.AreEqual(ContentJson.ReadFrom(ContentText), true, 'The request message content is not a valid JSON object.'); + Assert.AreEqual(ContentJson.Contains('value'), true, 'The request message content does not contain the expected property "value".'); + Assert.AreEqual(GetJsonToken(ContentJson, 'value').AsValue().AsText(), 'Hello World!', 'The request message content property "value" is not correct.'); + + HttpRequestMessage.Content.GetHeaders(ContentHeaders); + Assert.AreEqual(ContentHeaders.Contains('Content-Type'), true, 'The content type header is missing.'); + + ContentHeaders.GetValues('Content-Type', ContentHeaderValues); + Assert.AreEqual(ContentHeaderValues.Get(1), 'application/json', 'The request message content type is not correct.'); + end; + + [Test] + procedure TestAddRequestHeader() + var + ALHttpRequestMessage: Codeunit "Http Request Message"; + HttpRequestMessage: HttpRequestMessage; + ContentHeaders: HttpHeaders; + ContentHeaderValues: List of [Text]; + begin + // [GIVEN] An initialized Http Request Message + ALHttpRequestMessage.SetHttpMethod('GET'); + ALHttpRequestMessage.SetRequestUri('https://www.microsoft.com/'); + + // [GIVEN] The request message has a custom header + ALHttpRequestMessage.SetHeader('X-Custom-Header', 'My Request Header'); + + // [WHEN] The request message is read + HttpRequestMessage := ALHttpRequestMessage.GetHttpRequestMessage(); + + // [THEN] The request message is initialized correctly + HttpRequestMessage.GetHeaders(ContentHeaders); + Assert.AreEqual(ContentHeaders.Contains('X-Custom-Header'), true, 'The custom header is missing.'); + + ContentHeaders.GetValues('X-Custom-Header', ContentHeaderValues); + Assert.AreEqual(ContentHeaderValues.Get(1), 'My Request Header', 'The custom header value is not correct.'); + end; + + local procedure GetJsonToken(JsonObject: JsonObject; Name: Text) JsonToken: JsonToken + begin + JsonObject.Get(Name, JsonToken); + end; +} \ No newline at end of file diff --git a/Modules/System Tests/Rest Client/src/RestClientTests.Codeunit.al b/Modules/System Tests/Rest Client/src/RestClientTests.Codeunit.al new file mode 100644 index 0000000000..a29dde8c92 --- /dev/null +++ b/Modules/System Tests/Rest Client/src/RestClientTests.Codeunit.al @@ -0,0 +1,434 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Test.RestClient; + +using System.RestClient; +using System.TestLibraries.Utilities; + +codeunit 134971 "Rest Client Tests" +{ + Subtype = Test; + + var + Assert: Codeunit "Library Assert"; + HttpClientHandler: Codeunit "Test Http Client Handler"; + + [Test] + procedure TestGet() + var + RestClient: Codeunit "Rest Client"; + HttpResponseMessage: Codeunit "Http Response Message"; + JsonObject: JsonObject; + begin + // [SCENARIO] Test GET request + + // [GIVEN] An uninitialized Rest Client + RestClient.Initialize(HttpClientHandler); + + // [WHEN] The Get method is called + HttpResponseMessage := RestClient.Get('https://httpbin.org/get'); + + // [THEN] The response contains the expected data + Assert.AreEqual(HttpResponseMessage.GetHttpStatusCode(), 200, 'The response status code should be 200'); + Assert.IsTrue(HttpResponseMessage.GetIsSuccessStatusCode(), 'GetIsSuccessStatusCode should be true'); + JsonObject := HttpResponseMessage.GetContent().AsJson().AsObject(); + Assert.AreEqual(GetJsonToken(JsonObject, 'url').AsValue().AsText(), 'https://httpbin.org/get', 'The response should contain the expected url'); + end; + + [Test] + procedure TestPost() + var + RestClient: Codeunit "Rest Client"; + HttpGetContent: Codeunit "Http Content"; + HttpResponseMessage: Codeunit "Http Response Message"; + JsonObject: JsonObject; + begin + // [SCENARIO] Test POST request + + // [GIVEN] An uninitialized Rest Client + RestClient.Initialize(HttpClientHandler); + + // [WHEN] The Post method is called + HttpResponseMessage := RestClient.Post('https://httpbin.org/post', HttpGetContent.Create('Hello World')); + + // [THEN] The response contains the expected data + Assert.AreEqual(HttpResponseMessage.GetHttpStatusCode(), 200, 'The response status code should be 200'); + Assert.IsTrue(HttpResponseMessage.GetIsSuccessStatusCode(), 'GetIsSuccessStatusCode should be true'); + JsonObject := HttpResponseMessage.GetContent().AsJson().AsObject(); + Assert.AreEqual(GetJsonToken(JsonObject, 'url').AsValue().AsText(), 'https://httpbin.org/post', 'The response should contain the expected url'); + Assert.AreEqual(GetJsonToken(JsonObject, 'data').AsValue().AsText(), 'Hello World', 'The response should contain the expected data'); + end; + + [Test] + procedure TestPatch() + var + RestClient: Codeunit "Rest Client"; + HttpGetContent: Codeunit "Http Content"; + HttpResponseMessage: Codeunit "Http Response Message"; + JsonObject: JsonObject; + begin + // [SCENARIO] Test PATCH request + + // [GIVEN] An uninitialized Rest Client + RestClient.Initialize(HttpClientHandler); + + // [WHEN] The Patch method is called + HttpResponseMessage := RestClient.Patch('https://httpbin.org/patch', HttpGetContent.Create('Hello World')); + + // [THEN] The response contains the expected data + Assert.AreEqual(HttpResponseMessage.GetHttpStatusCode(), 200, 'The response status code should be 200'); + Assert.IsTrue(HttpResponseMessage.GetIsSuccessStatusCode(), 'GetIsSuccessStatusCode should be true'); + JsonObject := HttpResponseMessage.GetContent().AsJson().AsObject(); + Assert.AreEqual(GetJsonToken(JsonObject, 'url').AsValue().AsText(), 'https://httpbin.org/patch', 'The response should contain the expected url'); + Assert.AreEqual(GetJsonToken(JsonObject, 'data').AsValue().AsText(), 'Hello World', 'The response should contain the expected data'); + end; + + [Test] + procedure TestPut() + var + RestClient: Codeunit "Rest Client"; + HttpGetContent: Codeunit "Http Content"; + HttpResponseMessage: Codeunit "Http Response Message"; + JsonObject: JsonObject; + begin + // [SCENARIO] Test PUT request + + // [GIVEN] An uninitialized Rest Client + RestClient.Initialize(HttpClientHandler); + + // [WHEN] The Put method is called + HttpResponseMessage := RestClient.Put('https://httpbin.org/put', HttpGetContent.Create('Hello World')); + + // [THEN] The response contains the expected data + Assert.AreEqual(HttpResponseMessage.GetHttpStatusCode(), 200, 'The response status code should be 200'); + Assert.IsTrue(HttpResponseMessage.GetIsSuccessStatusCode(), 'GetIsSuccessStatusCode should be true'); + JsonObject := HttpResponseMessage.GetContent().AsJson().AsObject(); + Assert.AreEqual(GetJsonToken(JsonObject, 'url').AsValue().AsText(), 'https://httpbin.org/put', 'The response should contain the expected url'); + Assert.AreEqual(GetJsonToken(JsonObject, 'data').AsValue().AsText(), 'Hello World', 'The response should contain the expected data'); + end; + + [Test] + procedure TestDelete() + var + RestClient: Codeunit "Rest Client"; + HttpResponseMessage: Codeunit "Http Response Message"; + JsonObject: JsonObject; + begin + // [SCENARIO] Test DELETE request + + // [GIVEN] An uninitialized Rest Client + RestClient.Initialize(HttpClientHandler); + + // [WHEN] The Delete method is called + HttpResponseMessage := RestClient.Delete('https://httpbin.org/delete'); + + // [THEN] The response contains the expected data + Assert.AreEqual(HttpResponseMessage.GetHttpStatusCode(), 200, 'The response status code should be 200'); + Assert.IsTrue(HttpResponseMessage.GetIsSuccessStatusCode(), 'GetIsSuccessStatusCode should be true'); + JsonObject := HttpResponseMessage.GetContent().AsJson().AsObject(); + Assert.AreEqual(GetJsonToken(JsonObject, 'url').AsValue().AsText(), 'https://httpbin.org/delete', 'The response should contain the expected url'); + end; + + [Test] + procedure TestGetWithDefaultHeaders() + var + RestClient: Codeunit "Rest Client"; + HttpResponseMessage: Codeunit "Http Response Message"; + JsonObject: JsonObject; + begin + // [SCENARIO] Test GET request with headers + + // [GIVEN] An initialized Rest Client with default request headers + RestClient.Initialize(HttpClientHandler); + RestClient.SetDefaultRequestHeader('X-Test-Header', 'Test'); + + // [WHEN] The Get method is called with headers + HttpResponseMessage := RestClient.Get('https://httpbin.org/get'); + + // [THEN] The response contains the expected data + Assert.AreEqual(HttpResponseMessage.GetHttpStatusCode(), 200, 'The response status code should be 200'); + Assert.IsTrue(HttpResponseMessage.GetIsSuccessStatusCode(), 'GetIsSuccessStatusCode should be true'); + JsonObject := HttpResponseMessage.GetContent().AsJson().AsObject(); + Assert.AreEqual(GetJsonToken(JsonObject, 'url').AsValue().AsText(), 'https://httpbin.org/get', 'The response should contain the expected url'); + Assert.AreEqual(SelectJsonToken('$.headers.X-Test-Header', JsonObject).AsValue().AsText(), 'Test', 'The response should contain the expected header'); + end; + + [Test] + procedure TestBaseAddress() + var + RestClient: Codeunit "Rest Client"; + HttpResponseMessage: Codeunit "Http Response Message"; + JsonObject: JsonObject; + begin + // [SCENARIO] Test GET request with base address + + // [GIVEN] An initialized Rest Client with base address + RestClient.Initialize(HttpClientHandler); + RestClient.SetBaseAddress('https://httpbin.org'); + + // [WHEN] The Get method is called with relative url + HttpResponseMessage := RestClient.Get('/get'); + + // [THEN] The response contains the expected data + Assert.AreEqual(HttpResponseMessage.GetHttpStatusCode(), 200, 'The response status code should be 200'); + Assert.IsTrue(HttpResponseMessage.GetIsSuccessStatusCode(), 'GetIsSuccessStatusCode should be true'); + JsonObject := HttpResponseMessage.GetContent().AsJson().AsObject(); + Assert.AreEqual(GetJsonToken(JsonObject, 'url').AsValue().AsText(), 'https://httpbin.org/get', 'The response should contain the expected url'); + end; + + [Test] + procedure TestDefaultUserAgentHeader() + var + RestClient: Codeunit "Rest Client"; + HttpResponseMessage: Codeunit "Http Response Message"; + JsonObject: JsonObject; + begin + // [SCENARIO] Test GET request with default User-Agent header + + // [GIVEN] An uninitialized Rest Client + RestClient.Initialize(HttpClientHandler); + + // [WHEN] The Get method is called using the default User-Agent header + HttpResponseMessage := RestClient.Get('https://httpbin.org/get'); + + // [THEN] The response contains the expected data + Assert.AreEqual(HttpResponseMessage.GetHttpStatusCode(), 200, 'The response status code should be 200'); + Assert.IsTrue(HttpResponseMessage.GetIsSuccessStatusCode(), 'GetIsSuccessStatusCode should be true'); + JsonObject := HttpResponseMessage.GetContent().AsJson().AsObject(); + Assert.IsTrue(SelectJsonToken('$.headers.User-Agent', JsonObject).AsValue().AsText().StartsWith('Dynamics 365 Business Central '), 'The response should contain a User-Agent header'); + end; + + [Test] + procedure TestCustomUserAgentHeader() + var + RestClient: Codeunit "Rest Client"; + HttpResponseMessage: Codeunit "Http Response Message"; + JsonObject: JsonObject; + begin + // [SCENARIO] Test GET request with custom User-Agent header + + // [GIVEN] An initialized Rest Client with a customer User-Agent header + RestClient.Initialize(HttpClientHandler); + RestClient.SetUserAgentHeader('BC Rest Client Test'); + + // [WHEN] The Get method is called using a custom User-Agent header + HttpResponseMessage := RestClient.Get('https://httpbin.org/get'); + + // [THEN] The response contains the expected data + Assert.AreEqual(HttpResponseMessage.GetHttpStatusCode(), 200, 'The response status code should be 200'); + Assert.IsTrue(HttpResponseMessage.GetIsSuccessStatusCode(), 'GetIsSuccessStatusCode should be true'); + JsonObject := HttpResponseMessage.GetContent().AsJson().AsObject(); + Assert.AreEqual(SelectJsonToken('$.headers.User-Agent', JsonObject).AsValue().AsText(), 'BC Rest Client Test', 'The response should contain the expected User-Agent header'); + end; + + [Test] + procedure TestGetAsJson() + var + RestClient: Codeunit "Rest Client"; + JsonObject: JsonObject; + begin + // [SCENARIO] Test GET request with JSON response + + // [GIVEN] An uninitialized Rest Client + RestClient.Initialize(HttpClientHandler); + + // [WHEN] The GetAsJson method is called + JsonObject := RestClient.GetAsJson('https://httpbin.org/get').AsObject(); + + // [THEN] The response contains the expected data + Assert.AreEqual(GetJsonToken(JsonObject, 'url').AsValue().AsText(), 'https://httpbin.org/get', 'The response should contain the expected url'); + end; + + [Test] + procedure TestPostAsJson() + var + RestClient: Codeunit "Rest Client"; + HttpGetContent: Codeunit "Http Content"; + JsonObject1: JsonObject; + JsonObject2: JsonObject; + begin + // [SCENARIO] Test POST request with JSON request and response + + // [GIVEN] An uninitialized Rest Client + RestClient.Initialize(HttpClientHandler); + + // [GIVEN] A Json object + JsonObject1.Add('name', 'John'); + JsonObject1.Add('age', 30); + + // [WHEN] The PostAsJson method is called with a Json object + HttpGetContent := HttpGetContent.Create(JsonObject1); + JsonObject2 := RestClient.PostAsJson('https://httpbin.org/post', JsonObject1).AsObject(); + + // [THEN] The response contains the expected data + Assert.AreEqual(GetJsonToken(JsonObject2, 'url').AsValue().AsText(), 'https://httpbin.org/post', 'The response should contain the expected url'); + JsonObject2.ReadFrom(GetJsonToken(JsonObject2, 'data').AsValue().AsText()); + Assert.AreEqual(GetJsonToken(JsonObject2, 'name').AsValue().AsText(), 'John', 'The response should contain the expected data'); + Assert.AreEqual(GetJsonToken(JsonObject2, 'age').AsValue().AsInteger(), 30, 'The response should contain the expected data'); + end; + + [Test] + procedure TestPatchAsJson() + var + RestClient: Codeunit "Rest Client"; + HttpGetContent: Codeunit "Http Content"; + JsonObject1: JsonObject; + JsonObject2: JsonObject; + begin + // [SCENARIO] Test PATCH request with JSON request and response + + // [GIVEN] An uninitialized Rest Client + RestClient.Initialize(HttpClientHandler); + + // [GIVEN] A Json object + JsonObject1.Add('name', 'John'); + JsonObject1.Add('age', 30); + + // [WHEN] The PatchAsJson method is called with a Json object + HttpGetContent := HttpGetContent.Create(JsonObject1); + JsonObject2 := RestClient.PatchAsJson('https://httpbin.org/patch', JsonObject1).AsObject(); + + // [THEN] The response contains the expected data + Assert.AreEqual(GetJsonToken(JsonObject2, 'url').AsValue().AsText(), 'https://httpbin.org/patch', 'The response should contain the expected url'); + JsonObject2.ReadFrom(GetJsonToken(JsonObject2, 'data').AsValue().AsText()); + Assert.AreEqual(GetJsonToken(JsonObject2, 'name').AsValue().AsText(), 'John', 'The response should contain the expected data'); + Assert.AreEqual(GetJsonToken(JsonObject2, 'age').AsValue().AsInteger(), 30, 'The response should contain the expected data'); + end; + + [Test] + procedure TestPutAsJson() + var + RestClient: Codeunit "Rest Client"; + HttpGetContent: Codeunit "Http Content"; + JsonObject1: JsonObject; + JsonObject2: JsonObject; + begin + // [SCENARIO] Test PUT request with JSON request and response + + // [GIVEN] An uninitialized Rest Client + RestClient.Initialize(HttpClientHandler); + + // [GIVEN] A Json object + JsonObject1.Add('name', 'John'); + JsonObject1.Add('age', 30); + + // [WHEN] The PutAsJson method is called with a Json object + HttpGetContent := HttpGetContent.Create(JsonObject1); + JsonObject2 := RestClient.PutAsJson('https://httpbin.org/put', JsonObject1).AsObject(); + + // [THEN] The response contains the expected data + Assert.AreEqual(GetJsonToken(JsonObject2, 'url').AsValue().AsText(), 'https://httpbin.org/put', 'The response should contain the expected url'); + JsonObject2.ReadFrom(GetJsonToken(JsonObject2, 'data').AsValue().AsText()); + Assert.AreEqual(GetJsonToken(JsonObject2, 'name').AsValue().AsText(), 'John', 'The response should contain the expected data'); + Assert.AreEqual(GetJsonToken(JsonObject2, 'age').AsValue().AsInteger(), 30, 'The response should contain the expected data'); + end; + + [Test] + procedure TestSendWithoutGetContent() + var + RestClient: Codeunit "Rest Client"; + HttpResponseMessage: Codeunit "Http Response Message"; + JsonObject: JsonObject; + begin + // [SCENARIO] Test Send method without Getcontent + + // [GIVEN] An uninitialized Rest Client + RestClient.Initialize(HttpClientHandler); + + // [WHEN] The Send method is called without Getcontent + HttpResponseMessage := RestClient.Send(Enum::"Http Method"::GET, 'https://httpbin.org/get'); + + // [THEN] The response contains the expected data + Assert.AreEqual(HttpResponseMessage.GetHttpStatusCode(), 200, 'The response status code should be 200'); + Assert.IsTrue(HttpResponseMessage.GetIsSuccessStatusCode(), 'GetIsSuccessStatusCode should be true'); + JsonObject := HttpResponseMessage.GetContent().AsJson().AsObject(); + Assert.AreEqual(GetJsonToken(JsonObject, 'url').AsValue().AsText(), 'https://httpbin.org/get', 'The response should contain the expected url'); + end; + + [Test] + procedure TestSendWithGetContent() + var + RestClient: Codeunit "Rest Client"; + HttpContent: Codeunit "Http Content"; + HttpResponseMessage: Codeunit "Http Response Message"; + JsonObject: JsonObject; + begin + // [SCENARIO] Test Send method with Getcontent + + // [GIVEN] An uninitialized Rest Client + RestClient.Initialize(HttpClientHandler); + + // [WHEN] The Send method is called with Getcontent + HttpResponseMessage := RestClient.Send(Enum::"Http Method"::POST, 'https://httpbin.org/post', HttpContent.Create('Hello World')); + + // [THEN] The response contains the expected data + Assert.AreEqual(HttpResponseMessage.GetHttpStatusCode(), 200, 'The response status code should be 200'); + Assert.IsTrue(HttpResponseMessage.GetIsSuccessStatusCode(), 'GetIsSuccessStatusCode should be true'); + JsonObject := HttpResponseMessage.GetContent().AsJson().AsObject(); + Assert.AreEqual(GetJsonToken(JsonObject, 'url').AsValue().AsText(), 'https://httpbin.org/post', 'The response should contain the expected url'); + Assert.AreEqual(GetJsonToken(JsonObject, 'data').AsValue().AsText(), 'Hello World', 'The response should contain the expected data'); + end; + + [Test] + procedure TestSendRequestMessage() + var + RestClient: Codeunit "Rest Client"; + ALHttpRequestMessage: Codeunit "Http Request Message"; + HttpResponseMessage: Codeunit "Http Response Message"; + JsonObject: JsonObject; + begin + // [SCENARIO] Test Send method with request message + + // [GIVEN] An uninitialized Rest Client + RestClient.Initialize(HttpClientHandler); + + // [WHEN] The Send method is called with a request message + ALHttpRequestMessage.SetHttpMethod(Enum::"Http Method"::GET); + ALHttpRequestMessage.SetRequestUri('https://httpbin.org/get'); + HttpResponseMessage := RestClient.Send(ALHttpRequestMessage); + + // [THEN] The response contains the expected data + Assert.AreEqual(HttpResponseMessage.GetHttpStatusCode(), 200, 'The response status code should be 200'); + Assert.IsTrue(HttpResponseMessage.GetIsSuccessStatusCode(), 'GetIsSuccessStatusCode should be true'); + JsonObject := HttpResponseMessage.GetContent().AsJson().AsObject(); + Assert.AreEqual(GetJsonToken(JsonObject, 'url').AsValue().AsText(), 'https://httpbin.org/get', 'The response should contain the expected url'); + end; + + [Test] + procedure TestBasicAuthentication() + var + RestClient: Codeunit "Rest Client"; + HttpAuthenticationBasic: Codeunit "Http Authentication Basic"; + HttpResponseMessage: Codeunit "Http Response Message"; + JsonObject: JsonObject; + PasswordText: Text; + begin + // [SCENARIO] Test Http Get with Basic Authentication + + // [GIVEN] An initialized Rest Client with Basic Authentication + PasswordText := 'Password123'; + HttpAuthenticationBasic.Initialize('user01', PasswordText); + RestClient.Initialize(HttpClientHandler, HttpAuthenticationBasic); + + // [WHEN] The Get method is called + HttpResponseMessage := RestClient.Get('https://httpbin.org/basic-auth/user01/Password123'); + + // [THEN] The response contains the expected data + Assert.AreEqual(HttpResponseMessage.GetHttpStatusCode(), 200, 'The response status code should be 200'); + JsonObject := HttpResponseMessage.GetContent().AsJson().AsObject(); + Assert.AreEqual(GetJsonToken(JsonObject, 'authenticated').AsValue().AsBoolean(), true, 'The response should contain the expected data'); + end; + + local procedure GetJsonToken(JsonObject: JsonObject; Name: Text) JsonToken: JsonToken + begin + JsonObject.Get(Name, JsonToken); + end; + + local procedure SelectJsonToken(Path: Text; JsonObject: JsonObject) JsonToken: JsonToken + begin + JsonObject.SelectToken(Path, JsonToken); + end; +} \ No newline at end of file diff --git a/Modules/System Tests/Rest Client/src/TestHttpClientHandler.Codeunit.al b/Modules/System Tests/Rest Client/src/TestHttpClientHandler.Codeunit.al new file mode 100644 index 0000000000..a7df69fcf7 --- /dev/null +++ b/Modules/System Tests/Rest Client/src/TestHttpClientHandler.Codeunit.al @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.Test.RestClient; + +using System.RestClient; + +codeunit 134974 "Test Http Client Handler" implements "Http Client Handler" +{ + SingleInstance = true; + + procedure Send(HttpClient: HttpClient; HttpRequestMessage: codeunit "Http Request Message"; var HttpResponseMessage: codeunit "Http Response Message") Success: Boolean; + var + ResponseMessage: HttpResponseMessage; + begin + Success := HttpClient.Send(HttpRequestMessage.GetHttpRequestMessage(), ResponseMessage); + HttpResponseMessage.SetResponseMessage(ResponseMessage); + end; +} \ No newline at end of file diff --git a/Modules/System/Rest Client/app.json b/Modules/System/Rest Client/app.json new file mode 100644 index 0000000000..1e0a202bd9 --- /dev/null +++ b/Modules/System/Rest Client/app.json @@ -0,0 +1,42 @@ +{ + "id": "812b339d-a9db-4a6e-84e4-fe35cbef0c44", + "name": "Rest Client", + "publisher": "Microsoft", + "version": "23.0.0.0", + "brief": "Provides functionality to call REST services from AL", + "description": "Provides functionality to call REST services from AL", + "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009", + "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", + "help": "https://go.microsoft.com/fwlink/?linkid=2103698", + "url": "https://go.microsoft.com/fwlink/?linkid=724011", + "dependencies": [ + { + "id": "e31ad830-3d46-472e-afeb-1d3d35247943", + "name": "BLOB Storage", + "publisher": "Microsoft", + "version": "23.0.0.0" + }, + { + "id": "7e3b999e-1182-45d2-8b82-d5127ddba9b2", + "name": "DotNet Aliases", + "publisher": "Microsoft", + "version": "23.0.0.0" + } + ], + "screenshots": [], + "platform": "23.0.0.0", + "idRanges": [ + { + "from": 2350, + "to": 2360 + } + ], + "target": "OnPrem", + "runtime": "12.0", + "resourceExposurePolicy": { + "allowDebugging": true, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "features": ["NoImplicitWith","TranslationFile"] +} diff --git a/Modules/System/Rest Client/src/Authentication/HttpAuthentication.Interface.al b/Modules/System/Rest Client/src/Authentication/HttpAuthentication.Interface.al new file mode 100644 index 0000000000..35e0f3ecdc --- /dev/null +++ b/Modules/System/Rest Client/src/Authentication/HttpAuthentication.Interface.al @@ -0,0 +1,17 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +/// An interface to support different authorization mechanism in a generic way. +interface "Http Authentication" +{ + /// Indicates whether authentication is required for the request. + /// True if authentication is required, false otherwise. + procedure IsAuthenticationRequired(): Boolean + + /// Gets the authorization headers for the request. + /// A dictionary of authorization headers. + procedure GetAuthorizationHeaders(): Dictionary of [Text, SecretText] +} \ No newline at end of file diff --git a/Modules/System/Rest Client/src/Authentication/HttpAuthenticationAnonymous.Codeunit.al b/Modules/System/Rest Client/src/Authentication/HttpAuthenticationAnonymous.Codeunit.al new file mode 100644 index 0000000000..abf1c542c3 --- /dev/null +++ b/Modules/System/Rest Client/src/Authentication/HttpAuthenticationAnonymous.Codeunit.al @@ -0,0 +1,25 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +/// Implementation of the "Http Authentication" interface for a anonymous request. +codeunit 2358 "Http Authentication Anonymous" implements "Http Authentication" +{ + InherentEntitlements = X; + InherentPermissions = X; + + /// Indicates if authentication is required. + /// False, because no authentication is required. + procedure IsAuthenticationRequired(): Boolean + begin + exit(false); + end; + + /// Gets the authorization headers. + /// Empty dictionary, because no authentication is required. + procedure GetAuthorizationHeaders() Header: Dictionary of [Text, SecretText] + begin + end; +} \ No newline at end of file diff --git a/Modules/System/Rest Client/src/Authentication/HttpAuthenticationBasic.Codeunit.al b/Modules/System/Rest Client/src/Authentication/HttpAuthenticationBasic.Codeunit.al new file mode 100644 index 0000000000..552d139504 --- /dev/null +++ b/Modules/System/Rest Client/src/Authentication/HttpAuthenticationBasic.Codeunit.al @@ -0,0 +1,65 @@ +/// Implementation of the "Http Authentication" interface for a request that requires basic authentication +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +using System; + +codeunit 2359 "Http Authentication Basic" implements "Http Authentication" +{ + InherentEntitlements = X; + InherentPermissions = X; + + var + GlobalUsername: Text; + GlobalPassword: SecretText; + UsernameDomainTxt: Label '%1\%2', Locked = true; + + /// Initializes the authentication object with the given username and password + /// The username to use for authentication + /// The password to use for authentication + procedure Initialize(Username: Text; Password: SecretText) + begin + Initialize(Username, '', Password); + end; + + /// Initializes the authentication object with the given username, domain and password + /// The username to use for authentication + /// The domain to use for authentication + /// The password to use for authentication + procedure Initialize(Username: Text; Domain: Text; Password: SecretText) + begin + if Domain = '' then + GlobalUsername := Username + else + GlobalUsername := StrSubstNo(UsernameDomainTxt, Username, Domain); + + GlobalPassword := Password; + end; + + /// Checks if authentication is required for the request + /// Returns true because authentication is required + procedure IsAuthenticationRequired(): Boolean; + begin + exit(true); + end; + + /// Gets the authorization headers for the request + /// Returns a dictionary of headers that need to be added to the request + procedure GetAuthorizationHeaders() Header: Dictionary of [Text, SecretText]; + begin + Header.Add('Authorization', SecretStrSubstNo('Basic %1', ToBase64(SecretStrSubstNo('%1:%2', GlobalUsername, GlobalPassword)))); + end; + + local procedure ToBase64(String: SecretText) Base64String: SecretText + var + Convert: DotNet Convert; + Encoding: DotNet Encoding; + begin +#pragma warning disable AL0796 + Base64String := Convert.ToBase64String(Encoding.UTF8().GetBytes(String.Unwrap())); +#pragma warning restore AL0796 + end; +} \ No newline at end of file diff --git a/Modules/System/Rest Client/src/HttpClientHandler/HttpClientHandler.Codeunit.al b/Modules/System/Rest Client/src/HttpClientHandler/HttpClientHandler.Codeunit.al new file mode 100644 index 0000000000..d17d921197 --- /dev/null +++ b/Modules/System/Rest Client/src/HttpClientHandler/HttpClientHandler.Codeunit.al @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +codeunit 2360 "Http Client Handler" implements "Http Client Handler" +{ + InherentEntitlements = X; + InherentPermissions = X; + + procedure Send(HttpClient: HttpClient; HttpRequestMessage: codeunit "Http Request Message"; var HttpResponseMessage: codeunit "Http Response Message") Success: Boolean; + var + ResponseMessage: HttpResponseMessage; + begin + Success := HttpClient.Send(HttpRequestMessage.GetHttpRequestMessage(), ResponseMessage); + HttpResponseMessage.SetResponseMessage(ResponseMessage); + end; +} \ No newline at end of file diff --git a/Modules/System/Rest Client/src/HttpClientHandler/HttpClientHandler.Interface.al b/Modules/System/Rest Client/src/HttpClientHandler/HttpClientHandler.Interface.al new file mode 100644 index 0000000000..849a4605ea --- /dev/null +++ b/Modules/System/Rest Client/src/HttpClientHandler/HttpClientHandler.Interface.al @@ -0,0 +1,10 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +interface "Http Client Handler" +{ + procedure Send(HttpClient: HttpClient; HttpRequestMessage: Codeunit "Http Request Message"; var HttpResponseMessage: Codeunit "Http Response Message") Success: Boolean; +} \ No newline at end of file diff --git a/Modules/System/Rest Client/src/HttpContent.Codeunit.al b/Modules/System/Rest Client/src/HttpContent.Codeunit.al new file mode 100644 index 0000000000..507c3ed01c --- /dev/null +++ b/Modules/System/Rest Client/src/HttpContent.Codeunit.al @@ -0,0 +1,287 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +using System.Utilities; + +/// Holder object for the Http Content data. +codeunit 2354 "Http Content" +{ + InherentEntitlements = X; + InherentPermissions = X; + + var + HttpContentImpl: Codeunit "Http Content Impl."; + + #region Constructors + /// Initializes a new instance of the Http Content class with the specified Text content. + /// The content to send to the server. + /// The Http Content object. + /// The Content-Type header will be set to 'text/plain'. + procedure Create(Content: Text) HttpContent: Codeunit "Http Content" + begin + HttpContent := Create(Content, ''); + end; + + /// Initializes a new instance of the Http Content class with the specified SecretText content. + /// The content to send to the server. + /// The Http Content object. + /// The Content-Type header will be set to 'text/plain'. + procedure Create(Content: SecretText) HttpContent: Codeunit "Http Content" + begin + HttpContent := Create(Content, ''); + end; + + /// Initializes a new instance of the Http Content class with the specified Text content and content type. + /// The content to send to the server. + /// The content type of the content to send to the server. + /// The Http Content object. + /// If the ContentType parameter is not specified, it will be set to 'text/plain'. + procedure Create(Content: Text; ContentType: Text) HttpContent: Codeunit "Http Content" + begin + SetContent(Content, ContentType); + HttpContent.SetHttpContentImpl(HttpContentImpl); + end; + + /// Initializes a new instance of the Http Content class with the specified SecretText content and content type. + /// The content to send to the server. + /// The content type of the content to send to the server. + /// The Http Content object. + /// If the ContentType parameter is not specified, it will be set to 'text/plain'. + procedure Create(Content: SecretText; ContentType: Text) HttpContent: Codeunit "Http Content" + begin + SetContent(Content, ContentType); + HttpContent.SetHttpContentImpl(HttpContentImpl); + end; + + /// Initializes a new instance of the Http Content class with the specified JsonObject content. + /// The content to send to the server. + /// The Http Content object. + /// The Content-Type header will be set to 'application/json'. + procedure Create(Content: JsonObject) HttpContent: Codeunit "Http Content" + begin + SetContent(Content); + HttpContent.SetHttpContentImpl(HttpContentImpl); + end; + + /// Initializes a new instance of the Http Content class with the specified JsonArray content. + /// The content to send to the server. + /// The Http Content object. + /// The Content-Type header will be set to 'application/json'. + procedure Create(Content: JsonArray) HttpContent: Codeunit "Http Content" + begin + SetContent(Content); + HttpContent.SetHttpContentImpl(HttpContentImpl); + end; + + /// Initializes a new instance of the Http Content class with the specified JsonToken content. + /// The content to send to the server. + /// The Http Content object. + /// The Content-Type header will be set to 'application/json'. + procedure Create(Content: JsonToken) HttpContent: Codeunit "Http Content" + begin + SetContent(Content); + HttpContent.SetHttpContentImpl(HttpContentImpl); + end; + + /// Initializes a new instance of the Http Content class with the specified XmlDocument content. + /// The content to send to the server. + /// The Http Content object. + /// The Content-Type header will be set to 'text/xml'. + procedure Create(Content: XmlDocument) HttpContent: Codeunit "Http Content" + begin + SetContent(Content); + HttpContent.SetHttpContentImpl(HttpContentImpl); + end; + + /// Initializes a new instance of the Http Content class with the specified "Temp Blob" Codeunit content. + /// The content to send to the server. + /// The Http Content object. + /// The Content-Type header will be set to 'application/octet-stream'. + procedure Create(Content: Codeunit "Temp Blob") HttpContent: Codeunit "Http Content" + begin + HttpContent := Create(Content, ''); + end; + + /// Initializes a new instance of the Http Content class with the specified "Temp Blob" Codeunit content and content type. + /// The content to send to the server. + /// The content type of the content to send to the server. + /// The Http Content object. + /// If the ContentType parameter is not specified, it will be set to 'application/octet-stream'. + procedure Create(Content: Codeunit "Temp Blob"; ContentType: Text) HttpContent: Codeunit "Http Content" + begin + SetContent(Content, ContentType); + HttpContent.SetHttpContentImpl(HttpContentImpl); + end; + + /// Initializes a new instance of the Http Content class with the specified InStream content. + /// The content to send to the server. + /// The Http Content object. + /// The Content-Type header will be set to 'application/octet-stream'. + procedure Create(Content: InStream) HttpContent: Codeunit "Http Content" + begin + HttpContent := Create(Content, ''); + end; + + /// Initializes a new instance of the Http Content class with the specified InStream content and content type. + /// The content to send to the server. + /// The content type of the content to send to the server. + /// The Http Content object. + /// If the ContentType parameter is not specified, it will be set to 'application/octet-stream'. + procedure Create(Content: InStream; ContentType: Text) HttpContent: Codeunit "Http Content" + begin + SetContent(Content, ContentType); + HttpContent.SetHttpContentImpl(HttpContentImpl); + end; + + /// Initializes a new instance of the Http Content object with the specified HttpContent object. + /// The HttpContent object. + /// The HttpContent object. + /// The HttpContent must be properly prepared including the Content-Type header. + procedure Create(Content: HttpContent) HttpContent: Codeunit "Http Content" + begin + SetContent(Content); + HttpContent.SetHttpContentImpl(HttpContentImpl); + end; + #endregion + + #region Public Methods + + /// Sets the Content-Type header of the HttpContent object. + /// The value of the header to add. + /// If the header already exists, it will be overwritten. + procedure SetContentTypeHeader(ContentType: Text) + begin + HttpContentImpl.SetContentTypeHeader(ContentType); + end; + + /// Sets the Content-Encoding header of the HttpContent object. + /// The value of the header to add. + /// If the header already exists, the value will be added to the end of the list. + procedure AddContentEncoding(ContentEncoding: Text) + begin + HttpContentImpl.AddContentEncoding(ContentEncoding); + end; + + /// Sets a new value for an existing header of the HttpContent object, or adds the header if it does not already exist. + /// The name of the header to add. + /// The value of the header to add. + procedure SetHeader(Name: Text; Value: Text) + begin + HttpContentImpl.SetHeader(Name, Value); + end; + + /// Sets a new value for an existing header of the HttpContent object, or adds the header if it does not already exist. + /// The name of the header to add. + /// The value of the header to add. + procedure SetHeader(Name: Text; Value: SecretText) + begin + HttpContentImpl.SetHeader(Name, Value); + end; + + /// Gets the HttpContent object. + /// The HttpContent object. + procedure GetHttpContent() ReturnValue: HttpContent + begin + ReturnValue := HttpContentImpl.GetHttpContent(); + end; + + /// Gets the content of the HTTP response message as a string. + /// The content of the HTTP response message as a string. + procedure AsText() ReturnValue: Text + begin + ReturnValue := HttpContentImpl.AsText(); + end; + + /// Gets the content of the HTTP response message as a SecretText. + /// The content of the HTTP response message as a SecretText. + procedure AsSecretText() ReturnValue: SecretText + begin + ReturnValue := HttpContentImpl.AsSecretText(); + end; + + /// Gets the content of the HTTP response message as a "Temp Blob" Codeunit. + /// The content of the HTTP response message as a "Temp Blob" Codeunit. + procedure AsBlob() TempBlob: Codeunit "Temp Blob" + begin + TempBlob := HttpContentImpl.AsBlob(); + end; + + /// Gets the content of the HTTP response message as an InStream. + /// The content of the HTTP response message as an InStream. + procedure AsInStream() InStr: InStream + begin + HttpContentImpl.AsInStream(InStr); + end; + + /// Gets the content of the HTTP response message as an XmlDocument. + /// The content of the HTTP response message as an XmlDocument. + /// Fails in case the content is not a valid XML document. + procedure AsXmlDocument() XmlDoc: XmlDocument + begin + XmlDoc := HttpContentImpl.AsXmlDocument(); + end; + + /// Gets the content of the HTTP response message as a JsonToken. + /// The content of the HTTP response message as a JsonToken. + /// Fails in case the content is not a valid JSON document. + procedure AsJson() JsonToken: JsonToken + begin + JsonToken := HttpContentImpl.AsJson(); + end; + #endregion + + #region Internal Methods + internal procedure SetContent(Content: Text; ContentType: Text) + begin + HttpContentImpl.SetContent(Content, ContentType); + end; + + internal procedure SetContent(Content: SecretText; ContentType: Text) + begin + HttpContentImpl.SetContent(Content, ContentType); + end; + + internal procedure SetContent(Content: InStream; ContentType: Text) + begin + HttpContentImpl.SetContent(Content, ContentType); + end; + + internal procedure SetContent(TempBlob: Codeunit "Temp Blob"; ContentType: Text) + begin + HttpContentImpl.SetContent(TempBlob, ContentType); + end; + + internal procedure SetContent(Content: XmlDocument) + begin + HttpContentImpl.SetContent(Content); + end; + + internal procedure SetContent(Content: JsonObject) + begin + SetContent(Content.AsToken()); + end; + + internal procedure SetContent(Content: JsonArray) + begin + SetContent(Content.AsToken()); + end; + + internal procedure SetContent(Content: JsonToken) + begin + HttpContentImpl.SetContent(Content); + end; + + internal procedure SetContent(var Value: HttpContent) + begin + HttpContentImpl.SetContent(Value); + end; + + internal procedure SetHttpContentImpl(Value: Codeunit "Http Content Impl.") + begin + HttpContentImpl := Value; + end; + #endregion +} \ No newline at end of file diff --git a/Modules/System/Rest Client/src/HttpContentImpl.Codeunit.al b/Modules/System/Rest Client/src/HttpContentImpl.Codeunit.al new file mode 100644 index 0000000000..a49bceff42 --- /dev/null +++ b/Modules/System/Rest Client/src/HttpContentImpl.Codeunit.al @@ -0,0 +1,174 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +using System.Utilities; + +codeunit 2355 "Http Content Impl." +{ + InherentEntitlements = X; + InherentPermissions = X; + + Access = Internal; + + var + HttpContent: HttpContent; + ContentTypeEmptyErr: Label 'The value of the Content-Type header must be specified.'; + MimeTypeTextPlainTxt: Label 'text/plain', Locked = true; + MimeTypeTextXmlTxt: Label 'text/xml', Locked = true; + MimeTypeApplicationOctetStreamTxt: Label 'application/octet-stream', Locked = true; + MimeTypeApplicationJsonTxt: Label 'application/json', Locked = true; + + procedure SetContentTypeHeader(ContentType: Text) + begin + if ContentType = '' then + Error(ContentTypeEmptyErr); + SetHeader('Content-Type', ContentType); + end; + + procedure AddContentEncoding(ContentEncoding: Text) + var + Headers: HttpHeaders; + begin + if not HttpContent.GetHeaders(Headers) then begin + HttpContent.Clear(); + HttpContent.GetHeaders(Headers); + end; + Headers.Add('Content-Encoding', ContentEncoding); + end; + + procedure SetHeader(Name: Text; Value: Text) + var + Headers: HttpHeaders; + begin + if not HttpContent.GetHeaders(Headers) then begin + HttpContent.Clear(); + HttpContent.GetHeaders(Headers); + end; + + if Headers.Contains(Name) then + Headers.Remove(Name); + + Headers.Add(Name, Value); + end; + + procedure SetHeader(Name: Text; Value: SecretText) + var + Headers: HttpHeaders; + begin + if not HttpContent.GetHeaders(Headers) then begin + HttpContent.Clear(); + HttpContent.GetHeaders(Headers); + end; + + if Headers.Contains(Name) then + Headers.Remove(Name); + + Headers.Add(Name, Value); + end; + + procedure GetHttpContent() ReturnValue: HttpContent + begin + ReturnValue := HttpContent; + end; + + procedure AsText() ReturnValue: Text + begin + HttpContent.ReadAs(ReturnValue); + end; + + procedure AsSecretText() ReturnValue: SecretText + begin + HttpContent.ReadAs(ReturnValue); + end; + + procedure AsBlob() ReturnValue: Codeunit "Temp Blob" + var + InStr: InStream; + OutStr: OutStream; + begin + HttpContent.ReadAs(InStr); + ReturnValue.CreateOutStream(OutStr); + CopyStream(OutStr, InStr); + end; + + procedure AsInStream(var InStr: InStream) + begin + HttpContent.ReadAs(InStr); + end; + + procedure AsXmlDocument() ReturnValue: XmlDocument + var + XmlReadOptions: XmlReadOptions; + begin + XmlReadOptions.PreserveWhitespace(false); + XmlDocument.ReadFrom(AsText(), XmlReadOptions, ReturnValue); + end; + + procedure AsJson() ReturnValue: JsonToken + begin + ReturnValue.ReadFrom(AsText()); + end; + + procedure SetContent(Content: Text; ContentType: Text) + begin + HttpContent.Clear(); + HttpContent.WriteFrom(Content); + if ContentType = '' then + ContentType := MimeTypeTextPlainTxt; + SetContentTypeHeader(ContentType); + end; + + procedure SetContent(Content: SecretText; ContentType: Text) + begin + HttpContent.Clear(); + HttpContent.WriteFrom(Content); + if ContentType = '' then + ContentType := MimeTypeTextPlainTxt; + SetContentTypeHeader(ContentType); + end; + + procedure SetContent(Content: InStream; ContentType: Text) + begin + HttpContent.Clear(); + HttpContent.WriteFrom(Content); + if ContentType = '' then + ContentType := MimeTypeApplicationOctetStreamTxt; + SetContentTypeHeader(ContentType); + end; + + procedure SetContent(TempBlob: Codeunit "Temp Blob"; ContentType: Text) + var + InStream: InStream; + begin + InStream := TempBlob.CreateInStream(TextEncoding::UTF8); + if ContentType = '' then + ContentType := MimeTypeApplicationOctetStreamTxt; + SetContent(InStream, ContentType); + end; + + procedure SetContent(Content: XmlDocument) + var + Xml: Text; + XmlWriteOptions: XmlWriteOptions; + begin + XmlWriteOptions.PreserveWhitespace(false); + Content.WriteTo(XmlWriteOptions, Xml); + SetContent(Xml, MimeTypeTextXmlTxt); + end; + + procedure SetContent(Content: JsonToken) + var + Json: Text; + begin + Content.WriteTo(Json); + SetContent(Json, MimeTypeApplicationJsonTxt); + end; + + procedure SetContent(var Value: HttpContent) + begin + HttpContent := Value; + end; +} \ No newline at end of file diff --git a/Modules/System/Rest Client/src/HttpMethod.Enum.al b/Modules/System/Rest Client/src/HttpMethod.Enum.al new file mode 100644 index 0000000000..9bb1d51482 --- /dev/null +++ b/Modules/System/Rest Client/src/HttpMethod.Enum.al @@ -0,0 +1,69 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +/// +/// This enum contains the REST Http Methods. +/// +enum 2350 "Http Method" +{ + Extensible = true; + + /// + /// Specifies that the Http method is GET. + /// + value(0; GET) + { + Caption = 'GET', Locked = true; + } + + /// + /// Specifies that the Http method is POST. + /// + value(1; POST) + { + Caption = 'POST', Locked = true; + } + + /// + /// Specifies that the Http method is PATCH. + /// + value(2; PATCH) + { + Caption = 'PATCH', Locked = true; + } + + /// + /// Specifies that the Http method is PUT. + /// + value(3; PUT) + { + Caption = 'PUT', Locked = true; + } + + /// + /// Specifies that the Http method is DELETE. + /// + value(4; DELETE) + { + Caption = 'DELETE', Locked = true; + } + + /// + /// Specifies that the Http method is HEAD. + /// + value(5; HEAD) + { + Caption = 'HEAD', Locked = true; + } + + /// + /// Specifies that the Http method is OPTIONS. + /// + value(6; OPTIONS) + { + Caption = 'OPTIONS', Locked = true; + } +} \ No newline at end of file diff --git a/Modules/System/Rest Client/src/HttpRequestMessage.Codeunit.al b/Modules/System/Rest Client/src/HttpRequestMessage.Codeunit.al new file mode 100644 index 0000000000..fe0fe9319b --- /dev/null +++ b/Modules/System/Rest Client/src/HttpRequestMessage.Codeunit.al @@ -0,0 +1,91 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +/// Holder object for the HTTP request data. +codeunit 2352 "Http Request Message" +{ + InherentEntitlements = X; + InherentPermissions = X; + + var + HttpRequestMessageImpl: Codeunit "Http Request Message Impl."; + + + /// Sets the HTTP method or the HttpRequestMessage object. + /// The HTTP method to use. Valid options are GET, POST, PATCH, PUT, DELETE, HEAD, OPTIONS + /// Default method is GET + procedure SetHttpMethod(Method: Text) + begin + HttpRequestMessageImpl.SetHttpMethod(Method); + end; + + /// Sets the HTTP method for the HttpRequestMessage object. + /// The HTTP method to use. + /// Default method is GET + procedure SetHttpMethod(Method: Enum "Http Method") + begin + HttpRequestMessageImpl.SetHttpMethod(Method); + end; + + /// Gets the HTTP method for the HttpRequestMessage object. + /// The HTTP method for the HttpRequestMessage object. + procedure GetHttpMethod() Method: Text + begin + Method := HttpRequestMessageImpl.GetHttpMethod(); + end; + + /// Sets the Uri used for the HttpRequestMessage object. + /// The Uri to use for the HTTP request. + /// The valued must not be a relative URI. + procedure SetRequestUri(Uri: Text) + begin + HttpRequestMessageImpl.SetRequestUri(Uri); + end; + + /// Gets the Uri used for the HttpRequestMessage object. + /// The Uri used for the HttpRequestMessage object. + procedure GetRequestUri() Uri: Text + begin + Uri := HttpRequestMessageImpl.GetRequestUri(); + end; + + /// Sets a new value for an existing header of the Http Request object, or addds the header if it does not already exist. + /// The name of the header to add. + /// The value of the header to add. + procedure SetHeader(HeaderName: Text; HeaderValue: Text) + begin + HttpRequestMessageImpl.SetHeader(HeaderName, HeaderValue); + end; + + /// Sets a new value for an existing header of the Http Request object, or addds the header if it does not already exist. + /// The name of the header to add. + /// The value of the header to add. + procedure SetHeader(HeaderName: Text; HeaderValue: SecretText) + begin + HttpRequestMessageImpl.SetHeader(HeaderName, HeaderValue); + end; + + /// Sets the HttpRequestMessage that is represented by the HttpRequestMessage object. + /// The HttpRequestMessage to set. + procedure SetHttpRequestMessage(var RequestMessage: HttpRequestMessage) + begin + HttpRequestMessageImpl.SetHttpRequestMessage(RequestMessage); + end; + + /// Gets the HttpRequestMessage that is represented by the HttpRequestMessage object. + /// The HttpRequestMessage that is represented by the HttpRequestMessage object. + procedure GetHttpRequestMessage() ReturnValue: HttpRequestMessage + begin + ReturnValue := HttpRequestMessageImpl.GetRequestMessage(); + end; + + /// Sets the content of the HttpRequestMessage that is represented by the HttpRequestMessage object. + /// The Http Content object to set. + procedure SetContent(HttpContent: Codeunit "Http Content") + begin + HttpRequestMessageImpl.SetContent(HttpContent); + end; +} \ No newline at end of file diff --git a/Modules/System/Rest Client/src/HttpRequestMessageImpl.Codeunit.al b/Modules/System/Rest Client/src/HttpRequestMessageImpl.Codeunit.al new file mode 100644 index 0000000000..e95a1f0907 --- /dev/null +++ b/Modules/System/Rest Client/src/HttpRequestMessageImpl.Codeunit.al @@ -0,0 +1,75 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +codeunit 2353 "Http Request Message Impl." +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + HttpRequestMessage: HttpRequestMessage; + + procedure SetHttpMethod(Method: Text) + begin + HttpRequestMessage.Method := Method; + end; + + procedure SetHttpMethod(Method: Enum "Http Method") + begin + SetHttpMethod(Method.Names.Get(Method.Ordinals.IndexOf(Method.AsInteger()))); + end; + + procedure GetHttpMethod() ReturnValue: Text + begin + ReturnValue := HttpRequestMessage.Method; + end; + + procedure SetRequestUri(Uri: Text) + begin + HttpRequestMessage.SetRequestUri(Uri); + end; + + procedure GetRequestUri() Uri: Text + begin + Uri := HttpRequestMessage.GetRequestUri(); + end; + + procedure SetHeader(HeaderName: Text; HeaderValue: Text) + var + HttpHeaders: HttpHeaders; + begin + HttpRequestMessage.GetHeaders(HttpHeaders); + if HttpHeaders.Contains(HeaderName) then + HttpHeaders.Remove(HeaderName); + HttpHeaders.Add(HeaderName, HeaderValue); + end; + + procedure SetHeader(HeaderName: Text; HeaderValue: SecretText) + var + HttpHeaders: HttpHeaders; + begin + HttpRequestMessage.GetHeaders(HttpHeaders); + if HttpHeaders.Contains(HeaderName) then + HttpHeaders.Remove(HeaderName); + HttpHeaders.Add(HeaderName, HeaderValue); + end; + + procedure SetHttpRequestMessage(var RequestMessage: HttpRequestMessage) + begin + HttpRequestMessage := RequestMessage; + end; + + procedure SetContent(HttpContent: Codeunit "Http Content") + begin + HttpRequestMessage.Content := HttpContent.GetHttpContent(); + end; + + procedure GetRequestMessage() ReturnValue: HttpRequestMessage + begin + ReturnValue := HttpRequestMessage; + end; +} \ No newline at end of file diff --git a/Modules/System/Rest Client/src/HttpResponseMessage.Codeunit.al b/Modules/System/Rest Client/src/HttpResponseMessage.Codeunit.al new file mode 100644 index 0000000000..d53e3ca94e --- /dev/null +++ b/Modules/System/Rest Client/src/HttpResponseMessage.Codeunit.al @@ -0,0 +1,145 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +/// Holder object for the HTTP response data. +codeunit 2356 "Http Response Message" +{ + InherentEntitlements = X; + InherentPermissions = X; + + var + HttpResponseMessageImpl: Codeunit "Http Response Message Impl."; + + #region IsBlockedByEnvironment + /// Sets whether the request is blocked by the environment. + /// True if the request is blocked by the environment; otherwise, false. + procedure SetIsBlockedByEnvironment(Value: Boolean) + begin + HttpResponseMessageImpl.SetIsBlockedByEnvironment(Value); + end; + + /// Gets whether the request is blocked by the environment. + /// True if the request is blocked by the environment; otherwise, false. + procedure GetIsBlockedByEnvironment() ReturnValue: Boolean + begin + ReturnValue := HttpResponseMessageImpl.GetIsBlockedByEnvironment(); + end; + #endregion + + #region HttpStatusCode + procedure SetHttpStatusCode(Value: Integer) + begin + HttpResponseMessageImpl.SetHttpStatusCode(Value); + end; + + /// Gets the HTTP status code of the response message. + /// The HTTP status code. + procedure GetHttpStatusCode() ReturnValue: Integer + begin + ReturnValue := HttpResponseMessageImpl.GetHttpStatusCode(); + end; + #endregion + + #region IsSuccessStatusCode + /// Sets whether the HTTP response message has a success status code. + /// True if the HTTP response message has a success status code; otherwise, false. + /// Any value in the HTTP status code range 2xx is considered to be successful. + procedure SetIsSuccessStatusCode(Value: Boolean) + begin + HttpResponseMessageImpl.SetIsSuccessStatusCode(Value); + end; + + /// Indicates whether the HTTP response message has a success status code. + /// True if the HTTP response message has a success status code; otherwise, false. + /// Any value in the HTTP status code range 2xx is considered to be successful. + procedure GetIsSuccessStatusCode() Result: Boolean + begin + Result := HttpResponseMessageImpl.GetIsSuccessStatusCode(); + end; + + #endregion + + #region ReasonPhrase + /// Sets the reason phrase which typically is sent by servers together with the status code. + /// The reason phrase sent by the server. + procedure SetReasonPhrase(Value: Text) + begin + HttpResponseMessageImpl.SetReasonPhrase(Value); + end; + + /// Gets the reason phrase which typically is sent by servers together with the status code. + /// The reason phrase sent by the server. + procedure GetReasonPhrase() ReturnValue: Text + begin + ReturnValue := HttpResponseMessageImpl.GetReasonPhrase(); + end; + #endregion + + #region HttpContent + /// Sets the HTTP content sent back by the server. + /// The content of the HTTP response message. + procedure SetContent(Content: Codeunit "Http Content") + begin + HttpResponseMessageImpl.SetContent(Content); + end; + + /// Gets the HTTP content sent back by the server. + /// The content of the HTTP response message. + procedure GetContent() ReturnValue: Codeunit "Http Content" + begin + ReturnValue := HttpResponseMessageImpl.GetContent(); + end; + #endregion + + #region HttpResponseMessage + /// Sets the HTTP response message. + /// The HTTP response message. + procedure SetResponseMessage(ResponseMessage: HttpResponseMessage) + begin + HttpResponseMessageImpl.SetResponseMessage(ResponseMessage); + end; + + /// Gets the HTTP response message. + /// The HTTPResponseMessage object. + procedure GetResponseMessage() ReturnValue: HttpResponseMessage + begin + ReturnValue := HttpResponseMessageImpl.GetResponseMessage(); + end; + #endregion + + #region HttpHeaders + /// Sets the HTTP headers. + /// The HTTP headers. + procedure SetHeaders(Headers: HttpHeaders) + begin + HttpResponseMessageImpl.SetHeaders(Headers); + end; + + /// Gets the HTTP headers. + /// The HTTP headers. + procedure GetHeaders() ReturnValue: HttpHeaders + begin + ReturnValue := HttpResponseMessageImpl.GetHeaders(); + + end; + #endregion + + #region ErrorMessage + /// Sets an error message when the request failed. + /// The error message. + procedure SetErrorMessage(Value: Text) + begin + HttpResponseMessageImpl.SetErrorMessage(Value); + end; + + /// Gets the error message when the request failed. + /// The error message. + procedure GetErrorMessage() ReturnValue: Text + begin + ReturnValue := HttpResponseMessageImpl.GetErrorMessage(); + end; + #endregion +} \ No newline at end of file diff --git a/Modules/System/Rest Client/src/HttpResponseMessageImpl.Codeunit.al b/Modules/System/Rest Client/src/HttpResponseMessageImpl.Codeunit.al new file mode 100644 index 0000000000..7355f0244d --- /dev/null +++ b/Modules/System/Rest Client/src/HttpResponseMessageImpl.Codeunit.al @@ -0,0 +1,142 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +codeunit 2357 "Http Response Message Impl." +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + #region IsBlockedByEnvironment + var + IsBlockedByEnvironment: Boolean; + + procedure SetIsBlockedByEnvironment(Value: Boolean) + begin + IsBlockedByEnvironment := Value; + end; + + procedure GetIsBlockedByEnvironment() ReturnValue: Boolean + begin + ReturnValue := IsBlockedByEnvironment; + end; + #endregion + + #region HttpStatusCode + var + HttpStatusCode: Integer; + + procedure SetHttpStatusCode(Value: Integer) + begin + HttpStatusCode := Value; + IsSuccessStatusCode := Value in [200 .. 299]; + end; + + procedure GetHttpStatusCode() ReturnValue: Integer + begin + ReturnValue := HttpStatusCode + end; + #endregion + + #region IsSuccessStatusCode + var + IsSuccessStatusCode: Boolean; + + procedure GetIsSuccessStatusCode() Result: Boolean + begin + Result := HttpResponseMessage.IsSuccessStatusCode; + end; + + procedure SetIsSuccessStatusCode(Value: Boolean) + begin + IsSuccessStatusCode := Value; + end; + #endregion + + #region ReasonPhrase + var + ReasonPhrase: Text; + + procedure SetReasonPhrase(Value: Text) + begin + ReasonPhrase := Value; + end; + + procedure GetReasonPhrase() ReturnValue: Text + begin + ReturnValue := HttpResponseMessage.ReasonPhrase; + end; + #endregion + + #region HttpContent + var + HttpContent: Codeunit "Http Content"; + + procedure SetContent(Content: Codeunit "Http Content") + begin + HttpContent := Content; + end; + + procedure GetContent() ReturnValue: Codeunit "Http Content" + begin + ReturnValue := HttpContent; + end; + #endregion + + #region HttpResponseMessage + var + HttpResponseMessage: HttpResponseMessage; + + procedure SetResponseMessage(var ResponseMessage: HttpResponseMessage) + begin + HttpResponseMessage := ResponseMessage; + SetIsBlockedByEnvironment(ResponseMessage.IsBlockedByEnvironment); + SetHttpStatusCode(ResponseMessage.HttpStatusCode); + SetReasonPhrase(ResponseMessage.ReasonPhrase); + SetIsSuccessStatusCode(ResponseMessage.IsSuccessStatusCode); + SetHeaders(ResponseMessage.Headers); + SetContent(HttpContent.Create(ResponseMessage.Content)); + end; + + procedure GetResponseMessage() ReturnValue: HttpResponseMessage + begin + ReturnValue := HttpResponseMessage; + end; + #endregion + + #region HttpHeaders + var + HttpHeaders: HttpHeaders; + + procedure SetHeaders(Headers: HttpHeaders) + begin + HttpHeaders := Headers; + end; + + procedure GetHeaders() ReturnValue: HttpHeaders + begin + ReturnValue := HttpHeaders; + end; + #endregion + + #region ErrorMessage + var + ErrorMessage: Text; + + procedure SetErrorMessage(Value: Text) + begin + ErrorMessage := Value; + end; + + procedure GetErrorMessage() ReturnValue: Text + begin + if ErrorMessage <> '' then + ReturnValue := ErrorMessage + else + ReturnValue := GetLastErrorText(); + end; + #endregion +} \ No newline at end of file diff --git a/Modules/System/Rest Client/src/RestClient.Codeunit.al b/Modules/System/Rest Client/src/RestClient.Codeunit.al new file mode 100644 index 0000000000..e9afb1b08c --- /dev/null +++ b/Modules/System/Rest Client/src/RestClient.Codeunit.al @@ -0,0 +1,339 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +/// Provides functionality to easily work with the HttpClient object. +codeunit 2350 "Rest Client" +{ + InherentEntitlements = X; + InherentPermissions = X; + + var + RestClientImpl: Codeunit "Rest Client Impl."; + + #region Initialization + /// Initializes the Rest Client with the default Http Client Handler and anonymous Http authentication. + procedure Initialize() + begin + RestClientImpl.Initialize(); + end; + + /// Initializes the Reest Client with the given Http Client Handler + /// The Http Client Handler to use. + /// The anynomous Http Authentication will be used. + procedure Initialize(HttpClientHandler: Interface "Http Client Handler") + begin + RestClientImpl.Initialize(HttpClientHandler); + end; + + /// Initializes the Rest Client with the given Http Authentication. + /// The authentication to use. + /// The default Http Client Handler will be used. + procedure Initialize(HttpAuthentication: Interface "Http Authentication") + begin + RestClientImpl.Initialize(HttpAuthentication); + end; + + /// Initializes the Rest Client with the given Http Client Handler and Http Authentication. + /// The Http Client Handler to use. + /// The authentication to use. + procedure Initialize(HttpClientHandler: Interface "Http Client Handler"; HttpAuthentication: Interface "Http Authentication") + begin + RestClientImpl.Initialize(HttpClientHandler, HttpAuthentication); + end; + + /// Sets a new value for an existing default header of the Http Client object, or addds the header if it does not already exist. + /// The name of the request header. + /// The header of request header. + /// Default request headers will be added to every request that is sent with this Rest Client instance + /// The Rest Client will be initialized if it was not initialized before. + procedure SetDefaultRequestHeader(Name: Text; Value: Text) + begin + RestClientImpl.SetDefaultRequestHeader(Name, Value); + end; + + /// Sets a new value for an existing default header of the Http Client object, or addds the header if it does not already exist. + /// The name of the request header. + /// The header of request header. + /// Default request headers will be added to every request that is sent with this Rest Client instance + /// The Rest Client will be initialized if it was not initialized before. + procedure SetDefaultRequestHeader(Name: Text; Value: SecretText) + begin + RestClientImpl.SetDefaultRequestHeader(Name, Value); + end; + + /// Sets the base address of the Rest Client. + /// The base address will be used for every request that is sent with this Rest Client instance. + /// Calls to the Get, Post, Patch, Put and Delete methods must use a relative path which will be appended to the base address. + /// The Rest Client will be initialized if it was not initialized before. + /// The base address to use. + procedure SetBaseAddress(Url: Text) + begin + RestClientImpl.SetBaseAddress(Url); + end; + + /// Gets the base address of the Rest Client. + /// The base address of the Rest Client. + procedure GetBaseAddress() Url: Text + begin + Url := RestClientImpl.GetBaseAddress(); + end; + + /// Sets the timeout of the Rest Client. + /// The timeout to use. + /// The timeout will be used for every request that is sent with this Rest Client instance. + /// The Rest Client will be initialized if it was not initialized before. + procedure SetTimeOut(Timeout: Duration) + begin + RestClientImpl.SetTimeOut(Timeout); + end; + + /// Gets the timeout of the Rest Client. + /// The timeout of the Rest Client. + procedure GetTimeOut() Timeout: Duration + begin + Timeout := RestClientImpl.GetTimeOut(); + end; + + /// Adds a certificate to the Rest Client. + /// The Base64 encoded certificate + /// The certificate will be used for every request that is sent with this Rest Client instance. + /// The Rest Client will be initialized if it was not initialized before. + procedure AddCertificate(Certificate: Text) + begin + RestClientImpl.AddCertificate(Certificate); + end; + + /// Adds a certificate to the Rest Client. + /// The Base64 encoded certificate + /// The password of the certificate + /// The certificate will be used for every request that is sent with this Rest Client instance. + /// The Rest Client will be initialized if it was not initialized before. + + procedure AddCertificate(Certificate: Text; Password: SecretText) + begin + RestClientImpl.AddCertificate(Certificate, Password); + end; + + /// Sets the user agent header of the Rest Client. + /// Use this function to overwrite the default User-Agent header. + /// The default user agent header is "Dynamics 365 Business Central - |[Publisher]| [App Name]/[App Version]". + /// The Rest Client will be initialized if it was not initialized before. + /// The user agent header to use. + procedure SetUserAgentHeader(Value: Text) + begin + RestClientImpl.SetUserAgentHeader(Value); + end; + + /// Sets the authorization header of the Rest Client. + /// Use this function to set the authorization header. + /// The Rest Client will be initialized if it was not initialized before. + /// The authorization header to use. + procedure SetAuthorizationHeader(Value: SecretText) + begin + RestClientImpl.SetAuthorizationHeader(Value); + end; + #endregion + + #region BasicMethods + /// Sends a GET request to the specified Uri and returns the response message. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// The Uri the request is sent to. + /// The response message object + procedure Get(RequestUri: Text) HttpResponseMessage: Codeunit "Http Response Message" + begin + HttpResponseMessage := Send(Enum::"Http Method"::GET, RequestUri); + end; + + /// Sends a POST request to the specified Uri and returns the response message. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// If a response was received, then the response message object contains information about the status. + /// The Uri the request is sent to. + /// The content to send. + /// The response message object + procedure Post(RequestUri: Text; Content: Codeunit "Http Content") HttpResponseMessage: Codeunit "Http Response Message" + begin + HttpResponseMessage := Send(Enum::"Http Method"::POST, RequestUri, Content); + end; + + /// Sends a PATCH request to the specified Uri and returns the response message. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// If a response was received, then the response message object contains information about the status. + /// The Uri the request is sent to. + /// The content to send. + /// The response message object + procedure Patch(RequestUri: Text; Content: Codeunit "Http Content") HttpResponseMessage: Codeunit "Http Response Message" + begin + HttpResponseMessage := Send(Enum::"Http Method"::PATCH, RequestUri, Content); + end; + + /// Sends a PUT request to the specified Uri and returns the response message. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// If a response was received, then the response message object contains information about the status. + /// The Uri the request is sent to. + /// The content to send. + /// The response message object + procedure Put(RequestUri: Text; Content: Codeunit "Http Content") HttpResponseMessage: Codeunit "Http Response Message" + begin + HttpResponseMessage := Send(Enum::"Http Method"::PUT, RequestUri, Content); + end; + + /// Sends a DELETE request to the specified Uri and returns the response message. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// If a response was received, then the response message object contains information about the status. + /// The Uri the request is sent to. + /// The response message object + procedure Delete(RequestUri: Text) HttpResponseMessage: Codeunit "Http Response Message"; + begin + HttpResponseMessage := Send(Enum::"Http Method"::DELETE, RequestUri); + end; + + #endregion + + #region BasicMethodsAsJson + /// Sends a GET request to the specified Uri and returns the response content as JsonToken. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// The function also fails in case the response does not contain a success status code or a valid JSON content. + /// The Uri the request is sent to. + /// The response content as JsonToken + procedure GetAsJson(RequestUri: Text) JsonToken: JsonToken + begin + JsonToken := RestClientImpl.GetAsJson(RequestUri); + end; + + /// Sends a POST request to the specified Uri and returns the response content as JsonToken. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// The function also fails in case the response does not contain a success status code or a valid JSON content. + /// The Uri the request is sent to. + /// The content to send as a JsonObject. + /// The response content as JsonToken + procedure PostAsJson(RequestUri: Text; Content: JsonObject) Response: JsonToken + begin + Response := PostAsJson(RequestUri, Content.AsToken()); + end; + + /// Sends a POST request to the specified Uri and returns the response content as JsonToken. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// The function also fails in case the response does not contain a success status code or a valid JSON content. + /// The Uri the request is sent to. + /// The content to send as a JsonArray. + /// The response content as JsonToken + procedure PostAsJson(RequestUri: Text; Content: JsonArray) Response: JsonToken + begin + Response := PostAsJson(RequestUri, Content.AsToken()); + end; + + /// Sends a POST request to the specified Uri and returns the response content as JsonToken. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// The function also fails in case the response does not contain a success status code or a valid JSON content. + /// The Uri the request is sent to. + /// The content to send as a JsonToken. + /// The response content as JsonToken + procedure PostAsJson(RequestUri: Text; Content: JsonToken) Response: JsonToken + begin + Response := RestClientImpl.PostAsJson(RequestUri, Content); + end; + + /// Sends a PATCH request to the specified Uri and returns the response content as JsonToken. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// The function also fails in case the response does not contain a success status code or a valid JSON content. + /// The Uri the request is sent to. + /// The content to send as a JsonObject. + /// The response content as JsonToken + procedure PatchAsJson(RequestUri: Text; Content: JsonObject) Response: JsonToken + begin + Response := PatchAsJson(RequestUri, Content.AsToken()); + end; + + /// Sends a PATCH request to the specified Uri and returns the response content as JsonToken. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// The function also fails in case the response does not contain a success status code or a valid JSON content. + /// The Uri the request is sent to. + /// The content to send as a JsonArray. + /// The response content as JsonToken + procedure PatchAsJson(RequestUri: Text; Content: JsonArray) Response: JsonToken + begin + Response := PatchAsJson(RequestUri, Content.AsToken()); + end; + + /// Sends a PATCH request to the specified Uri and returns the response content as JsonToken. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// The function also fails in case the response does not contain a success status code or a valid JSON content. + /// The Uri the request is sent to. + /// The content to send as a JsonToken. + /// The response content as JsonToken + procedure PatchAsJson(RequestUri: Text; Content: JSonToken) Response: JsonToken + begin + Response := RestClientImpl.PatchAsJson(RequestUri, Content); + end; + + /// Sends a PUT request to the specified Uri and returns the response content as JsonToken. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// The function also fails in case the response does not contain a success status code or a valid JSON content. + /// The Uri the request is sent to. + /// The content to send as a JsonObject. + /// The response content as JsonToken + procedure PutAsJson(RequestUri: Text; Content: JsonObject) Response: JsonToken + begin + Response := PutAsJson(RequestUri, Content.AsToken()); + end; + + /// Sends a PUT request to the specified Uri and returns the response content as JsonToken. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// The function also fails in case the response does not contain a success status code or a valid JSON content. + /// The Uri the request is sent to. + /// The content to send as a JsonArray. + /// The response content as JsonToken + procedure PutAsJson(RequestUri: Text; Content: JsonArray) Response: JsonToken + begin + Response := PutAsJson(RequestUri, Content.AsToken()); + end; + + /// Sends a PUT request to the specified Uri and returns the response content as JsonToken. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// The function also fails in case the response does not contain a success status code or a valid JSON content. + /// The Uri the request is sent to. + /// The content to send as a JsonToken. + /// The response content as JsonToken + procedure PutAsJson(RequestUri: Text; Content: JSonToken) Response: JsonToken + begin + Response := RestClientImpl.PutAsJson(RequestUri, Content); + end; + #endregion + + #region GenericSendMethods + /// Sends a request with the specific Http method and an empty content to the specified Uri and returns the response message. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// If a response was received, then the response message object contains information about the status. + /// The HTTP method to use. + /// The Uri the request is sent to. + /// The response message object + procedure Send(Method: Enum "Http Method"; RequestUri: Text) HttpResponseMessage: Codeunit "Http Response Message" + begin + HttpResponseMessage := RestClientImpl.Send(Method, RequestUri); + end; + + /// Sends a request with the specific Http method and the given content to the specified Uri and returns the response message. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// If a response was received, then the response message object contains information about the status. + /// The HTTP method to use. + /// The Uri the request is sent to. + /// The content to send. + /// The response message object + procedure Send(Method: Enum "Http Method"; RequestUri: Text; Content: Codeunit "Http Content") HttpResponseMessage: Codeunit "Http Response Message" + begin + HttpResponseMessage := RestClientImpl.Send(Method, RequestUri, Content); + end; + + /// Sends the given request message and returns the response message. + /// The function fails with an error message if the request could not be sent or a response was not received. + /// The request message to send. + /// The response message object + procedure Send(var HttpRequestMessage: Codeunit "Http Request Message") HttpResponseMessage: Codeunit "Http Response Message" + begin + HttpResponseMessage := RestClientImpl.Send(HttpRequestMessage); + end; + #endregion +} \ No newline at end of file diff --git a/Modules/System/Rest Client/src/RestClientImpl.Codeunit.al b/Modules/System/Rest Client/src/RestClientImpl.Codeunit.al new file mode 100644 index 0000000000..fd5afd4051 --- /dev/null +++ b/Modules/System/Rest Client/src/RestClientImpl.Codeunit.al @@ -0,0 +1,259 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace System.RestClient; + +codeunit 2351 "Rest Client Impl." +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + DefaultHttpClientHandler: Codeunit "Http Client Handler"; + HttpAuthenticationAnonymous: Codeunit "Http Authentication Anonymous"; + HttpAuthentication: Interface "Http Authentication"; + HttpClientHandler: Interface "Http Client Handler"; + HttpClient: HttpClient; + IsInitialized: Boolean; + EnvironmentBlocksErr: label 'Environment blocks an outgoing HTTP request to ''%1''.', Comment = '%1 = url, e.g. https://microsoft.com'; + ConnectionErr: label 'Connection to the remote service ''%1'' could not be established.', Comment = '%1 = url, e.g. https://microsoft.com'; + RequestFailedErr: label 'The request failed: %1 %2', Comment = '%1 = HTTP status code, %2 = Reason phrase'; + UserAgentLbl: Label 'Dynamics 365 Business Central - |%1| %2/%3', Locked = true, Comment = '%1 = App Publisher; %2 = App Name; %3 = App Version'; + + #region Initialization + procedure Initialize() + begin + Initialize(DefaultHttpClientHandler, HttpAuthenticationAnonymous); + end; + + #pragma warning disable AA0244 + procedure Initialize(HttpClientHandler: Interface "Http Client Handler") + begin + Initialize(HttpClientHandler, HttpAuthenticationAnonymous); + end; + + procedure Initialize(HttpAuthentication: Interface "Http Authentication") + begin + Initialize(DefaultHttpClientHandler, HttpAuthentication); + end; + #pragma warning restore AA0244 + + procedure Initialize(HttpClientHandlerInstance: Interface "Http Client Handler"; HttpAuthenticationInstance: Interface "Http Authentication") + begin + ClearAll(); + + HttpClient.Clear(); + HttpClientHandler := HttpClientHandlerInstance; + HttpAuthentication := HttpAuthenticationInstance; + IsInitialized := true; + SetDefaultUserAgentHeader(); + end; + + procedure SetDefaultRequestHeader(Name: Text; Value: Text) + begin + CheckInitialized(); + if HttpClient.DefaultRequestHeaders.Contains(Name) then + HttpClient.DefaultRequestHeaders.Remove(Name); + HttpClient.DefaultRequestHeaders.Add(Name, Value); + end; + + procedure SetDefaultRequestHeader(Name: Text; Value: SecretText) + begin + CheckInitialized(); + if HttpClient.DefaultRequestHeaders.Contains(Name) then + HttpClient.DefaultRequestHeaders.Remove(Name); + HttpClient.DefaultRequestHeaders.Add(Name, Value); + end; + + procedure SetBaseAddress(Url: Text) + begin + CheckInitialized(); + HttpClient.SetBaseAddress(Url); + end; + + procedure GetBaseAddress() Url: Text + begin + CheckInitialized(); + Url := HttpClient.GetBaseAddress; + end; + + procedure SetTimeOut(TimeOut: Duration) + begin + CheckInitialized(); + HttpClient.Timeout := TimeOut; + end; + + procedure GetTimeOut() TimeOut: Duration + begin + CheckInitialized(); + TimeOut := HttpClient.Timeout; + end; + + procedure AddCertificate(Certificate: Text) + begin + CheckInitialized(); + HttpClient.AddCertificate(Certificate); + end; + + procedure AddCertificate(Certificate: Text; Password: SecretText) + begin + CheckInitialized(); + HttpClient.AddCertificate(Certificate, Password); + end; + + procedure SetAuthorizationHeader(Value: SecretText) + begin + SetDefaultRequestHeader('Authorization', Value); + end; + + procedure SetUserAgentHeader(Value: Text) + begin + SetDefaultRequestHeader('User-Agent', Value); + end; + #endregion + + + #region BasicMethodsAsJson + procedure GetAsJson(RequestUri: Text) JsonToken: JsonToken + var + HttpResponseMessage: Codeunit "Http Response Message"; + begin + HttpResponseMessage := Send(Enum::"Http Method"::GET, RequestUri); + if not HttpResponseMessage.GetIsSuccessStatusCode() then + Error(HttpResponseMessage.GetErrorMessage()); + + JsonToken := HttpResponseMessage.GetContent().AsJson(); + end; + + procedure PostAsJson(RequestUri: Text; Content: JsonToken) Response: JsonToken + var + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + begin + HttpResponseMessage := Send(Enum::"Http Method"::POST, RequestUri, HttpContent.Create(Content)); + + if not HttpResponseMessage.GetIsSuccessStatusCode() then + Error(HttpResponseMessage.GetErrorMessage()); + + Response := HttpResponseMessage.GetContent().AsJson(); + end; + + procedure PatchAsJson(RequestUri: Text; Content: JSonToken) Response: JsonToken + var + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + begin + HttpResponseMessage := Send(Enum::"Http Method"::PATCH, RequestUri, HttpContent.Create(Content)); + + if not HttpResponseMessage.GetIsSuccessStatusCode() then + Error(HttpResponseMessage.GetErrorMessage()); + + Response := HttpResponseMessage.GetContent().AsJson(); + end; + + procedure PutAsJson(RequestUri: Text; Content: JSonToken) Response: JsonToken + var + HttpResponseMessage: Codeunit "Http Response Message"; + HttpContent: Codeunit "Http Content"; + begin + HttpResponseMessage := Send(Enum::"Http Method"::PUT, RequestUri, HttpContent.Create(Content)); + + if not HttpResponseMessage.GetIsSuccessStatusCode() then + Error(HttpResponseMessage.GetErrorMessage()); + + Response := HttpResponseMessage.GetContent().AsJson(); + end; + #endregion + + #region GenericSendMethods + procedure Send(Method: Enum "Http Method"; RequestUri: Text) HttpResponseMessage: Codeunit "Http Response Message" + var + EmptyHttpContent: Codeunit "Http Content"; + begin + HttpResponseMessage := Send(Method, RequestUri, EmptyHttpContent); + end; + + procedure Send(Method: Enum "Http Method"; RequestUri: Text; Content: Codeunit "Http Content") HttpResponseMessage: Codeunit "Http Response Message" + var + HttpRequestMessage: Codeunit "Http Request Message"; + begin + CheckInitialized(); + + HttpRequestMessage.SetHttpMethod(Method); + if RequestUri.StartsWith('http://') or RequestUri.StartsWith('https://') then + HttpRequestMessage.SetRequestUri(RequestUri) + else + HttpRequestMessage.SetRequestUri(GetBaseAddress() + RequestUri); + HttpRequestMessage.SetContent(Content); + + HttpResponseMessage := Send(HttpRequestMessage); + end; + + procedure Send(var HttpRequestMessage: Codeunit "Http Request Message") HttpResponseMessage: Codeunit "Http Response Message" + begin + CheckInitialized(); + + if not SendRequest(HttpRequestMessage, HttpResponseMessage) then + Error(HttpResponseMessage.GetErrorMessage()); + end; + #endregion + + #region Local Methods + local procedure CheckInitialized() + begin + if not IsInitialized then + Initialize(); + end; + + local procedure SetDefaultUserAgentHeader() + var + ModuleInfo: ModuleInfo; + UserAgentString: Text; + begin + if NavApp.GetCurrentModuleInfo(ModuleInfo) then + UserAgentString := StrSubstNo(UserAgentLbl, ModuleInfo.Publisher(), ModuleInfo.Name(), ModuleInfo.AppVersion()); + + SetUserAgentHeader(UserAgentString); + end; + + local procedure SendRequest(var HttpRequestMessage: Codeunit "Http Request Message"; var HttpResponseMessage: Codeunit "Http Response Message"): Boolean + var + ErrorMessage: Text; + begin + Clear(HttpResponseMessage); + + if HttpAuthentication.IsAuthenticationRequired() then + Authorize(HttpRequestMessage); + + if not HttpClientHandler.Send(HttpClient, HttpRequestMessage, HttpResponseMessage) then begin + if HttpResponseMessage.GetIsBlockedByEnvironment() then + ErrorMessage := StrSubstNo(EnvironmentBlocksErr, HttpRequestMessage.GetRequestUri()) + else + ErrorMessage := StrSubstNo(ConnectionErr, HttpRequestMessage.GetRequestUri()); + exit(false); + end; + + if not HttpResponseMessage.GetIsSuccessStatusCode() then begin + ErrorMessage := StrSubstNo(RequestFailedErr, HttpResponseMessage.GetHttpStatusCode(), HttpResponseMessage.GetReasonPhrase()); + HttpResponseMessage.SetErrorMessage(ErrorMessage); + end; + + exit(true); + end; + + local procedure Authorize(HttpRequestMessage: Codeunit "Http Request Message") + var + AuthorizationHeaders: Dictionary of [Text, SecretText]; + HeaderName: Text; + HeaderValue: SecretText; + begin + AuthorizationHeaders := HttpAuthentication.GetAuthorizationHeaders(); + foreach HeaderName in AuthorizationHeaders.Keys do begin + HeaderValue := AuthorizationHeaders.Get(HeaderName); + HttpRequestMessage.SetHeader(HeaderName, HeaderValue); + end; + end; + #endregion +} \ No newline at end of file