feat: add suitelink client runtime and test harness
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
namespace SuiteLink.Client.IntegrationTests;
|
||||
|
||||
internal sealed record class IntegrationSettings(
|
||||
SuiteLinkConnectionOptions Connection,
|
||||
string? BooleanTag,
|
||||
string? IntegerTag,
|
||||
string? FloatTag,
|
||||
string? StringTag)
|
||||
{
|
||||
public static bool TryLoad(out IntegrationSettings settings, out string reason)
|
||||
{
|
||||
settings = null!;
|
||||
|
||||
var enabled = Environment.GetEnvironmentVariable("SUITELINK_IT_ENABLED");
|
||||
if (!string.Equals(enabled, "true", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
reason = "Set SUITELINK_IT_ENABLED=true to run live integration tests.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetRequired("SUITELINK_IT_HOST", out var host, out reason) ||
|
||||
!TryGetRequired("SUITELINK_IT_APPLICATION", out var application, out reason) ||
|
||||
!TryGetRequired("SUITELINK_IT_TOPIC", out var topic, out reason) ||
|
||||
!TryGetRequired("SUITELINK_IT_CLIENT_NAME", out var clientName, out reason) ||
|
||||
!TryGetRequired("SUITELINK_IT_CLIENT_NODE", out var clientNode, out reason) ||
|
||||
!TryGetRequired("SUITELINK_IT_USER_NAME", out var userName, out reason) ||
|
||||
!TryGetRequired("SUITELINK_IT_SERVER_NODE", out var serverNode, out reason))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var timezone = Environment.GetEnvironmentVariable("SUITELINK_IT_TIMEZONE");
|
||||
|
||||
var port = 5413;
|
||||
var portRaw = Environment.GetEnvironmentVariable("SUITELINK_IT_PORT");
|
||||
if (!string.IsNullOrWhiteSpace(portRaw) && !int.TryParse(portRaw, out port))
|
||||
{
|
||||
reason = "SUITELINK_IT_PORT must be a valid integer.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var connection = new SuiteLinkConnectionOptions(
|
||||
host: host,
|
||||
application: application,
|
||||
topic: topic,
|
||||
clientName: clientName,
|
||||
clientNode: clientNode,
|
||||
userName: userName,
|
||||
serverNode: serverNode,
|
||||
timezone: timezone,
|
||||
port: port);
|
||||
|
||||
settings = new IntegrationSettings(
|
||||
Connection: connection,
|
||||
BooleanTag: Normalize(Environment.GetEnvironmentVariable("SUITELINK_IT_BOOL_TAG")),
|
||||
IntegerTag: Normalize(Environment.GetEnvironmentVariable("SUITELINK_IT_INT_TAG")),
|
||||
FloatTag: Normalize(Environment.GetEnvironmentVariable("SUITELINK_IT_FLOAT_TAG")),
|
||||
StringTag: Normalize(Environment.GetEnvironmentVariable("SUITELINK_IT_STRING_TAG")));
|
||||
|
||||
reason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string? Normalize(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
|
||||
private static bool TryGetRequired(string name, out string value, out string reason)
|
||||
{
|
||||
value = Environment.GetEnvironmentVariable(name) ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
reason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
reason = $"Missing required environment variable: {name}.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
36
tests/SuiteLink.Client.IntegrationTests/README.md
Normal file
36
tests/SuiteLink.Client.IntegrationTests/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# SuiteLink Integration Tests
|
||||
|
||||
These tests are intentionally safe by default and run only when explicitly enabled.
|
||||
|
||||
## Enable
|
||||
|
||||
Set:
|
||||
|
||||
- `SUITELINK_IT_ENABLED=true`
|
||||
|
||||
Required connection variables:
|
||||
|
||||
- `SUITELINK_IT_HOST`
|
||||
- `SUITELINK_IT_APPLICATION`
|
||||
- `SUITELINK_IT_TOPIC`
|
||||
- `SUITELINK_IT_CLIENT_NAME`
|
||||
- `SUITELINK_IT_CLIENT_NODE`
|
||||
- `SUITELINK_IT_USER_NAME`
|
||||
- `SUITELINK_IT_SERVER_NODE`
|
||||
|
||||
Optional connection variables:
|
||||
|
||||
- `SUITELINK_IT_PORT` (default `5413`)
|
||||
- `SUITELINK_IT_TIMEZONE` (defaults to `UTC` via `SuiteLinkConnectionOptions`)
|
||||
|
||||
Optional tag variables (tests run only for the tags provided):
|
||||
|
||||
- `SUITELINK_IT_BOOL_TAG`
|
||||
- `SUITELINK_IT_INT_TAG`
|
||||
- `SUITELINK_IT_FLOAT_TAG`
|
||||
- `SUITELINK_IT_STRING_TAG`
|
||||
|
||||
## Notes
|
||||
|
||||
- If integration settings are missing, tests return immediately and do not perform network calls.
|
||||
- These tests are intended as a live harness, not deterministic CI tests.
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SuiteLink.Client\SuiteLink.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
109
tests/SuiteLink.Client.IntegrationTests/TagRoundTripTests.cs
Normal file
109
tests/SuiteLink.Client.IntegrationTests/TagRoundTripTests.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
namespace SuiteLink.Client.IntegrationTests;
|
||||
|
||||
public sealed class TagRoundTripTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task BooleanTag_RoundTrip_WhenConfigured()
|
||||
{
|
||||
if (!TryGetTagSettings(out var settings, out var tagName, "bool"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await RunRoundTripAsync(
|
||||
settings,
|
||||
tagName,
|
||||
SuiteLinkValue.FromBoolean(true),
|
||||
value => value.TryGetBoolean(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IntegerTag_RoundTrip_WhenConfigured()
|
||||
{
|
||||
if (!TryGetTagSettings(out var settings, out var tagName, "int"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await RunRoundTripAsync(
|
||||
settings,
|
||||
tagName,
|
||||
SuiteLinkValue.FromInt32(42),
|
||||
value => value.TryGetInt32(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FloatTag_RoundTrip_WhenConfigured()
|
||||
{
|
||||
if (!TryGetTagSettings(out var settings, out var tagName, "float"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await RunRoundTripAsync(
|
||||
settings,
|
||||
tagName,
|
||||
SuiteLinkValue.FromFloat32(12.25f),
|
||||
value => value.TryGetFloat32(out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StringTag_RoundTrip_WhenConfigured()
|
||||
{
|
||||
if (!TryGetTagSettings(out var settings, out var tagName, "string"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await RunRoundTripAsync(
|
||||
settings,
|
||||
tagName,
|
||||
SuiteLinkValue.FromString("integration-test"),
|
||||
value => value.TryGetString(out _));
|
||||
}
|
||||
|
||||
private static bool TryGetTagSettings(
|
||||
out IntegrationSettings settings,
|
||||
out string tagName,
|
||||
string type)
|
||||
{
|
||||
settings = null!;
|
||||
tagName = string.Empty;
|
||||
|
||||
if (!IntegrationSettings.TryLoad(out settings, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
tagName = type switch
|
||||
{
|
||||
"bool" => settings.BooleanTag ?? string.Empty,
|
||||
"int" => settings.IntegerTag ?? string.Empty,
|
||||
"float" => settings.FloatTag ?? string.Empty,
|
||||
"string" => settings.StringTag ?? string.Empty,
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
return !string.IsNullOrWhiteSpace(tagName);
|
||||
}
|
||||
|
||||
private static async Task RunRoundTripAsync(
|
||||
IntegrationSettings settings,
|
||||
string tagName,
|
||||
SuiteLinkValue writeValue,
|
||||
Func<SuiteLinkValue, bool> typeCheck)
|
||||
{
|
||||
await using var client = new SuiteLinkClient();
|
||||
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
||||
await client.ConnectAsync(settings.Connection, cts.Token);
|
||||
|
||||
var readBefore = await client.ReadAsync(tagName, TimeSpan.FromSeconds(10), cts.Token);
|
||||
Assert.True(typeCheck(readBefore.Value));
|
||||
|
||||
await client.WriteAsync(tagName, writeValue, cts.Token);
|
||||
|
||||
var readAfter = await client.ReadAsync(tagName, TimeSpan.FromSeconds(10), cts.Token);
|
||||
Assert.True(typeCheck(readAfter.Value));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user