Skip to content

Commit

Permalink
Merge pull request #79 from bgmulinari/2.0.0
Browse files Browse the repository at this point in the history
2.0.0
  • Loading branch information
bgmulinari authored Sep 5, 2024
2 parents 9aa6214 + 13f57fd commit 1717d26
Show file tree
Hide file tree
Showing 26 changed files with 2,607 additions and 1,924 deletions.
25 changes: 25 additions & 0 deletions B1SLayer.Test/B1SLayer.Test.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<LangVersion>12</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\B1SLayer\B1SLayer.csproj" />
</ItemGroup>

</Project>
16 changes: 16 additions & 0 deletions B1SLayer.Test/FlurlExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Flurl.Http.Content;
using Flurl.Http.Testing;

namespace B1SLayer.Test;

internal static class FlurlExtensions
{
public static HttpCallAssertion WithRequestMultipart(this HttpCallAssertion assertion, Func<CapturedStringContent, bool> predicate)
{
return assertion.With(x =>
{
string content = x.HttpRequestMessage.Content.ReadAsStringAsync().Result;
return predicate(new CapturedStringContent(content, "CapturedMultipartContent body"));
});
}
}
1 change: 1 addition & 0 deletions B1SLayer.Test/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Xunit;
7 changes: 7 additions & 0 deletions B1SLayer.Test/Models/MarketingDocument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace B1SLayer.Test.Models;

internal class MarketingDocument
{
public int DocEntry { get; set; }
public string CardCode { get; set; }
}
30 changes: 30 additions & 0 deletions B1SLayer.Test/TestBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Flurl.Http.Testing;

namespace B1SLayer.Test;

public abstract class TestBase : IDisposable
{
protected HttpTest HttpTest { get; private set; }
protected static SLConnection SLConnectionV1 { get; } = new SLConnection("https://sapserver:50000/b1s/v1", "CompanyDB", "manager", "12345");
protected static SLConnection SLConnectionV2 { get; } = new SLConnection("https://sapserver:50000/b1s/v2", "CompanyDB", "manager", "12345");
protected static SLLoginResponse LoginResponse { get; } = new() { SessionId = "00000000-0000-0000-0000-000000000000", Version = "1000000", SessionTimeout = 30 };

public TestBase()
{
HttpTest = new HttpTest();

// standard Login response
HttpTest.ForCallsTo("*/b1s/v*/Login").RespondWithJson(LoginResponse);
}

public static IEnumerable<object[]> SLConnections()
{
yield return new SLConnection[] { SLConnectionV1 };
yield return new SLConnection[] { SLConnectionV2 };
}

public void Dispose()
{
HttpTest.Dispose();
}
}
66 changes: 66 additions & 0 deletions B1SLayer.Test/Tests/SLBatchRequestTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Flurl;
using Flurl.Http;
using Flurl.Http.Testing;
using System.IO.Enumeration;
using System.Net;

namespace B1SLayer.Test;

