fix(external-system-gateway): resolve ExternalSystemGateway-011 — name-keyed repository lookups replace fetch-all-then-filter on the call hot path
This commit is contained in:
@@ -76,6 +76,94 @@ public class ExternalSystemRepositoryTests : IDisposable
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new ExternalSystemRepository(null!));
|
||||
}
|
||||
|
||||
// ── ExternalSystemGateway-011: name-keyed repository lookups ──
|
||||
|
||||
[Fact]
|
||||
public async Task GetExternalSystemByName_ReturnsMatchingRow()
|
||||
{
|
||||
await _repository.AddExternalSystemAsync(
|
||||
new ExternalSystemDefinition("Alpha", "https://alpha.test", "ApiKey"));
|
||||
await _repository.AddExternalSystemAsync(
|
||||
new ExternalSystemDefinition("Beta", "https://beta.test", "Basic"));
|
||||
await _repository.SaveChangesAsync();
|
||||
|
||||
var loaded = await _repository.GetExternalSystemByNameAsync("Beta");
|
||||
|
||||
Assert.NotNull(loaded);
|
||||
Assert.Equal("Beta", loaded!.Name);
|
||||
Assert.Equal("https://beta.test", loaded.EndpointUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetExternalSystemByName_MissingName_ReturnsNull()
|
||||
{
|
||||
await _repository.AddExternalSystemAsync(
|
||||
new ExternalSystemDefinition("Alpha", "https://alpha.test", "ApiKey"));
|
||||
await _repository.SaveChangesAsync();
|
||||
|
||||
Assert.Null(await _repository.GetExternalSystemByNameAsync("DoesNotExist"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMethodByName_ReturnsMethodScopedToParentSystem()
|
||||
{
|
||||
var sysA = new ExternalSystemDefinition("SysA", "https://a.test", "ApiKey");
|
||||
var sysB = new ExternalSystemDefinition("SysB", "https://b.test", "ApiKey");
|
||||
await _repository.AddExternalSystemAsync(sysA);
|
||||
await _repository.AddExternalSystemAsync(sysB);
|
||||
await _repository.SaveChangesAsync();
|
||||
|
||||
// Same method name on two different systems — the lookup must be scoped.
|
||||
await _repository.AddExternalSystemMethodAsync(
|
||||
new ExternalSystemMethod("getData", "GET", "/a") { ExternalSystemDefinitionId = sysA.Id });
|
||||
await _repository.AddExternalSystemMethodAsync(
|
||||
new ExternalSystemMethod("getData", "POST", "/b") { ExternalSystemDefinitionId = sysB.Id });
|
||||
await _repository.SaveChangesAsync();
|
||||
|
||||
var method = await _repository.GetMethodByNameAsync(sysB.Id, "getData");
|
||||
|
||||
Assert.NotNull(method);
|
||||
Assert.Equal(sysB.Id, method!.ExternalSystemDefinitionId);
|
||||
Assert.Equal("POST", method.HttpMethod);
|
||||
Assert.Equal("/b", method.Path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetMethodByName_MissingMethod_ReturnsNull()
|
||||
{
|
||||
var sys = new ExternalSystemDefinition("SysA", "https://a.test", "ApiKey");
|
||||
await _repository.AddExternalSystemAsync(sys);
|
||||
await _repository.SaveChangesAsync();
|
||||
|
||||
Assert.Null(await _repository.GetMethodByNameAsync(sys.Id, "noSuchMethod"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDatabaseConnectionByName_ReturnsMatchingRow()
|
||||
{
|
||||
await _repository.AddDatabaseConnectionAsync(
|
||||
new DatabaseConnectionDefinition("Plant", "Server=plant;Database=p;"));
|
||||
await _repository.AddDatabaseConnectionAsync(
|
||||
new DatabaseConnectionDefinition("Historian", "Server=hist;Database=h;"));
|
||||
await _repository.SaveChangesAsync();
|
||||
|
||||
var loaded = await _repository.GetDatabaseConnectionByNameAsync("Historian");
|
||||
|
||||
Assert.NotNull(loaded);
|
||||
Assert.Equal("Historian", loaded!.Name);
|
||||
Assert.Equal("Server=hist;Database=h;", loaded.ConnectionString);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDatabaseConnectionByName_MissingName_ReturnsNull()
|
||||
{
|
||||
await _repository.AddDatabaseConnectionAsync(
|
||||
new DatabaseConnectionDefinition("Plant", "Server=plant;Database=p;"));
|
||||
await _repository.SaveChangesAsync();
|
||||
|
||||
Assert.Null(await _repository.GetDatabaseConnectionByNameAsync("DoesNotExist"));
|
||||
}
|
||||
}
|
||||
|
||||
public class NotificationRepositoryTests : IDisposable
|
||||
|
||||
@@ -14,10 +14,25 @@ public class DatabaseGatewayTests
|
||||
{
|
||||
private readonly IExternalSystemRepository _repository = Substitute.For<IExternalSystemRepository>();
|
||||
|
||||
/// <summary>
|
||||
/// Configures the repository substitute for the name-keyed connection-resolution
|
||||
/// path used by <c>DatabaseGateway</c> (ExternalSystemGateway-011). A <c>null</c>
|
||||
/// connection models a "not found" — the substitute returns <c>null</c> by default,
|
||||
/// so no stub is needed for the absent entity.
|
||||
/// </summary>
|
||||
private void StubConnection(DatabaseConnectionDefinition? connection)
|
||||
{
|
||||
if (connection != null)
|
||||
{
|
||||
_repository.GetDatabaseConnectionByNameAsync(connection.Name, Arg.Any<CancellationToken>())
|
||||
.Returns(connection);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetConnection_NotFound_Throws()
|
||||
{
|
||||
_repository.GetAllDatabaseConnectionsAsync().Returns(new List<DatabaseConnectionDefinition>());
|
||||
StubConnection(connection: null);
|
||||
|
||||
var gateway = new DatabaseGateway(
|
||||
_repository,
|
||||
@@ -31,8 +46,7 @@ public class DatabaseGatewayTests
|
||||
public async Task CachedWrite_NoStoreAndForward_Throws()
|
||||
{
|
||||
var conn = new DatabaseConnectionDefinition("testDb", "Server=localhost;Database=test") { Id = 1 };
|
||||
_repository.GetAllDatabaseConnectionsAsync()
|
||||
.Returns(new List<DatabaseConnectionDefinition> { conn });
|
||||
StubConnection(conn);
|
||||
|
||||
var gateway = new DatabaseGateway(
|
||||
_repository,
|
||||
@@ -46,7 +60,7 @@ public class DatabaseGatewayTests
|
||||
[Fact]
|
||||
public async Task CachedWrite_ConnectionNotFound_Throws()
|
||||
{
|
||||
_repository.GetAllDatabaseConnectionsAsync().Returns(new List<DatabaseConnectionDefinition>());
|
||||
StubConnection(connection: null);
|
||||
|
||||
var gateway = new DatabaseGateway(
|
||||
_repository,
|
||||
@@ -67,8 +81,7 @@ public class DatabaseGatewayTests
|
||||
MaxRetries = 5,
|
||||
RetryDelay = TimeSpan.FromSeconds(12),
|
||||
};
|
||||
_repository.GetAllDatabaseConnectionsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<DatabaseConnectionDefinition> { conn });
|
||||
StubConnection(conn);
|
||||
|
||||
var dbName = $"EsgCachedWrite_{Guid.NewGuid():N}";
|
||||
var connStr = $"Data Source={dbName};Mode=Memory;Cache=Shared";
|
||||
@@ -108,8 +121,7 @@ public class DatabaseGatewayTests
|
||||
MaxRetries = 0,
|
||||
RetryDelay = TimeSpan.FromSeconds(3),
|
||||
};
|
||||
_repository.GetAllDatabaseConnectionsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<DatabaseConnectionDefinition> { conn });
|
||||
StubConnection(conn);
|
||||
|
||||
var dbName = $"EsgCachedWriteZero_{Guid.NewGuid():N}";
|
||||
var connStr = $"Data Source={dbName};Mode=Memory;Cache=Shared";
|
||||
@@ -153,7 +165,7 @@ public class DatabaseGatewayTests
|
||||
[Fact]
|
||||
public async Task DeliverBuffered_ConnectionNoLongerExists_ReturnsFalseSoMessageParks()
|
||||
{
|
||||
_repository.GetAllDatabaseConnectionsAsync().Returns(new List<DatabaseConnectionDefinition>());
|
||||
StubConnection(connection: null);
|
||||
var gateway = new DatabaseGateway(_repository, NullLogger<DatabaseGateway>.Instance);
|
||||
|
||||
var message = new ScadaLink.StoreAndForward.StoreAndForwardMessage
|
||||
@@ -176,7 +188,7 @@ public class DatabaseGatewayTests
|
||||
public async Task GetConnection_OpenFails_DisposesConnectionBeforeRethrowing()
|
||||
{
|
||||
var conn = new DatabaseConnectionDefinition("testDb", "Server=localhost;Database=test") { Id = 1 };
|
||||
_repository.GetAllDatabaseConnectionsAsync().Returns(new List<DatabaseConnectionDefinition> { conn });
|
||||
StubConnection(conn);
|
||||
|
||||
var fake = new ThrowingDbConnection();
|
||||
var gateway = new ConnectionFactoryStubGateway(_repository, fake);
|
||||
|
||||
@@ -18,10 +18,31 @@ public class ExternalSystemClientTests
|
||||
private readonly IExternalSystemRepository _repository = Substitute.For<IExternalSystemRepository>();
|
||||
private readonly IHttpClientFactory _httpClientFactory = Substitute.For<IHttpClientFactory>();
|
||||
|
||||
/// <summary>
|
||||
/// Configures the repository substitute for the name-keyed resolution path used by
|
||||
/// <c>ExternalSystemClient</c> (ExternalSystemGateway-011). A <c>null</c> system or
|
||||
/// method models a "not found" — the substitute returns <c>null</c> by default, so
|
||||
/// no stub is needed for the absent entity.
|
||||
/// </summary>
|
||||
private void StubResolution(ExternalSystemDefinition? system, ExternalSystemMethod? method)
|
||||
{
|
||||
if (system != null)
|
||||
{
|
||||
_repository.GetExternalSystemByNameAsync(system.Name, Arg.Any<CancellationToken>())
|
||||
.Returns(system);
|
||||
}
|
||||
|
||||
if (system != null && method != null)
|
||||
{
|
||||
_repository.GetMethodByNameAsync(system.Id, method.Name, Arg.Any<CancellationToken>())
|
||||
.Returns(method);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Call_SystemNotFound_ReturnsError()
|
||||
{
|
||||
_repository.GetAllExternalSystemsAsync().Returns(new List<ExternalSystemDefinition>());
|
||||
StubResolution(system: null, method: null);
|
||||
|
||||
var client = new ExternalSystemClient(
|
||||
_httpClientFactory, _repository,
|
||||
@@ -37,8 +58,7 @@ public class ExternalSystemClientTests
|
||||
public async Task Call_MethodNotFound_ReturnsError()
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
_repository.GetAllExternalSystemsAsync().Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1).Returns(new List<ExternalSystemMethod>());
|
||||
StubResolution(system, method: null);
|
||||
|
||||
var client = new ExternalSystemClient(
|
||||
_httpClientFactory, _repository,
|
||||
@@ -56,8 +76,7 @@ public class ExternalSystemClientTests
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
|
||||
_repository.GetAllExternalSystemsAsync().Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1).Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new MockHttpMessageHandler(HttpStatusCode.OK, "{\"result\": 42}");
|
||||
var httpClient = new HttpClient(handler);
|
||||
@@ -79,8 +98,7 @@ public class ExternalSystemClientTests
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("failMethod", "POST", "/fail") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
|
||||
_repository.GetAllExternalSystemsAsync().Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1).Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new MockHttpMessageHandler(HttpStatusCode.InternalServerError, "server error");
|
||||
var httpClient = new HttpClient(handler);
|
||||
@@ -102,8 +120,7 @@ public class ExternalSystemClientTests
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("badMethod", "POST", "/bad") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
|
||||
_repository.GetAllExternalSystemsAsync().Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1).Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new MockHttpMessageHandler(HttpStatusCode.BadRequest, "bad request");
|
||||
var httpClient = new HttpClient(handler);
|
||||
@@ -122,7 +139,7 @@ public class ExternalSystemClientTests
|
||||
[Fact]
|
||||
public async Task CachedCall_SystemNotFound_ReturnsError()
|
||||
{
|
||||
_repository.GetAllExternalSystemsAsync().Returns(new List<ExternalSystemDefinition>());
|
||||
StubResolution(system: null, method: null);
|
||||
|
||||
var client = new ExternalSystemClient(
|
||||
_httpClientFactory, _repository,
|
||||
@@ -140,8 +157,7 @@ public class ExternalSystemClientTests
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
|
||||
_repository.GetAllExternalSystemsAsync().Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1).Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new MockHttpMessageHandler(HttpStatusCode.OK, "{\"ok\": true}");
|
||||
var httpClient = new HttpClient(handler);
|
||||
@@ -175,8 +191,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync().Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1).Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var httpClient = new HttpClient(new MockHttpMessageHandler(HttpStatusCode.OK, "{\"ok\":true}"));
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(httpClient);
|
||||
@@ -192,7 +207,7 @@ public class ExternalSystemClientTests
|
||||
[Fact]
|
||||
public async Task DeliverBuffered_SystemNoLongerExists_ReturnsFalseSoMessageParks()
|
||||
{
|
||||
_repository.GetAllExternalSystemsAsync().Returns(new List<ExternalSystemDefinition>());
|
||||
StubResolution(system: null, method: null);
|
||||
|
||||
var client = new ExternalSystemClient(
|
||||
_httpClientFactory, _repository, NullLogger<ExternalSystemClient>.Instance);
|
||||
@@ -207,8 +222,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("failMethod", "POST", "/fail") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync().Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1).Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var httpClient = new HttpClient(new MockHttpMessageHandler(HttpStatusCode.InternalServerError, "boom"));
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(httpClient);
|
||||
@@ -227,10 +241,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("postData", "POST", "/post") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
// The HTTP layer always fails transiently (500).
|
||||
var httpClient = new HttpClient(new MockHttpMessageHandler(HttpStatusCode.InternalServerError, "boom"));
|
||||
@@ -275,10 +286,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
// Handler that hangs far longer than the configured timeout and the test budget.
|
||||
var httpClient = new HttpClient(new HangingHttpMessageHandler(TimeSpan.FromMinutes(10)));
|
||||
@@ -308,10 +316,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var httpClient = new HttpClient(new HangingHttpMessageHandler(TimeSpan.FromMinutes(10)));
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(httpClient);
|
||||
@@ -342,10 +347,7 @@ public class ExternalSystemClientTests
|
||||
RetryDelay = TimeSpan.FromSeconds(42),
|
||||
};
|
||||
var method = new ExternalSystemMethod("postData", "POST", "/post") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var httpClient = new HttpClient(new MockHttpMessageHandler(HttpStatusCode.InternalServerError, "boom"));
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(httpClient);
|
||||
@@ -404,10 +406,7 @@ public class ExternalSystemClientTests
|
||||
RetryDelay = TimeSpan.FromSeconds(5),
|
||||
};
|
||||
var method = new ExternalSystemMethod("postData", "POST", "/post") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var httpClient = new HttpClient(new MockHttpMessageHandler(HttpStatusCode.InternalServerError, "boom"));
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(httpClient);
|
||||
@@ -443,10 +442,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new DisposalTrackingHandler(HttpStatusCode.OK, "{\"ok\":true}");
|
||||
var httpClient = new HttpClient(handler);
|
||||
@@ -466,10 +462,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("badMethod", "POST", "/bad") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new DisposalTrackingHandler(HttpStatusCode.BadRequest, "bad request");
|
||||
var httpClient = new HttpClient(handler);
|
||||
@@ -491,10 +484,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com/api", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("root", "GET", "") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new RequestCapturingHandler(HttpStatusCode.OK, "{}");
|
||||
var httpClient = new HttpClient(handler);
|
||||
@@ -513,10 +503,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com/api", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new RequestCapturingHandler(HttpStatusCode.OK, "{}");
|
||||
var httpClient = new HttpClient(handler);
|
||||
@@ -537,10 +524,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("badMethod", "POST", "/bad") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var hugeBody = new string('X', 500_000);
|
||||
var handler = new MockHttpMessageHandler(HttpStatusCode.BadRequest, hugeBody);
|
||||
@@ -566,10 +550,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("postData", "POST", "/post") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var httpClient = new HttpClient(new HangingHttpMessageHandler(TimeSpan.FromMinutes(10)));
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(httpClient);
|
||||
@@ -612,10 +593,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("search", "GET", "/search") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new RequestCapturingHandler(HttpStatusCode.OK, "{}");
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(handler));
|
||||
@@ -642,10 +620,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("create", "POST", "/create") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new RequestCapturingHandler(HttpStatusCode.OK, "{}");
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(handler));
|
||||
@@ -668,10 +643,7 @@ public class ExternalSystemClientTests
|
||||
AuthConfiguration = "secret-key-123",
|
||||
};
|
||||
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new RequestCapturingHandler(HttpStatusCode.OK, "{}");
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(handler));
|
||||
@@ -694,10 +666,7 @@ public class ExternalSystemClientTests
|
||||
AuthConfiguration = "Authorization-Token:abc",
|
||||
};
|
||||
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new RequestCapturingHandler(HttpStatusCode.OK, "{}");
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(handler));
|
||||
@@ -720,10 +689,7 @@ public class ExternalSystemClientTests
|
||||
AuthConfiguration = "alice:s3cret",
|
||||
};
|
||||
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new RequestCapturingHandler(HttpStatusCode.OK, "{}");
|
||||
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(handler));
|
||||
@@ -745,10 +711,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
// A connection-level failure (e.g. host unreachable) surfaces as HttpRequestException.
|
||||
var handler = new ThrowingHttpMessageHandler(new HttpRequestException("connection refused"));
|
||||
@@ -770,10 +733,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("badMethod", "POST", "/bad") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new MockHttpMessageHandler(HttpStatusCode.BadRequest, "bad request");
|
||||
var httpClient = new HttpClient(handler);
|
||||
@@ -795,10 +755,7 @@ public class ExternalSystemClientTests
|
||||
{
|
||||
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "none") { Id = 1 };
|
||||
var method = new ExternalSystemMethod("failMethod", "POST", "/fail") { Id = 1, ExternalSystemDefinitionId = 1 };
|
||||
_repository.GetAllExternalSystemsAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemDefinition> { system });
|
||||
_repository.GetMethodsByExternalSystemIdAsync(1, Arg.Any<CancellationToken>())
|
||||
.Returns(new List<ExternalSystemMethod> { method });
|
||||
StubResolution(system, method);
|
||||
|
||||
var handler = new MockHttpMessageHandler(HttpStatusCode.InternalServerError, "boom");
|
||||
var httpClient = new HttpClient(handler);
|
||||
|
||||
@@ -84,6 +84,101 @@ public class SiteRepositoryTests : IDisposable
|
||||
Assert.Equal("StableSystem", found.Name);
|
||||
}
|
||||
|
||||
// ── ExternalSystemGateway-011: name-keyed repository lookups ──
|
||||
|
||||
/// <summary>
|
||||
/// ExternalSystemGateway-011: the site repository's name-keyed external-system
|
||||
/// lookup returns the matching row, and the same synthetic ID as the by-ID path.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ExternalSystemRepository_GetByName_ReturnsMatchingDefinition()
|
||||
{
|
||||
var storage = NewStorage();
|
||||
await storage.InitializeAsync();
|
||||
await storage.StoreExternalSystemAsync(
|
||||
"Alpha", "https://alpha.test", "ApiKey", "{\"key\":\"x\"}", null);
|
||||
await storage.StoreExternalSystemAsync(
|
||||
"Beta", "https://beta.test", "Basic", null, null);
|
||||
|
||||
var repo = new SiteExternalSystemRepository(storage);
|
||||
|
||||
var found = await repo.GetExternalSystemByNameAsync("Beta");
|
||||
Assert.NotNull(found);
|
||||
Assert.Equal("Beta", found!.Name);
|
||||
Assert.Equal("https://beta.test", found.EndpointUrl);
|
||||
|
||||
// The by-name path must produce the same synthetic ID as the by-id path.
|
||||
var byId = await repo.GetExternalSystemByIdAsync(found.Id);
|
||||
Assert.NotNull(byId);
|
||||
Assert.Equal("Beta", byId!.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ExternalSystemGateway-011: a missing name resolves to <c>null</c>.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ExternalSystemRepository_GetByName_MissingName_ReturnsNull()
|
||||
{
|
||||
var storage = NewStorage();
|
||||
await storage.InitializeAsync();
|
||||
await storage.StoreExternalSystemAsync(
|
||||
"Alpha", "https://alpha.test", "ApiKey", null, null);
|
||||
|
||||
var repo = new SiteExternalSystemRepository(storage);
|
||||
|
||||
Assert.Null(await repo.GetExternalSystemByNameAsync("DoesNotExist"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ExternalSystemGateway-011: the site repository's name-keyed method lookup
|
||||
/// returns the method scoped to its parent system, or <c>null</c> for a miss.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ExternalSystemRepository_GetMethodByName_ResolvesScopedToSystem()
|
||||
{
|
||||
var storage = NewStorage();
|
||||
await storage.InitializeAsync();
|
||||
var methodDefs = "[{\"Name\":\"getData\",\"HttpMethod\":\"GET\",\"Path\":\"/data\"}]";
|
||||
await storage.StoreExternalSystemAsync(
|
||||
"WeatherApi", "https://api.example.com", "ApiKey", null, methodDefs);
|
||||
|
||||
var repo = new SiteExternalSystemRepository(storage);
|
||||
var system = await repo.GetExternalSystemByNameAsync("WeatherApi");
|
||||
Assert.NotNull(system);
|
||||
|
||||
var method = await repo.GetMethodByNameAsync(system!.Id, "getData");
|
||||
Assert.NotNull(method);
|
||||
Assert.Equal("getData", method!.Name);
|
||||
Assert.Equal("GET", method.HttpMethod);
|
||||
|
||||
Assert.Null(await repo.GetMethodByNameAsync(system.Id, "noSuchMethod"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ExternalSystemGateway-011: the site repository's name-keyed database-connection
|
||||
/// lookup returns the matching row, or <c>null</c> for a miss.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task DatabaseConnectionRepository_GetByName_ReturnsMatchingDefinition()
|
||||
{
|
||||
var storage = NewStorage();
|
||||
await storage.InitializeAsync();
|
||||
await storage.StoreDatabaseConnectionAsync(
|
||||
"Plant", "Server=plant;Database=p;", maxRetries: 3, retryDelay: TimeSpan.FromSeconds(2));
|
||||
await storage.StoreDatabaseConnectionAsync(
|
||||
"Historian", "Server=hist;Database=h;", maxRetries: 0, retryDelay: TimeSpan.FromSeconds(5));
|
||||
|
||||
var repo = new SiteExternalSystemRepository(storage);
|
||||
|
||||
var found = await repo.GetDatabaseConnectionByNameAsync("Historian");
|
||||
Assert.NotNull(found);
|
||||
Assert.Equal("Historian", found!.Name);
|
||||
Assert.Equal("Server=hist;Database=h;", found.ConnectionString);
|
||||
Assert.Equal(0, found.MaxRetries);
|
||||
|
||||
Assert.Null(await repo.GetDatabaseConnectionByNameAsync("DoesNotExist"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SiteRuntime-007: the same stability guarantee for notification lists.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user