diff --git a/rosette_api/ApiClient.cs b/rosette_api/ApiClient.cs
index 366de44..9012106 100644
--- a/rosette_api/ApiClient.cs
+++ b/rosette_api/ApiClient.cs
@@ -52,7 +52,7 @@ public class ApiClient : IDisposable
///
/// Required Rosette API key
public ApiClient(string apiKey) {
- ArgumentNullException.ThrowIfNull(apiKey);
+ ArgumentException.ThrowIfNullOrWhiteSpace(apiKey);
APIKey = apiKey;
URI = "https://analytics.babelstreet.com/rest/v1/";
Client = null;
@@ -70,6 +70,14 @@ public ApiClient(string apiKey) {
/// Destination URL string
/// RosetteAPI object
public ApiClient UseAlternateURL(string urlString) {
+ ArgumentException.ThrowIfNullOrWhiteSpace(urlString);
+
+ // Validate URL format by attempting to create Uri
+ if (!Uri.TryCreate(urlString, UriKind.Absolute, out _))
+ {
+ throw new UriFormatException($"Invalid URL format: {urlString}");
+ }
+
URI = urlString.EndsWith("/") ? urlString : urlString + "/";
return Prepare();
@@ -82,6 +90,7 @@ public ApiClient UseAlternateURL(string urlString) {
/// A valid HttpClient
/// RosetteAPI object
public ApiClient AssignClient(HttpClient client) {
+ ArgumentNullException.ThrowIfNull(client);
Client = client;
return Prepare();
@@ -131,10 +140,11 @@ public ApiClient SetDebug(bool state=true) {
/// Value of header
/// RosetteAPI object
public ApiClient AddCustomHeader(string headerName, string headerValue) {
- if (!headerName.StartsWith("X-RosetteAPI-", StringComparison.OrdinalIgnoreCase))
+ if (!headerName.StartsWith("X-RosetteAPI-", StringComparison.OrdinalIgnoreCase) ||
+ headerName.Length <= "X-RosetteAPI-".Length)
{
throw new ArgumentException(
- $"Custom header name must begin with 'X-RosetteAPI-'. Provided: {headerName}",
+ $"Custom header name must begin with 'X-RosetteAPI-' and have additional characters. Provided: {headerName}",
nameof(headerName));
}
if (_customHeaders.ContainsKey(headerName) && headerValue == null)
@@ -169,7 +179,10 @@ private ApiClient Prepare(bool forceUpdate=false) {
});
_disposeClient = true;
}
- Client.Timeout = TimeSpan.FromSeconds(Timeout);
+ // HttpClient.Timeout must be > 0 or Infinite
+ Client.Timeout = Timeout == 0
+ ? System.Threading.Timeout.InfiniteTimeSpan
+ : TimeSpan.FromSeconds(Timeout);
if (Client.BaseAddress == null) {
Client.BaseAddress = new Uri(URI); // base address must be the rosette URI regardless of whether the client is external or internal
diff --git a/rosette_api/Models/JsonConverter/RecordSimilarityFieldConverter.cs b/rosette_api/Models/JsonConverter/RecordSimilarityFieldConverter.cs
index 12a8a8c..be8c59a 100644
--- a/rosette_api/Models/JsonConverter/RecordSimilarityFieldConverter.cs
+++ b/rosette_api/Models/JsonConverter/RecordSimilarityFieldConverter.cs
@@ -20,15 +20,6 @@ public override void Write(Utf8JsonWriter writer, RecordSimilarityField value, J
{
switch (value)
{
- case UnfieldedNameRecord unfieldedName:
- writer.WriteStringValue(unfieldedName.Text);
- break;
- case UnfieldedDateRecord unfieldedDate:
- writer.WriteStringValue(unfieldedDate.Date);
- break;
- case UnfieldedAddressRecord unfieldedAddress:
- writer.WriteStringValue(unfieldedAddress.Address);
- break;
case FieldedNameRecord fieldedName:
JsonSerializer.Serialize(writer, fieldedName, options);
break;
@@ -38,24 +29,11 @@ public override void Write(Utf8JsonWriter writer, RecordSimilarityField value, J
case FieldedAddressRecord fieldedAddress:
JsonSerializer.Serialize(writer, fieldedAddress, options);
break;
- case NumberRecord numberRecord:
- writer.WriteNumberValue(numberRecord.Number);
- break;
- case BooleanRecord booleanRecord:
- writer.WriteBooleanValue(booleanRecord.Boolean);
- break;
- case StringRecord stringRecord:
- writer.WriteStringValue(stringRecord.Text);
- break;
- case UnknownFieldRecord unknownField:
- if (unknownField.Data != null)
- {
- unknownField.Data.WriteTo(writer, options);
- }
- else
- {
- writer.WriteNullValue();
- }
+ // Unfielded records are handled by UnfieldedRecordSimilarityConverter
+ case UnfieldedNameRecord or UnfieldedDateRecord or UnfieldedAddressRecord:
+ case NumberRecord or BooleanRecord or StringRecord or UnknownFieldRecord:
+ // Use the UnfieldedRecordSimilarityConverter for these types
+ JsonSerializer.Serialize(writer, value, value.GetType(), options);
break;
default:
JsonSerializer.Serialize(writer, value, value.GetType(), options);
diff --git a/rosette_api/Models/JsonConverter/UnfieldedRecordSimilarityConverter.cs b/rosette_api/Models/JsonConverter/UnfieldedRecordSimilarityConverter.cs
index e4ff1e2..91d2241 100644
--- a/rosette_api/Models/JsonConverter/UnfieldedRecordSimilarityConverter.cs
+++ b/rosette_api/Models/JsonConverter/UnfieldedRecordSimilarityConverter.cs
@@ -18,10 +18,13 @@ public UnfieldedRecordSimilarityConverter()
public override bool CanConvert(Type typeToConvert)
{
- return typeToConvert == typeof(UnknownFieldRecord) ||
+ return typeToConvert == typeof(UnfieldedNameRecord) ||
+ typeToConvert == typeof(UnfieldedDateRecord) ||
+ typeToConvert == typeof(UnfieldedAddressRecord) ||
typeToConvert == typeof(NumberRecord) ||
typeToConvert == typeof(BooleanRecord) ||
- typeToConvert == typeof(UnfieldedAddressRecord) ||
+ typeToConvert == typeof(StringRecord) ||
+ typeToConvert == typeof(UnknownFieldRecord) ||
typeToConvert == typeof(object);
}
@@ -31,9 +34,14 @@ public override bool CanConvert(Type typeToConvert)
{
JsonTokenType.Number => new NumberRecord { Number = reader.GetDouble() },
JsonTokenType.True or JsonTokenType.False => new BooleanRecord { Boolean = reader.GetBoolean() },
- JsonTokenType.String => typeToConvert == typeof(UnfieldedAddressRecord)
- ? new UnfieldedAddressRecord { Address = reader.GetString() ?? string.Empty }
- : reader.GetString(),
+ JsonTokenType.String => typeToConvert switch
+ {
+ Type t when t == typeof(UnfieldedNameRecord) => new UnfieldedNameRecord { Text = reader.GetString() ?? string.Empty },
+ Type t when t == typeof(UnfieldedDateRecord) => new UnfieldedDateRecord { Date = reader.GetString() ?? string.Empty },
+ Type t when t == typeof(UnfieldedAddressRecord) => new UnfieldedAddressRecord { Address = reader.GetString() ?? string.Empty },
+ Type t when t == typeof(StringRecord) => new StringRecord { Text = reader.GetString() ?? string.Empty },
+ _ => reader.GetString()
+ },
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, options),
_ => null
};
@@ -43,18 +51,34 @@ public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOp
{
switch (value)
{
+ case UnfieldedNameRecord unfieldedName:
+ writer.WriteStringValue(unfieldedName.Text);
+ break;
+ case UnfieldedDateRecord unfieldedDate:
+ writer.WriteStringValue(unfieldedDate.Date);
+ break;
case UnfieldedAddressRecord unfieldedAddress:
writer.WriteStringValue(unfieldedAddress.Address);
break;
- case UnknownFieldRecord unknownField:
- JsonSerializer.Serialize(writer, unknownField.Data, options);
- break;
case NumberRecord numberRecord:
writer.WriteNumberValue(numberRecord.Number);
break;
case BooleanRecord booleanRecord:
writer.WriteBooleanValue(booleanRecord.Boolean);
break;
+ case StringRecord stringRecord:
+ writer.WriteStringValue(stringRecord.Text);
+ break;
+ case UnknownFieldRecord unknownField:
+ if (unknownField.Data != null)
+ {
+ unknownField.Data.WriteTo(writer, options);
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
+ break;
case string stringValue:
writer.WriteStringValue(stringValue);
break;
diff --git a/rosette_api/Utilities.cs b/rosette_api/Utilities.cs
index ff2adc3..8757d88 100644
--- a/rosette_api/Utilities.cs
+++ b/rosette_api/Utilities.cs
@@ -25,7 +25,7 @@ public static bool DictionaryEquals(this IDictionary
{
return false;
}
- if (!kvp.Value.Equals(secondValue))
+ if (!EqualityComparer.Default.Equals(kvp.Value, secondValue))
{
return false;
}
diff --git a/tests/ApiClientTests.cs b/tests/ApiClientTests.cs
index 73e2102..52ba197 100644
--- a/tests/ApiClientTests.cs
+++ b/tests/ApiClientTests.cs
@@ -6,17 +6,28 @@ public class ApiClientTests
{
private static readonly string _defaultUri = "https://analytics.babelstreet.com/rest/v1/";
private static readonly string _testKey = "testKey";
-
+
private static ApiClient Init() {
return new ApiClient(_testKey);
}
+ #region Basic Property Tests
+
[Fact]
public void ApiKey_ReturnsProvidedKey_WhenInitialized() {
ApiClient api = Init();
Assert.Equal(_testKey, api.APIKey);
}
+ [Fact]
+ public void Version_IsNotEmpty_Always() {
+ Assert.NotEmpty(ApiClient.Version);
+ }
+
+ #endregion
+
+ #region Constructor Tests
+
[Fact]
public void Constructor_ThrowsArgumentNullException_WhenApiKeyIsNull() {
#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
@@ -29,6 +40,22 @@ public void Constructor_ThrowsArgumentNullException_WhenApiKeyIsNull() {
Assert.Equal("Value cannot be null. (Parameter 'apiKey')", ex.Message);
}
+ [Fact]
+ public void Constructor_EmptyApiKey_ThrowsArgumentException()
+ {
+ Assert.Throws(() => new ApiClient(""));
+ }
+
+ [Fact]
+ public void Constructor_WhitespaceApiKey_ThrowsArgumentException()
+ {
+ Assert.Throws(() => new ApiClient(" "));
+ }
+
+ #endregion
+
+ #region URI and UseAlternateURL Tests
+
[Fact]
public void URI_ReturnsDefaultAndAppendsTrailingSlash_WhenUsingAlternateUrl() {
ApiClient api = Init();
@@ -40,6 +67,166 @@ public void URI_ReturnsDefaultAndAppendsTrailingSlash_WhenUsingAlternateUrl() {
Assert.Equal(alternateUrl + "/", api.URI);
}
+ [Fact]
+ public void UseAlternateURL_NullUrl_ThrowsArgumentNullException()
+ {
+ var api = new ApiClient("key");
+ Assert.Throws(() => api.UseAlternateURL(null));
+ }
+
+ [Fact]
+ public void UseAlternateURL_InvalidUrl_ThrowsException()
+ {
+ var api = new ApiClient("key");
+ Assert.Throws(() => api.UseAlternateURL("not a url"));
+ }
+
+ [Fact]
+ public void UseAlternateURL_EmptyString_ThrowsArgumentException()
+ {
+ var api = Init();
+ Assert.Throws(() => api.UseAlternateURL(""));
+ }
+
+ [Fact]
+ public void UseAlternateURL_WhitespaceOnly_ThrowsArgumentException()
+ {
+ var api = Init();
+ Assert.Throws(() => api.UseAlternateURL(" "));
+ }
+
+ [Fact]
+ public void UseAlternateURL_UrlWithoutScheme_ThrowsUriFormatException()
+ {
+ var api = Init();
+ Assert.Throws(() => api.UseAlternateURL("example.com/api"));
+ }
+
+ [Fact]
+ public void UseAlternateURL_MultipleTrailingSlashes_NormalizesToOneSlash()
+ {
+ var api = Init();
+ api.UseAlternateURL("https://api.example.com////");
+ // Code adds one slash if not present, doesn't normalize multiple slashes
+ Assert.Equal("https://api.example.com////", api.URI);
+ }
+
+ [Fact]
+ public void UseAlternateURL_ValidUrlWithQueryString_PreservesQueryString()
+ {
+ var api = Init();
+ string urlWithQuery = "https://api.example.com/v1?key=value";
+ api.UseAlternateURL(urlWithQuery);
+ Assert.Equal(urlWithQuery + "/", api.URI);
+ }
+
+ [Fact]
+ public void UseAlternateURL_CalledMultipleTimes_UpdatesURIEachTime()
+ {
+ var api = Init();
+
+ api.UseAlternateURL("https://url1.com");
+ Assert.Equal("https://url1.com/", api.URI);
+
+ api.UseAlternateURL("https://url2.com");
+ Assert.Equal("https://url2.com/", api.URI);
+ }
+
+ #endregion
+
+ #region Client Configuration Tests
+
+ [Fact]
+ public void Client_HasCorrectDefaultConfiguration_WhenInitialized() {
+ ApiClient api = Init();
+
+ Assert.Equal(_defaultUri, api.Client.BaseAddress.AbsoluteUri);
+ var acceptHeader = new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json");
+ Assert.Contains(acceptHeader, api.Client.DefaultRequestHeaders.Accept);
+ foreach (string encodingType in new List() { "gzip", "deflate" }) {
+ var encodingHeader = new System.Net.Http.Headers.StringWithQualityHeaderValue(encodingType);
+ Assert.Contains(encodingHeader, api.Client.DefaultRequestHeaders.AcceptEncoding);
+ }
+ Assert.Equal(api.Timeout, api.Client.Timeout.TotalSeconds);
+ }
+
+ #endregion
+
+ #region AssignClient Tests
+
+ [Fact]
+ public void AssignClient_NullClient_ThrowsArgumentNullException()
+ {
+ var api = new ApiClient("key");
+ Assert.Throws(() => api.AssignClient(null));
+ }
+
+ [Fact]
+ public void AssignClient_WithDisposedClient_ThrowsObjectDisposedException()
+ {
+ var api = Init();
+ var disposedClient = new HttpClient();
+ disposedClient.Dispose();
+
+ // Assigning disposed client throws when Prepare() tries to set Timeout
+ Assert.Throws(() => api.AssignClient(disposedClient));
+ }
+
+ [Fact]
+ public void AssignClient_WithExistingBaseAddress_DoesNotOverwriteImmediately()
+ {
+ var api = Init();
+ var otherUri = "https://other.com/api/";
+ var client = new HttpClient { BaseAddress = new Uri(otherUri) };
+
+ api.AssignClient(client);
+
+ // BaseAddress is set in Prepare() only if null
+ // User client's BaseAddress is preserved
+ Assert.Equal(otherUri, client.BaseAddress!.ToString());
+ }
+
+ [Fact]
+ public void AssignClient_PreservesUserClient_DoesNotDisposeOnApiDispose()
+ {
+ var api = Init();
+ var userClient = new HttpClient();
+
+ api.AssignClient(userClient);
+ api.Dispose();
+
+ // User's client should still be usable (not disposed)
+ // Verify by checking BaseAddress is still accessible
+ var exception = Record.Exception(() => { var _ = userClient.BaseAddress; });
+ Assert.Null(exception);
+
+ // Clean up
+ userClient.Dispose();
+ }
+
+ [Fact]
+ public void AssignConcurrentConnections_AfterUserClient_ReplacesClientWithInternalOne()
+ {
+ var api = Init();
+ var userClient = new HttpClient();
+
+ api.AssignClient(userClient);
+ var assignedClient = api.Client;
+ Assert.Same(userClient, assignedClient);
+
+ // Changing concurrent connections forces new internal client
+ api.AssignConcurrentConnections(5);
+
+ Assert.NotSame(userClient, api.Client);
+
+ // Clean up
+ userClient.Dispose();
+ }
+
+ #endregion
+
+ #region ConcurrentConnections Tests
+
[Fact]
public void ConcurrentConnections_ReturnsCorrectValue_WhenDefaultAndAssigned() {
ApiClient api = Init();
@@ -50,18 +237,46 @@ public void ConcurrentConnections_ReturnsCorrectValue_WhenDefaultAndAssigned() {
}
[Fact]
- public void AddCustomHeader_ThrowsArgumentException_WhenHeaderNameIsInvalid() {
- ApiClient api = Init();
- Exception ex = Assert.Throws(() => api.AddCustomHeader("BogusHeader", "BogusValue"));
+ public void AssignConcurrentConnections_One_ClampsToMinimumOfTwo()
+ {
+ var api = Init();
+ api.AssignConcurrentConnections(1);
- Assert.Contains(@"Custom header name must begin with 'X-RosetteAPI-'", ex.Message);
+ // Should be clamped to minimum of 2
+ Assert.Equal(2, api.ConcurrentConnections);
}
[Fact]
- public void Version_IsNotEmpty_Always() {
- Assert.NotEmpty(ApiClient.Version);
+ public void AssignConcurrentConnections_Zero_ClampsToMinimumOfTwo()
+ {
+ var api = Init();
+ api.AssignConcurrentConnections(0);
+
+ Assert.Equal(2, api.ConcurrentConnections);
}
+ [Fact]
+ public void AssignConcurrentConnections_NegativeValue_ClampsToMinimumOfTwo()
+ {
+ var api = Init();
+ api.AssignConcurrentConnections(-5);
+
+ Assert.Equal(2, api.ConcurrentConnections);
+ }
+
+ [Fact]
+ public void AssignConcurrentConnections_LargeValue_AcceptsValue()
+ {
+ var api = Init();
+ api.AssignConcurrentConnections(1000);
+
+ Assert.Equal(1000, api.ConcurrentConnections);
+ }
+
+ #endregion
+
+ #region Timeout Tests
+
[Fact]
public void Timeout_ReturnsCorrectValue_WhenDefaultAndAssigned() {
ApiClient api = Init();
@@ -71,6 +286,60 @@ public void Timeout_ReturnsCorrectValue_WhenDefaultAndAssigned() {
Assert.Equal(15, api.Timeout);
}
+ [Fact]
+ public void AssignTimeout_Zero_SetsTimeoutToInfinite()
+ {
+ var api = Init();
+ api.AssignTimeout(0);
+
+ Assert.Equal(0, api.Timeout);
+ // HttpClient.Timeout of 0 gets set to Infinite
+ Assert.Equal(System.Threading.Timeout.InfiniteTimeSpan, api.Client!.Timeout);
+ }
+
+ [Fact]
+ public void AssignTimeout_NegativeValue_ClampsToZeroAndSetsInfiniteTimeout()
+ {
+ var api = Init();
+ api.AssignTimeout(-10);
+
+ // Negative values should be clamped to 0
+ Assert.Equal(0, api.Timeout);
+ Assert.Equal(System.Threading.Timeout.InfiniteTimeSpan, api.Client!.Timeout);
+ }
+
+ [Fact]
+ public void AssignTimeout_VeryLargeValue_SetsTimeoutSuccessfully()
+ {
+ var api = Init();
+ // Use a large but reasonable timeout value (1 day in seconds)
+ int largeTimeout = 86400; // 24 hours
+
+ api.AssignTimeout(largeTimeout);
+
+ Assert.Equal(largeTimeout, api.Timeout);
+ Assert.Equal(TimeSpan.FromSeconds(largeTimeout), api.Client!.Timeout);
+ }
+
+ [Fact]
+ public void AssignTimeout_CalledMultipleTimes_UpdatesEachTime()
+ {
+ var api = Init();
+
+ api.AssignTimeout(10);
+ Assert.Equal(10, api.Timeout);
+
+ api.AssignTimeout(20);
+ Assert.Equal(20, api.Timeout);
+
+ api.AssignTimeout(30);
+ Assert.Equal(30, api.Timeout);
+ }
+
+ #endregion
+
+ #region Debug Tests
+
[Fact]
public void Debug_TogglesToTrue_WhenSetDebugCalled() {
ApiClient api = Init();
@@ -80,18 +349,141 @@ public void Debug_TogglesToTrue_WhenSetDebugCalled() {
Assert.True(api.Debug);
}
+ #endregion
+
+ #region AddCustomHeader Tests
+
[Fact]
- public void Client_HasCorrectDefaultConfiguration_WhenInitialized() {
+ public void AddCustomHeader_ThrowsArgumentException_WhenHeaderNameIsInvalid() {
ApiClient api = Init();
+ Exception ex = Assert.Throws(() => api.AddCustomHeader("BogusHeader", "BogusValue"));
- Assert.Equal(_defaultUri, api.Client.BaseAddress.AbsoluteUri);
- var acceptHeader = new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json");
- Assert.Contains(acceptHeader, api.Client.DefaultRequestHeaders.Accept);
- foreach (string encodingType in new List() { "gzip", "deflate" }) {
- var encodingHeader = new System.Net.Http.Headers.StringWithQualityHeaderValue(encodingType);
- Assert.Contains(encodingHeader, api.Client.DefaultRequestHeaders.AcceptEncoding);
- }
- Assert.Equal(api.Timeout, api.Client.Timeout.TotalSeconds);
+ Assert.Contains(@"Custom header name must begin with 'X-RosetteAPI-'", ex.Message);
+ }
+
+ [Fact]
+ public void AddCustomHeader_JustPrefix_ThrowsArgumentException()
+ {
+ var api = Init();
+ var exception = Assert.Throws(() =>
+ api.AddCustomHeader("X-RosetteAPI-", "value"));
+
+ Assert.Contains("X-RosetteAPI-", exception.Message);
+ }
+
+ [Fact]
+ public void AddCustomHeader_EmptyHeaderValue_AddsHeaderSuccessfully()
+ {
+ var api = Init();
+
+ // Empty string is valid for HTTP headers
+ var exception = Record.Exception(() =>
+ api.AddCustomHeader("X-RosetteAPI-Test", ""));
+
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void AddCustomHeader_CaseInsensitivePrefix_AcceptsLowercase()
+ {
+ var api = Init();
+
+ // The check uses OrdinalIgnoreCase, so lowercase should work
+ var exception = Record.Exception(() =>
+ api.AddCustomHeader("x-rosetteapi-custom", "value"));
+
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void AddCustomHeader_NullValue_RemovesExistingHeader()
+ {
+ var api = Init();
+
+ api.AddCustomHeader("X-RosetteAPI-Test", "value");
+
+ // Setting to null should remove the header
+ var exception = Record.Exception(() =>
+ api.AddCustomHeader("X-RosetteAPI-Test", null!));
+
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void AddCustomHeader_NullValueForNonExistentHeader_DoesNotThrow()
+ {
+ var api = Init();
+
+ // Removing non-existent header should not throw
+ var exception = Record.Exception(() =>
+ api.AddCustomHeader("X-RosetteAPI-NonExistent", null!));
+
+ Assert.Null(exception);
+ }
+
+ #endregion
+
+ #region Dispose Pattern Tests
+
+ [Fact]
+ public void Dispose_CalledTwice_DoesNotThrow()
+ {
+ var api = Init();
+ api.Dispose();
+
+ var exception = Record.Exception(() => api.Dispose());
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void Dispose_DisposesInternalClient_WhenNotUserProvided()
+ {
+ var api = Init();
+ var client = api.Client;
+
+ api.Dispose();
+
+ // In .NET the HttpClient might not immediately throw on property access after dispose
+ // Instead verify that operations fail
+ Assert.Throws(() => client!.GetAsync("http://test.com").GetAwaiter().GetResult());
+ }
+
+ [Fact]
+ public void Dispose_DoesNotDisposeUserClient_WhenUserProvided()
+ {
+ var api = Init();
+ var userClient = new HttpClient();
+
+ api.AssignClient(userClient);
+ api.Dispose();
+
+ // User client should still be usable
+ var exception = Record.Exception(() => { var _ = userClient.BaseAddress; });
+ Assert.Null(exception);
+
+ // Clean up
+ userClient.Dispose();
+ }
+
+ #endregion
+
+ #region Fluent API and State Management Tests
+
+ [Fact]
+ public void FluentAPI_MultipleMethodCalls_AllReturnSameInstance()
+ {
+ var api = Init();
+
+ var result = api
+ .UseAlternateURL("https://test.com/")
+ .AssignTimeout(60)
+ .SetDebug()
+ .AddCustomHeader("X-RosetteAPI-Test", "value")
+ .AssignConcurrentConnections(5);
+
+ Assert.Same(api, result);
}
+ #endregion
+
}
diff --git a/tests/EndpointBaseTests.cs b/tests/EndpointBaseTests.cs
deleted file mode 100644
index 4a7acfb..0000000
--- a/tests/EndpointBaseTests.cs
+++ /dev/null
@@ -1,629 +0,0 @@
-using RichardSzalay.MockHttp;
-using Rosette.Api.Client;
-using Rosette.Api.Client.Endpoints;
-using Rosette.Api.Client.Models;
-using System.Net;
-
-namespace Rosette.Api.Tests
-{
- public class EndpointBaseTests
- {
- private static readonly string _defaultUri = "https://analytics.babelstreet.com/rest/v1/*";
-
- [Fact]
- public void Constructor_ValidEndpoint_SetsEndpointName()
- {
- // Arrange & Act
- var endpoint = new Entities("test content");
-
- // Assert
- Assert.Equal("entities", endpoint.Endpoint);
- }
-
- [Fact]
- public void Options_InitialState_IsEmpty()
- {
- // Arrange & Act
- var endpoint = new Entities("foo");
-
- // Assert
- Assert.Empty(endpoint.Options);
- }
-
- [Fact]
- public void Params_InitialState_IsNotEmpty()
- {
- // Arrange & Act
- var endpoint = new Entities("foo");
-
- // Assert
- Assert.NotEmpty(endpoint.Params);
- }
-
- [Fact]
- public void UrlParameters_InitialState_IsEmpty()
- {
- // Arrange & Act
- var endpoint = new Entities("foo");
-
- // Assert
- Assert.Empty(endpoint.UrlParameters);
- }
-
- [Fact]
- public void SetOption_ValidOption_AddsToOptions()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act
- e.SetOption("test", "value");
-
- // Assert
- Assert.Equal("value", e.Options["test"]);
- }
-
- [Fact]
- public void SetOption_MultipleOptions_AddsAllOptions()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act
- e.SetOption("test", "value");
- e.SetOption("test2", "value2");
-
- // Assert
- Assert.Equal("value", e.Options["test"]);
- Assert.Equal("value2", e.Options["test2"]);
- }
-
- [Fact]
- public void SetOption_NullOptionName_ThrowsArgumentNullException()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act & Assert
- Assert.Throws(() => e.SetOption(null!, "value"));
- }
-
- [Fact]
- public void SetOption_EmptyOptionName_ThrowsArgumentException()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act & Assert
- Assert.Throws(() => e.SetOption(string.Empty, "value"));
- }
-
- [Fact]
- public void SetOption_NullOptionValue_ThrowsArgumentNullException()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act & Assert
- Assert.Throws(() => e.SetOption("test", null!));
- }
-
- [Fact]
- public void SetOption_ReturnsInstance_ForFluentAPI()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act
- var result = e.SetOption("test", "value");
-
- // Assert
- Assert.Same(e, result);
- }
-
- [Fact]
- public void RemoveOption_ExistingOption_RemovesOption()
- {
- // Arrange
- Entities e = new Entities("foo")
- .SetOption("test", "value");
-
- // Act
- e.RemoveOption("test");
-
- // Assert
- Assert.False(e.Options.ContainsKey("test"));
- }
-
- [Fact]
- public void RemoveOption_NonExistingOption_DoesNotThrow()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act & Assert
- var exception = Record.Exception(() => e.RemoveOption("nonexistent"));
- Assert.Null(exception);
- }
-
- [Fact]
- public void RemoveOption_NullKey_ThrowsArgumentNullException()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act & Assert
- Assert.Throws(() => e.RemoveOption(null!));
- }
-
- [Fact]
- public void RemoveOption_ReturnsInstance_ForFluentAPI()
- {
- // Arrange
- Entities e = new Entities("foo")
- .SetOption("test", "value");
-
- // Act
- var result = e.RemoveOption("test");
-
- // Assert
- Assert.Same(e, result);
- }
-
- [Fact]
- public void ClearOptions_WithOptions_RemovesAllOptions()
- {
- // Arrange
- Entities e = new Entities("foo")
- .SetOption("test1", "value1")
- .SetOption("test2", "value2");
-
- // Act
- e.ClearOptions();
-
- // Assert
- Assert.Empty(e.Options);
- }
-
- [Fact]
- public void ClearOptions_ReturnsInstance_ForFluentAPI()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act
- var result = e.ClearOptions();
-
- // Assert
- Assert.Same(e, result);
- }
-
- [Fact]
- public void SetUrlParameter_ValidParameter_AddsToUrlParameters()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act
- e.SetUrlParameter("test", "value");
-
- // Assert
- Assert.Equal("value", e.UrlParameters["test"]);
- }
-
- [Fact]
- public void SetUrlParameter_NullKey_ThrowsArgumentNullException()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act & Assert
- Assert.Throws(() => e.SetUrlParameter(null!, "value"));
- }
-
- [Fact]
- public void SetUrlParameter_NullValue_ThrowsArgumentNullException()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act & Assert
- Assert.Throws(() => e.SetUrlParameter("key", null!));
- }
-
- [Fact]
- public void SetUrlParameter_ReturnsInstance_ForFluentAPI()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act
- var result = e.SetUrlParameter("test", "value");
-
- // Assert
- Assert.Same(e, result);
- }
-
- [Fact]
- public void RemoveUrlParameter_ExistingParameter_RemovesParameter()
- {
- // Arrange
- Entities e = new Entities("foo")
- .SetUrlParameter("test", "value");
-
- // Act
- e.RemoveUrlParameter("test");
-
- // Assert
- Assert.Empty(e.UrlParameters);
- }
-
- [Fact]
- public void RemoveUrlParameter_NullKey_ThrowsArgumentNullException()
- {
- // Arrange
- Entities e = new("foo");
-
- // Act & Assert
- Assert.Throws(() => e.RemoveUrlParameter(null!));
- }
-
- [Fact]
- public void RemoveUrlParameter_ReturnsInstance_ForFluentAPI()
- {
- // Arrange
- Entities e = new Entities("foo")
- .SetUrlParameter("test", "value");
-
- // Act
- var result = e.RemoveUrlParameter("test");
-
- // Assert
- Assert.Same(e, result);
- }
-
- [Fact]
- public void FluentAPI_ChainMultipleMethods_AllSettersReturnSameInstance()
- {
- // Arrange & Act
- var endpoint = new Entities("content")
- .SetOption("opt1", "val1")
- .SetOption("opt2", "val2")
- .SetUrlParameter("param", "value")
- .SetLanguage("eng");
-
- // Assert
- Assert.Equal("val1", endpoint.Options["opt1"]);
- Assert.Equal("val2", endpoint.Options["opt2"]);
- Assert.Equal("value", endpoint.UrlParameters["param"]);
- Assert.Equal("eng", endpoint.Language);
- }
-
- [Fact]
- public void Call_ValidEndpoint_ReturnsResponse()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When(_defaultUri)
- .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Entities endpoint = new("test content");
-
- // Act
- Response result = endpoint.Call(api);
-
- // Assert
- Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
- Assert.NotNull(result.Content);
- }
-
- #region Async Tests
-
- [Fact]
- public async Task CallAsync_ValidEndpoint_ReturnsResponse()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When(_defaultUri)
- .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Entities endpoint = new("test content");
-
- // Act
- Response result = await endpoint.CallAsync(api);
-
- // Assert
- Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
- Assert.NotNull(result.Content);
- }
-
- [Fact]
- public async Task CallAsync_WithCancellationToken_ReturnsResponse()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When(_defaultUri)
- .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Entities endpoint = new("test content");
- using var cts = new CancellationTokenSource();
-
- // Act
- Response result = await endpoint.CallAsync(api, cts.Token);
-
- // Assert
- Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
- Assert.NotNull(result.Content);
- }
-
- [Fact]
- public async Task CallAsync_CancelledToken_ThrowsOperationCanceledException()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When(_defaultUri)
- .Respond(async () =>
- {
- await Task.Delay(100);
- return new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new StringContent("{\"test\": \"OK\"}")
- };
- });
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Entities endpoint = new("test content");
- var cts = new CancellationTokenSource();
- cts.Cancel(); // Cancel immediately
-
- // Act & Assert
- await Assert.ThrowsAsync(
- async () => await endpoint.CallAsync(api, cts.Token));
- }
-
- [Fact]
- public async Task CallAsync_WithTimeout_ThrowsOperationCanceledException()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When(_defaultUri)
- .Respond(async () =>
- {
- await Task.Delay(5000); // 5 second delay
- return new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new StringContent("{\"test\": \"OK\"}")
- };
- });
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Entities endpoint = new("test content");
- using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); // 100ms timeout
-
- // Act & Assert
- await Assert.ThrowsAnyAsync(
- async () => await endpoint.CallAsync(api, cts.Token));
- }
-
- [Fact]
- public async Task CallAsync_WithOptions_SendsOptionsToServer()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When(_defaultUri)
- .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Entities endpoint = new Entities("test content")
- .SetOption("linkEntities", true);
-
- // Act
- Response result = await endpoint.CallAsync(api);
-
- // Assert
- Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
- }
-
- [Fact]
- public async Task CallAsync_WithUrlParameters_AppendsQueryString()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When("https://analytics.babelstreet.com/rest/v1/entities?output=rosette")
- .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Entities endpoint = new Entities("test content")
- .SetUrlParameter("output", "rosette");
-
- // Act
- Response result = await endpoint.CallAsync(api);
-
- // Assert
- Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
- }
-
- [Fact]
- public async Task CallAsync_ErrorResponse_ThrowsHttpRequestException()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When(_defaultUri)
- .Respond(HttpStatusCode.BadRequest, "application/json", "{\"message\": \"Bad Request\"}");
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Entities endpoint = new("test content");
-
- // Act & Assert
- await Assert.ThrowsAsync(
- async () => await endpoint.CallAsync(api));
- }
-
- [Fact]
- public async Task CallAsync_NotFoundResponse_ThrowsHttpRequestException()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When(_defaultUri)
- .Respond(HttpStatusCode.NotFound, "application/json", "{\"message\": \"Not Found\"}");
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Entities endpoint = new("test content");
-
- // Act & Assert
- await Assert.ThrowsAsync(
- async () => await endpoint.CallAsync(api));
- }
-
- [Fact]
- public async Task CallAsync_UnauthorizedResponse_ThrowsHttpRequestException()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When(_defaultUri)
- .Respond(HttpStatusCode.Unauthorized, "application/json", "{\"message\": \"Unauthorized\"}");
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Entities endpoint = new("test content");
-
- // Act & Assert
- await Assert.ThrowsAsync(
- async () => await endpoint.CallAsync(api));
- }
-
- [Fact]
- public async Task CallAsync_WithLanguage_SendsLanguageParameter()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When(_defaultUri)
- .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Entities endpoint = new Entities("test content")
- .SetLanguage("eng");
-
- // Act
- Response result = await endpoint.CallAsync(api);
-
- // Assert
- Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
- }
-
- [Fact]
- public async Task CallAsync_MultipleCallsToSameEndpoint_EachReturnsResponse()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When(_defaultUri)
- .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Entities endpoint = new("test content");
-
- // Act
- Response result1 = await endpoint.CallAsync(api);
- Response result2 = await endpoint.CallAsync(api);
-
- // Assert
- Assert.Equal((int)HttpStatusCode.OK, result1.StatusCode);
- Assert.Equal((int)HttpStatusCode.OK, result2.StatusCode);
- Assert.NotSame(result1, result2); // Different instances
- }
-
- [Fact]
- public async Task CallAsync_ConcurrentCalls_BothComplete()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When(_defaultUri)
- .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Entities endpoint1 = new("content1");
- Entities endpoint2 = new("content2");
-
- // Act
- var task1 = endpoint1.CallAsync(api);
- var task2 = endpoint2.CallAsync(api);
- var results = await Task.WhenAll(task1, task2);
-
- // Assert
- Assert.Equal((int)HttpStatusCode.OK, results[0].StatusCode);
- Assert.Equal((int)HttpStatusCode.OK, results[1].StatusCode);
- }
-
- [Fact]
- public async Task CallAsync_InfoEndpoint_UsesGetCall()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When("https://analytics.babelstreet.com/rest/v1/info")
- .Respond(HttpStatusCode.OK, "application/json", "{\"name\": \"Rosette API\"}");
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Info endpoint = new();
-
- // Act
- Response result = await endpoint.CallAsync(api);
-
- // Assert
- Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
- Assert.NotNull(result.Content);
- }
-
- [Fact]
- public async Task CallAsync_PingEndpoint_UsesGetCall()
- {
- // Arrange
- ApiClient api = new("testkey");
- var mockHttp = new MockHttpMessageHandler();
- mockHttp.When("https://analytics.babelstreet.com/rest/v1/ping")
- .Respond(HttpStatusCode.OK, "application/json", "{\"message\": \"pong\"}");
- var client = mockHttp.ToHttpClient();
- api.AssignClient(client);
-
- Ping endpoint = new();
-
- // Act
- Response result = await endpoint.CallAsync(api);
-
- // Assert
- Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
- Assert.NotNull(result.Content);
- }
-
- #endregion
- }
-}
\ No newline at end of file
diff --git a/tests/Endpoints/AddressSimilarityTests.cs b/tests/Endpoints/AddressSimilarityTests.cs
new file mode 100644
index 0000000..4cdabfd
--- /dev/null
+++ b/tests/Endpoints/AddressSimilarityTests.cs
@@ -0,0 +1,276 @@
+using Rosette.Api.Client.Endpoints;
+using Rosette.Api.Client.Models;
+
+namespace Rosette.Api.Tests.Endpoints;
+
+public class AddressSimilarityTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_SetsEndpoint_WhenCalledWithUnfieldedAddresses()
+ {
+ var address1 = new UnfieldedAddressRecord { Address = "1600 Pennsylvania Avenue, Washington, D.C., 20500" };
+ var address2 = new UnfieldedAddressRecord { Address = "160 Pennsylvana Avenue, Washington, D.C., 20500" };
+
+ AddressSimilarity asim = new(address1, address2);
+
+ Assert.Equal("address-similarity", asim.Endpoint);
+ }
+
+ [Fact]
+ public void Constructor_SetsEndpoint_WhenCalledWithFieldedAddresses()
+ {
+ var address1 = new FieldedAddressRecord(
+ houseNumber: "1600",
+ road: "Pennsylvania Avenue NW",
+ city: "Washington",
+ state: "DC",
+ postcode: "20500"
+ );
+ var address2 = new FieldedAddressRecord(
+ houseNumber: "1600",
+ road: "Pennsylvania Ave N.W.",
+ city: "Washington",
+ state: "DC",
+ postcode: "20500"
+ );
+
+ AddressSimilarity asim = new(address1, address2);
+
+ Assert.Equal("address-similarity", asim.Endpoint);
+ }
+
+ [Fact]
+ public void Constructor_SetsEndpoint_WhenCalledWithMixedAddresses()
+ {
+ var address1 = new UnfieldedAddressRecord { Address = "1600 Pennsylvania Avenue, Washington, D.C., 20500" };
+ var address2 = new FieldedAddressRecord(
+ houseNumber: "1600",
+ road: "Pennsylvania Avenue NW",
+ city: "Washington",
+ state: "DC",
+ postcode: "20500"
+ );
+
+ AddressSimilarity asim = new(address1, address2);
+
+ Assert.Equal("address-similarity", asim.Endpoint);
+ }
+
+ [Fact]
+ public void Constructor_ThrowsArgumentNullException_WhenAddress1IsNull()
+ {
+ var address2 = new UnfieldedAddressRecord { Address = "Some address" };
+
+ Assert.Throws(() => new AddressSimilarity(null!, address2));
+ }
+
+ [Fact]
+ public void Constructor_ThrowsArgumentNullException_WhenAddress2IsNull()
+ {
+ var address1 = new UnfieldedAddressRecord { Address = "Some address" };
+
+ Assert.Throws(() => new AddressSimilarity(address1, null!));
+ }
+
+ [Fact]
+ public void Constructor_ThrowsArgumentNullException_WhenBothAddressesAreNull()
+ {
+ Assert.Throws(() => new AddressSimilarity(null!, null!));
+ }
+
+ #endregion
+
+ #region Parameter Tests
+
+ [Fact]
+ public void Constructor_AddsAddress1ToParams_WhenCalled()
+ {
+ var address1 = new UnfieldedAddressRecord { Address = "Address 1" };
+ var address2 = new UnfieldedAddressRecord { Address = "Address 2" };
+
+ AddressSimilarity asim = new(address1, address2);
+
+ Assert.Contains("address1", asim.Params.Keys);
+ Assert.Equal(address1, asim.Params["address1"]);
+ }
+
+ [Fact]
+ public void Constructor_AddsAddress2ToParams_WhenCalled()
+ {
+ var address1 = new UnfieldedAddressRecord { Address = "Address 1" };
+ var address2 = new UnfieldedAddressRecord { Address = "Address 2" };
+
+ AddressSimilarity asim = new(address1, address2);
+
+ Assert.Contains("address2", asim.Params.Keys);
+ Assert.Equal(address2, asim.Params["address2"]);
+ }
+
+ #endregion
+
+ #region UnfieldedAddressRecord Tests
+
+ [Fact]
+ public void Constructor_AcceptsUnfieldedAddress_WithSimpleAddress()
+ {
+ var address1 = new UnfieldedAddressRecord { Address = "123 Main St" };
+ var address2 = new UnfieldedAddressRecord { Address = "123 Main Street" };
+
+ AddressSimilarity asim = new(address1, address2);
+
+ var storedAddress1 = (UnfieldedAddressRecord)asim.Params["address1"];
+ Assert.Equal("123 Main St", storedAddress1.Address);
+ }
+
+ [Fact]
+ public void Constructor_AcceptsUnfieldedAddress_WithComplexAddress()
+ {
+ var address1 = new UnfieldedAddressRecord
+ {
+ Address = "Suite 500, 1234 Technology Drive, San Francisco, CA 94102, United States"
+ };
+ var address2 = new UnfieldedAddressRecord
+ {
+ Address = "1234 Technology Dr, Suite 500, San Francisco, California 94102"
+ };
+
+ AddressSimilarity asim = new(address1, address2);
+
+ Assert.NotNull(asim.Params["address1"]);
+ Assert.NotNull(asim.Params["address2"]);
+ }
+
+ [Fact]
+ public void Constructor_AcceptsUnfieldedAddress_WithInternationalAddress()
+ {
+ var address1 = new UnfieldedAddressRecord { Address = "10 Downing Street, London, SW1A 2AA, UK" };
+ var address2 = new UnfieldedAddressRecord { Address = "10 Downing St, Westminster, London SW1A 2AA" };
+
+ AddressSimilarity asim = new(address1, address2);
+
+ Assert.Equal("address-similarity", asim.Endpoint);
+ }
+
+ #endregion
+
+ #region FieldedAddressRecord Tests
+
+ [Fact]
+ public void Constructor_AcceptsFieldedAddress_WithAllFields()
+ {
+ var address1 = new FieldedAddressRecord(
+ houseNumber: "1600",
+ road: "Pennsylvania Avenue NW",
+ city: "Washington",
+ state: "DC",
+ postcode: "20500",
+ country: "United States"
+ );
+ var address2 = new FieldedAddressRecord(
+ houseNumber: "1600",
+ road: "Pennsylvania Ave",
+ city: "Washington",
+ state: "DC",
+ postcode: "20500"
+ );
+
+ AddressSimilarity asim = new(address1, address2);
+
+ var storedAddress1 = (FieldedAddressRecord)asim.Params["address1"];
+ Assert.Equal("1600", storedAddress1.HouseNumber);
+ Assert.Equal("Pennsylvania Avenue NW", storedAddress1.Road);
+ }
+
+ [Fact]
+ public void Constructor_AcceptsFieldedAddress_WithMinimalFields()
+ {
+ var address1 = new FieldedAddressRecord(city: "Boston");
+ var address2 = new FieldedAddressRecord(city: "Boston");
+
+ AddressSimilarity asim = new(address1, address2);
+
+ Assert.Equal("address-similarity", asim.Endpoint);
+ }
+
+ #endregion
+
+ #region Options Tests
+
+ [Fact]
+ public void SetOption_AddsOptionToOptions_WhenCalled()
+ {
+ var address1 = new UnfieldedAddressRecord { Address = "Address 1" };
+ var address2 = new UnfieldedAddressRecord { Address = "Address 2" };
+
+ AddressSimilarity asim = new AddressSimilarity(address1, address2)
+ .SetOption("explain", true);
+
+ Assert.True((bool)asim.Options["explain"]);
+ }
+
+ [Fact]
+ public void SetOption_SupportsMultipleOptions_WhenCalledMultipleTimes()
+ {
+ var address1 = new UnfieldedAddressRecord { Address = "Address 1" };
+ var address2 = new UnfieldedAddressRecord { Address = "Address 2" };
+
+ AddressSimilarity asim = new AddressSimilarity(address1, address2)
+ .SetOption("explain", true)
+ .SetOption("debug", "detailed");
+
+ Assert.True((bool)asim.Options["explain"]);
+ Assert.Equal("detailed", asim.Options["debug"]);
+ }
+
+ #endregion
+
+ #region Fluent API Tests
+
+ [Fact]
+ public void FluentAPI_AllowsMethodChaining_WhenSettingOptions()
+ {
+ var address1 = new UnfieldedAddressRecord { Address = "123 Main St" };
+ var address2 = new UnfieldedAddressRecord { Address = "123 Main Street" };
+
+ AddressSimilarity asim = new AddressSimilarity(address1, address2)
+ .SetOption("explain", true);
+
+ Assert.True((bool)asim.Options["explain"]);
+ }
+
+ #endregion
+
+ #region Real-World Address Tests
+
+ [Fact]
+ public void Constructor_HandlesTypicalUSAddress_WhenCalled()
+ {
+ var address1 = new UnfieldedAddressRecord { Address = "1 Apple Park Way, Cupertino, CA 95014" };
+ var address2 = new FieldedAddressRecord(
+ houseNumber: "1",
+ road: "Apple Park Way",
+ city: "Cupertino",
+ state: "CA",
+ postcode: "95014"
+ );
+
+ AddressSimilarity asim = new(address1, address2);
+
+ Assert.Equal("address-similarity", asim.Endpoint);
+ }
+
+ [Fact]
+ public void Constructor_HandlesAbbreviationsAndSpellingVariations_WhenCalled()
+ {
+ var address1 = new UnfieldedAddressRecord { Address = "1600 Penn Ave NW" };
+ var address2 = new UnfieldedAddressRecord { Address = "1600 Pennsylvania Avenue Northwest" };
+
+ AddressSimilarity asim = new(address1, address2);
+
+ Assert.Equal("address-similarity", asim.Endpoint);
+ }
+
+ #endregion
+}
diff --git a/tests/CategoriesTests.cs b/tests/Endpoints/CategoriesTests.cs
similarity index 85%
rename from tests/CategoriesTests.cs
rename to tests/Endpoints/CategoriesTests.cs
index 6d91b69..41051ae 100644
--- a/tests/CategoriesTests.cs
+++ b/tests/Endpoints/CategoriesTests.cs
@@ -1,9 +1,11 @@
using Rosette.Api.Client.Endpoints;
-namespace Rosette.Api.Tests;
+namespace Rosette.Api.Tests.Endpoints;
public class CategoriesTests
{
+ #region Constructor Tests
+
[Fact]
public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
{
@@ -13,6 +15,10 @@ public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
Assert.Equal("Sample text for categorization", c.Content);
}
+ #endregion
+
+ #region Property Configuration Tests
+
[Fact]
public void SetLanguage_SetsLanguageProperty_WhenCalled()
{
@@ -32,6 +38,10 @@ public void SetGenre_SetsGenreProperty_WhenCalled()
Assert.Equal("", c.Genre);
}
+ #endregion
+
+ #region Fluent API Tests
+
[Fact]
public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
{
@@ -42,4 +52,6 @@ public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
Assert.Equal("eng", c.Language);
Assert.Equal("value", c.Options["customOption"]);
}
-}
\ No newline at end of file
+
+ #endregion
+}
diff --git a/tests/ContentEndpointBaseTests.cs b/tests/Endpoints/Core/ContentEndpointBaseTests.cs
similarity index 98%
rename from tests/ContentEndpointBaseTests.cs
rename to tests/Endpoints/Core/ContentEndpointBaseTests.cs
index 1639b54..c7e2dda 100644
--- a/tests/ContentEndpointBaseTests.cs
+++ b/tests/Endpoints/Core/ContentEndpointBaseTests.cs
@@ -1,6 +1,6 @@
-using Rosette.Api.Client.Endpoints.Core;
+using Rosette.Api.Client.Endpoints.Core;
-namespace Rosette.Api.Tests;
+namespace Rosette.Api.Tests.Endpoints.Core;
// Concrete test implementation of ContentEndpointBase for testing
internal class TestContentEndpoint : ContentEndpointBase
@@ -308,4 +308,4 @@ public void UrlParameters_SetUrlParameter_CanSetQueryParameters()
// Assert
Assert.Equal("rosette", endpoint.UrlParameters["output"]);
}
-}
\ No newline at end of file
+}
diff --git a/tests/Endpoints/Core/EndpointBaseTests.cs b/tests/Endpoints/Core/EndpointBaseTests.cs
new file mode 100644
index 0000000..b7ce17b
--- /dev/null
+++ b/tests/Endpoints/Core/EndpointBaseTests.cs
@@ -0,0 +1,628 @@
+using RichardSzalay.MockHttp;
+using Rosette.Api.Client;
+using Rosette.Api.Client.Endpoints;
+using Rosette.Api.Client.Models;
+using System.Net;
+
+namespace Rosette.Api.Tests.Endpoints.Core;
+
+public class EndpointBaseTests
+{
+ private static readonly string _defaultUri = "https://analytics.babelstreet.com/rest/v1/*";
+
+ [Fact]
+ public void Constructor_ValidEndpoint_SetsEndpointName()
+ {
+ // Arrange & Act
+ var endpoint = new Entities("test content");
+
+ // Assert
+ Assert.Equal("entities", endpoint.Endpoint);
+ }
+
+ [Fact]
+ public void Options_InitialState_IsEmpty()
+ {
+ // Arrange & Act
+ var endpoint = new Entities("foo");
+
+ // Assert
+ Assert.Empty(endpoint.Options);
+ }
+
+ [Fact]
+ public void Params_InitialState_IsNotEmpty()
+ {
+ // Arrange & Act
+ var endpoint = new Entities("foo");
+
+ // Assert
+ Assert.NotEmpty(endpoint.Params);
+ }
+
+ [Fact]
+ public void UrlParameters_InitialState_IsEmpty()
+ {
+ // Arrange & Act
+ var endpoint = new Entities("foo");
+
+ // Assert
+ Assert.Empty(endpoint.UrlParameters);
+ }
+
+ [Fact]
+ public void SetOption_ValidOption_AddsToOptions()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act
+ e.SetOption("test", "value");
+
+ // Assert
+ Assert.Equal("value", e.Options["test"]);
+ }
+
+ [Fact]
+ public void SetOption_MultipleOptions_AddsAllOptions()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act
+ e.SetOption("test", "value");
+ e.SetOption("test2", "value2");
+
+ // Assert
+ Assert.Equal("value", e.Options["test"]);
+ Assert.Equal("value2", e.Options["test2"]);
+ }
+
+ [Fact]
+ public void SetOption_NullOptionName_ThrowsArgumentNullException()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act & Assert
+ Assert.Throws(() => e.SetOption(null!, "value"));
+ }
+
+ [Fact]
+ public void SetOption_EmptyOptionName_ThrowsArgumentException()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act & Assert
+ Assert.Throws(() => e.SetOption(string.Empty, "value"));
+ }
+
+ [Fact]
+ public void SetOption_NullOptionValue_ThrowsArgumentNullException()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act & Assert
+ Assert.Throws(() => e.SetOption("test", null!));
+ }
+
+ [Fact]
+ public void SetOption_ReturnsInstance_ForFluentAPI()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act
+ var result = e.SetOption("test", "value");
+
+ // Assert
+ Assert.Same(e, result);
+ }
+
+ [Fact]
+ public void RemoveOption_ExistingOption_RemovesOption()
+ {
+ // Arrange
+ Entities e = new Entities("foo")
+ .SetOption("test", "value");
+
+ // Act
+ e.RemoveOption("test");
+
+ // Assert
+ Assert.False(e.Options.ContainsKey("test"));
+ }
+
+ [Fact]
+ public void RemoveOption_NonExistingOption_DoesNotThrow()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act & Assert
+ var exception = Record.Exception(() => e.RemoveOption("nonexistent"));
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void RemoveOption_NullKey_ThrowsArgumentNullException()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act & Assert
+ Assert.Throws(() => e.RemoveOption(null!));
+ }
+
+ [Fact]
+ public void RemoveOption_ReturnsInstance_ForFluentAPI()
+ {
+ // Arrange
+ Entities e = new Entities("foo")
+ .SetOption("test", "value");
+
+ // Act
+ var result = e.RemoveOption("test");
+
+ // Assert
+ Assert.Same(e, result);
+ }
+
+ [Fact]
+ public void ClearOptions_WithOptions_RemovesAllOptions()
+ {
+ // Arrange
+ Entities e = new Entities("foo")
+ .SetOption("test1", "value1")
+ .SetOption("test2", "value2");
+
+ // Act
+ e.ClearOptions();
+
+ // Assert
+ Assert.Empty(e.Options);
+ }
+
+ [Fact]
+ public void ClearOptions_ReturnsInstance_ForFluentAPI()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act
+ var result = e.ClearOptions();
+
+ // Assert
+ Assert.Same(e, result);
+ }
+
+ [Fact]
+ public void SetUrlParameter_ValidParameter_AddsToUrlParameters()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act
+ e.SetUrlParameter("test", "value");
+
+ // Assert
+ Assert.Equal("value", e.UrlParameters["test"]);
+ }
+
+ [Fact]
+ public void SetUrlParameter_NullKey_ThrowsArgumentNullException()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act & Assert
+ Assert.Throws(() => e.SetUrlParameter(null!, "value"));
+ }
+
+ [Fact]
+ public void SetUrlParameter_NullValue_ThrowsArgumentNullException()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act & Assert
+ Assert.Throws(() => e.SetUrlParameter("key", null!));
+ }
+
+ [Fact]
+ public void SetUrlParameter_ReturnsInstance_ForFluentAPI()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act
+ var result = e.SetUrlParameter("test", "value");
+
+ // Assert
+ Assert.Same(e, result);
+ }
+
+ [Fact]
+ public void RemoveUrlParameter_ExistingParameter_RemovesParameter()
+ {
+ // Arrange
+ Entities e = new Entities("foo")
+ .SetUrlParameter("test", "value");
+
+ // Act
+ e.RemoveUrlParameter("test");
+
+ // Assert
+ Assert.Empty(e.UrlParameters);
+ }
+
+ [Fact]
+ public void RemoveUrlParameter_NullKey_ThrowsArgumentNullException()
+ {
+ // Arrange
+ Entities e = new("foo");
+
+ // Act & Assert
+ Assert.Throws(() => e.RemoveUrlParameter(null!));
+ }
+
+ [Fact]
+ public void RemoveUrlParameter_ReturnsInstance_ForFluentAPI()
+ {
+ // Arrange
+ Entities e = new Entities("foo")
+ .SetUrlParameter("test", "value");
+
+ // Act
+ var result = e.RemoveUrlParameter("test");
+
+ // Assert
+ Assert.Same(e, result);
+ }
+
+ [Fact]
+ public void FluentAPI_ChainMultipleMethods_AllSettersReturnSameInstance()
+ {
+ // Arrange & Act
+ var endpoint = new Entities("content")
+ .SetOption("opt1", "val1")
+ .SetOption("opt2", "val2")
+ .SetUrlParameter("param", "value")
+ .SetLanguage("eng");
+
+ // Assert
+ Assert.Equal("val1", endpoint.Options["opt1"]);
+ Assert.Equal("val2", endpoint.Options["opt2"]);
+ Assert.Equal("value", endpoint.UrlParameters["param"]);
+ Assert.Equal("eng", endpoint.Language);
+ }
+
+ [Fact]
+ public void Call_ValidEndpoint_ReturnsResponse()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When(_defaultUri)
+ .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Entities endpoint = new("test content");
+
+ // Act
+ Response result = endpoint.Call(api);
+
+ // Assert
+ Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
+ Assert.NotNull(result.Content);
+ }
+
+ #region Async Tests
+
+ [Fact]
+ public async Task CallAsync_ValidEndpoint_ReturnsResponse()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When(_defaultUri)
+ .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Entities endpoint = new("test content");
+
+ // Act
+ Response result = await endpoint.CallAsync(api);
+
+ // Assert
+ Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
+ Assert.NotNull(result.Content);
+ }
+
+ [Fact]
+ public async Task CallAsync_WithCancellationToken_ReturnsResponse()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When(_defaultUri)
+ .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Entities endpoint = new("test content");
+ using var cts = new CancellationTokenSource();
+
+ // Act
+ Response result = await endpoint.CallAsync(api, cts.Token);
+
+ // Assert
+ Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
+ Assert.NotNull(result.Content);
+ }
+
+ [Fact]
+ public async Task CallAsync_CancelledToken_ThrowsOperationCanceledException()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When(_defaultUri)
+ .Respond(async () =>
+ {
+ await Task.Delay(100);
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("{\"test\": \"OK\"}")
+ };
+ });
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Entities endpoint = new("test content");
+ var cts = new CancellationTokenSource();
+ cts.Cancel(); // Cancel immediately
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ async () => await endpoint.CallAsync(api, cts.Token));
+ }
+
+ [Fact]
+ public async Task CallAsync_WithTimeout_ThrowsOperationCanceledException()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When(_defaultUri)
+ .Respond(async () =>
+ {
+ await Task.Delay(5000); // 5 second delay
+ return new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent("{\"test\": \"OK\"}")
+ };
+ });
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Entities endpoint = new("test content");
+ using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); // 100ms timeout
+
+ // Act & Assert
+ await Assert.ThrowsAnyAsync(
+ async () => await endpoint.CallAsync(api, cts.Token));
+ }
+
+ [Fact]
+ public async Task CallAsync_WithOptions_SendsOptionsToServer()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When(_defaultUri)
+ .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Entities endpoint = new Entities("test content")
+ .SetOption("linkEntities", true);
+
+ // Act
+ Response result = await endpoint.CallAsync(api);
+
+ // Assert
+ Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
+ }
+
+ [Fact]
+ public async Task CallAsync_WithUrlParameters_AppendsQueryString()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When("https://analytics.babelstreet.com/rest/v1/entities?output=rosette")
+ .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Entities endpoint = new Entities("test content")
+ .SetUrlParameter("output", "rosette");
+
+ // Act
+ Response result = await endpoint.CallAsync(api);
+
+ // Assert
+ Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
+ }
+
+ [Fact]
+ public async Task CallAsync_ErrorResponse_ThrowsHttpRequestException()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When(_defaultUri)
+ .Respond(HttpStatusCode.BadRequest, "application/json", "{\"message\": \"Bad Request\"}");
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Entities endpoint = new("test content");
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ async () => await endpoint.CallAsync(api));
+ }
+
+ [Fact]
+ public async Task CallAsync_NotFoundResponse_ThrowsHttpRequestException()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When(_defaultUri)
+ .Respond(HttpStatusCode.NotFound, "application/json", "{\"message\": \"Not Found\"}");
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Entities endpoint = new("test content");
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ async () => await endpoint.CallAsync(api));
+ }
+
+ [Fact]
+ public async Task CallAsync_UnauthorizedResponse_ThrowsHttpRequestException()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When(_defaultUri)
+ .Respond(HttpStatusCode.Unauthorized, "application/json", "{\"message\": \"Unauthorized\"}");
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Entities endpoint = new("test content");
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ async () => await endpoint.CallAsync(api));
+ }
+
+ [Fact]
+ public async Task CallAsync_WithLanguage_SendsLanguageParameter()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When(_defaultUri)
+ .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Entities endpoint = new Entities("test content")
+ .SetLanguage("eng");
+
+ // Act
+ Response result = await endpoint.CallAsync(api);
+
+ // Assert
+ Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
+ }
+
+ [Fact]
+ public async Task CallAsync_MultipleCallsToSameEndpoint_EachReturnsResponse()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When(_defaultUri)
+ .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Entities endpoint = new("test content");
+
+ // Act
+ Response result1 = await endpoint.CallAsync(api);
+ Response result2 = await endpoint.CallAsync(api);
+
+ // Assert
+ Assert.Equal((int)HttpStatusCode.OK, result1.StatusCode);
+ Assert.Equal((int)HttpStatusCode.OK, result2.StatusCode);
+ Assert.NotSame(result1, result2); // Different instances
+ }
+
+ [Fact]
+ public async Task CallAsync_ConcurrentCalls_BothComplete()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When(_defaultUri)
+ .Respond(HttpStatusCode.OK, "application/json", "{\"test\": \"OK\"}");
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Entities endpoint1 = new("content1");
+ Entities endpoint2 = new("content2");
+
+ // Act
+ var task1 = endpoint1.CallAsync(api);
+ var task2 = endpoint2.CallAsync(api);
+ var results = await Task.WhenAll(task1, task2);
+
+ // Assert
+ Assert.Equal((int)HttpStatusCode.OK, results[0].StatusCode);
+ Assert.Equal((int)HttpStatusCode.OK, results[1].StatusCode);
+ }
+
+ [Fact]
+ public async Task CallAsync_InfoEndpoint_UsesGetCall()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When("https://analytics.babelstreet.com/rest/v1/info")
+ .Respond(HttpStatusCode.OK, "application/json", "{\"name\": \"Rosette API\"}");
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Info endpoint = new();
+
+ // Act
+ Response result = await endpoint.CallAsync(api);
+
+ // Assert
+ Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
+ Assert.NotNull(result.Content);
+ }
+
+ [Fact]
+ public async Task CallAsync_PingEndpoint_UsesGetCall()
+ {
+ // Arrange
+ ApiClient api = new("testkey");
+ var mockHttp = new MockHttpMessageHandler();
+ mockHttp.When("https://analytics.babelstreet.com/rest/v1/ping")
+ .Respond(HttpStatusCode.OK, "application/json", "{\"message\": \"pong\"}");
+ var client = mockHttp.ToHttpClient();
+ api.AssignClient(client);
+
+ Ping endpoint = new();
+
+ // Act
+ Response result = await endpoint.CallAsync(api);
+
+ // Assert
+ Assert.Equal((int)HttpStatusCode.OK, result.StatusCode);
+ Assert.NotNull(result.Content);
+ }
+
+ #endregion
+}
diff --git a/tests/EndpointExecutorTests.cs b/tests/Endpoints/Core/EndpointExecutorTests.cs
similarity index 99%
rename from tests/EndpointExecutorTests.cs
rename to tests/Endpoints/Core/EndpointExecutorTests.cs
index 9634d15..b2dd2db 100644
--- a/tests/EndpointExecutorTests.cs
+++ b/tests/Endpoints/Core/EndpointExecutorTests.cs
@@ -1,4 +1,4 @@
-using RichardSzalay.MockHttp;
+using RichardSzalay.MockHttp;
using System.Collections.Specialized;
using System.Net;
using System.Text.Json;
@@ -6,7 +6,7 @@
using Rosette.Api.Client.Models;
using Rosette.Api.Client;
-namespace Rosette.Api.Tests;
+namespace Rosette.Api.Tests.Endpoints.Core;
public class EndpointExecutorTests
{
diff --git a/tests/ValidEndpointTests.cs b/tests/Endpoints/Core/ValidEndpointTests.cs
similarity index 91%
rename from tests/ValidEndpointTests.cs
rename to tests/Endpoints/Core/ValidEndpointTests.cs
index f2ded89..6fa6cc6 100644
--- a/tests/ValidEndpointTests.cs
+++ b/tests/Endpoints/Core/ValidEndpointTests.cs
@@ -1,10 +1,11 @@
-using Rosette.Api.Client.Endpoints;
+using Rosette.Api.Client.Endpoints;
using Rosette.Api.Client.Models;
-namespace Rosette.Api.Tests;
+namespace Rosette.Api.Tests.Endpoints.Core;
public class ValidEndpointTests
{
+ #region Address and Record Endpoints
[Fact]
public void Constructor_SetsEndpoint_WhenCreatingAddressSimilarity()
@@ -15,6 +16,22 @@ public void Constructor_SetsEndpoint_WhenCreatingAddressSimilarity()
Assert.Equal("address-similarity", asim.Endpoint);
}
+ [Fact]
+ public void Constructor_SetsEndpoint_WhenCreatingRecordSimilarity()
+ {
+ var fields = new Dictionary();
+ var properties = new RecordSimilarityProperties();
+ var records = new RecordSimilarityRecords();
+
+ RecordSimilarity rs = new(fields, properties, records);
+
+ Assert.Equal("record-similarity", rs.Endpoint);
+ }
+
+ #endregion
+
+ #region Content Analysis Endpoints
+
[Fact]
public void Constructor_SetsEndpointAndContent_WhenCreatingCategories()
{
@@ -41,12 +58,35 @@ public void Constructor_SetsEndpointAndContent_WhenCreatingEvents()
}
[Fact]
- public void Constructor_SetsEndpoint_WhenCreatingInfo()
+ public void Constructor_SetsEndpointAndContent_WhenCreatingRelationships()
{
- Info i = new();
- Assert.Equal("info", i.Endpoint);
+ Relationships r = new("foo");
+
+ Assert.Equal("relationships", r.Endpoint);
+ Assert.Equal("foo", r.Content);
+ }
+
+ [Fact]
+ public void Constructor_SetsEndpointAndContent_WhenCreatingSentiment()
+ {
+ Sentiment s = new("foo");
+ Assert.Equal("sentiment", s.Endpoint);
+ Assert.Equal("foo", s.Content);
+ }
+
+ [Fact]
+ public void Constructor_SetsEndpointAndContent_WhenCreatingTopics()
+ {
+ Topics t = new("foo");
+
+ Assert.Equal("topics", t.Endpoint);
+ Assert.Equal("foo", t.Content);
}
+ #endregion
+
+ #region Language and Text Processing Endpoints
+
[Fact]
public void Constructor_SetsEndpointAndContent_WhenCreatingLanguage()
{
@@ -56,6 +96,36 @@ public void Constructor_SetsEndpointAndContent_WhenCreatingLanguage()
Assert.Equal("foo", l.Content);
}
+ [Fact]
+ public void Constructor_SetsEndpointAndContent_WhenCreatingSentences()
+ {
+ Sentences s = new("foo");
+ Assert.Equal("sentences", s.Endpoint);
+ Assert.Equal("foo", s.Content);
+ }
+
+ [Fact]
+ public void Constructor_SetsEndpointAndContent_WhenCreatingTokens()
+ {
+ Tokens t = new("foo");
+
+ Assert.Equal("tokens", t.Endpoint);
+ Assert.Equal("foo", t.Content);
+ }
+
+ [Fact]
+ public void Constructor_SetsEndpointAndContent_WhenCreatingTransliteration()
+ {
+ Transliteration t = new("foo");
+
+ Assert.Equal("transliteration", t.Endpoint);
+ Assert.Equal("foo", t.Content);
+ }
+
+ #endregion
+
+ #region Morphology Endpoints
+
[Theory]
[InlineData(MorphologyFeature.complete)]
[InlineData(MorphologyFeature.compoundComponents)]
@@ -70,6 +140,10 @@ public void Constructor_SetsEndpointAndContent_WhenCreatingMorphologyWithFeature
Assert.Equal("foo", m.Content);
}
+ #endregion
+
+ #region Name Endpoints
+
[Fact]
public void Constructor_SetsEndpoint_WhenCreatingNameDeduplication()
{
@@ -98,33 +172,9 @@ public void Constructor_SetsEndpoint_WhenCreatingNameTranslation()
Assert.Equal("name-translation", nt.Endpoint);
}
- [Fact]
- public void Constructor_SetsEndpoint_WhenCreatingPing()
- {
- Ping p = new();
- Assert.Equal("ping", p.Endpoint);
- }
-
- [Fact]
- public void Constructor_SetsEndpoint_WhenCreatingRecordSimilarity()
- {
- var fields = new Dictionary();
- var properties = new RecordSimilarityProperties();
- var records = new RecordSimilarityRecords();
-
- RecordSimilarity rs = new(fields, properties, records);
+ #endregion
- Assert.Equal("record-similarity", rs.Endpoint);
- }
-
- [Fact]
- public void Constructor_SetsEndpointAndContent_WhenCreatingRelationships()
- {
- Relationships r = new("foo");
-
- Assert.Equal("relationships", r.Endpoint);
- Assert.Equal("foo", r.Content);
- }
+ #region Semantics Endpoints
[Fact]
public void Constructor_SetsEndpointAndContent_WhenCreatingSemanticsVector()
@@ -134,22 +184,6 @@ public void Constructor_SetsEndpointAndContent_WhenCreatingSemanticsVector()
Assert.Equal("foo", s.Content);
}
- [Fact]
- public void Constructor_SetsEndpointAndContent_WhenCreatingSentences()
- {
- Sentences s = new("foo");
- Assert.Equal("sentences", s.Endpoint);
- Assert.Equal("foo", s.Content);
- }
-
- [Fact]
- public void Constructor_SetsEndpointAndContent_WhenCreatingSentiment()
- {
- Sentiment s = new("foo");
- Assert.Equal("sentiment", s.Endpoint);
- Assert.Equal("foo", s.Content);
- }
-
[Fact]
public void Constructor_SetsEndpointAndContent_WhenCreatingSimilarTerms()
{
@@ -159,15 +193,6 @@ public void Constructor_SetsEndpointAndContent_WhenCreatingSimilarTerms()
Assert.Equal("foo", st.Content);
}
- [Fact]
- public void Constructor_SetsEndpointAndContent_WhenCreatingSyntaxDependencies()
- {
- SyntaxDependencies s = new("foo");
-
- Assert.Equal("syntax/dependencies", s.Endpoint);
- Assert.Equal("foo", s.Content);
- }
-
[Fact]
public void Constructor_SetsEndpointAndContent_WhenCreatingTextEmbedding()
{
@@ -177,30 +202,36 @@ public void Constructor_SetsEndpointAndContent_WhenCreatingTextEmbedding()
Assert.Equal("foo", t.Content);
}
+ #endregion
+
+ #region Syntax Endpoints
+
[Fact]
- public void Constructor_SetsEndpointAndContent_WhenCreatingTokens()
+ public void Constructor_SetsEndpointAndContent_WhenCreatingSyntaxDependencies()
{
- Tokens t = new("foo");
+ SyntaxDependencies s = new("foo");
- Assert.Equal("tokens", t.Endpoint);
- Assert.Equal("foo", t.Content);
+ Assert.Equal("syntax/dependencies", s.Endpoint);
+ Assert.Equal("foo", s.Content);
}
+ #endregion
+
+ #region Utility Endpoints
+
[Fact]
- public void Constructor_SetsEndpointAndContent_WhenCreatingTopics()
+ public void Constructor_SetsEndpoint_WhenCreatingInfo()
{
- Topics t = new("foo");
-
- Assert.Equal("topics", t.Endpoint);
- Assert.Equal("foo", t.Content);
+ Info i = new();
+ Assert.Equal("info", i.Endpoint);
}
[Fact]
- public void Constructor_SetsEndpointAndContent_WhenCreatingTransliteration()
+ public void Constructor_SetsEndpoint_WhenCreatingPing()
{
- Transliteration t = new("foo");
-
- Assert.Equal("transliteration", t.Endpoint);
- Assert.Equal("foo", t.Content);
+ Ping p = new();
+ Assert.Equal("ping", p.Endpoint);
}
-}
\ No newline at end of file
+
+ #endregion
+}
diff --git a/tests/EntitiesTests.cs b/tests/Endpoints/EntitiesTests.cs
similarity index 87%
rename from tests/EntitiesTests.cs
rename to tests/Endpoints/EntitiesTests.cs
index c83af7d..072b4d6 100644
--- a/tests/EntitiesTests.cs
+++ b/tests/Endpoints/EntitiesTests.cs
@@ -1,9 +1,11 @@
using Rosette.Api.Client.Endpoints;
-namespace Rosette.Api.Tests;
+namespace Rosette.Api.Tests.Endpoints;
public class EntitiesTests
{
+ #region Constructor Tests
+
[Fact]
public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
{
@@ -14,6 +16,10 @@ public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
Assert.Equal(text, e.Content);
}
+ #endregion
+
+ #region Property Configuration Tests
+
[Fact]
public void SetLanguageAndGenre_SetsProperties_WhenCalled()
{
@@ -34,6 +40,10 @@ public void SetUrlParameter_AddsParameterToUrlParameters_WhenCalled()
Assert.Equal("rosette", e.UrlParameters["output"]);
}
+ #endregion
+
+ #region Fluent API Tests
+
[Fact]
public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
{
@@ -47,4 +57,6 @@ public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
Assert.True((bool)e.Options["linkEntities"]);
Assert.Equal("rosette", e.UrlParameters["output"]);
}
-}
\ No newline at end of file
+
+ #endregion
+}
diff --git a/tests/Endpoints/EventsTests.cs b/tests/Endpoints/EventsTests.cs
new file mode 100644
index 0000000..4eb87e1
--- /dev/null
+++ b/tests/Endpoints/EventsTests.cs
@@ -0,0 +1,149 @@
+using Rosette.Api.Client.Endpoints;
+
+namespace Rosette.Api.Tests.Endpoints;
+
+public class EventsTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
+ {
+ string text = "Bill Gates went to the store.";
+ Events e = new(text);
+
+ Assert.Equal("events", e.Endpoint);
+ Assert.Equal(text, e.Content);
+ }
+
+ #endregion
+
+ #region Property Configuration Tests
+
+ [Fact]
+ public void SetLanguage_UpdatesLanguage_WhenCalled()
+ {
+ Events e = new Events("Sample text")
+ .SetLanguage("eng");
+
+ Assert.Equal("eng", e.Language);
+ }
+
+ [Fact]
+ public void SetGenre_UpdatesGenre_WhenCalled()
+ {
+ Events e = new Events("Sample text")
+ .SetGenre("social-media");
+
+ Assert.Equal("", e.Genre);
+ }
+
+ [Fact]
+ public void SetFileContentType_UpdatesFileContentType_WhenCalled()
+ {
+ Events e = new Events("Sample text")
+ .SetFileContentType("text/plain");
+
+ Assert.Equal("text/plain", e.FileContentType);
+ }
+
+ #endregion
+
+ #region Fluent API Tests
+
+ [Fact]
+ public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
+ {
+ Events e = new Events("The CEO announced a merger yesterday.")
+ .SetLanguage("eng")
+ .SetGenre("news")
+ .SetOption("output", "rosette");
+
+ Assert.Equal("eng", e.Language);
+ Assert.Equal("", e.Genre);
+ Assert.Equal("rosette", e.Options["output"]);
+ }
+
+ #endregion
+
+ #region Content Type Tests
+
+ [Fact]
+ public void Constructor_AcceptsUri_AsContent()
+ {
+ Uri uri = new("https://example.com/article");
+ Events e = new(uri);
+
+ Assert.Equal(uri, e.Content);
+ }
+
+ [Fact]
+ public void Constructor_AcceptsString_AsContent()
+ {
+ string text = "The company launched a new product last week.";
+ Events e = new(text);
+
+ Assert.Equal(text, e.Content);
+ }
+
+ #endregion
+
+ #region URL Parameter Tests
+
+ [Fact]
+ public void SetUrlParameter_AddsParameterToUrlParameters_WhenCalled()
+ {
+ Events e = new Events("Sample text")
+ .SetUrlParameter("output", "rosette");
+
+ Assert.Equal("rosette", e.UrlParameters["output"]);
+ }
+
+ #endregion
+
+ #region Negation Option Tests
+
+ [Fact]
+ public void SetOption_SupportsNegationOption_WhenCalledWithOnlyPositive()
+ {
+ Events e = new Events("Bill Gates did not go to the store.")
+ .SetOption("negation", "ONLY_POSITIVE");
+
+ Assert.Equal("ONLY_POSITIVE", e.Options["negation"]);
+ }
+
+ [Fact]
+ public void SetOption_SupportsNegationOption_WhenCalledWithOnlyNegative()
+ {
+ Events e = new Events("Bill Gates did not go to the store.")
+ .SetOption("negation", "ONLY_NEGATIVE");
+
+ Assert.Equal("ONLY_NEGATIVE", e.Options["negation"]);
+ }
+
+ [Fact]
+ public void SetOption_SupportsNegationOption_WhenCalledWithBoth()
+ {
+ Events e = new Events("Bill Gates did not go to the store.")
+ .SetOption("negation", "BOTH");
+
+ Assert.Equal("BOTH", e.Options["negation"]);
+ }
+
+ #endregion
+
+ #region Options Tests
+
+ [Fact]
+ public void SetOption_SupportsMultipleOptions_WhenCalledMultipleTimes()
+ {
+ Events e = new Events("Sample text")
+ .SetOption("negation", "ONLY_POSITIVE")
+ .SetOption("output", "rosette");
+
+ Assert.Equal("ONLY_POSITIVE", e.Options["negation"]);
+ Assert.Equal("rosette", e.Options["output"]);
+ }
+
+ #endregion
+}
diff --git a/tests/Endpoints/InfoTests.cs b/tests/Endpoints/InfoTests.cs
new file mode 100644
index 0000000..a5674e9
--- /dev/null
+++ b/tests/Endpoints/InfoTests.cs
@@ -0,0 +1,107 @@
+using Rosette.Api.Client.Endpoints;
+
+namespace Rosette.Api.Tests.Endpoints;
+
+public class InfoTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_SetsEndpoint_WhenCalled()
+ {
+ Info i = new();
+
+ Assert.Equal("info", i.Endpoint);
+ }
+
+ #endregion
+
+ #region Endpoint Validation Tests
+
+ [Fact]
+ public void Endpoint_ReturnsCorrectValue_Always()
+ {
+ Info i = new();
+
+ Assert.Equal("info", i.Endpoint);
+ }
+
+ #endregion
+
+ #region Options Tests
+
+ [Fact]
+ public void SetOption_AddsOptionToOptions_WhenCalled()
+ {
+ Info i = new Info()
+ .SetOption("test", "value");
+
+ Assert.Equal("value", i.Options["test"]);
+ }
+
+ [Fact]
+ public void SetOption_SupportsMultipleOptions_WhenCalledMultipleTimes()
+ {
+ Info i = new Info()
+ .SetOption("option1", "value1")
+ .SetOption("option2", "value2");
+
+ Assert.Equal("value1", i.Options["option1"]);
+ Assert.Equal("value2", i.Options["option2"]);
+ }
+
+ #endregion
+
+ #region URL Parameter Tests
+
+ [Fact]
+ public void SetUrlParameter_AddsParameterToUrlParameters_WhenCalled()
+ {
+ Info i = new Info()
+ .SetUrlParameter("output", "rosette");
+
+ Assert.Equal("rosette", i.UrlParameters["output"]);
+ }
+
+ [Fact]
+ public void SetUrlParameter_SupportsMultipleParameters_WhenCalledMultipleTimes()
+ {
+ Info i = new Info()
+ .SetUrlParameter("output", "rosette")
+ .SetUrlParameter("format", "json");
+
+ Assert.Equal("rosette", i.UrlParameters["output"]);
+ Assert.Equal("json", i.UrlParameters["format"]);
+ }
+
+ #endregion
+
+ #region Fluent API Tests
+
+ [Fact]
+ public void FluentAPI_AllowsMethodChaining_WhenSettingProperties()
+ {
+ Info i = new Info()
+ .SetOption("test", "value")
+ .SetUrlParameter("output", "rosette");
+
+ Assert.Equal("value", i.Options["test"]);
+ Assert.Equal("rosette", i.UrlParameters["output"]);
+ }
+
+ #endregion
+
+ #region Instance Creation Tests
+
+ [Fact]
+ public void Constructor_CreatesNewInstance_WhenCalled()
+ {
+ Info i1 = new();
+ Info i2 = new();
+
+ Assert.NotSame(i1, i2);
+ }
+
+ #endregion
+
+}
diff --git a/tests/LanguageTests.cs b/tests/Endpoints/LanguageTests.cs
similarity index 87%
rename from tests/LanguageTests.cs
rename to tests/Endpoints/LanguageTests.cs
index 082ec33..4a6a7af 100644
--- a/tests/LanguageTests.cs
+++ b/tests/Endpoints/LanguageTests.cs
@@ -1,9 +1,11 @@
using Rosette.Api.Client.Endpoints;
-namespace Rosette.Api.Tests;
+namespace Rosette.Api.Tests.Endpoints;
public class LanguageTests
{
+ #region Constructor Tests
+
[Fact]
public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
{
@@ -22,6 +24,10 @@ public void Constructor_SetsContentFromUri_WhenCalledWithUri()
Assert.Equal("http://example.com/", l.Content.ToString());
}
+ #endregion
+
+ #region Property Configuration Tests
+
[Fact]
public void SetContent_UpdatesContent_WhenCalled()
{
@@ -39,4 +45,6 @@ public void SetGenre_SetsGenreProperty_WhenCalled()
Assert.Equal("", l.Genre);
}
-}
\ No newline at end of file
+
+ #endregion
+}
diff --git a/tests/Endpoints/MorphologyTests.cs b/tests/Endpoints/MorphologyTests.cs
new file mode 100644
index 0000000..82cc402
--- /dev/null
+++ b/tests/Endpoints/MorphologyTests.cs
@@ -0,0 +1,148 @@
+using Rosette.Api.Client.Endpoints;
+
+namespace Rosette.Api.Tests.Endpoints;
+
+public class MorphologyTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_SetsEndpointAndContent_WhenCalledWithDefaultFeature()
+ {
+ string text = "The quick brown fox jumped over the lazy dog.";
+ Morphology m = new(text);
+
+ Assert.Equal("morphology/complete", m.Endpoint);
+ Assert.Equal(text, m.Content);
+ }
+
+ [Theory]
+ [InlineData(MorphologyFeature.complete, "morphology/complete")]
+ [InlineData(MorphologyFeature.lemmas, "morphology/lemmas")]
+ [InlineData(MorphologyFeature.partsOfSpeech, "morphology/parts-of-speech")]
+ [InlineData(MorphologyFeature.compoundComponents, "morphology/compound-components")]
+ [InlineData(MorphologyFeature.hanReadings, "morphology/han-readings")]
+ public void Constructor_SetsCorrectEndpoint_ForEachFeature(MorphologyFeature feature, string expectedEndpoint)
+ {
+ Morphology m = new("Sample text", feature);
+
+ Assert.Equal(expectedEndpoint, m.Endpoint);
+ }
+
+ #endregion
+
+ #region Property Configuration Tests
+
+ [Fact]
+ public void SetContent_UpdatesContent_WhenCalled()
+ {
+ Morphology m = new("initial content");
+ string newContent = "updated content";
+
+ m.SetContent(newContent);
+
+ Assert.Equal(newContent, m.Content);
+ }
+
+ [Fact]
+ public void SetLanguage_UpdatesLanguage_WhenCalled()
+ {
+ Morphology m = new("Sample text");
+ string language = "eng";
+
+ m.SetLanguage(language);
+
+ Assert.Equal("eng", m.Language);
+ }
+
+ [Fact]
+ public void SetGenre_UpdatesGenre_WhenCalled()
+ {
+ Morphology m = new("Sample text");
+ string genre = "social-media";
+
+ m.SetGenre(genre);
+
+ Assert.Equal("social-media", m.Genre);
+ }
+
+ [Fact]
+ public void SetFileContentType_UpdatesFileContentType_WhenCalled()
+ {
+ Morphology m = new("Sample text");
+ string fileContentType = "text/plain";
+
+ m.SetFileContentType(fileContentType);
+
+ Assert.Equal("text/plain", m.FileContentType);
+ }
+
+ #endregion
+
+ #region Fluent API Tests
+
+ [Fact]
+ public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
+ {
+ Morphology m = new Morphology("The geese went back to get a rest", MorphologyFeature.lemmas)
+ .SetLanguage("eng")
+ .SetGenre("news")
+ .SetOption("debug", true);
+
+ Assert.Equal("morphology/lemmas", m.Endpoint);
+ Assert.Equal("eng", m.Language);
+ Assert.Equal("news", m.Genre);
+ Assert.True((bool)m.Options["debug"]);
+ }
+
+ #endregion
+
+ #region Feature-Specific Tests
+
+ [Fact]
+ public void Constructor_WithLemmasFeature_SetsLemmasEndpoint()
+ {
+ Morphology m = new("banking on their return", MorphologyFeature.lemmas);
+
+ Assert.Equal("morphology/lemmas", m.Endpoint);
+ }
+
+ [Fact]
+ public void Constructor_WithPartsOfSpeechFeature_SetsPartsOfSpeechEndpoint()
+ {
+ Morphology m = new("The cat sat on the mat", MorphologyFeature.partsOfSpeech);
+
+ Assert.Equal("morphology/parts-of-speech", m.Endpoint);
+ }
+
+ [Fact]
+ public void Constructor_WithCompoundComponentsFeature_SetsCompoundComponentsEndpoint()
+ {
+ Morphology m = new("Rechtsschutzversicherungsgesellschaften", MorphologyFeature.compoundComponents);
+
+ Assert.Equal("morphology/compound-components", m.Endpoint);
+ }
+
+ [Fact]
+ public void Constructor_WithHanReadingsFeature_SetsHanReadingsEndpoint()
+ {
+ Morphology m = new("北京大学生物系主任办公室内部会议", MorphologyFeature.hanReadings);
+
+ Assert.Equal("morphology/han-readings", m.Endpoint);
+ }
+
+ #endregion
+
+ #region Content Type Tests
+
+ [Fact]
+ public void Constructor_AcceptsUri_AsContent()
+ {
+ Uri uri = new("https://example.com/text");
+ Morphology m = new(uri, MorphologyFeature.complete);
+
+ Assert.Equal(uri, m.Content);
+ }
+
+ #endregion
+}
diff --git a/tests/NameDeduplicationTests.cs b/tests/Endpoints/NameDeduplicationTests.cs
similarity index 85%
rename from tests/NameDeduplicationTests.cs
rename to tests/Endpoints/NameDeduplicationTests.cs
index 97bd113..dd9f9ca 100644
--- a/tests/NameDeduplicationTests.cs
+++ b/tests/Endpoints/NameDeduplicationTests.cs
@@ -1,10 +1,12 @@
-using Rosette.Api.Client.Endpoints;
+using Rosette.Api.Client.Endpoints;
using Rosette.Api.Client.Models;
-namespace Rosette.Api.Tests;
+namespace Rosette.Api.Tests.Endpoints;
public class NameDeduplicationTests
{
+ #region Constructor Tests
+
[Fact]
public void Constructor_SetsNamesAndThreshold_WhenCalledWithNames() {
List names = new()
@@ -17,6 +19,10 @@ public void Constructor_SetsNamesAndThreshold_WhenCalledWithNames() {
Assert.Equal(0.75f, n.Threshold);
}
+ #endregion
+
+ #region Property Configuration Tests
+
[Fact]
public void SetProfileID_SetsProfileID_WhenCalled() {
List names = new()
@@ -40,4 +46,6 @@ public void SetThreshold_SetsThreshold_WhenCalled() {
Assert.Equal(names, n.Names);
Assert.Equal(0.8f, n.Threshold);
}
+
+ #endregion
}
diff --git a/tests/Endpoints/NameSimilarityTests.cs b/tests/Endpoints/NameSimilarityTests.cs
new file mode 100644
index 0000000..d2a17c2
--- /dev/null
+++ b/tests/Endpoints/NameSimilarityTests.cs
@@ -0,0 +1,89 @@
+using Rosette.Api.Client.Endpoints;
+using Rosette.Api.Client.Models;
+
+namespace Rosette.Api.Tests.Endpoints;
+
+public class NameSimilarityTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_ThrowsArgumentNullException_WhenNamesAreNull() {
+ var exception = Record.Exception(() => new NameSimilarity(null, null));
+ Assert.IsType(exception);
+ Assert.Equal("Value cannot be null. (Parameter 'name1')", exception.Message);
+
+ Name rn = new("foo");
+ exception = Record.Exception(() => new NameSimilarity(rn, null));
+ Assert.IsType(exception);
+ Assert.Equal("Value cannot be null. (Parameter 'name2')", exception.Message);
+ }
+
+ [Fact]
+ public void Constructor_SetsEndpointCorrectly_WhenCalledWithValidNames()
+ {
+ // Arrange
+ var name1 = new Name("John Smith");
+ var name2 = new Name("Jon Smyth");
+
+ // Act
+ var endpoint = new NameSimilarity(name1, name2);
+
+ // Assert
+ Assert.Equal("name-similarity", endpoint.Endpoint);
+ }
+
+ [Fact]
+ public void Constructor_SetsParamsCorrectly_WhenBothNamesProvided()
+ {
+ // Arrange
+ var name1 = new Name("Alice Johnson");
+ var name2 = new Name("Alicia Johnston");
+
+ // Act
+ var endpoint = new NameSimilarity(name1, name2);
+
+ // Assert
+ Assert.Contains("name1", endpoint.Params.Keys);
+ Assert.Contains("name2", endpoint.Params.Keys);
+ Assert.Equal(name1, endpoint.Params["name1"]);
+ Assert.Equal(name2, endpoint.Params["name2"]);
+ }
+
+ [Fact]
+ public void Constructor_HandlesNamesWithLanguage_WhenProvided()
+ {
+ // Arrange
+ var name1 = new Name("José García", language: "spa");
+ var name2 = new Name("Jose Garcia", language: "eng");
+
+ // Act
+ var endpoint = new NameSimilarity(name1, name2);
+
+ // Assert
+ Assert.Equal(name1, endpoint.Params["name1"]);
+ Assert.Equal(name2, endpoint.Params["name2"]);
+ }
+
+ [Fact]
+ public void Constructor_ThrowsArgumentNullException_WhenName1IsNull()
+ {
+ // Arrange
+ var name2 = new Name("Test Name");
+
+ // Act & Assert
+ Assert.Throws(() => new NameSimilarity(null, name2));
+ }
+
+ [Fact]
+ public void Constructor_ThrowsArgumentNullException_WhenName2IsNull()
+ {
+ // Arrange
+ var name1 = new Name("Test Name");
+
+ // Act & Assert
+ Assert.Throws(() => new NameSimilarity(name1, null));
+ }
+
+ #endregion
+}
diff --git a/tests/NameTranslationTests.cs b/tests/Endpoints/NameTranslationTests.cs
similarity index 88%
rename from tests/NameTranslationTests.cs
rename to tests/Endpoints/NameTranslationTests.cs
index a25c66a..5720756 100644
--- a/tests/NameTranslationTests.cs
+++ b/tests/Endpoints/NameTranslationTests.cs
@@ -1,9 +1,11 @@
-using Rosette.Api.Client.Endpoints;
+using Rosette.Api.Client.Endpoints;
-namespace Rosette.Api.Tests;
+namespace Rosette.Api.Tests.Endpoints;
public class NameTranslationTests
{
+ #region Constructor Tests
+
[Fact]
public void Constructor_SetsNameAndDefaults_WhenCalledWithName() {
NameTranslation n = new("foo");
@@ -17,6 +19,10 @@ public void Constructor_SetsNameAndDefaults_WhenCalledWithName() {
Assert.Empty(n.TargetScript);
}
+ #endregion
+
+ #region Fluent API Tests
+
[Fact]
public void FluentAPI_SetsAllProperties_WhenChaining() {
NameTranslation n = new NameTranslation("foo")
@@ -37,4 +43,6 @@ public void FluentAPI_SetsAllProperties_WhenChaining() {
Assert.Equal("eng", n.TargetScript);
}
+ #endregion
+
}
diff --git a/tests/Endpoints/PingTests.cs b/tests/Endpoints/PingTests.cs
new file mode 100644
index 0000000..4c90a64
--- /dev/null
+++ b/tests/Endpoints/PingTests.cs
@@ -0,0 +1,106 @@
+using Rosette.Api.Client.Endpoints;
+
+namespace Rosette.Api.Tests.Endpoints;
+
+public class PingTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_SetsEndpoint_WhenCalled()
+ {
+ Ping p = new();
+
+ Assert.Equal("ping", p.Endpoint);
+ }
+
+ #endregion
+
+ #region Endpoint Validation Tests
+
+ [Fact]
+ public void Endpoint_ReturnsCorrectValue_Always()
+ {
+ Ping p = new();
+
+ Assert.Equal("ping", p.Endpoint);
+ }
+
+ #endregion
+
+ #region Options Tests
+
+ [Fact]
+ public void SetOption_AddsOptionToOptions_WhenCalled()
+ {
+ Ping p = new Ping()
+ .SetOption("test", "value");
+
+ Assert.Equal("value", p.Options["test"]);
+ }
+
+ [Fact]
+ public void SetOption_SupportsMultipleOptions_WhenCalledMultipleTimes()
+ {
+ Ping p = new Ping()
+ .SetOption("option1", "value1")
+ .SetOption("option2", "value2");
+
+ Assert.Equal("value1", p.Options["option1"]);
+ Assert.Equal("value2", p.Options["option2"]);
+ }
+
+ #endregion
+
+ #region URL Parameter Tests
+
+ [Fact]
+ public void SetUrlParameter_AddsParameterToUrlParameters_WhenCalled()
+ {
+ Ping p = new Ping()
+ .SetUrlParameter("output", "rosette");
+
+ Assert.Equal("rosette", p.UrlParameters["output"]);
+ }
+
+ [Fact]
+ public void SetUrlParameter_SupportsMultipleParameters_WhenCalledMultipleTimes()
+ {
+ Ping p = new Ping()
+ .SetUrlParameter("output", "rosette")
+ .SetUrlParameter("format", "json");
+
+ Assert.Equal("rosette", p.UrlParameters["output"]);
+ Assert.Equal("json", p.UrlParameters["format"]);
+ }
+
+ #endregion
+
+ #region Fluent API Tests
+
+ [Fact]
+ public void FluentAPI_AllowsMethodChaining_WhenSettingProperties()
+ {
+ Ping p = new Ping()
+ .SetOption("test", "value")
+ .SetUrlParameter("output", "rosette");
+
+ Assert.Equal("value", p.Options["test"]);
+ Assert.Equal("rosette", p.UrlParameters["output"]);
+ }
+
+ #endregion
+
+ #region Instance Creation Tests
+
+ [Fact]
+ public void Constructor_CreatesNewInstance_WhenCalled()
+ {
+ Ping p1 = new();
+ Ping p2 = new();
+
+ Assert.NotSame(p1, p2);
+ }
+
+ #endregion
+}
diff --git a/tests/Endpoints/RecordSimilarityTests.cs b/tests/Endpoints/RecordSimilarityTests.cs
new file mode 100644
index 0000000..34c0892
--- /dev/null
+++ b/tests/Endpoints/RecordSimilarityTests.cs
@@ -0,0 +1,231 @@
+using Rosette.Api.Client.Endpoints;
+using Rosette.Api.Client.Models;
+
+namespace Rosette.Api.Tests.Endpoints;
+
+public class RecordSimilarityTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_SetsEndpoint_WhenCalledWithValidParameters()
+ {
+ var fields = new Dictionary
+ {
+ { "name", new RecordSimilarityFieldInfo("rni_name", 1.0, 0.0) }
+ };
+ var properties = new RecordSimilarityProperties();
+ var records = new RecordSimilarityRecords();
+
+ RecordSimilarity rs = new(fields, properties, records);
+
+ Assert.Equal("record-similarity", rs.Endpoint);
+ }
+
+ [Fact]
+ public void Constructor_ThrowsArgumentNullException_WhenFieldsIsNull()
+ {
+ var properties = new RecordSimilarityProperties();
+ var records = new RecordSimilarityRecords();
+
+ Assert.Throws(() => new RecordSimilarity(null!, properties, records));
+ }
+
+ [Fact]
+ public void Constructor_ThrowsArgumentNullException_WhenPropertiesIsNull()
+ {
+ var fields = new Dictionary();
+ var records = new RecordSimilarityRecords();
+
+ Assert.Throws(() => new RecordSimilarity(fields, null!, records));
+ }
+
+ [Fact]
+ public void Constructor_ThrowsArgumentNullException_WhenRecordsIsNull()
+ {
+ var fields = new Dictionary();
+ var properties = new RecordSimilarityProperties();
+
+ Assert.Throws(() => new RecordSimilarity(fields, properties, null!));
+ }
+
+ #endregion
+
+ #region Parameter Tests
+
+ [Fact]
+ public void Constructor_AddsFieldsToParams_WhenCalled()
+ {
+ var fields = new Dictionary
+ {
+ { "name", new RecordSimilarityFieldInfo("rni_name", 1.0, 0.0) }
+ };
+ var properties = new RecordSimilarityProperties();
+ var records = new RecordSimilarityRecords();
+
+ RecordSimilarity rs = new(fields, properties, records);
+
+ Assert.Contains("fields", rs.Params.Keys);
+ Assert.Equal(fields, rs.Params["fields"]);
+ }
+
+ [Fact]
+ public void Constructor_AddsPropertiesToParams_WhenCalled()
+ {
+ var fields = new Dictionary();
+ var properties = new RecordSimilarityProperties();
+ var records = new RecordSimilarityRecords();
+
+ RecordSimilarity rs = new(fields, properties, records);
+
+ Assert.Contains("properties", rs.Params.Keys);
+ Assert.Equal(properties, rs.Params["properties"]);
+ }
+
+ [Fact]
+ public void Constructor_AddsRecordsToParams_WhenCalled()
+ {
+ var fields = new Dictionary();
+ var properties = new RecordSimilarityProperties();
+ var records = new RecordSimilarityRecords();
+
+ RecordSimilarity rs = new(fields, properties, records);
+
+ Assert.Contains("records", rs.Params.Keys);
+ Assert.Equal(records, rs.Params["records"]);
+ }
+
+ #endregion
+
+ #region Field Configuration Tests
+
+ [Fact]
+ public void Constructor_AcceptsMultipleFields_WhenCalled()
+ {
+ var fields = new Dictionary
+ {
+ { "name", new RecordSimilarityFieldInfo("rni_name", 1.0, 0.0) },
+ { "address", new RecordSimilarityFieldInfo("rni_address", 0.8, 0.0) },
+ { "birthDate", new RecordSimilarityFieldInfo("rni_date", 0.6, 0.0) }
+ };
+ var properties = new RecordSimilarityProperties();
+ var records = new RecordSimilarityRecords();
+
+ RecordSimilarity rs = new(fields, properties, records);
+
+ var storedFields = (Dictionary)rs.Params["fields"];
+ Assert.Equal(3, storedFields.Count);
+ Assert.Contains("name", storedFields.Keys);
+ Assert.Contains("address", storedFields.Keys);
+ Assert.Contains("birthDate", storedFields.Keys);
+ }
+
+ [Fact]
+ public void Constructor_AcceptsFieldInfoWithNullWeight_WhenCalled()
+ {
+ var fields = new Dictionary
+ {
+ { "name", new RecordSimilarityFieldInfo("rni_name", null, 0.0) }
+ };
+ var properties = new RecordSimilarityProperties();
+ var records = new RecordSimilarityRecords();
+
+ RecordSimilarity rs = new(fields, properties, records);
+
+ var storedFields = (Dictionary)rs.Params["fields"];
+ Assert.Null(storedFields["name"].Weight);
+ }
+
+ [Fact]
+ public void Constructor_AcceptsFieldInfoWithNullScoreIfNull_WhenCalled()
+ {
+ var fields = new Dictionary
+ {
+ { "name", new RecordSimilarityFieldInfo("rni_name", 1.0, null) }
+ };
+ var properties = new RecordSimilarityProperties();
+ var records = new RecordSimilarityRecords();
+
+ RecordSimilarity rs = new(fields, properties, records);
+
+ var storedFields = (Dictionary)rs.Params["fields"];
+ Assert.Null(storedFields["name"].ScoreIfNull);
+ }
+
+ #endregion
+
+ #region Field Type Tests
+
+ [Theory]
+ [InlineData("rni_name")]
+ [InlineData("rni_address")]
+ [InlineData("rni_date")]
+ [InlineData("string")]
+ [InlineData("number")]
+ [InlineData("boolean")]
+ public void Constructor_AcceptsDifferentFieldTypes_WhenCalled(string fieldType)
+ {
+ var fields = new Dictionary
+ {
+ { "testField", new RecordSimilarityFieldInfo(fieldType, 1.0, 0.0) }
+ };
+ var properties = new RecordSimilarityProperties();
+ var records = new RecordSimilarityRecords();
+
+ RecordSimilarity rs = new(fields, properties, records);
+
+ var storedFields = (Dictionary)rs.Params["fields"];
+ Assert.Equal(fieldType, storedFields["testField"].Type);
+ }
+
+ #endregion
+
+ #region Options Tests
+
+ [Fact]
+ public void SetOption_AddsOptionToOptions_WhenCalled()
+ {
+ var fields = new Dictionary();
+ var properties = new RecordSimilarityProperties();
+ var records = new RecordSimilarityRecords();
+
+ RecordSimilarity rs = new RecordSimilarity(fields, properties, records)
+ .SetOption("debug", true);
+
+ Assert.True((bool)rs.Options["debug"]);
+ }
+
+ [Fact]
+ public void SetOption_SupportsMultipleOptions_WhenCalledMultipleTimes()
+ {
+ var fields = new Dictionary();
+ var properties = new RecordSimilarityProperties();
+ var records = new RecordSimilarityRecords();
+
+ RecordSimilarity rs = new RecordSimilarity(fields, properties, records)
+ .SetOption("debug", true)
+ .SetOption("explain", "detailed");
+
+ Assert.True((bool)rs.Options["debug"]);
+ Assert.Equal("detailed", rs.Options["explain"]);
+ }
+
+ #endregion
+
+ #region Empty Collection Tests
+
+ [Fact]
+ public void Constructor_AcceptsEmptyFields_WhenCalled()
+ {
+ var fields = new Dictionary();
+ var properties = new RecordSimilarityProperties();
+ var records = new RecordSimilarityRecords();
+
+ RecordSimilarity rs = new(fields, properties, records);
+
+ var storedFields = (Dictionary)rs.Params["fields"];
+ Assert.Empty(storedFields);
+ }
+
+ #endregion
+}
diff --git a/tests/RelationshipsTests.cs b/tests/Endpoints/RelationshipsTests.cs
similarity index 85%
rename from tests/RelationshipsTests.cs
rename to tests/Endpoints/RelationshipsTests.cs
index 8a58036..659bbf3 100644
--- a/tests/RelationshipsTests.cs
+++ b/tests/Endpoints/RelationshipsTests.cs
@@ -4,6 +4,8 @@ namespace Rosette.Api.Tests
{
public class RelationshipsTests
{
+ #region Constructor Tests
+
[Fact]
public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
{
@@ -14,6 +16,10 @@ public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
Assert.Equal(text, r.Content);
}
+ #endregion
+
+ #region Property Configuration Tests
+
[Fact]
public void SetLanguage_SetsLanguageProperty_WhenCalled()
{
@@ -23,6 +29,10 @@ public void SetLanguage_SetsLanguageProperty_WhenCalled()
Assert.Equal("eng", r.Language);
}
+ #endregion
+
+ #region Fluent API Tests
+
[Fact]
public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
{
@@ -33,5 +43,7 @@ public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
Assert.Equal("eng", r.Language);
Assert.Equal("high", r.Options["accuracy"]);
}
+
+ #endregion
}
-}
\ No newline at end of file
+}
diff --git a/tests/Endpoints/SemanticsVectorTests.cs b/tests/Endpoints/SemanticsVectorTests.cs
new file mode 100644
index 0000000..9893ffd
--- /dev/null
+++ b/tests/Endpoints/SemanticsVectorTests.cs
@@ -0,0 +1,127 @@
+using Rosette.Api.Client.Endpoints;
+
+namespace Rosette.Api.Tests.Endpoints;
+
+public class SemanticsVectorTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
+ {
+ string text = "Cambridge, Massachusetts";
+ SemanticsVector sv = new(text);
+
+ Assert.Equal("semantics/vector", sv.Endpoint);
+ Assert.Equal(text, sv.Content);
+ }
+
+ #endregion
+
+ #region Property Configuration Tests
+
+ [Fact]
+ public void SetLanguage_UpdatesLanguage_WhenCalled()
+ {
+ SemanticsVector sv = new SemanticsVector("Sample text")
+ .SetLanguage("eng");
+
+ Assert.Equal("eng", sv.Language);
+ }
+
+ [Fact]
+ public void SetGenre_UpdatesGenre_WhenCalled()
+ {
+ SemanticsVector sv = new SemanticsVector("Sample text")
+ .SetGenre("social-media");
+
+ Assert.Equal("", sv.Genre);
+ }
+
+ [Fact]
+ public void SetFileContentType_UpdatesFileContentType_WhenCalled()
+ {
+ SemanticsVector sv = new SemanticsVector("Sample text")
+ .SetFileContentType("text/plain");
+
+ Assert.Equal("text/plain", sv.FileContentType);
+ }
+
+ #endregion
+
+ #region Fluent API Tests
+
+ [Fact]
+ public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
+ {
+ SemanticsVector sv = new SemanticsVector("New York City")
+ .SetLanguage("eng")
+ .SetGenre("news")
+ .SetOption("vectorType", "embedding");
+
+ Assert.Equal("eng", sv.Language);
+ Assert.Equal("", sv.Genre);
+ Assert.Equal("embedding", sv.Options["vectorType"]);
+ }
+
+ #endregion
+
+ #region Content Type Tests
+
+ [Fact]
+ public void Constructor_AcceptsUri_AsContent()
+ {
+ Uri uri = new("https://example.com/text");
+ SemanticsVector sv = new(uri);
+
+ Assert.Equal(uri, sv.Content);
+ }
+
+ [Fact]
+ public void Constructor_AcceptsString_AsContent()
+ {
+ string text = "San Francisco, California";
+ SemanticsVector sv = new(text);
+
+ Assert.Equal(text, sv.Content);
+ }
+
+ #endregion
+
+ #region URL Parameter Tests
+
+ [Fact]
+ public void SetUrlParameter_AddsParameterToUrlParameters_WhenCalled()
+ {
+ SemanticsVector sv = new SemanticsVector("Sample text")
+ .SetUrlParameter("output", "rosette");
+
+ Assert.Equal("rosette", sv.UrlParameters["output"]);
+ }
+
+ #endregion
+
+ #region Options Tests
+
+ [Fact]
+ public void SetOption_AddsOptionToOptions_WhenCalled()
+ {
+ SemanticsVector sv = new SemanticsVector("Sample text")
+ .SetOption("vectorType", "dense");
+
+ Assert.Equal("dense", sv.Options["vectorType"]);
+ }
+
+ [Fact]
+ public void SetOption_SupportsMultipleOptions_WhenCalledMultipleTimes()
+ {
+ SemanticsVector sv = new SemanticsVector("Sample text")
+ .SetOption("vectorType", "dense")
+ .SetOption("normalize", true);
+
+ Assert.Equal("dense", sv.Options["vectorType"]);
+ Assert.True((bool)sv.Options["normalize"]);
+ }
+
+ #endregion
+}
diff --git a/tests/SentencesTests.cs b/tests/Endpoints/SentencesTests.cs
similarity index 80%
rename from tests/SentencesTests.cs
rename to tests/Endpoints/SentencesTests.cs
index 9756684..eec5d37 100644
--- a/tests/SentencesTests.cs
+++ b/tests/Endpoints/SentencesTests.cs
@@ -1,9 +1,11 @@
using Rosette.Api.Client.Endpoints;
-namespace Rosette.Api.Tests;
+namespace Rosette.Api.Tests.Endpoints;
public class SentencesTests
{
+ #region Constructor Tests
+
[Fact]
public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
{
@@ -14,6 +16,10 @@ public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
Assert.Equal(text, s.Content);
}
+ #endregion
+
+ #region Property Configuration Tests
+
[Fact]
public void SetLanguage_SetsLanguageProperty_WhenCalled()
{
@@ -23,6 +29,10 @@ public void SetLanguage_SetsLanguageProperty_WhenCalled()
Assert.Equal("eng", s.Language);
}
+ #endregion
+
+ #region Fluent API Tests
+
[Fact]
public void FluentAPI_AllowsMethodChaining_WhenSettingLanguage()
{
@@ -31,4 +41,6 @@ public void FluentAPI_AllowsMethodChaining_WhenSettingLanguage()
Assert.Equal("eng", s.Language);
}
-}
\ No newline at end of file
+
+ #endregion
+}
diff --git a/tests/SentimentTests.cs b/tests/Endpoints/SentimentTests.cs
similarity index 85%
rename from tests/SentimentTests.cs
rename to tests/Endpoints/SentimentTests.cs
index 26f55c3..9380ad8 100644
--- a/tests/SentimentTests.cs
+++ b/tests/Endpoints/SentimentTests.cs
@@ -1,9 +1,11 @@
using Rosette.Api.Client.Endpoints;
-namespace Rosette.Api.Tests;
+namespace Rosette.Api.Tests.Endpoints;
public class SentimentTests
{
+ #region Constructor Tests
+
[Fact]
public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
{
@@ -13,6 +15,10 @@ public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
Assert.Equal("This is a great product!", s.Content);
}
+ #endregion
+
+ #region Property Configuration Tests
+
[Fact]
public void SetLanguage_SetsLanguageProperty_WhenCalled()
{
@@ -32,6 +38,10 @@ public void SetGenre_SetsGenreProperty_WhenCalled()
Assert.Equal("", s.Genre);
}
+ #endregion
+
+ #region Fluent API Tests
+
[Fact]
public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
{
@@ -42,4 +52,6 @@ public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
Assert.Equal("eng", s.Language);
Assert.Equal(0.5, s.Options["sentiment.threshold"]);
}
-}
\ No newline at end of file
+
+ #endregion
+}
diff --git a/tests/SimilarTermsTests.cs b/tests/Endpoints/SimilarTermsTests.cs
similarity index 82%
rename from tests/SimilarTermsTests.cs
rename to tests/Endpoints/SimilarTermsTests.cs
index b657a1f..f533217 100644
--- a/tests/SimilarTermsTests.cs
+++ b/tests/Endpoints/SimilarTermsTests.cs
@@ -1,9 +1,11 @@
using Rosette.Api.Client.Endpoints;
-namespace Rosette.Api.Tests;
+namespace Rosette.Api.Tests.Endpoints;
public class SimilarTermsTests
{
+ #region Constructor Tests
+
[Fact]
public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
{
@@ -13,6 +15,10 @@ public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
Assert.Equal("happy", st.Content);
}
+ #endregion
+
+ #region Property Configuration Tests
+
[Fact]
public void SetLanguage_SetsLanguageProperty_WhenCalled()
{
@@ -23,6 +29,10 @@ public void SetLanguage_SetsLanguageProperty_WhenCalled()
Assert.Equal("computer", st.Content);
}
+ #endregion
+
+ #region Fluent API Tests
+
[Fact]
public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
{
@@ -33,4 +43,6 @@ public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
Assert.Equal("eng", st.Language);
Assert.Equal(10, st.Options["count"]);
}
-}
\ No newline at end of file
+
+ #endregion
+}
diff --git a/tests/Endpoints/SyntaxDependenciesTests.cs b/tests/Endpoints/SyntaxDependenciesTests.cs
new file mode 100644
index 0000000..0d9812b
--- /dev/null
+++ b/tests/Endpoints/SyntaxDependenciesTests.cs
@@ -0,0 +1,103 @@
+using Rosette.Api.Client.Endpoints;
+
+namespace Rosette.Api.Tests.Endpoints;
+
+public class SyntaxDependenciesTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
+ {
+ string text = "Yoshinori Ohsumi won the Nobel Prize.";
+ SyntaxDependencies sd = new(text);
+
+ Assert.Equal("syntax/dependencies", sd.Endpoint);
+ Assert.Equal(text, sd.Content);
+ }
+
+ #endregion
+
+ #region Property Configuration Tests
+
+ [Fact]
+ public void SetLanguage_UpdatesLanguage_WhenCalled()
+ {
+ SyntaxDependencies sd = new SyntaxDependencies("Sample text")
+ .SetLanguage("eng");
+
+ Assert.Equal("eng", sd.Language);
+ }
+
+ [Fact]
+ public void SetGenre_UpdatesGenre_WhenCalled()
+ {
+ SyntaxDependencies sd = new SyntaxDependencies("Sample text")
+ .SetGenre("social-media");
+
+ Assert.Equal("", sd.Genre);
+ }
+
+ [Fact]
+ public void SetFileContentType_UpdatesFileContentType_WhenCalled()
+ {
+ SyntaxDependencies sd = new SyntaxDependencies("Sample text")
+ .SetFileContentType("text/plain");
+
+ Assert.Equal("text/plain", sd.FileContentType);
+ }
+
+ #endregion
+
+ #region Fluent API Tests
+
+ [Fact]
+ public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
+ {
+ SyntaxDependencies sd = new SyntaxDependencies("The cat sat on the mat.")
+ .SetLanguage("eng")
+ .SetGenre("news")
+ .SetOption("output", "rosette");
+
+ Assert.Equal("eng", sd.Language);
+ Assert.Equal("", sd.Genre);
+ Assert.Equal("rosette", sd.Options["output"]);
+ }
+
+ #endregion
+
+ #region Content Type Tests
+
+ [Fact]
+ public void Constructor_AcceptsUri_AsContent()
+ {
+ Uri uri = new("https://example.com/text");
+ SyntaxDependencies sd = new(uri);
+
+ Assert.Equal(uri, sd.Content);
+ }
+
+ [Fact]
+ public void Constructor_AcceptsString_AsContent()
+ {
+ string text = "A complex sentence with multiple dependencies.";
+ SyntaxDependencies sd = new(text);
+
+ Assert.Equal(text, sd.Content);
+ }
+
+ #endregion
+
+ #region URL Parameter Tests
+
+ [Fact]
+ public void SetUrlParameter_AddsParameterToUrlParameters_WhenCalled()
+ {
+ SyntaxDependencies sd = new SyntaxDependencies("Sample text")
+ .SetUrlParameter("output", "rosette");
+
+ Assert.Equal("rosette", sd.UrlParameters["output"]);
+ }
+
+ #endregion
+}
diff --git a/tests/Endpoints/TextEmbeddingTests.cs b/tests/Endpoints/TextEmbeddingTests.cs
new file mode 100644
index 0000000..122689b
--- /dev/null
+++ b/tests/Endpoints/TextEmbeddingTests.cs
@@ -0,0 +1,149 @@
+using Rosette.Api.Client.Endpoints;
+
+namespace Rosette.Api.Tests.Endpoints;
+
+public class TextEmbeddingTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
+ {
+ string text = "Cambridge, Massachusetts";
+ TextEmbedding te = new(text);
+
+ Assert.Equal("text-embedding", te.Endpoint);
+ Assert.Equal(text, te.Content);
+ }
+
+ #endregion
+
+ #region Property Configuration Tests
+
+ [Fact]
+ public void SetLanguage_UpdatesLanguage_WhenCalled()
+ {
+ TextEmbedding te = new TextEmbedding("Sample text")
+ .SetLanguage("eng");
+
+ Assert.Equal("eng", te.Language);
+ }
+
+ [Fact]
+ public void SetGenre_UpdatesGenre_WhenCalled()
+ {
+ TextEmbedding te = new TextEmbedding("Sample text")
+ .SetGenre("social-media");
+
+ Assert.Equal("", te.Genre);
+ }
+
+ [Fact]
+ public void SetFileContentType_UpdatesFileContentType_WhenCalled()
+ {
+ TextEmbedding te = new TextEmbedding("Sample text")
+ .SetFileContentType("text/plain");
+
+ Assert.Equal("text/plain", te.FileContentType);
+ }
+
+ #endregion
+
+ #region Fluent API Tests
+
+ [Fact]
+ public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
+ {
+ TextEmbedding te = new TextEmbedding("Boston, Massachusetts")
+ .SetLanguage("eng")
+ .SetGenre("news")
+ .SetOption("model", "multilingual");
+
+ Assert.Equal("eng", te.Language);
+ Assert.Equal("", te.Genre);
+ Assert.Equal("multilingual", te.Options["model"]);
+ }
+
+ #endregion
+
+ #region Content Type Tests
+
+ [Fact]
+ public void Constructor_AcceptsUri_AsContent()
+ {
+ Uri uri = new("https://example.com/text");
+ TextEmbedding te = new(uri);
+
+ Assert.Equal(uri, te.Content);
+ }
+
+ [Fact]
+ public void Constructor_AcceptsString_AsContent()
+ {
+ string text = "New York City";
+ TextEmbedding te = new(text);
+
+ Assert.Equal(text, te.Content);
+ }
+
+ #endregion
+
+ #region URL Parameter Tests
+
+ [Fact]
+ public void SetUrlParameter_AddsParameterToUrlParameters_WhenCalled()
+ {
+ TextEmbedding te = new TextEmbedding("Sample text")
+ .SetUrlParameter("output", "rosette");
+
+ Assert.Equal("rosette", te.UrlParameters["output"]);
+ }
+
+ #endregion
+
+ #region Options Tests
+
+ [Fact]
+ public void SetOption_AddsOptionToOptions_WhenCalled()
+ {
+ TextEmbedding te = new TextEmbedding("Sample text")
+ .SetOption("model", "english");
+
+ Assert.Equal("english", te.Options["model"]);
+ }
+
+ [Fact]
+ public void SetOption_SupportsMultipleOptions_WhenCalledMultipleTimes()
+ {
+ TextEmbedding te = new TextEmbedding("Sample text")
+ .SetOption("model", "multilingual")
+ .SetOption("pooling", "mean");
+
+ Assert.Equal("multilingual", te.Options["model"]);
+ Assert.Equal("mean", te.Options["pooling"]);
+ }
+
+ #endregion
+
+ #region Model-Specific Tests
+
+ [Fact]
+ public void SetOption_AcceptsMultilingualModel_WhenCalled()
+ {
+ TextEmbedding te = new TextEmbedding("Sample text")
+ .SetOption("model", "multilingual");
+
+ Assert.Equal("multilingual", te.Options["model"]);
+ }
+
+ [Fact]
+ public void SetOption_AcceptsEnglishModel_WhenCalled()
+ {
+ TextEmbedding te = new TextEmbedding("Sample text")
+ .SetOption("model", "english");
+
+ Assert.Equal("english", te.Options["model"]);
+ }
+
+ #endregion
+}
diff --git a/tests/TokensTests.cs b/tests/Endpoints/TokensTests.cs
similarity index 79%
rename from tests/TokensTests.cs
rename to tests/Endpoints/TokensTests.cs
index be742d0..3c73d2f 100644
--- a/tests/TokensTests.cs
+++ b/tests/Endpoints/TokensTests.cs
@@ -1,9 +1,11 @@
using Rosette.Api.Client.Endpoints;
-namespace Rosette.Api.Tests;
+namespace Rosette.Api.Tests.Endpoints;
public class TokensTests
{
+ #region Constructor Tests
+
[Fact]
public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
{
@@ -13,6 +15,10 @@ public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
Assert.Equal("This is sample text", t.Content);
}
+ #endregion
+
+ #region Property Configuration Tests
+
[Fact]
public void SetLanguage_SetsLanguageProperty_WhenCalled()
{
@@ -22,6 +28,10 @@ public void SetLanguage_SetsLanguageProperty_WhenCalled()
Assert.Equal("eng", t.Language);
}
+ #endregion
+
+ #region Fluent API Tests
+
[Fact]
public void FluentAPI_AllowsMethodChaining_WhenSettingLanguage()
{
@@ -30,4 +40,6 @@ public void FluentAPI_AllowsMethodChaining_WhenSettingLanguage()
Assert.Equal("jpn", t.Language);
}
-}
\ No newline at end of file
+
+ #endregion
+}
diff --git a/tests/Endpoints/TopicsTests.cs b/tests/Endpoints/TopicsTests.cs
new file mode 100644
index 0000000..0d930f4
--- /dev/null
+++ b/tests/Endpoints/TopicsTests.cs
@@ -0,0 +1,127 @@
+using Rosette.Api.Client.Endpoints;
+
+namespace Rosette.Api.Tests.Endpoints;
+
+public class TopicsTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
+ {
+ string text = "Lily Collins stars in the new Tolkien biopic.";
+ Topics t = new(text);
+
+ Assert.Equal("topics", t.Endpoint);
+ Assert.Equal(text, t.Content);
+ }
+
+ #endregion
+
+ #region Property Configuration Tests
+
+ [Fact]
+ public void SetLanguage_UpdatesLanguage_WhenCalled()
+ {
+ Topics t = new Topics("Sample text")
+ .SetLanguage("eng");
+
+ Assert.Equal("eng", t.Language);
+ }
+
+ [Fact]
+ public void SetGenre_UpdatesGenre_WhenCalled()
+ {
+ Topics t = new Topics("Sample text")
+ .SetGenre("social-media");
+
+ Assert.Equal("", t.Genre);
+ }
+
+ [Fact]
+ public void SetFileContentType_UpdatesFileContentType_WhenCalled()
+ {
+ Topics t = new Topics("Sample text")
+ .SetFileContentType("text/plain");
+
+ Assert.Equal("text/plain", t.FileContentType);
+ }
+
+ #endregion
+
+ #region Fluent API Tests
+
+ [Fact]
+ public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
+ {
+ Topics t = new Topics("Article about climate change and renewable energy.")
+ .SetLanguage("eng")
+ .SetGenre("news")
+ .SetOption("maxTopics", 5);
+
+ Assert.Equal("eng", t.Language);
+ Assert.Equal("", t.Genre);
+ Assert.Equal(5, t.Options["maxTopics"]);
+ }
+
+ #endregion
+
+ #region Content Type Tests
+
+ [Fact]
+ public void Constructor_AcceptsUri_AsContent()
+ {
+ Uri uri = new("https://example.com/article");
+ Topics t = new(uri);
+
+ Assert.Equal(uri, t.Content);
+ }
+
+ [Fact]
+ public void Constructor_AcceptsString_AsContent()
+ {
+ string text = "Technology companies are investing in artificial intelligence.";
+ Topics t = new(text);
+
+ Assert.Equal(text, t.Content);
+ }
+
+ #endregion
+
+ #region URL Parameter Tests
+
+ [Fact]
+ public void SetUrlParameter_AddsParameterToUrlParameters_WhenCalled()
+ {
+ Topics t = new Topics("Sample text")
+ .SetUrlParameter("output", "rosette");
+
+ Assert.Equal("rosette", t.UrlParameters["output"]);
+ }
+
+ #endregion
+
+ #region Options Tests
+
+ [Fact]
+ public void SetOption_AddsOptionToOptions_WhenCalled()
+ {
+ Topics t = new Topics("Sample text")
+ .SetOption("maxTopics", 10);
+
+ Assert.Equal(10, t.Options["maxTopics"]);
+ }
+
+ [Fact]
+ public void SetOption_SupportsMultipleOptions_WhenCalledMultipleTimes()
+ {
+ Topics t = new Topics("Sample text")
+ .SetOption("maxTopics", 10)
+ .SetOption("minConfidence", 0.75);
+
+ Assert.Equal(10, t.Options["maxTopics"]);
+ Assert.Equal(0.75, t.Options["minConfidence"]);
+ }
+
+ #endregion
+}
diff --git a/tests/Endpoints/TransliterationTests.cs b/tests/Endpoints/TransliterationTests.cs
new file mode 100644
index 0000000..3b0d073
--- /dev/null
+++ b/tests/Endpoints/TransliterationTests.cs
@@ -0,0 +1,134 @@
+using Rosette.Api.Client.Endpoints;
+
+namespace Rosette.Api.Tests.Endpoints;
+
+public class TransliterationTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_SetsEndpointAndContent_WhenCalledWithText()
+ {
+ string text = "ana r2ye7 el gam3a el sa3a 3 el 3asr";
+ Transliteration tr = new(text);
+
+ Assert.Equal("transliteration", tr.Endpoint);
+ Assert.Equal(text, tr.Content);
+ }
+
+ #endregion
+
+ #region Property Configuration Tests
+
+ [Fact]
+ public void SetLanguage_UpdatesLanguage_WhenCalled()
+ {
+ Transliteration tr = new Transliteration("Sample text")
+ .SetLanguage("ara");
+
+ Assert.Equal("ara", tr.Language);
+ }
+
+ [Fact]
+ public void SetGenre_UpdatesGenre_WhenCalled()
+ {
+ Transliteration tr = new Transliteration("Sample text")
+ .SetGenre("social-media");
+
+ Assert.Equal("", tr.Genre);
+ }
+
+ [Fact]
+ public void SetFileContentType_UpdatesFileContentType_WhenCalled()
+ {
+ Transliteration tr = new Transliteration("Sample text")
+ .SetFileContentType("text/plain");
+
+ Assert.Equal("text/plain", tr.FileContentType);
+ }
+
+ #endregion
+
+ #region Fluent API Tests
+
+ [Fact]
+ public void FluentAPI_AllowsMethodChaining_WhenSettingMultipleProperties()
+ {
+ Transliteration tr = new Transliteration("ana r2ye7 el gam3a")
+ .SetLanguage("ara")
+ .SetGenre("social-media")
+ .SetOption("romanization", "native");
+
+ Assert.Equal("ara", tr.Language);
+ Assert.Equal("", tr.Genre);
+ Assert.Equal("native", tr.Options["romanization"]);
+ }
+
+ #endregion
+
+ #region Language-Specific Tests
+
+ [Fact]
+ public void SetLanguage_AcceptsArabic_WhenCalled()
+ {
+ Transliteration tr = new Transliteration("arabic text")
+ .SetLanguage("ara");
+
+ Assert.Equal("ara", tr.Language);
+ }
+
+ [Fact]
+ public void SetLanguage_AcceptsJapanese_WhenCalled()
+ {
+ Transliteration tr = new Transliteration("japanese text")
+ .SetLanguage("jpn");
+
+ Assert.Equal("jpn", tr.Language);
+ }
+
+ [Fact]
+ public void SetLanguage_AcceptsChinese_WhenCalled()
+ {
+ Transliteration tr = new Transliteration("chinese text")
+ .SetLanguage("zho");
+
+ Assert.Equal("zho", tr.Language);
+ }
+
+ #endregion
+
+ #region Content Type Tests
+
+ [Fact]
+ public void Constructor_AcceptsUri_AsContent()
+ {
+ Uri uri = new("https://example.com/text");
+ Transliteration tr = new(uri);
+
+ Assert.Equal(uri, tr.Content);
+ }
+
+ [Fact]
+ public void Constructor_AcceptsString_AsContent()
+ {
+ string text = "Text to transliterate";
+ Transliteration tr = new(text);
+
+ Assert.Equal(text, tr.Content);
+ }
+
+ #endregion
+
+ #region URL Parameter Tests
+
+ [Fact]
+ public void SetUrlParameter_AddsParameterToUrlParameters_WhenCalled()
+ {
+ Transliteration tr = new Transliteration("Sample text")
+ .SetUrlParameter("output", "rosette");
+
+ Assert.Equal("rosette", tr.UrlParameters["output"]);
+ }
+
+ #endregion
+}
diff --git a/tests/Models/BooleanRecordTests.cs b/tests/Models/BooleanRecordTests.cs
new file mode 100644
index 0000000..7143ef3
--- /dev/null
+++ b/tests/Models/BooleanRecordTests.cs
@@ -0,0 +1,255 @@
+using Rosette.Api.Client.Models;
+
+namespace Rosette.Api.Tests.Models;
+
+///
+/// Tests for BooleanRecord class
+///
+public class BooleanRecordTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_NoArgs_CreatesFalseRecord()
+ {
+ // Act
+ var record = new BooleanRecord();
+
+ // Assert
+ Assert.False(record.Boolean);
+ }
+
+ [Fact]
+ public void Constructor_WithTrue_SetsBooleanToTrue()
+ {
+ // Act
+ var record = new BooleanRecord(true);
+
+ // Assert
+ Assert.True(record.Boolean);
+ }
+
+ [Fact]
+ public void Constructor_WithFalse_SetsBooleanToFalse()
+ {
+ // Act
+ var record = new BooleanRecord(false);
+
+ // Assert
+ Assert.False(record.Boolean);
+ }
+
+ #endregion
+
+ #region Property Tests
+
+ [Fact]
+ public void Boolean_CanBeSetToTrue()
+ {
+ // Arrange
+ var record = new BooleanRecord();
+
+ // Act
+ record.Boolean = true;
+
+ // Assert
+ Assert.True(record.Boolean);
+ }
+
+ [Fact]
+ public void Boolean_CanBeSetToFalse()
+ {
+ // Arrange
+ var record = new BooleanRecord(true);
+
+ // Act
+ record.Boolean = false;
+
+ // Assert
+ Assert.False(record.Boolean);
+ }
+
+ [Fact]
+ public void Boolean_CanBeToggledMultipleTimes()
+ {
+ // Arrange
+ var record = new BooleanRecord();
+
+ // Act & Assert
+ record.Boolean = true;
+ Assert.True(record.Boolean);
+
+ record.Boolean = false;
+ Assert.False(record.Boolean);
+
+ record.Boolean = true;
+ Assert.True(record.Boolean);
+ }
+
+ #endregion
+
+ #region Equals Tests
+
+ [Fact]
+ public void Equals_BothTrue_ReturnsTrue()
+ {
+ // Arrange
+ var record1 = new BooleanRecord(true);
+ var record2 = new BooleanRecord(true);
+
+ // Act & Assert
+ Assert.True(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_BothFalse_ReturnsTrue()
+ {
+ // Arrange
+ var record1 = new BooleanRecord(false);
+ var record2 = new BooleanRecord(false);
+
+ // Act & Assert
+ Assert.True(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_OneTrueOneFalse_ReturnsFalse()
+ {
+ // Arrange
+ var record1 = new BooleanRecord(true);
+ var record2 = new BooleanRecord(false);
+
+ // Act & Assert
+ Assert.False(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_Null_ReturnsFalse()
+ {
+ // Arrange
+ var record = new BooleanRecord(true);
+
+ // Act & Assert
+ Assert.False(record.Equals(null));
+ }
+
+ [Fact]
+ public void Equals_DifferentType_ReturnsFalse()
+ {
+ // Arrange
+ var record = new BooleanRecord(true);
+ var differentType = true;
+
+ // Act & Assert
+ Assert.False(record.Equals(differentType));
+ }
+
+ [Fact]
+ public void Equals_SameReference_ReturnsTrue()
+ {
+ // Arrange
+ var record = new BooleanRecord(true);
+
+ // Act & Assert
+ Assert.True(record.Equals(record));
+ }
+
+ #endregion
+
+ #region GetHashCode Tests
+
+ [Fact]
+ public void GetHashCode_BothTrue_ReturnsSameHashCode()
+ {
+ // Arrange
+ var record1 = new BooleanRecord(true);
+ var record2 = new BooleanRecord(true);
+
+ // Act
+ int hash1 = record1.GetHashCode();
+ int hash2 = record2.GetHashCode();
+
+ // Assert
+ Assert.Equal(hash1, hash2);
+ }
+
+ [Fact]
+ public void GetHashCode_BothFalse_ReturnsSameHashCode()
+ {
+ // Arrange
+ var record1 = new BooleanRecord(false);
+ var record2 = new BooleanRecord(false);
+
+ // Act
+ int hash1 = record1.GetHashCode();
+ int hash2 = record2.GetHashCode();
+
+ // Assert
+ Assert.Equal(hash1, hash2);
+ }
+
+ [Fact]
+ public void GetHashCode_TrueAndFalse_ReturnsDifferentHashCode()
+ {
+ // Arrange
+ var record1 = new BooleanRecord(true);
+ var record2 = new BooleanRecord(false);
+
+ // Act
+ int hash1 = record1.GetHashCode();
+ int hash2 = record2.GetHashCode();
+
+ // Assert
+ Assert.NotEqual(hash1, hash2);
+ }
+
+ #endregion
+
+ #region ToString Tests
+
+ [Fact]
+ public void ToString_True_ReturnsLowercaseTrue()
+ {
+ // Arrange
+ var record = new BooleanRecord(true);
+
+ // Act
+ string result = record.ToString();
+
+ // Assert
+ Assert.Equal("true", result);
+ }
+
+ [Fact]
+ public void ToString_False_ReturnsLowercaseFalse()
+ {
+ // Arrange
+ var record = new BooleanRecord(false);
+
+ // Act
+ string result = record.ToString();
+
+ // Assert
+ Assert.Equal("false", result);
+ }
+
+ [Fact]
+ public void ToString_ReturnsLowercaseFormat()
+ {
+ // Arrange
+ var trueRecord = new BooleanRecord(true);
+ var falseRecord = new BooleanRecord(false);
+
+ // Act
+ string trueResult = trueRecord.ToString();
+ string falseResult = falseRecord.ToString();
+
+ // Assert - Verify lowercase (not "True" or "False")
+ Assert.Equal("true", trueResult);
+ Assert.Equal("false", falseResult);
+ Assert.NotEqual("True", trueResult);
+ Assert.NotEqual("False", falseResult);
+ }
+
+ #endregion
+}
diff --git a/tests/Models/FieldedAddressRecordTests.cs b/tests/Models/FieldedAddressRecordTests.cs
new file mode 100644
index 0000000..d805983
--- /dev/null
+++ b/tests/Models/FieldedAddressRecordTests.cs
@@ -0,0 +1,516 @@
+using Rosette.Api.Client.Models;
+using System.Text.Json;
+
+namespace Rosette.Api.Tests.Models;
+
+///
+/// Tests for FieldedAddressRecord class
+///
+public class FieldedAddressRecordTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_NoArgs_CreatesEmptyRecord()
+ {
+ // Act
+ var record = new FieldedAddressRecord();
+
+ // Assert
+ Assert.Null(record.House);
+ Assert.Null(record.HouseNumber);
+ Assert.Null(record.Road);
+ Assert.Null(record.City);
+ Assert.Null(record.State);
+ Assert.Null(record.Country);
+ Assert.Null(record.Postcode);
+ }
+
+ [Fact]
+ public void Constructor_WithAllParameters_SetsAllProperties()
+ {
+ // Act
+ var record = new FieldedAddressRecord(
+ house: "White House",
+ houseNumber: "1600",
+ road: "Pennsylvania Avenue NW",
+ city: "Washington",
+ state: "DC",
+ country: "USA",
+ postcode: "20500"
+ );
+
+ // Assert
+ Assert.Equal("White House", record.House);
+ Assert.Equal("1600", record.HouseNumber);
+ Assert.Equal("Pennsylvania Avenue NW", record.Road);
+ Assert.Equal("Washington", record.City);
+ Assert.Equal("DC", record.State);
+ Assert.Equal("USA", record.Country);
+ Assert.Equal("20500", record.Postcode);
+ }
+
+ [Fact]
+ public void Constructor_WithSubsetOfParameters_SetsSpecifiedProperties()
+ {
+ // Act
+ var record = new FieldedAddressRecord(
+ houseNumber: "123",
+ road: "Main Street",
+ city: "Springfield"
+ );
+
+ // Assert
+ Assert.Equal("123", record.HouseNumber);
+ Assert.Equal("Main Street", record.Road);
+ Assert.Equal("Springfield", record.City);
+ Assert.Null(record.House);
+ Assert.Null(record.State);
+ Assert.Null(record.Country);
+ }
+
+ #endregion
+
+ #region Property Tests - Basic Fields
+
+ [Fact]
+ public void House_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.House = "Empire State Building";
+
+ // Assert
+ Assert.Equal("Empire State Building", record.House);
+ }
+
+ [Fact]
+ public void HouseNumber_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.HouseNumber = "350";
+
+ // Assert
+ Assert.Equal("350", record.HouseNumber);
+ }
+
+ [Fact]
+ public void Road_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.Road = "5th Avenue";
+
+ // Assert
+ Assert.Equal("5th Avenue", record.Road);
+ }
+
+ [Fact]
+ public void City_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.City = "New York";
+
+ // Assert
+ Assert.Equal("New York", record.City);
+ }
+
+ [Fact]
+ public void State_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.State = "NY";
+
+ // Assert
+ Assert.Equal("NY", record.State);
+ }
+
+ [Fact]
+ public void Country_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.Country = "United States";
+
+ // Assert
+ Assert.Equal("United States", record.Country);
+ }
+
+ [Fact]
+ public void Postcode_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.Postcode = "10118";
+
+ // Assert
+ Assert.Equal("10118", record.Postcode);
+ }
+
+ #endregion
+
+ #region Property Tests - Extended Fields
+
+ [Fact]
+ public void Unit_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.Unit = "Apt 3B";
+
+ // Assert
+ Assert.Equal("Apt 3B", record.Unit);
+ }
+
+ [Fact]
+ public void Level_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.Level = "Floor 5";
+
+ // Assert
+ Assert.Equal("Floor 5", record.Level);
+ }
+
+ [Fact]
+ public void Staircase_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.Staircase = "Staircase A";
+
+ // Assert
+ Assert.Equal("Staircase A", record.Staircase);
+ }
+
+ [Fact]
+ public void Entrance_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.Entrance = "North Entrance";
+
+ // Assert
+ Assert.Equal("North Entrance", record.Entrance);
+ }
+
+ [Fact]
+ public void Suburb_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.Suburb = "Brooklyn";
+
+ // Assert
+ Assert.Equal("Brooklyn", record.Suburb);
+ }
+
+ [Fact]
+ public void CityDistrict_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.CityDistrict = "Manhattan";
+
+ // Assert
+ Assert.Equal("Manhattan", record.CityDistrict);
+ }
+
+ [Fact]
+ public void Island_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.Island = "Long Island";
+
+ // Assert
+ Assert.Equal("Long Island", record.Island);
+ }
+
+ [Fact]
+ public void StateDistrict_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.StateDistrict = "Southern District";
+
+ // Assert
+ Assert.Equal("Southern District", record.StateDistrict);
+ }
+
+ [Fact]
+ public void CountryRegion_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.CountryRegion = "New England";
+
+ // Assert
+ Assert.Equal("New England", record.CountryRegion);
+ }
+
+ [Fact]
+ public void WorldRegion_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.WorldRegion = "North America";
+
+ // Assert
+ Assert.Equal("North America", record.WorldRegion);
+ }
+
+ [Fact]
+ public void PoBox_CanBeSetAndRetrieved()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ record.PoBox = "PO Box 123";
+
+ // Assert
+ Assert.Equal("PO Box 123", record.PoBox);
+ }
+
+ #endregion
+
+ #region Equals Tests
+
+ [Fact]
+ public void Equals_SameProperties_ReturnsTrue()
+ {
+ // Arrange
+ var record1 = new FieldedAddressRecord(
+ houseNumber: "123",
+ road: "Main St",
+ city: "Springfield",
+ state: "IL",
+ country: "USA",
+ postcode: "62701"
+ );
+ var record2 = new FieldedAddressRecord(
+ houseNumber: "123",
+ road: "Main St",
+ city: "Springfield",
+ state: "IL",
+ country: "USA",
+ postcode: "62701"
+ );
+
+ // Act & Assert
+ Assert.True(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_AllNullProperties_ReturnsTrue()
+ {
+ // Arrange
+ var record1 = new FieldedAddressRecord();
+ var record2 = new FieldedAddressRecord();
+
+ // Act & Assert
+ Assert.True(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_DifferentHouseNumber_ReturnsFalse()
+ {
+ // Arrange
+ var record1 = new FieldedAddressRecord(houseNumber: "123");
+ var record2 = new FieldedAddressRecord(houseNumber: "456");
+
+ // Act & Assert
+ Assert.False(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_DifferentCity_ReturnsFalse()
+ {
+ // Arrange
+ var record1 = new FieldedAddressRecord(city: "Springfield");
+ var record2 = new FieldedAddressRecord(city: "Boston");
+
+ // Act & Assert
+ Assert.False(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_OnePropertySetOneNull_ReturnsFalse()
+ {
+ // Arrange
+ var record1 = new FieldedAddressRecord(city: "Springfield");
+ var record2 = new FieldedAddressRecord();
+
+ // Act & Assert
+ Assert.False(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_Null_ReturnsFalse()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act & Assert
+ Assert.False(record.Equals(null));
+ }
+
+ [Fact]
+ public void Equals_DifferentType_ReturnsFalse()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+ var differentType = "address";
+
+ // Act & Assert
+ Assert.False(record.Equals(differentType));
+ }
+
+ #endregion
+
+ #region GetHashCode Tests
+
+ [Fact]
+ public void GetHashCode_SameProperties_ReturnsSameHashCode()
+ {
+ // Arrange
+ var record1 = new FieldedAddressRecord(
+ houseNumber: "123",
+ road: "Main St",
+ city: "Springfield"
+ );
+ var record2 = new FieldedAddressRecord(
+ houseNumber: "123",
+ road: "Main St",
+ city: "Springfield"
+ );
+
+ // Act
+ int hash1 = record1.GetHashCode();
+ int hash2 = record2.GetHashCode();
+
+ // Assert
+ Assert.Equal(hash1, hash2);
+ }
+
+ [Fact]
+ public void GetHashCode_DifferentProperties_ReturnsDifferentHashCode()
+ {
+ // Arrange
+ var record1 = new FieldedAddressRecord(city: "Springfield");
+ var record2 = new FieldedAddressRecord(city: "Boston");
+
+ // Act
+ int hash1 = record1.GetHashCode();
+ int hash2 = record2.GetHashCode();
+
+ // Assert
+ Assert.NotEqual(hash1, hash2);
+ }
+
+ #endregion
+
+ #region ToString Tests
+
+ [Fact]
+ public void ToString_ReturnsValidJson()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord(
+ houseNumber: "123",
+ road: "Main St",
+ city: "Springfield"
+ );
+
+ // Act
+ string json = record.ToString();
+
+ // Assert
+ Assert.NotEmpty(json);
+ Assert.Contains("123", json);
+ Assert.Contains("Main St", json);
+ Assert.Contains("Springfield", json);
+ }
+
+ [Fact]
+ public void ToString_CanBeDeserialized()
+ {
+ // Arrange
+ var original = new FieldedAddressRecord(
+ houseNumber: "123",
+ road: "Main St",
+ city: "Springfield",
+ state: "IL",
+ country: "USA",
+ postcode: "62701"
+ );
+
+ // Act
+ string json = original.ToString();
+ var deserialized = JsonSerializer.Deserialize(json);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.HouseNumber, deserialized.HouseNumber);
+ Assert.Equal(original.Road, deserialized.Road);
+ Assert.Equal(original.City, deserialized.City);
+ Assert.Equal(original.State, deserialized.State);
+ Assert.Equal(original.Country, deserialized.Country);
+ Assert.Equal(original.Postcode, deserialized.Postcode);
+ }
+
+ [Fact]
+ public void ToString_EmptyRecord_ProducesValidJson()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ string json = record.ToString();
+ var deserialized = JsonSerializer.Deserialize(json);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ }
+
+ #endregion
+}
diff --git a/tests/Models/FieldedDateRecordTests.cs b/tests/Models/FieldedDateRecordTests.cs
new file mode 100644
index 0000000..692a491
--- /dev/null
+++ b/tests/Models/FieldedDateRecordTests.cs
@@ -0,0 +1,295 @@
+using Rosette.Api.Client.Models;
+using System.Text.Json;
+
+namespace Rosette.Api.Tests.Models;
+
+///
+/// Tests for FieldedDateRecord class
+///
+public class FieldedDateRecordTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_NoArgs_CreatesRecordWithEmptyDate()
+ {
+ // Act
+ var record = new FieldedDateRecord { Date = string.Empty };
+
+ // Assert
+ Assert.Equal(string.Empty, record.Date);
+ Assert.Null(record.Format);
+ }
+
+ [Fact]
+ public void Constructor_WithDateOnly_SetsDateProperty()
+ {
+ // Arrange
+ const string date = "2024-03-10";
+
+ // Act
+ var record = new FieldedDateRecord { Date = date };
+
+ // Assert
+ Assert.Equal(date, record.Date);
+ Assert.Null(record.Format);
+ }
+
+ [Fact]
+ public void Constructor_WithDateAndFormat_SetsBothProperties()
+ {
+ // Arrange
+ const string date = "03/10/2024";
+ const string format = "MM/dd/yyyy";
+
+ // Act
+ var record = new FieldedDateRecord
+ {
+ Date = date,
+ Format = format
+ };
+
+ // Assert
+ Assert.Equal(date, record.Date);
+ Assert.Equal(format, record.Format);
+ }
+
+ #endregion
+
+ #region Property Tests
+
+ [Fact]
+ public void Date_CanBeSet_WithValidValue()
+ {
+ // Arrange
+ var record = new FieldedDateRecord { Date = "2024-01-01" };
+
+ // Act
+ record.Date = "2024-12-31";
+
+ // Assert
+ Assert.Equal("2024-12-31", record.Date);
+ }
+
+ [Fact]
+ public void Format_CanBeSet_WithValidValue()
+ {
+ // Arrange
+ var record = new FieldedDateRecord { Date = "2024-03-10" };
+
+ // Act
+ record.Format = "yyyy-MM-dd";
+
+ // Assert
+ Assert.Equal("yyyy-MM-dd", record.Format);
+ }
+
+ [Fact]
+ public void Format_AcceptsJavaDateTimeFormatterPatterns()
+ {
+ // Arrange
+ var formats = new[]
+ {
+ "yyyy-MM-dd",
+ "MM/dd/yyyy",
+ "dd.MM.yyyy",
+ "yyyy/MM/dd",
+ "yyyyMMdd",
+ "dd-MMM-yyyy"
+ };
+
+ // Act & Assert
+ foreach (var format in formats)
+ {
+ var record = new FieldedDateRecord { Date = "2024-03-10", Format = format };
+ Assert.Equal(format, record.Format);
+ }
+ }
+
+ [Fact]
+ public void Date_AcceptsDifferentDateFormats()
+ {
+ // Arrange
+ var dates = new[]
+ {
+ "2024-03-10",
+ "03/10/2024",
+ "March 10, 2024",
+ "10-Mar-2024",
+ "20240310"
+ };
+
+ // Act & Assert
+ foreach (var date in dates)
+ {
+ var record = new FieldedDateRecord { Date = date };
+ Assert.Equal(date, record.Date);
+ }
+ }
+
+ #endregion
+
+ #region Equals Tests
+
+ [Fact]
+ public void Equals_SameDateAndFormat_ReturnsTrue()
+ {
+ // Arrange
+ var record1 = new FieldedDateRecord { Date = "2024-03-10", Format = "yyyy-MM-dd" };
+ var record2 = new FieldedDateRecord { Date = "2024-03-10", Format = "yyyy-MM-dd" };
+
+ // Act & Assert
+ Assert.True(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_SameDateNoFormat_ReturnsTrue()
+ {
+ // Arrange
+ var record1 = new FieldedDateRecord { Date = "2024-03-10" };
+ var record2 = new FieldedDateRecord { Date = "2024-03-10" };
+
+ // Act & Assert
+ Assert.True(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_DifferentDate_ReturnsFalse()
+ {
+ // Arrange
+ var record1 = new FieldedDateRecord { Date = "2024-03-10" };
+ var record2 = new FieldedDateRecord { Date = "2024-03-11" };
+
+ // Act & Assert
+ Assert.False(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_DifferentFormat_ReturnsFalse()
+ {
+ // Arrange
+ var record1 = new FieldedDateRecord { Date = "2024-03-10", Format = "yyyy-MM-dd" };
+ var record2 = new FieldedDateRecord { Date = "2024-03-10", Format = "MM/dd/yyyy" };
+
+ // Act & Assert
+ Assert.False(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_OneWithFormatOneWithout_ReturnsFalse()
+ {
+ // Arrange
+ var record1 = new FieldedDateRecord { Date = "2024-03-10", Format = "yyyy-MM-dd" };
+ var record2 = new FieldedDateRecord { Date = "2024-03-10" };
+
+ // Act & Assert
+ Assert.False(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_Null_ReturnsFalse()
+ {
+ // Arrange
+ var record = new FieldedDateRecord { Date = "2024-03-10" };
+
+ // Act & Assert
+ Assert.False(record.Equals(null));
+ }
+
+ [Fact]
+ public void Equals_DifferentType_ReturnsFalse()
+ {
+ // Arrange
+ var record = new FieldedDateRecord { Date = "2024-03-10" };
+ var differentType = "2024-03-10";
+
+ // Act & Assert
+ Assert.False(record.Equals(differentType));
+ }
+
+ #endregion
+
+ #region GetHashCode Tests
+
+ [Fact]
+ public void GetHashCode_SameProperties_ReturnsSameHashCode()
+ {
+ // Arrange
+ var record1 = new FieldedDateRecord { Date = "2024-03-10", Format = "yyyy-MM-dd" };
+ var record2 = new FieldedDateRecord { Date = "2024-03-10", Format = "yyyy-MM-dd" };
+
+ // Act
+ int hash1 = record1.GetHashCode();
+ int hash2 = record2.GetHashCode();
+
+ // Assert
+ Assert.Equal(hash1, hash2);
+ }
+
+ [Fact]
+ public void GetHashCode_DifferentProperties_ReturnsDifferentHashCode()
+ {
+ // Arrange
+ var record1 = new FieldedDateRecord { Date = "2024-03-10" };
+ var record2 = new FieldedDateRecord { Date = "2024-03-11" };
+
+ // Act
+ int hash1 = record1.GetHashCode();
+ int hash2 = record2.GetHashCode();
+
+ // Assert
+ Assert.NotEqual(hash1, hash2);
+ }
+
+ #endregion
+
+ #region ToString Tests
+
+ [Fact]
+ public void ToString_ReturnsValidJson()
+ {
+ // Arrange
+ var record = new FieldedDateRecord { Date = "2024-03-10", Format = "yyyy-MM-dd" };
+
+ // Act
+ string json = record.ToString();
+
+ // Assert
+ Assert.NotEmpty(json);
+ Assert.Contains("\"date\"", json);
+ Assert.Contains("2024-03-10", json);
+ }
+
+ [Fact]
+ public void ToString_CanBeDeserialized()
+ {
+ // Arrange
+ var original = new FieldedDateRecord { Date = "2024-03-10", Format = "yyyy-MM-dd" };
+
+ // Act
+ string json = original.ToString();
+ var deserialized = JsonSerializer.Deserialize(json);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.Date, deserialized.Date);
+ Assert.Equal(original.Format, deserialized.Format);
+ }
+
+ [Fact]
+ public void ToString_NullFormat_ProducesValidJson()
+ {
+ // Arrange
+ var record = new FieldedDateRecord { Date = "2024-03-10" };
+
+ // Act
+ string json = record.ToString();
+ var deserialized = JsonSerializer.Deserialize(json);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal("2024-03-10", deserialized.Date);
+ }
+
+ #endregion
+}
diff --git a/tests/Models/FieldedNameRecordTests.cs b/tests/Models/FieldedNameRecordTests.cs
new file mode 100644
index 0000000..36a530a
--- /dev/null
+++ b/tests/Models/FieldedNameRecordTests.cs
@@ -0,0 +1,374 @@
+using Rosette.Api.Client.Models;
+using System.Text.Json;
+
+namespace Rosette.Api.Tests.Models;
+
+///
+/// Tests for FieldedNameRecord class
+///
+public class FieldedNameRecordTests
+{
+ #region Constructor Tests
+
+ [Fact]
+ public void Constructor_NoArgs_CreatesRecordWithEmptyText()
+ {
+ // Act
+ var record = new FieldedNameRecord { Text = string.Empty };
+
+ // Assert
+ Assert.Equal(string.Empty, record.Text);
+ Assert.Null(record.Language);
+ Assert.Null(record.LanguageOfOrigin);
+ Assert.Null(record.Script);
+ Assert.Null(record.EntityType);
+ }
+
+ [Fact]
+ public void Constructor_WithTextOnly_SetsTextProperty()
+ {
+ // Arrange
+ const string text = "John Smith";
+
+ // Act
+ var record = new FieldedNameRecord { Text = text };
+
+ // Assert
+ Assert.Equal(text, record.Text);
+ Assert.Null(record.Language);
+ Assert.Null(record.LanguageOfOrigin);
+ Assert.Null(record.Script);
+ Assert.Null(record.EntityType);
+ }
+
+ [Fact]
+ public void Constructor_WithAllParameters_SetsAllProperties()
+ {
+ // Arrange
+ const string text = "John Smith";
+ const string language = "eng";
+ const string languageOfOrigin = "eng";
+ const string script = "Latn";
+ const string entityType = "PERSON";
+
+ // Act
+ var record = new FieldedNameRecord
+ {
+ Text = text,
+ Language = language,
+ LanguageOfOrigin = languageOfOrigin,
+ Script = script,
+ EntityType = entityType
+ };
+
+ // Assert
+ Assert.Equal(text, record.Text);
+ Assert.Equal(language, record.Language);
+ Assert.Equal(languageOfOrigin, record.LanguageOfOrigin);
+ Assert.Equal(script, record.Script);
+ Assert.Equal(entityType, record.EntityType);
+ }
+
+ #endregion
+
+ #region Property Tests
+
+ [Fact]
+ public void Properties_CanBeSet_Individually()
+ {
+ // Arrange
+ var record = new FieldedNameRecord { Text = "Test" };
+
+ // Act
+ record.Language = "eng";
+ record.LanguageOfOrigin = "spa";
+ record.Script = "Latn";
+ record.EntityType = "PERSON";
+
+ // Assert
+ Assert.Equal("eng", record.Language);
+ Assert.Equal("spa", record.LanguageOfOrigin);
+ Assert.Equal("Latn", record.Script);
+ Assert.Equal("PERSON", record.EntityType);
+ }
+
+ [Fact]
+ public void EntityType_AcceptsPersonType()
+ {
+ // Arrange & Act
+ var record = new FieldedNameRecord { Text = "John Smith", EntityType = "PERSON" };
+
+ // Assert
+ Assert.Equal("PERSON", record.EntityType);
+ }
+
+ [Fact]
+ public void EntityType_AcceptsLocationType()
+ {
+ // Arrange & Act
+ var record = new FieldedNameRecord { Text = "New York", EntityType = "LOCATION" };
+
+ // Assert
+ Assert.Equal("LOCATION", record.EntityType);
+ }
+
+ [Fact]
+ public void EntityType_AcceptsOrganizationType()
+ {
+ // Arrange & Act
+ var record = new FieldedNameRecord { Text = "Microsoft", EntityType = "ORGANIZATION" };
+
+ // Assert
+ Assert.Equal("ORGANIZATION", record.EntityType);
+ }
+
+ [Fact]
+ public void Script_AcceptsISO15924Codes()
+ {
+ // Arrange
+ var scripts = new[] { "Latn", "Cyrl", "Arab", "Hans", "Hant", "Jpan", "Kore" };
+
+ // Act & Assert
+ foreach (var script in scripts)
+ {
+ var record = new FieldedNameRecord { Text = "Test", Script = script };
+ Assert.Equal(script, record.Script);
+ }
+ }
+
+ [Fact]
+ public void Language_AcceptsISO6393Codes()
+ {
+ // Arrange
+ var languages = new[] { "eng", "spa", "fra", "deu", "jpn", "cmn", "ara" };
+
+ // Act & Assert
+ foreach (var language in languages)
+ {
+ var record = new FieldedNameRecord { Text = "Test", Language = language };
+ Assert.Equal(language, record.Language);
+ }
+ }
+
+ #endregion
+
+ #region Equals Tests
+
+ [Fact]
+ public void Equals_SameProperties_ReturnsTrue()
+ {
+ // Arrange
+ var record1 = new FieldedNameRecord
+ {
+ Text = "John Smith",
+ Language = "eng",
+ LanguageOfOrigin = "eng",
+ Script = "Latn",
+ EntityType = "PERSON"
+ };
+ var record2 = new FieldedNameRecord
+ {
+ Text = "John Smith",
+ Language = "eng",
+ LanguageOfOrigin = "eng",
+ Script = "Latn",
+ EntityType = "PERSON"
+ };
+
+ // Act & Assert
+ Assert.True(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_DifferentText_ReturnsFalse()
+ {
+ // Arrange
+ var record1 = new FieldedNameRecord { Text = "John Smith" };
+ var record2 = new FieldedNameRecord { Text = "Jane Doe" };
+
+ // Act & Assert
+ Assert.False(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_DifferentLanguage_ReturnsFalse()
+ {
+ // Arrange
+ var record1 = new FieldedNameRecord { Text = "Test", Language = "eng" };
+ var record2 = new FieldedNameRecord { Text = "Test", Language = "spa" };
+
+ // Act & Assert
+ Assert.False(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_DifferentLanguageOfOrigin_ReturnsFalse()
+ {
+ // Arrange
+ var record1 = new FieldedNameRecord { Text = "Test", LanguageOfOrigin = "eng" };
+ var record2 = new FieldedNameRecord { Text = "Test", LanguageOfOrigin = "spa" };
+
+ // Act & Assert
+ Assert.False(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_DifferentScript_ReturnsFalse()
+ {
+ // Arrange
+ var record1 = new FieldedNameRecord { Text = "Test", Script = "Latn" };
+ var record2 = new FieldedNameRecord { Text = "Test", Script = "Cyrl" };
+
+ // Act & Assert
+ Assert.False(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_DifferentEntityType_ReturnsFalse()
+ {
+ // Arrange
+ var record1 = new FieldedNameRecord { Text = "Test", EntityType = "PERSON" };
+ var record2 = new FieldedNameRecord { Text = "Test", EntityType = "LOCATION" };
+
+ // Act & Assert
+ Assert.False(record1.Equals(record2));
+ }
+
+ [Fact]
+ public void Equals_Null_ReturnsFalse()
+ {
+ // Arrange
+ var record = new FieldedNameRecord { Text = "Test" };
+
+ // Act & Assert
+ Assert.False(record.Equals(null));
+ }
+
+ [Fact]
+ public void Equals_DifferentType_ReturnsFalse()
+ {
+ // Arrange
+ var record = new FieldedNameRecord { Text = "Test" };
+ var differentType = "Test";
+
+ // Act & Assert
+ Assert.False(record.Equals(differentType));
+ }
+
+ #endregion
+
+ #region GetHashCode Tests
+
+ [Fact]
+ public void GetHashCode_SameProperties_ReturnsSameHashCode()
+ {
+ // Arrange
+ var record1 = new FieldedNameRecord
+ {
+ Text = "John Smith",
+ Language = "eng",
+ LanguageOfOrigin = "eng",
+ Script = "Latn",
+ EntityType = "PERSON"
+ };
+ var record2 = new FieldedNameRecord
+ {
+ Text = "John Smith",
+ Language = "eng",
+ LanguageOfOrigin = "eng",
+ Script = "Latn",
+ EntityType = "PERSON"
+ };
+
+ // Act
+ int hash1 = record1.GetHashCode();
+ int hash2 = record2.GetHashCode();
+
+ // Assert
+ Assert.Equal(hash1, hash2);
+ }
+
+ [Fact]
+ public void GetHashCode_DifferentProperties_ReturnsDifferentHashCode()
+ {
+ // Arrange
+ var record1 = new FieldedNameRecord { Text = "John Smith" };
+ var record2 = new FieldedNameRecord { Text = "Jane Doe" };
+
+ // Act
+ int hash1 = record1.GetHashCode();
+ int hash2 = record2.GetHashCode();
+
+ // Assert
+ Assert.NotEqual(hash1, hash2);
+ }
+
+ #endregion
+
+ #region ToString Tests
+
+ [Fact]
+ public void ToString_ReturnsValidJson()
+ {
+ // Arrange
+ var record = new FieldedNameRecord
+ {
+ Text = "John Smith",
+ Language = "eng",
+ LanguageOfOrigin = "eng",
+ Script = "Latn",
+ EntityType = "PERSON"
+ };
+
+ // Act
+ string json = record.ToString();
+
+ // Assert
+ Assert.NotEmpty(json);
+ Assert.Contains("\"text\"", json);
+ Assert.Contains("John Smith", json);
+ }
+
+ [Fact]
+ public void ToString_CanBeDeserialized()
+ {
+ // Arrange
+ var original = new FieldedNameRecord
+ {
+ Text = "John Smith",
+ Language = "eng",
+ LanguageOfOrigin = "eng",
+ Script = "Latn",
+ EntityType = "PERSON"
+ };
+
+ // Act
+ string json = original.ToString();
+ var deserialized = JsonSerializer.Deserialize(json);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal(original.Text, deserialized.Text);
+ Assert.Equal(original.Language, deserialized.Language);
+ Assert.Equal(original.LanguageOfOrigin, deserialized.LanguageOfOrigin);
+ Assert.Equal(original.Script, deserialized.Script);
+ Assert.Equal(original.EntityType, deserialized.EntityType);
+ }
+
+ [Fact]
+ public void ToString_NullOptionalFields_ProducesValidJson()
+ {
+ // Arrange
+ var record = new FieldedNameRecord { Text = "Test" };
+
+ // Act
+ string json = record.ToString();
+ var deserialized = JsonSerializer.Deserialize(json);
+
+ // Assert
+ Assert.NotNull(deserialized);
+ Assert.Equal("Test", deserialized.Text);
+ }
+
+ #endregion
+}
diff --git a/tests/Models/JsonConverter/RecordSimilarityFieldConverterTests.cs b/tests/Models/JsonConverter/RecordSimilarityFieldConverterTests.cs
new file mode 100644
index 0000000..b54c510
--- /dev/null
+++ b/tests/Models/JsonConverter/RecordSimilarityFieldConverterTests.cs
@@ -0,0 +1,466 @@
+using Rosette.Api.Client.Models;
+using Rosette.Api.Client.Models.JsonConverter;
+using System.Text.Json;
+
+namespace Rosette.Api.Tests.Models.JsonConverter;
+
+///
+/// Tests for RecordSimilarityFieldConverter class
+///
+public class RecordSimilarityFieldConverterTests
+{
+ private readonly JsonSerializerOptions _options;
+
+ public RecordSimilarityFieldConverterTests()
+ {
+ _options = new JsonSerializerOptions();
+ _options.Converters.Add(new RecordSimilarityFieldConverter());
+ _options.Converters.Add(new UnfieldedRecordSimilarityConverter());
+ }
+
+ #region Read Tests
+
+ [Fact]
+ public void Read_ThrowsNotSupportedException()
+ {
+ // Arrange
+ var converter = new RecordSimilarityFieldConverter();
+ string json = "{}";
+
+ // Act & Assert
+ NotSupportedException exception = null!;
+ try
+ {
+ var reader = new Utf8JsonReader(System.Text.Encoding.UTF8.GetBytes(json));
+ reader.Read();
+ converter.Read(ref reader, typeof(RecordSimilarityField), _options);
+ }
+ catch (NotSupportedException ex)
+ {
+ exception = ex;
+ }
+
+ Assert.NotNull(exception);
+ Assert.Contains("Deserialization of RecordSimilarityField is not supported", exception.Message);
+ Assert.Contains("type information is lost", exception.Message);
+ }
+
+ #endregion
+
+ #region Write Tests - FieldedNameRecord
+
+ [Fact]
+ public void Write_FieldedNameRecord_SerializesAsObject()
+ {
+ // Arrange
+ var record = new FieldedNameRecord
+ {
+ Text = "John Smith",
+ Language = "eng",
+ EntityType = "PERSON"
+ };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.Contains("\"text\"", json);
+ Assert.Contains("John Smith", json);
+ Assert.Contains("\"language\"", json);
+ Assert.Contains("eng", json);
+ Assert.Contains("\"entityType\"", json);
+ Assert.Contains("PERSON", json);
+ }
+
+ [Fact]
+ public void Write_FieldedNameRecord_WithAllProperties_SerializesCompletely()
+ {
+ // Arrange
+ var record = new FieldedNameRecord
+ {
+ Text = "José García",
+ Language = "spa",
+ LanguageOfOrigin = "spa",
+ Script = "Latn",
+ EntityType = "PERSON"
+ };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert - JSON may escape Unicode
+ Assert.Contains("Jos", json);
+ Assert.Contains("Garc", json);
+ Assert.Contains("spa", json);
+ Assert.Contains("Latn", json);
+ Assert.Contains("PERSON", json);
+ }
+
+ [Fact]
+ public void Write_FieldedNameRecord_WithNullOptionalFields_SerializesTextOnly()
+ {
+ // Arrange
+ var record = new FieldedNameRecord { Text = "Test Name" };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.Contains("\"text\"", json);
+ Assert.Contains("Test Name", json);
+ }
+
+ #endregion
+
+ #region Write Tests - FieldedDateRecord
+
+ [Fact]
+ public void Write_FieldedDateRecord_SerializesAsObject()
+ {
+ // Arrange
+ var record = new FieldedDateRecord
+ {
+ Date = "2024-03-10",
+ Format = "yyyy-MM-dd"
+ };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.Contains("\"date\"", json);
+ Assert.Contains("2024-03-10", json);
+ Assert.Contains("\"format\"", json);
+ Assert.Contains("yyyy-MM-dd", json);
+ }
+
+ [Fact]
+ public void Write_FieldedDateRecord_WithoutFormat_SerializesDateOnly()
+ {
+ // Arrange
+ var record = new FieldedDateRecord { Date = "2024-03-10" };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.Contains("\"date\"", json);
+ Assert.Contains("2024-03-10", json);
+ }
+
+ #endregion
+
+ #region Write Tests - FieldedAddressRecord
+
+ [Fact]
+ public void Write_FieldedAddressRecord_SerializesAsObject()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord
+ {
+ HouseNumber = "123",
+ Road = "Main Street",
+ City = "Springfield",
+ State = "IL",
+ Postcode = "62701"
+ };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.Contains("123", json);
+ Assert.Contains("Main Street", json);
+ Assert.Contains("Springfield", json);
+ Assert.Contains("IL", json);
+ Assert.Contains("62701", json);
+ }
+
+ [Fact]
+ public void Write_FieldedAddressRecord_WithAllFields_SerializesCompletely()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord
+ {
+ House = "White House",
+ HouseNumber = "1600",
+ Road = "Pennsylvania Avenue NW",
+ City = "Washington",
+ State = "DC",
+ Country = "USA",
+ Postcode = "20500"
+ };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.Contains("White House", json);
+ Assert.Contains("1600", json);
+ Assert.Contains("Pennsylvania Avenue NW", json);
+ Assert.Contains("Washington", json);
+ Assert.Contains("DC", json);
+ Assert.Contains("USA", json);
+ Assert.Contains("20500", json);
+ }
+
+ [Fact]
+ public void Write_FieldedAddressRecord_Empty_SerializesAsEmptyObject()
+ {
+ // Arrange
+ var record = new FieldedAddressRecord();
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.Contains("{", json);
+ Assert.Contains("}", json);
+ }
+
+ #endregion
+
+ #region Write Tests - Unfielded Records Delegation
+
+ [Fact]
+ public void Write_UnfieldedNameRecord_DelegatesToUnfieldedConverter()
+ {
+ // Arrange
+ RecordSimilarityField record = new UnfieldedNameRecord { Text = "John Smith" };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.Equal("\"John Smith\"", json);
+ }
+
+ [Fact]
+ public void Write_UnfieldedDateRecord_DelegatesToUnfieldedConverter()
+ {
+ // Arrange
+ RecordSimilarityField record = new UnfieldedDateRecord { Date = "2024-03-10" };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.Equal("\"2024-03-10\"", json);
+ }
+
+ [Fact]
+ public void Write_UnfieldedAddressRecord_DelegatesToUnfieldedConverter()
+ {
+ // Arrange
+ RecordSimilarityField record = new UnfieldedAddressRecord { Address = "123 Main St" };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.Equal("\"123 Main St\"", json);
+ }
+
+ [Fact]
+ public void Write_NumberRecord_DelegatesToUnfieldedConverter()
+ {
+ // Arrange
+ RecordSimilarityField record = new NumberRecord(42);
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.Equal("42", json);
+ }
+
+ [Fact]
+ public void Write_BooleanRecord_DelegatesToUnfieldedConverter()
+ {
+ // Arrange
+ RecordSimilarityField record = new BooleanRecord(true);
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.Equal("true", json);
+ }
+
+ [Fact]
+ public void Write_StringRecord_DelegatesToUnfieldedConverter()
+ {
+ // Arrange
+ RecordSimilarityField record = new StringRecord { Text = "Test" };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.Equal("\"Test\"", json);
+ }
+
+ [Fact]
+ public void Write_UnknownFieldRecord_DelegatesToDefaultCase()
+ {
+ // Arrange
+ RecordSimilarityField record = new UnknownFieldRecord(null);
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.NotNull(json);
+ Assert.Contains("null", json);
+ }
+
+ #endregion
+
+ #region Write Tests - Switch Case Coverage
+
+ [Fact]
+ public void Write_UnfieldedNameRecord_ExecutesUnfieldedCaseBranch()
+ {
+ // Arrange - Test the specific switch case for UnfieldedNameRecord
+ RecordSimilarityField record = new UnfieldedNameRecord { Text = "Jane Doe" };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.NotNull(json);
+ Assert.Contains("Jane Doe", json);
+ }
+
+ [Fact]
+ public void Write_UnfieldedDateRecord_ExecutesUnfieldedCaseBranch()
+ {
+ // Arrange - Test the specific switch case for UnfieldedDateRecord
+ RecordSimilarityField record = new UnfieldedDateRecord { Date = "2026-03-10" };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.NotNull(json);
+ Assert.Contains("2026-03-10", json);
+ }
+
+ [Fact]
+ public void Write_UnfieldedAddressRecord_ExecutesUnfieldedCaseBranch()
+ {
+ // Arrange - Test the specific switch case for UnfieldedAddressRecord
+ RecordSimilarityField record = new UnfieldedAddressRecord { Address = "456 Oak Avenue" };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.NotNull(json);
+ Assert.Contains("456 Oak Avenue", json);
+ }
+
+ [Fact]
+ public void Write_NumberRecord_ExecutesSimpleRecordCaseBranch()
+ {
+ // Arrange - Test the specific switch case for NumberRecord
+ RecordSimilarityField record = new NumberRecord(99);
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.NotNull(json);
+ Assert.Equal("99", json);
+ }
+
+ [Fact]
+ public void Write_BooleanRecord_False_ExecutesSimpleRecordCaseBranch()
+ {
+ // Arrange - Test the specific switch case for BooleanRecord with false value
+ RecordSimilarityField record = new BooleanRecord(false);
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.NotNull(json);
+ Assert.Equal("false", json);
+ }
+
+ [Fact]
+ public void Write_StringRecord_WithSpecialCharacters_ExecutesSimpleRecordCaseBranch()
+ {
+ // Arrange - Test the specific switch case for StringRecord with special characters
+ RecordSimilarityField record = new StringRecord { Text = "Test with special chars" };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.NotNull(json);
+ Assert.Contains("Test with", json);
+ }
+
+ [Fact]
+ public void Write_UnknownFieldRecord_ExecutesDefaultCaseBranch()
+ {
+ // Arrange - Test the default case of the switch statement
+ RecordSimilarityField record = new UnknownFieldRecord(System.Text.Json.Nodes.JsonNode.Parse("{}"));
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert
+ Assert.NotNull(json);
+ // UnknownFieldRecord should serialize the JsonNode data
+ Assert.Contains("{", json);
+ }
+
+ #endregion
+
+ #region Integration Tests
+
+ [Fact]
+ public void Serialize_MixedFieldedAndUnfieldedRecords_HandlesCorrectly()
+ {
+ // Arrange
+ var records = new List
+ {
+ new FieldedNameRecord { Text = "John", Language = "eng" },
+ new UnfieldedNameRecord { Text = "Jane" },
+ new NumberRecord(42),
+ new BooleanRecord(true)
+ };
+
+ // Act
+ string json = JsonSerializer.Serialize(records, _options);
+
+ // Assert
+ Assert.Contains("John", json);
+ Assert.Contains("eng", json);
+ Assert.Contains("Jane", json);
+ Assert.Contains("42", json);
+ Assert.Contains("true", json);
+ }
+
+ [Fact]
+ public void Serialize_FieldedRecord_ProducesValidJson()
+ {
+ // Arrange
+ var record = new FieldedNameRecord
+ {
+ Text = "Test",
+ Language = "eng"
+ };
+
+ // Act
+ string json = JsonSerializer.Serialize(record, _options);
+
+ // Assert - Should be able to parse as valid JSON
+ var parsed = JsonDocument.Parse(json);
+ Assert.NotNull(parsed);
+ }
+
+ #endregion
+}
diff --git a/tests/Models/JsonConverter/RecordSimilarityRecordsConverterTests.cs b/tests/Models/JsonConverter/RecordSimilarityRecordsConverterTests.cs
new file mode 100644
index 0000000..82d1bb5
--- /dev/null
+++ b/tests/Models/JsonConverter/RecordSimilarityRecordsConverterTests.cs
@@ -0,0 +1,378 @@
+using Rosette.Api.Client.Models;
+using Rosette.Api.Client.Models.JsonConverter;
+using System.Text.Json;
+
+namespace Rosette.Api.Tests.Models.JsonConverter;
+
+///
+/// Tests for RecordSimilarityRecordsConverter class
+///
+public class RecordSimilarityRecordsConverterTests
+{
+ private readonly JsonSerializerOptions _options;
+
+ public RecordSimilarityRecordsConverterTests()
+ {
+ _options = new JsonSerializerOptions();
+ _options.Converters.Add(new RecordSimilarityRecordsConverter());
+ _options.Converters.Add(new RecordSimilarityFieldConverter());
+ _options.Converters.Add(new UnfieldedRecordSimilarityConverter());
+ }
+
+ #region Write Tests - Basic Functionality
+
+ [Fact]
+ public void Write_EmptyRecords_SerializesCorrectly()
+ {
+ // Arrange
+ var records = new RecordSimilarityRecords(
+ new List>(),
+ new List>()
+ );
+
+ // Act
+ string json = JsonSerializer.Serialize(records, _options);
+
+ // Assert
+ Assert.Contains("\"left\"", json);
+ Assert.Contains("\"right\"", json);
+ Assert.Contains("[]", json);
+ }
+
+ [Fact]
+ public void Write_SingleRecord_SerializesCorrectly()
+ {
+ // Arrange
+ var leftRecords = new List>
+ {
+ new Dictionary
+ {
+ { "name", new UnfieldedNameRecord { Text = "John Smith" } },
+ { "age", new NumberRecord(30) }
+ }
+ };
+ var rightRecords = new List>
+ {
+ new Dictionary
+ {
+ { "name", new UnfieldedNameRecord { Text = "Jane Doe" } },
+ { "age", new NumberRecord(28) }
+ }
+ };
+ var records = new RecordSimilarityRecords(leftRecords, rightRecords);
+
+ // Act
+ string json = JsonSerializer.Serialize(records, _options);
+
+ // Assert
+ Assert.Contains("\"left\"", json);
+ Assert.Contains("\"right\"", json);
+ Assert.Contains("John Smith", json);
+ Assert.Contains("Jane Doe", json);
+ Assert.Contains("30", json);
+ Assert.Contains("28", json);
+ }
+
+ [Fact]
+ public void Write_MultipleRecords_SerializesAll()
+ {
+ // Arrange
+ var leftRecords = new List>
+ {
+ new Dictionary
+ {
+ { "name", new UnfieldedNameRecord { Text = "Person 1" } }
+ },
+ new Dictionary
+ {
+ { "name", new UnfieldedNameRecord { Text = "Person 2" } }
+ }
+ };
+ var rightRecords = new List>
+ {
+ new Dictionary
+ {
+ { "name", new UnfieldedNameRecord { Text = "Person 3" } }
+ }
+ };
+ var records = new RecordSimilarityRecords(leftRecords, rightRecords);
+
+ // Act
+ string json = JsonSerializer.Serialize(records, _options);
+
+ // Assert
+ Assert.Contains("Person 1", json);
+ Assert.Contains("Person 2", json);
+ Assert.Contains("Person 3", json);
+ }
+
+ #endregion
+
+ #region Write Tests - Mixed Field Types
+
+ [Fact]
+ public void Write_FieldedAndUnfieldedRecords_SerializesBoth()
+ {
+ // Arrange
+ var leftRecords = new List>
+ {
+ new Dictionary
+ {
+ { "name", new FieldedNameRecord { Text = "John", Language = "eng" } },
+ { "simpleText", new UnfieldedNameRecord { Text = "Simple" } },
+ { "count", new NumberRecord(5) },
+ { "active", new BooleanRecord(true) }
+ }
+ };
+ var rightRecords = new List>();
+ var records = new RecordSimilarityRecords(leftRecords, rightRecords);
+
+ // Act
+ string json = JsonSerializer.Serialize(records, _options);
+
+ // Assert
+ Assert.Contains("John", json);
+ Assert.Contains("eng", json);
+ Assert.Contains("Simple", json);
+ Assert.Contains("5", json);
+ Assert.Contains("true", json);
+ }
+
+ [Fact]
+ public void Write_DatesAndAddresses_SerializesCorrectly()
+ {
+ // Arrange
+ var leftRecords = new List>
+ {
+ new Dictionary
+ {
+ { "date", new UnfieldedDateRecord { Date = "2024-03-10" } },
+ { "address", new UnfieldedAddressRecord { Address = "123 Main St" } },
+ { "fieldedDate", new FieldedDateRecord { Date = "2024-03-10", Format = "yyyy-MM-dd" } }
+ }
+ };
+ var rightRecords = new List>();
+ var records = new RecordSimilarityRecords(leftRecords, rightRecords);
+
+ // Act
+ string json = JsonSerializer.Serialize(records, _options);
+
+ // Assert
+ Assert.Contains("2024-03-10", json);
+ Assert.Contains("123 Main St", json);
+ Assert.Contains("yyyy-MM-dd", json);
+ }
+
+ #endregion
+
+ #region Write Tests - Complex Scenarios
+
+ [Fact]
+ public void Write_ComplexFieldedAddress_SerializesAllProperties()
+ {
+ // Arrange
+ var leftRecords = new List>
+ {
+ new Dictionary
+ {
+ {
+ "address",
+ new FieldedAddressRecord
+ {
+ HouseNumber = "123",
+ Road = "Main Street",
+ City = "Springfield",
+ State = "IL",
+ Postcode = "62701",
+ Country = "USA"
+ }
+ }
+ }
+ };
+ var rightRecords = new List>();
+ var records = new RecordSimilarityRecords(leftRecords, rightRecords);
+
+ // Act
+ string json = JsonSerializer.Serialize(records, _options);
+
+ // Assert
+ Assert.Contains("123", json);
+ Assert.Contains("Main Street", json);
+ Assert.Contains("Springfield", json);
+ Assert.Contains("IL", json);
+ Assert.Contains("62701", json);
+ Assert.Contains("USA", json);
+ }
+
+ [Fact]
+ public void Write_StringRecord_SerializesAsString()
+ {
+ // Arrange
+ var leftRecords = new List>
+ {
+ new Dictionary
+ {
+ { "text", new StringRecord { Text = "Sample Text" } }
+ }
+ };
+ var rightRecords = new List>();
+ var records = new RecordSimilarityRecords(leftRecords, rightRecords);
+
+ // Act
+ string json = JsonSerializer.Serialize(records, _options);
+
+ // Assert
+ Assert.Contains("Sample Text", json);
+ }
+
+ #endregion
+
+ #region Read Tests
+
+ [Fact]
+ public void Read_EmptyRecords_DeserializesCorrectly()
+ {
+ // Arrange
+ string json = "{\"left\":[],\"right\":[]}";
+
+ // Act
+ var result = JsonSerializer.Deserialize(json, _options);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.Empty(result.Left);
+ Assert.Empty(result.Right);
+ }
+
+ [Fact]
+ public void Read_AnyNonEmptyRecords_ThrowsNotSupported()
+ {
+ // Arrange - Any records with fields will fail because RecordSimilarityField deserialization is not supported
+ string json = @"{
+ ""left"": [
+ {
+ ""age"": 30,
+ ""active"": true
+ }
+ ],
+ ""right"": []
+ }";
+
+ // Act & Assert - Reading throws because RecordSimilarityField.Read is not supported
+ Assert.Throws(() =>
+ JsonSerializer.Deserialize(json, _options));
+ }
+
+ #endregion
+
+ #region Round-Trip Tests
+
+ [Fact]
+ public void RoundTrip_SimpleRecords_SerializationWorks()
+ {
+ // Arrange - Note: Deserialization is not supported, only testing serialization
+ var original = new RecordSimilarityRecords(
+ new List>
+ {
+ new Dictionary
+ {
+ { "name", new UnfieldedNameRecord { Text = "John" } },
+ { "age", new NumberRecord(30) }
+ }
+ },
+ new List>
+ {
+ new Dictionary
+ {
+ { "name", new UnfieldedNameRecord { Text = "Jane" } },
+ { "age", new NumberRecord(28) }
+ }
+ }
+ );
+
+ // Act
+ string json = JsonSerializer.Serialize(original, _options);
+
+ // Assert - Verify JSON structure
+ Assert.Contains("\"left\"", json);
+ Assert.Contains("\"right\"", json);
+ Assert.Contains("John", json);
+ Assert.Contains("Jane", json);
+ Assert.Contains("30", json);
+ Assert.Contains("28", json);
+ }
+
+ [Fact]
+ public void RoundTrip_EmptyRecords_SerializationWorks()
+ {
+ // Arrange - Note: Deserialization is not supported, only testing serialization
+ var original = new RecordSimilarityRecords(
+ new List>(),
+ new List>()
+ );
+
+ // Act
+ string json = JsonSerializer.Serialize(original, _options);
+
+ // Assert - Verify JSON structure
+ Assert.Contains("\"left\"", json);
+ Assert.Contains("\"right\"", json);
+ Assert.Contains("[]", json);
+ }
+
+ #endregion
+
+ #region Validation Tests
+
+ [Fact]
+ public void Serialize_ProducesValidJson()
+ {
+ // Arrange
+ var records = new RecordSimilarityRecords(
+ new List>
+ {
+ new Dictionary
+ {
+ { "field1", new UnfieldedNameRecord { Text = "Value1" } }
+ }
+ },
+ new List>()
+ );
+
+ // Act
+ string json = JsonSerializer.Serialize(records, _options);
+
+ // Assert - Should be valid JSON
+ var parsed = JsonDocument.Parse(json);
+ Assert.NotNull(parsed);
+ Assert.True(parsed.RootElement.TryGetProperty("left", out _));
+ Assert.True(parsed.RootElement.TryGetProperty("right", out _));
+ }
+
+ [Fact]
+ public void Write_HasCorrectStructure()
+ {
+ // Arrange
+ var records = new RecordSimilarityRecords(
+ new List>
+ {
+ new Dictionary
+ {
+ { "name", new UnfieldedNameRecord { Text = "Test" } }
+ }
+ },
+ new List>()
+ );
+
+ // Act
+ string json = JsonSerializer.Serialize(records, _options);
+ var doc = JsonDocument.Parse(json);
+
+ // Assert
+ Assert.Equal(JsonValueKind.Object, doc.RootElement.ValueKind);
+ Assert.Equal(JsonValueKind.Array, doc.RootElement.GetProperty("left").ValueKind);
+ Assert.Equal(JsonValueKind.Array, doc.RootElement.GetProperty("right").ValueKind);
+ }
+
+ #endregion
+}
diff --git a/tests/Models/JsonConverter/UnfieldedRecordSimilarityConverterTests.cs b/tests/Models/JsonConverter/UnfieldedRecordSimilarityConverterTests.cs
new file mode 100644
index 0000000..cfe33f5
--- /dev/null
+++ b/tests/Models/JsonConverter/UnfieldedRecordSimilarityConverterTests.cs
@@ -0,0 +1,571 @@
+using Rosette.Api.Client.Models;
+using Rosette.Api.Client.Models.JsonConverter;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace Rosette.Api.Tests.Models.JsonConverter;
+
+///
+/// Tests for UnfieldedRecordSimilarityConverter class
+///
+public class UnfieldedRecordSimilarityConverterTests
+{
+ private readonly JsonSerializerOptions _options;
+
+ public UnfieldedRecordSimilarityConverterTests()
+ {
+ _options = new JsonSerializerOptions();
+ _options.Converters.Add(new UnfieldedRecordSimilarityConverter());
+ }
+
+ #region CanConvert Tests
+
+ [Fact]
+ public void CanConvert_UnfieldedNameRecord_ReturnsTrue()
+ {
+ // Arrange
+ var converter = new UnfieldedRecordSimilarityConverter();
+
+ // Act
+ bool result = converter.CanConvert(typeof(UnfieldedNameRecord));
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void CanConvert_UnfieldedDateRecord_ReturnsTrue()
+ {
+ // Arrange
+ var converter = new UnfieldedRecordSimilarityConverter();
+
+ // Act
+ bool result = converter.CanConvert(typeof(UnfieldedDateRecord));
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void CanConvert_UnfieldedAddressRecord_ReturnsTrue()
+ {
+ // Arrange
+ var converter = new UnfieldedRecordSimilarityConverter();
+
+ // Act
+ bool result = converter.CanConvert(typeof(UnfieldedAddressRecord));
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void CanConvert_NumberRecord_ReturnsTrue()
+ {
+ // Arrange
+ var converter = new UnfieldedRecordSimilarityConverter();
+
+ // Act
+ bool result = converter.CanConvert(typeof(NumberRecord));
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void CanConvert_BooleanRecord_ReturnsTrue()
+ {
+ // Arrange
+ var converter = new UnfieldedRecordSimilarityConverter();
+
+ // Act
+ bool result = converter.CanConvert(typeof(BooleanRecord));
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void CanConvert_StringRecord_ReturnsTrue()
+ {
+ // Arrange
+ var converter = new UnfieldedRecordSimilarityConverter();
+
+ // Act
+ bool result = converter.CanConvert(typeof(StringRecord));
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void CanConvert_UnknownFieldRecord_ReturnsTrue()
+ {
+ // Arrange
+ var converter = new UnfieldedRecordSimilarityConverter();
+
+ // Act
+ bool result = converter.CanConvert(typeof(UnknownFieldRecord));
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void CanConvert_Object_ReturnsTrue()
+ {
+ // Arrange
+ var converter = new UnfieldedRecordSimilarityConverter();
+
+ // Act
+ bool result = converter.CanConvert(typeof(object));
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void CanConvert_UnsupportedType_ReturnsFalse()
+ {
+ // Arrange
+ var converter = new UnfieldedRecordSimilarityConverter();
+
+ // Act
+ bool result = converter.CanConvert(typeof(FieldedNameRecord));
+
+ // Assert
+ Assert.False(result);
+ }
+
+ #endregion
+
+ #region Write Tests - UnfieldedNameRecord
+
+ [Fact]
+ public void Write_UnfieldedNameRecord_SerializesAsString()
+ {
+ // Arrange
+ var record = new UnfieldedNameRecord { Text = "John Smith" };
+
+ // Act
+ string json = JsonSerializer.Serialize