public class SLBatchRequestTests : TestBase
{
private const string
expectedRequestBody = "*--*-*-*-*-*Content-Type: multipart/mixed; boundary=\"changeset_*-*-*-*-*\"*--changeset_*-*-*-*-*Content-Type: application/http; msgtype=request*content-transfer-encoding: binary*Content-ID: 1*POST /b1s/v*/BusinessPartners HTTP/1.1*Host: *:50000*Content-Type: application/json; charset=utf-8*{\"CardCode\":\"C00001\",\"CardName\":\"I am a new BP\"}*--changeset_*-*-*-*-*Content-Type: application/http; msgtype=request*content-transfer-encoding: binary*Content-ID: 2*PATCH /b1s/v*/BusinessPartners('C00001') HTTP/1.1*Host: *:50000*Content-Type: application/json; charset=utf-8*{\"CardName\":\"This is my updated name\"}*--changeset_*-*-*-*-*Content-Type: application/http; msgtype=request*content-transfer-encoding: binary*Content-ID: 3*DELETE /b1s/v*/BusinessPartners('C00001') HTTP/1.1*Host: *:50000*--changeset_*-*-*-*-*--*--*-*-*-*-*--*",
v1Response = "--batchresponse_00000000-0000-0000-0000-000000000000\r\nContent-Type: multipart/mixed; boundary=changesetresponse_00000000-0000-0000-0000-000000000000\r\n\r\n--changesetresponse_00000000-0000-0000-0000-000000000000\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 201 Created\r\nContent-ID: 1\r\nContent-Type: application/json;odata=minimalmetadata;charset=utf-8\r\nContent-Length: 8811\r\nDataServiceVersion: 3.0\r\nETag: W/\"0000000000000000000000000000000000000000\"\r\nLocation: https://localhost:50000/b1s/v1/BusinessPartners('C00001')\r\n\r\n{\"some\":\"content\"}\n\r\n--changesetresponse_00000000-0000-0000-0000-000000000000\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nContent-ID: 2\r\nDataServiceVersion: 3.0\r\n\r\n\r\n--changesetresponse_00000000-0000-0000-0000-000000000000\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\n\r\nHTTP/1.1 204 No Content\r\nContent-ID: 3\r\nDataServiceVersion: 3.0\r\n\r\n\r\n--changesetresponse_00000000-0000-0000-0000-000000000000--\r\n--batchresponse_00000000-0000-0000-0000-000000000000--\r\n",
v2Response = "--batchresponse_00000000-0000-0000-0000-000000000000\r\nContent-Type: multipart/mixed; boundary=changesetresponse_00000000-0000-0000-0000-000000000000\r\n\r\n--changesetresponse_00000000-0000-0000-0000-000000000000\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\nContent-ID: 1\r\n\r\nHTTP/1.1 201 Created\r\nContent-Type: application/json;odata.metadata=minimal;charset=utf-8\r\nContent-Length: 8811\r\nETag: W/\"0000000000000000000000000000000000000000\"\r\nLocation: https://localhost:50000/b1s/v2/BusinessPartners('C00001')\r\nOData-Version: 4.0\r\n\r\n{\"some\":\"content\"}\n\r\n--changesetresponse_00000000-0000-0000-0000-000000000000\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\nContent-ID: 2\r\n\r\nHTTP/1.1 204 No Content\r\nOData-Version: 4.0\r\n\r\n\r\n--changesetresponse_00000000-0000-0000-0000-000000000000\r\nContent-Type: application/http\r\nContent-Transfer-Encoding: binary\r\nContent-ID: 3\r\n\r\nHTTP/1.1 204 No Content\r\nOData-Version: 4.0\r\n\r\n\r\n--changesetresponse_00000000-0000-0000-0000-000000000000--\r\n--batchresponse_00000000-0000-0000-0000-000000000000--\r\n";

private static readonly SLBatchRequest[] _requests =
[
new SLBatchRequest(HttpMethod.Post, "BusinessPartners", new { CardCode = "C00001", CardName = "I am a new BP" }, 1),
new SLBatchRequest(HttpMethod.Patch, "BusinessPartners('C00001')", new { CardName = "This is my updated name" }, 2),
new SLBatchRequest(HttpMethod.Delete, "BusinessPartners('C00001')", contentID: 3)
];

[Fact]
public async Task PostBatchAsyncV1_ReturnsCorrectData()
{
HttpTest.RespondWith(
body: v1Response,
status: 202,
headers: new Dictionary<string, string> { { "Content-Type", "multipart/mixed;boundary=batchresponse_00000000-0000-0000-0000-000000000000" } });

var batchResult = await SLConnectionV1.PostBatchAsync(_requests);

HttpTest.ShouldHaveCalled(SLConnectionV1.ServiceLayerRoot.AppendPathSegment("$batch"))
.WithVerb(HttpMethod.Post)
.WithRequestMultipart(call => FileSystemName.MatchesSimpleExpression(expectedRequestBody, call.Content))
.Times(1);

Assert.Equal(3, batchResult.Length);
Assert.Equal(HttpStatusCode.Created, batchResult[0].StatusCode);
Assert.Equal(HttpStatusCode.NoContent, batchResult[1].StatusCode);
Assert.Equal(HttpStatusCode.NoContent, batchResult[2].StatusCode);
Assert.Equal("{\"some\":\"content\"}", await batchResult[0].Content.ReadAsStringAsync());
}

[Fact]
public async Task PostBatchAsyncV2_ReturnsCorrectData()
{
HttpTest.RespondWith(
body: v2Response,
status: 202,
headers: new Dictionary<string, string> { { "Content-Type", "multipart/mixed;boundary=batchresponse_00000000-0000-0000-0000-000000000000" } });

var batchResult = await SLConnectionV2.PostBatchAsync(_requests);

HttpTest.ShouldHaveCalled(SLConnectionV2.ServiceLayerRoot.AppendPathSegment("$batch"))
.WithVerb(HttpMethod.Post)
.WithRequestMultipart(call => FileSystemName.MatchesSimpleExpression(expectedRequestBody, call.Content))
.Times(1);

Assert.Equal(3, batchResult.Length);
Assert.Equal(HttpStatusCode.Created, batchResult[0].StatusCode);
Assert.Equal(HttpStatusCode.NoContent, batchResult[1].StatusCode);
Assert.Equal(HttpStatusCode.NoContent, batchResult[2].StatusCode);
Assert.Equal("{\"some\":\"content\"}", await batchResult[0].Content.ReadAsStringAsync());
}
}
128 changes: 128 additions & 0 deletions B1SLayer.Test/Tests/SLRequestTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using B1SLayer.Models;
using B1SLayer.Test.Models;
using Flurl;
using Flurl.Http.Testing;

