search and ai stabilization work, localization stablized.

This commit is contained in:
master
2026-02-24 23:29:36 +02:00
parent 4f947a8b61
commit b07d27772e
766 changed files with 55299 additions and 3221 deletions

View File

@@ -0,0 +1,347 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http.Json;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class IdentityProviderEndpointsTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory factory;
public IdentityProviderEndpointsTests(PlatformWebApplicationFactory factory)
{
this.factory = factory;
}
private HttpClient CreateClient(string tenantId = "tenant-idp", string actorId = "actor-idp")
{
var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", tenantId);
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", actorId);
return client;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task List_ReturnsEmptyForNewTenant()
{
using var client = CreateClient("tenant-idp-empty");
var items = await client.GetFromJsonAsync<List<IdentityProviderConfigDto>>(
"/api/v1/platform/identity-providers",
TestContext.Current.CancellationToken);
Assert.NotNull(items);
Assert.Empty(items!);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task CrudLifecycle_LdapProvider()
{
using var client = CreateClient("tenant-idp-crud");
// Create
var createRequest = new CreateIdentityProviderRequest(
"test-ldap",
"ldap",
true,
new Dictionary<string, string?>
{
["host"] = "ldap.example.com",
["port"] = "389",
["bindDn"] = "cn=admin,dc=example,dc=com",
["bindPassword"] = "secret",
["searchBase"] = "dc=example,dc=com"
},
"Test LDAP provider");
var createResponse = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
createRequest,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
var created = await createResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(created);
Assert.Equal("test-ldap", created!.Name);
Assert.Equal("ldap", created.Type);
Assert.True(created.Enabled);
Assert.Equal("ldap.example.com", created.Configuration["host"]);
// Read
var getResponse = await client.GetFromJsonAsync<IdentityProviderConfigDto>(
$"/api/v1/platform/identity-providers/{created.Id}",
TestContext.Current.CancellationToken);
Assert.NotNull(getResponse);
Assert.Equal(created.Id, getResponse!.Id);
// Update
var updateRequest = new UpdateIdentityProviderRequest(
null,
new Dictionary<string, string?>
{
["host"] = "ldap2.example.com",
["port"] = "636",
["bindDn"] = "cn=admin,dc=example,dc=com",
["bindPassword"] = "new-secret",
["searchBase"] = "dc=example,dc=com"
},
"Updated LDAP");
var updateResponse = await client.PutAsJsonAsync(
$"/api/v1/platform/identity-providers/{created.Id}",
updateRequest,
TestContext.Current.CancellationToken);
updateResponse.EnsureSuccessStatusCode();
var updated = await updateResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal("ldap2.example.com", updated!.Configuration["host"]);
Assert.Equal("Updated LDAP", updated.Description);
// List
var items = await client.GetFromJsonAsync<List<IdentityProviderConfigDto>>(
"/api/v1/platform/identity-providers",
TestContext.Current.CancellationToken);
Assert.Single(items!);
// Delete
var deleteResponse = await client.DeleteAsync(
$"/api/v1/platform/identity-providers/{created.Id}",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
// Verify deleted
var afterDelete = await client.GetAsync(
$"/api/v1/platform/identity-providers/{created.Id}",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, afterDelete.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Create_ValidationError_MissingRequiredFields()
{
using var client = CreateClient("tenant-idp-validation");
var request = new CreateIdentityProviderRequest(
"invalid-ldap",
"ldap",
true,
new Dictionary<string, string?>
{
["host"] = "ldap.example.com"
// Missing port, bindDn, bindPassword, searchBase
},
null);
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
request,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Create_ValidationError_InvalidType()
{
using var client = CreateClient("tenant-idp-type");
var request = new CreateIdentityProviderRequest(
"invalid-type",
"kerberos",
true,
new Dictionary<string, string?>(),
null);
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
request,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Create_DuplicateName_ReturnsBadRequest()
{
using var client = CreateClient("tenant-idp-dup");
var request = new CreateIdentityProviderRequest(
"duplicate-provider",
"standard",
true,
new Dictionary<string, string?>(),
null);
var first = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
request,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, first.StatusCode);
var second = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
request,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, second.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task EnableDisable_TogglesState()
{
using var client = CreateClient("tenant-idp-toggle");
var createRequest = new CreateIdentityProviderRequest(
"toggle-provider",
"standard",
true,
new Dictionary<string, string?>(),
null);
var createResponse = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
createRequest,
TestContext.Current.CancellationToken);
var created = await createResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
// Disable
var disableResponse = await client.PostAsync(
$"/api/v1/platform/identity-providers/{created!.Id}/disable",
null,
TestContext.Current.CancellationToken);
disableResponse.EnsureSuccessStatusCode();
var disabled = await disableResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.False(disabled!.Enabled);
// Enable
var enableResponse = await client.PostAsync(
$"/api/v1/platform/identity-providers/{created.Id}/enable",
null,
TestContext.Current.CancellationToken);
enableResponse.EnsureSuccessStatusCode();
var enabled = await enableResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.True(enabled!.Enabled);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TestConnection_StandardProvider_AlwaysSucceeds()
{
using var client = CreateClient("tenant-idp-test");
var request = new TestConnectionRequest(
"standard",
new Dictionary<string, string?>());
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers/test-connection",
request,
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TestConnectionResult>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.True(result!.Success);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task GetTypes_ReturnsAllProviderTypes()
{
using var client = CreateClient("tenant-idp-types");
var types = await client.GetFromJsonAsync<List<IdentityProviderTypeSchema>>(
"/api/v1/platform/identity-providers/types",
TestContext.Current.CancellationToken);
Assert.NotNull(types);
Assert.Equal(4, types!.Count);
Assert.Contains(types, t => t.Type == "standard");
Assert.Contains(types, t => t.Type == "ldap");
Assert.Contains(types, t => t.Type == "saml");
Assert.Contains(types, t => t.Type == "oidc");
var ldap = types.Find(t => t.Type == "ldap");
Assert.NotNull(ldap);
Assert.Contains(ldap!.RequiredFields, f => f.Name == "host");
Assert.Contains(ldap.RequiredFields, f => f.Name == "bindDn");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task TenantIsolation_CannotSeeOtherTenantProviders()
{
using var clientA = CreateClient("tenant-idp-a", "actor-a");
using var clientB = CreateClient("tenant-idp-b", "actor-b");
var requestA = new CreateIdentityProviderRequest(
"tenant-a-provider",
"standard",
true,
new Dictionary<string, string?>(),
null);
var createA = await clientA.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
requestA,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, createA.StatusCode);
var created = await createA.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
// Tenant B cannot see tenant A's provider
var listB = await clientB.GetFromJsonAsync<List<IdentityProviderConfigDto>>(
"/api/v1/platform/identity-providers",
TestContext.Current.CancellationToken);
Assert.Empty(listB!);
// Tenant B cannot get tenant A's provider by ID
var getB = await clientB.GetAsync(
$"/api/v1/platform/identity-providers/{created!.Id}",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, getB.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Health_ReturnsStatusForProvider()
{
using var client = CreateClient("tenant-idp-health");
var createRequest = new CreateIdentityProviderRequest(
"health-check-provider",
"standard",
true,
new Dictionary<string, string?>(),
null);
var createResponse = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
createRequest,
TestContext.Current.CancellationToken);
var created = await createResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
var healthResponse = await client.GetFromJsonAsync<TestConnectionResult>(
$"/api/v1/platform/identity-providers/{created!.Id}/health",
TestContext.Current.CancellationToken);
Assert.NotNull(healthResponse);
Assert.True(healthResponse!.Success);
}
}

View File

@@ -0,0 +1,259 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using StellaOps.Platform.WebService.Contracts;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Platform.WebService.Tests.Integration;
/// <summary>
/// Integration tests that require real IDP containers (OpenLDAP + Keycloak).
/// Run: docker compose -f devops/compose/docker-compose.idp-testing.yml --profile idp up -d
/// Execute: dotnet test --filter "FullyQualifiedName~IdentityProviderContainerTests"
/// </summary>
[Trait("Category", TestCategories.Integration)]
[Collection("IdpContainerTests")]
public sealed class IdentityProviderContainerTests : IClassFixture<PlatformWebApplicationFactory>
{
private const string LdapHost = "localhost";
private const int LdapPort = 3389;
private const string KeycloakBaseUrl = "http://localhost:8280";
private readonly PlatformWebApplicationFactory factory;
public IdentityProviderContainerTests(PlatformWebApplicationFactory factory)
{
this.factory = factory;
}
private HttpClient CreateClient()
{
var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-container-test");
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "actor-container-test");
return client;
}
[Fact(Skip = "Requires docker compose idp containers")]
public async Task TestConnection_Ldap_CorrectCredentials_Succeeds()
{
using var client = CreateClient();
var request = new TestConnectionRequest(
"ldap",
new Dictionary<string, string?>
{
["host"] = LdapHost,
["port"] = LdapPort.ToString(),
["bindDn"] = "cn=admin,dc=stellaops,dc=test",
["bindPassword"] = "admin-secret",
["searchBase"] = "dc=stellaops,dc=test"
});
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers/test-connection",
request,
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TestConnectionResult>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.True(result!.Success);
Assert.NotNull(result.LatencyMs);
Assert.True(result.LatencyMs > 0);
}
[Fact(Skip = "Requires docker compose idp containers")]
public async Task TestConnection_Ldap_WrongCredentials_Fails()
{
using var client = CreateClient();
// TCP connect will succeed but bind would fail
// (our current test only does TCP connect, so this tests unreachable host)
var request = new TestConnectionRequest(
"ldap",
new Dictionary<string, string?>
{
["host"] = "198.51.100.1", // RFC 5737 TEST-NET-2
["port"] = "389",
["bindDn"] = "cn=wrong,dc=stellaops,dc=test",
["bindPassword"] = "wrong-password",
["searchBase"] = "dc=stellaops,dc=test"
});
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers/test-connection",
request,
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TestConnectionResult>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.False(result!.Success);
}
[Fact(Skip = "Requires docker compose idp containers")]
public async Task TestConnection_SamlMetadata_Succeeds()
{
using var client = CreateClient();
var metadataUrl = $"{KeycloakBaseUrl}/realms/stellaops/protocol/saml/descriptor";
var request = new TestConnectionRequest(
"saml",
new Dictionary<string, string?>
{
["spEntityId"] = "stellaops-saml-sp",
["idpEntityId"] = $"{KeycloakBaseUrl}/realms/stellaops",
["idpMetadataUrl"] = metadataUrl
});
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers/test-connection",
request,
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TestConnectionResult>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.True(result!.Success);
Assert.Contains("metadata", result.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact(Skip = "Requires docker compose idp containers")]
public async Task TestConnection_OidcDiscovery_Succeeds()
{
using var client = CreateClient();
var request = new TestConnectionRequest(
"oidc",
new Dictionary<string, string?>
{
["authority"] = $"{KeycloakBaseUrl}/realms/stellaops",
["clientId"] = "stellaops-oidc-client",
["clientSecret"] = "stellaops-oidc-test-secret"
});
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers/test-connection",
request,
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TestConnectionResult>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.True(result!.Success);
Assert.Contains("discovery", result.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact(Skip = "Requires docker compose idp containers")]
public async Task TestConnection_UnreachableHost_TimesOut()
{
using var client = CreateClient();
var request = new TestConnectionRequest(
"ldap",
new Dictionary<string, string?>
{
["host"] = "198.51.100.1", // TEST-NET-2 -- should be unreachable
["port"] = "389",
["bindDn"] = "cn=admin,dc=test",
["bindPassword"] = "secret",
["searchBase"] = "dc=test"
});
var response = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers/test-connection",
request,
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<TestConnectionResult>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.False(result!.Success);
Assert.Contains("failed", result.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact(Skip = "Requires docker compose idp containers")]
public async Task FullCrudLifecycle_WithHealthCheck()
{
using var client = CreateClient();
// Create LDAP provider
var createRequest = new CreateIdentityProviderRequest(
"container-test-ldap",
"ldap",
true,
new Dictionary<string, string?>
{
["host"] = LdapHost,
["port"] = LdapPort.ToString(),
["bindDn"] = "cn=admin,dc=stellaops,dc=test",
["bindPassword"] = "admin-secret",
["searchBase"] = "dc=stellaops,dc=test"
},
"Container integration test LDAP provider");
var createResponse = await client.PostAsJsonAsync(
"/api/v1/platform/identity-providers",
createRequest,
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
var created = await createResponse.Content.ReadFromJsonAsync<IdentityProviderConfigDto>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(created);
// Health check
var healthResponse = await client.GetFromJsonAsync<TestConnectionResult>(
$"/api/v1/platform/identity-providers/{created!.Id}/health",
TestContext.Current.CancellationToken);
Assert.NotNull(healthResponse);
Assert.True(healthResponse!.Success);
// Update
var updateRequest = new UpdateIdentityProviderRequest(
null,
new Dictionary<string, string?>
{
["host"] = LdapHost,
["port"] = LdapPort.ToString(),
["bindDn"] = "cn=admin,dc=stellaops,dc=test",
["bindPassword"] = "admin-secret",
["searchBase"] = "ou=users,dc=stellaops,dc=test"
},
"Updated container test LDAP provider");
var updateResponse = await client.PutAsJsonAsync(
$"/api/v1/platform/identity-providers/{created.Id}",
updateRequest,
TestContext.Current.CancellationToken);
updateResponse.EnsureSuccessStatusCode();
// List
var list = await client.GetFromJsonAsync<List<IdentityProviderConfigDto>>(
"/api/v1/platform/identity-providers",
TestContext.Current.CancellationToken);
Assert.Contains(list!, p => p.Name == "container-test-ldap");
// Delete
var deleteResponse = await client.DeleteAsync(
$"/api/v1/platform/identity-providers/{created.Id}",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
}
}

View File

@@ -0,0 +1,185 @@
using System.Net.Http.Json;
using System.Text.Json.Nodes;
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class LocalizationEndpointsTests : IClassFixture<PlatformWebApplicationFactory>
{
private readonly PlatformWebApplicationFactory _factory;
public LocalizationEndpointsTests(PlatformWebApplicationFactory factory)
{
_factory = factory;
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UiBundleEndpoint_ReturnsDefaultMergedBundle()
{
using var client = _factory.CreateClient();
using var response = await client.GetAsync(
"/platform/i18n/en-US.json",
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
Assert.Equal("public, max-age=300", response.Headers.CacheControl?.ToString());
var bundle = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(bundle);
Assert.True(bundle!.ContainsKey("common.actions.save"));
Assert.True(bundle.ContainsKey("ui.actions.save"));
Assert.False(string.IsNullOrWhiteSpace(bundle["common.actions.save"]));
Assert.False(string.IsNullOrWhiteSpace(bundle["ui.actions.save"]));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UpsertOverride_IsReturnedFromUiBundle()
{
using var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-localization");
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-localization");
client.DefaultRequestHeaders.Add("X-Actor", "test-actor");
var upsertPayload = new
{
locale = "en-US",
strings = new Dictionary<string, string>
{
["ui.actions.save"] = "Speichern"
}
};
using var upsertResponse = await client.PutAsJsonAsync(
"/api/v1/platform/localization/bundles",
upsertPayload,
TestContext.Current.CancellationToken);
upsertResponse.EnsureSuccessStatusCode();
using var bundleResponse = await client.GetAsync(
"/platform/i18n/en-US.json",
TestContext.Current.CancellationToken);
bundleResponse.EnsureSuccessStatusCode();
var bundle = await bundleResponse.Content.ReadFromJsonAsync<Dictionary<string, string>>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(bundle);
Assert.Equal("Speichern", bundle!["ui.actions.save"]);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task UiBundles_IncludeCommonLayerForAllSupportedLocales()
{
using var client = _factory.CreateClient();
var locales = new[]
{
"en-US",
"de-DE",
"bg-BG",
"ru-RU",
"es-ES",
"fr-FR",
"uk-UA",
"zh-TW",
"zh-CN",
};
foreach (var locale in locales)
{
using var response = await client.GetAsync(
$"/platform/i18n/{locale}.json",
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var bundle = await response.Content.ReadFromJsonAsync<Dictionary<string, string>>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(bundle);
Assert.Contains("common.actions.save", bundle!);
Assert.Contains("ui.actions.save", bundle);
}
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task AvailableLocales_IncludesExpandedLocaleSet()
{
using var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-localization");
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-localization");
client.DefaultRequestHeaders.Add("X-Actor", "test-actor");
using var response = await client.GetAsync(
"/api/v1/platform/localization/locales",
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<JsonObject>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(payload);
var locales = payload!["locales"]?.AsArray().Select(node => node?.GetValue<string>()).ToHashSet(StringComparer.OrdinalIgnoreCase);
Assert.NotNull(locales);
Assert.Contains("en-US", locales!);
Assert.Contains("de-DE", locales);
Assert.Contains("bg-BG", locales);
Assert.Contains("ru-RU", locales);
Assert.Contains("es-ES", locales);
Assert.Contains("fr-FR", locales);
Assert.Contains("uk-UA", locales);
Assert.Contains("zh-TW", locales);
Assert.Contains("zh-CN", locales);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task PlatformNamespaceBundles_AreAvailableForAllSupportedLocales()
{
using var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-localization");
client.DefaultRequestHeaders.Add("X-Tenant-Id", "tenant-localization");
client.DefaultRequestHeaders.Add("X-Actor", "test-actor");
var locales = new[]
{
"en-US",
"de-DE",
"bg-BG",
"ru-RU",
"es-ES",
"fr-FR",
"uk-UA",
"zh-TW",
"zh-CN",
};
foreach (var locale in locales)
{
using var response = await client.GetAsync(
$"/api/v1/platform/localization/bundles/{locale}/platform",
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<JsonObject>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(payload);
var strings = payload!["strings"]?.AsObject();
Assert.NotNull(strings);
Assert.Contains("platform.health.status_healthy", strings!);
Assert.Contains("platform.migration.failed", strings);
}
}
}

View File

@@ -0,0 +1,67 @@
using StellaOps.TestKit;
namespace StellaOps.Platform.WebService.Tests;
public sealed class PlatformTranslationsMigrationScriptTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration057_DefinesPlatformTranslationsSchemaObjects()
{
var scriptPath = GetMigrationPath("057_PlatformTranslations.sql");
var sql = File.ReadAllText(scriptPath);
Assert.Contains("CREATE TABLE IF NOT EXISTS platform.translations", sql, StringComparison.Ordinal);
Assert.Contains("CONSTRAINT ux_translations_tenant_locale_key UNIQUE (tenant_id, locale, key)", sql, StringComparison.Ordinal);
Assert.Contains("CREATE INDEX IF NOT EXISTS ix_translations_tenant_locale", sql, StringComparison.Ordinal);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Migration057_IsPresentInReleaseMigrationSequence()
{
var migrationsDir = GetMigrationsDirectory();
var migrationNames = Directory.GetFiles(migrationsDir, "*.sql")
.Select(Path.GetFileName)
.Where(static name => name is not null)
.Select(static name => name!)
.OrderBy(static name => name, StringComparer.Ordinal)
.ToArray();
var index056 = Array.IndexOf(migrationNames, "056_RunCapsuleReplayLinkage.sql");
var index057 = Array.IndexOf(migrationNames, "057_PlatformTranslations.sql");
Assert.True(index056 >= 0, "Expected migration 056 to exist.");
Assert.True(index057 > index056, "Expected migration 057 to appear after migration 056.");
}
private static string GetMigrationPath(string fileName)
{
return Path.Combine(GetMigrationsDirectory(), fileName);
}
private static string GetMigrationsDirectory()
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
{
var candidate = Path.Combine(
current.FullName,
"src",
"Platform",
"__Libraries",
"StellaOps.Platform.Database",
"Migrations",
"Release");
if (Directory.Exists(candidate))
{
return candidate;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate Platform release migrations directory.");
}
}

View File

@@ -1,4 +1,5 @@
using System.Linq;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json.Nodes;
using StellaOps.Platform.WebService.Contracts;
@@ -44,4 +45,87 @@ public sealed class PreferencesEndpointsTests : IClassFixture<PlatformWebApplica
Assert.NotNull(widgets);
Assert.Equal(new[] { "health", "quota" }, widgets!.Select(widget => widget!.GetValue<string>()).ToArray());
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LanguagePreference_RoundTripAndSurvivesDashboardUpdate()
{
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-preferences");
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "actor-preferences");
var initial = await client.GetFromJsonAsync<PlatformLanguagePreference>(
"/api/v1/platform/preferences/language",
TestContext.Current.CancellationToken);
Assert.NotNull(initial);
Assert.Null(initial!.Locale);
var setLanguage = await client.PutAsJsonAsync(
"/api/v1/platform/preferences/language",
new PlatformLanguagePreferenceRequest("es-ES"),
TestContext.Current.CancellationToken);
setLanguage.EnsureSuccessStatusCode();
var updated = await setLanguage.Content.ReadFromJsonAsync<PlatformLanguagePreference>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(updated);
Assert.Equal("es-ES", updated!.Locale);
var dashboardUpdate = new PlatformDashboardPreferencesRequest(new JsonObject
{
["layout"] = "incident",
["widgets"] = new JsonArray("health"),
["filters"] = new JsonObject { ["scope"] = "tenant" }
});
var dashboardResponse = await client.PutAsJsonAsync(
"/api/v1/platform/preferences/dashboard",
dashboardUpdate,
TestContext.Current.CancellationToken);
dashboardResponse.EnsureSuccessStatusCode();
var reloaded = await client.GetFromJsonAsync<PlatformLanguagePreference>(
"/api/v1/platform/preferences/language",
TestContext.Current.CancellationToken);
Assert.NotNull(reloaded);
Assert.Equal("es-ES", reloaded!.Locale);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LanguagePreference_RejectsUnsupportedLocale()
{
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-preferences");
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "actor-preferences");
var response = await client.PutAsJsonAsync(
"/api/v1/platform/preferences/language",
new PlatformLanguagePreferenceRequest("xx-XX"),
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task LanguagePreference_NormalizesUkrainianAlias()
{
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Add("X-StellaOps-Tenant", "tenant-preferences");
client.DefaultRequestHeaders.Add("X-StellaOps-Actor", "actor-preferences");
var response = await client.PutAsJsonAsync(
"/api/v1/platform/preferences/language",
new PlatformLanguagePreferenceRequest("uk"),
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var updated = await response.Content.ReadFromJsonAsync<PlatformLanguagePreference>(
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(updated);
Assert.Equal("uk-UA", updated!.Locale);
}
}

View File

@@ -19,3 +19,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
| AUDIT-0762-A | DONE | Waived (test project; revalidated 2026-01-07). |
| TASK-030-019 | BLOCKED | Added analytics maintenance + cache normalization + query executor tests; analytics schema fixtures blocked by ingestion dependencies. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| SPRINT_20260224_001-LOC-002-T | DONE | Sprint `docs/implplan/SPRINT_20260224_001_Platform_unified_translation_gap_closure.md`: added migration script + localization endpoint tests for translation persistence and override behavior. |
| SPRINT_20260224_004-LOC-302-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: added language preference endpoint coverage in `PreferencesEndpointsTests` (round-trip persistence + invalid locale rejection) and expanded locale catalog verification in `LocalizationEndpointsTests`. |
| SPRINT_20260224_004-LOC-305-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: extended `LocalizationEndpointsTests` to verify common-layer and `platform.*` namespace bundle availability for all supported locales. |
| SPRINT_20260224_004-LOC-307-T | DONE | Sprint `docs/implplan/SPRINT_20260224_004_Platform_user_locale_expansion_and_cli_persistence.md`: extended localization and preference endpoint tests for Ukrainian rollout (`uk-UA` locale catalog/bundle assertions and alias normalization to canonical `uk-UA`). |