Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/test_linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ jobs:
cd tests/NATS.Client.CoreUnit.Tests
dotnet test -c Release --no-build

- name: Test Abstractions
run: |
cd tests/NATS.Client.Abstractions.Tests
dotnet test -c Release --no-build

- name: Test JetStream
run: |
killall nats-server 2> /dev/null | echo -n
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/test_windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ jobs:
cd tests/NATS.Client.CoreUnit.Tests
dotnet test -c Release --no-build

- name: Test Abstractions
run: |
cd tests/NATS.Client.Abstractions.Tests
dotnet test -c Release --no-build

- name: Test JetStream
run: |
tasklist | grep -i nats-server && taskkill -F -IM nats-server.exe
Expand Down
1 change: 1 addition & 0 deletions NATS.Net.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
<Project Path="src/NATS.Net/NATS.Net.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/NATS.Client.Abstractions.Tests/NATS.Client.Abstractions.Tests.csproj" />
<Project Path="tests/NATS.Client.CheckAbi/NATS.Client.CheckAbi.csproj" />
<Project Path="tests/NATS.Client.CheckAbiTransientLib/NATS.Client.CheckAbiTransientLib.csproj" />
<Project Path="tests/NATS.Client.CheckNativeAot/NATS.Client.CheckNativeAot.csproj" />
Expand Down
119 changes: 119 additions & 0 deletions tests/NATS.Client.Abstractions.Tests/AbstractionsBoundaryTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System.Buffers;
using System.Text;
using Microsoft.Extensions.Primitives;
using NATS.Client.Core;

namespace NATS.Client.Abstractions.Tests;

// Everything here is written against NATS.Client.Abstractions only (see the csproj: no Core
// reference). It stands in for a third-party adapter such as the CloudEvents SDK from issue #851,
// proving that consuming code can deconstruct a received message and construct an outgoing one
// using just the envelope (NatsMsgContext / INatsHeaders) and the serializer interfaces.
public class AbstractionsBoundaryTest
{
[Fact]
public void Deserialize_with_context_reads_envelope_and_payload()
{
var headers = new TestHeaders { ["ce-type"] = "com.example.order.created" };
var context = new NatsMsgContext("orders.new", replyTo: "_INBOX.reply", headers: headers);
var payload = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes("order-123"));

INatsDeserialize<CloudEventLike> deserializer = new CloudEventDeserializer();
var result = deserializer.Deserialize(payload, context);

result.Should().NotBeNull();
result!.Source.Should().Be("orders.new");
result.Type.Should().Be("com.example.order.created");
result.Data.Should().Be("order-123");
}

[Fact]
public void Serialize_writes_payload_through_the_interface()
{
INatsSerialize<CloudEventLike> serializer = new CloudEventDeserializer();
var writer = new TestBufferWriter();

serializer.Serialize(writer, new CloudEventLike("orders.new", "com.example.order.created", "order-123"));

Encoding.UTF8.GetString(writer.ToArray()).Should().Be("order-123");
}

private sealed class CloudEventLike
{
public CloudEventLike(string source, string? type, string data)
{
Source = source;
Type = type;
Data = data;
}

public string Source { get; }

public string? Type { get; }

public string Data { get; }
}

// A context-aware (de)serializer backed only by Abstractions types.
private sealed class CloudEventDeserializer : INatsSerializeWithContext<CloudEventLike>, INatsDeserializeWithContext<CloudEventLike>
{
public void Serialize(IBufferWriter<byte> bufferWriter, CloudEventLike value)
{
var bytes = Encoding.UTF8.GetBytes(value.Data);
bufferWriter.Write(bytes);
}

public void Serialize(IBufferWriter<byte> bufferWriter, CloudEventLike value, in NatsMsgContext context) =>
Serialize(bufferWriter, value);

public CloudEventLike? Deserialize(in ReadOnlySequence<byte> buffer) =>
new(source: string.Empty, type: null, data: Encoding.UTF8.GetString(buffer.ToArray()));

public CloudEventLike? Deserialize(in ReadOnlySequence<byte> buffer, in NatsMsgContext context)
{
var type = context.Headers is not null && context.Headers.TryGetValue("ce-type", out var values)
? values.ToString()
: null;

return new CloudEventLike(
source: context.Subject,
type: type,
data: Encoding.UTF8.GetString(buffer.ToArray()));
}
}

// INatsHeaders adds nothing beyond IDictionary<string, StringValues>, so Dictionary supplies
// every member. A consumer can read headers off a received message with only Abstractions.
private sealed class TestHeaders : Dictionary<string, StringValues>, INatsHeaders;

// Minimal IBufferWriter<byte>; ArrayBufferWriter<byte> is unavailable on net481.
private sealed class TestBufferWriter : IBufferWriter<byte>
{
private byte[] _buffer = new byte[256];
private int _written;

public void Advance(int count) => _written += count;

public Memory<byte> GetMemory(int sizeHint = 0)
{
Ensure(sizeHint);
return _buffer.AsMemory(_written);
}

public Span<byte> GetSpan(int sizeHint = 0)
{
Ensure(sizeHint);
return _buffer.AsSpan(_written);
}

public byte[] ToArray() => _buffer.AsSpan(0, _written).ToArray();

private void Ensure(int sizeHint)
{
if (_written + Math.Max(sizeHint, 1) <= _buffer.Length)
return;

Array.Resize(ref _buffer, Math.Max(_buffer.Length * 2, _written + sizeHint));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<TargetFrameworks Condition="'$(OS)' == 'Windows_NT'">$(TargetFrameworks);net481</TargetFrameworks>
<RuntimeIdentifiers Condition="'$(OS)' == 'Windows_NT'">any;win-x86</RuntimeIdentifiers>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);CS8002</NoWarn>
<ImplicitUsings>enable</ImplicitUsings>
<RunSettingsFilePath>$(MSBuildProjectDirectory)\..\xunit.runsettings</RunSettingsFilePath>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="[7,8)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit.v3" Version="1.0.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
<Using Include="FluentAssertions" />
</ItemGroup>

<!-- Deliberately references ONLY the Abstractions package: this project guards the use case
from issue #851 that a third party (e.g. a CloudEvents adapter) can build and deconstruct
NATS messages with just this small dependency and no transitive dependency on Core. -->
<ItemGroup>
<ProjectReference Include="..\..\src\NATS.Client.Abstractions\NATS.Client.Abstractions.csproj" />
</ItemGroup>

</Project>
Loading