fix(esg): structured JSON AuthConfiguration (matches entity doc + UI placeholders) with legacy colon fallback — keys containing ':' no longer misparse

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 04:24:11 -04:00
parent b5e80d4c00
commit 2e7be1e852
3 changed files with 189 additions and 2 deletions
@@ -828,6 +828,105 @@ public class ExternalSystemClientTests
Assert.Equal("alice:s3cret", decoded);
}
[Fact]
public async Task ApiKeyAuth_JsonConfig_SetsConfiguredHeaderAndKeepsColons()
{
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "apikey")
{
Id = 1,
AuthConfiguration = "{\"header\":\"X-Auth\",\"key\":\"ab:cd:ef\"}",
};
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
StubResolution(system, method);
var handler = new RequestCapturingHandler(HttpStatusCode.OK, "{}");
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(handler));
var client = new ExternalSystemClient(
_httpClientFactory, _repository, NullLogger<ExternalSystemClient>.Instance);
await client.CallAsync("TestAPI", "getData");
Assert.True(handler.LastHeaders!.TryGetValues("X-Auth", out var values));
Assert.Equal("ab:cd:ef", values!.Single());
}
[Fact]
public async Task ApiKeyAuth_JsonConfig_DefaultHeader()
{
// The UI-placeholder shape ({"key":"xyz"}) — silently misparsed by the
// legacy colon-split before Task 15 (it produced header name '{"key').
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "apikey")
{
Id = 1,
AuthConfiguration = "{\"key\":\"xyz\"}",
};
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
StubResolution(system, method);
var handler = new RequestCapturingHandler(HttpStatusCode.OK, "{}");
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(handler));
var client = new ExternalSystemClient(
_httpClientFactory, _repository, NullLogger<ExternalSystemClient>.Instance);
await client.CallAsync("TestAPI", "getData");
Assert.True(handler.LastHeaders!.TryGetValues("X-API-Key", out var values));
Assert.Equal("xyz", values!.Single());
}
[Fact]
public async Task BasicAuth_JsonConfig_EncodesUsernamePassword()
{
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "basic")
{
Id = 1,
AuthConfiguration = "{\"username\":\"u\",\"password\":\"p:w\"}",
};
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
StubResolution(system, method);
var handler = new RequestCapturingHandler(HttpStatusCode.OK, "{}");
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(handler));
var client = new ExternalSystemClient(
_httpClientFactory, _repository, NullLogger<ExternalSystemClient>.Instance);
await client.CallAsync("TestAPI", "getData");
var auth = handler.LastHeaders!.Authorization;
Assert.NotNull(auth);
Assert.Equal("Basic", auth!.Scheme);
var decoded = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(auth.Parameter!));
Assert.Equal("u:p:w", decoded);
}
[Fact]
public async Task ApiKeyAuth_LegacyColonConfig_StillWorks()
{
// Backward compatibility: the legacy "HeaderName:KeyValue" colon form must
// keep working for existing rows.
var system = new ExternalSystemDefinition("TestAPI", "https://api.example.com", "apikey")
{
Id = 1,
AuthConfiguration = "X-Custom:secret",
};
var method = new ExternalSystemMethod("getData", "GET", "/data") { Id = 1, ExternalSystemDefinitionId = 1 };
StubResolution(system, method);
var handler = new RequestCapturingHandler(HttpStatusCode.OK, "{}");
_httpClientFactory.CreateClient(Arg.Any<string>()).Returns(new HttpClient(handler));
var client = new ExternalSystemClient(
_httpClientFactory, _repository, NullLogger<ExternalSystemClient>.Instance);
await client.CallAsync("TestAPI", "getData");
Assert.True(handler.LastHeaders!.TryGetValues("X-Custom", out var values));
Assert.Equal("secret", values!.Single());
}
[Fact]
public async Task Call_ConnectionError_IsClassifiedAsTransient()
{