namespace B1SLayer.Test;

public class SLRequestTests : TestBase
{
[Theory]
[MemberData(nameof(SLConnections))]
public async Task RequestParameters_AreApplied(SLConnection slConnection)
{
HttpTest.RespondWith("{}");

await slConnection.Request("$crossjoin(Orders,Orders/DocumentLines)")
.Select("DocEntry,CardCode")
.Expand("Orders/DocumentLines($select=ItemCode,LineNum)")
.Filter("Orders/DocEntry eq Orders/DocumentLines/DocEntry")
.OrderBy("DocTotal asc,DocEntry desc")
.Apply("aggregate(DocRate with sum as TotalDocRate)")
.Top(1)
.Skip(2)
.SetQueryParam("X", "Z")
.WithReturnNoContent()
.WithCaseInsensitive()
.WithReplaceCollectionsOnPatch()
.WithPageSize(50)
.WithHeader("A", "B")
.GetAsync<object>();

HttpTest.ShouldHaveCalled(slConnection.ServiceLayerRoot.AppendPathSegment("$crossjoin(Orders,Orders/DocumentLines)"))
.WithVerb(HttpMethod.Get)
.WithQueryParam("$select", "DocEntry,CardCode")
.WithQueryParam("$expand", "Orders/DocumentLines($select=ItemCode,LineNum)")
.WithQueryParam("$filter", "Orders/DocEntry eq Orders/DocumentLines/DocEntry")
.WithQueryParam("$orderby", "DocTotal asc,DocEntry desc")
.WithQueryParam("$apply", "aggregate(DocRate with sum as TotalDocRate)")
.WithQueryParam("$top", 1)
.WithQueryParam("$skip", 2)
.WithQueryParam("X", "Z")
.WithHeader("Prefer", "return-no-content")
.WithHeader("B1S-CaseInsensitive", "true")
.WithHeader("B1S-ReplaceCollectionsOnPatch", "true")
.WithHeader("B1S-PageSize", 50)
.WithHeader("A", "B")
.Times(1);
}

[Theory]
[MemberData(nameof(SLConnections))]
public async Task LoginAsync_IsPerformedAutomatically(SLConnection slConnection)
{
await slConnection.Request("Orders").GetStringAsync(); // random request

Assert.Equal(LoginResponse.SessionId, slConnection.LoginResponse.SessionId);
Assert.Equal(LoginResponse.Version, slConnection.LoginResponse.Version);
Assert.Equal(LoginResponse.SessionTimeout, slConnection.LoginResponse.SessionTimeout);

await slConnection.LogoutAsync();
}

[Theory]
[MemberData(nameof(SLConnections))]
public async Task GetAsync_ReturnsCorrectData(SLConnection slConnection)
{
var expectedData = new List<MarketingDocument>
{
new() { DocEntry = 1, CardCode = "C20001" },
new() { DocEntry = 2, CardCode = "C20002" }
};

HttpTest.RespondWithJson(new SLCollectionRoot<MarketingDocument> { Value = expectedData });

var result = await slConnection
.Request("Orders")
.GetAsync<List<MarketingDocument>>();

HttpTest.ShouldHaveCalled(slConnection.ServiceLayerRoot.AppendPathSegment("Orders"))
.WithVerb(HttpMethod.Get)
.Times(1);

Assert.Equal(2, result.Count);
Assert.Equal(expectedData[0].DocEntry, result[0].DocEntry);
Assert.Equal(expectedData[1].CardCode, result[1].CardCode);
}

[Theory]
[MemberData(nameof(SLConnections))]
public async Task GetAllAsync_ReturnsCorrectData(SLConnection slConnection)
{
var page1 = new SLCollectionRoot<MarketingDocument>
{
Value =
[
new() { DocEntry = 1, CardCode = "C20001" },
new() { DocEntry = 2, CardCode = "C20002" }
],
ODataNextLinkJson = "Orders?$select=DocEntry,CardCode&$skip=2"
};

var page2 = new SLCollectionRoot<MarketingDocument>
{
Value =
[
new() { DocEntry = 3, CardCode = "C20003" },
new() { DocEntry = 4, CardCode = "C20004" }
],
ODataNextLinkJson = "Orders?$select=DocEntry,CardCode&$skip=4"
};

HttpTest.RespondWithJson(page1);
HttpTest.RespondWithJson(page2);
HttpTest.RespondWith("{\"value\":[]}");

var orderList = await slConnection.Request("Orders").WithPageSize(2).GetAllAsync<MarketingDocument>();

HttpTest.ShouldHaveCalled(slConnection.ServiceLayerRoot.AppendPathSegment("Orders"))
.WithVerb(HttpMethod.Get)
.Times(3);

Assert.Equal(4, orderList.Count);
Assert.Equal((page1.Value[0].DocEntry, page1.Value[0].CardCode), (orderList[0].DocEntry, orderList[0].CardCode));
Assert.Equal((page1.Value[1].DocEntry, page1.Value[1].CardCode), (orderList[1].DocEntry, orderList[1].CardCode));
Assert.Equal((page2.Value[0].DocEntry, page2.Value[0].CardCode), (orderList[2].DocEntry, orderList[2].CardCode));
Assert.Equal((page2.Value[1].DocEntry, page2.Value[1].CardCode), (orderList[3].DocEntry, orderList[3].CardCode));
}
}
12 changes: 9 additions & 3 deletions B1SLayer.sln
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30907.101
# Visual Studio Version 17
VisualStudioVersion = 17.7.34003.232
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "B1SLayer", "B1SLayer\B1SLayer.csproj", "{47FDED3E-3365-478D-8EB4-5D4E636A7A05}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "B1SLayer", "B1SLayer\B1SLayer.csproj", "{47FDED3E-3365-478D-8EB4-5D4E636A7A05}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "B1SLayer.Tests", "B1SLayer.Test\B1SLayer.Test.csproj", "{C191C3CE-E24B-4D93-A37C-FB4E2ECFD268}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -15,6 +17,10 @@ Global
{47FDED3E-3365-478D-8EB4-5D4E636A7A05}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47FDED3E-3365-478D-8EB4-5D4E636A7A05}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47FDED3E-3365-478D-8EB4-5D4E636A7A05}.Release|Any CPU.Build.0 = Release|Any CPU
{C191C3CE-E24B-4D93-A37C-FB4E2ECFD268}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C191C3CE-E24B-4D93-A37C-FB4E2ECFD268}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C191C3CE-E24B-4D93-A37C-FB4E2ECFD268}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C191C3CE-E24B-4D93-A37C-FB4E2ECFD268}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
33 changes: 17 additions & 16 deletions B1SLayer/B1SLayer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>8.0</LangVersion>
<Authors>Bruno Giovani Mulinari</Authors>
<PackageProjectUrl>https://github.com/bgmulinari/B1SLayer</PackageProjectUrl>
<PackageIcon>icon.png</PackageIcon>
Expand All @@ -12,31 +13,31 @@
<PackageTags>sap business one;b1;service layer;http client;fluent;flurl</PackageTags>
<PackageReleaseNotes>https://github.com/bgmulinari/B1SLayer/releases</PackageReleaseNotes>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>1.3.2</Version>
<PackageId>B1SLayer</PackageId>
<Version>2.0.0</Version>
<LangVersion>12</LangVersion>
</PropertyGroup>

<PropertyGroup>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DocumentationFile>B1SLayer.xml</DocumentationFile>
</PropertyGroup>

<ItemGroup>
<None Remove="B1SLayer.xml" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="B1SLayer.xml" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
<PackageReference Include="Flurl.Http" Version="4.0.2" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<None Include="icon.png">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
<None Include="..\README.md" Pack="true" PackagePath="\" />
<None Include="icon.png" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
</Project>
Loading

0 comments on commit 1717d26

Please sign in to comment.