V13-FE-006: consolidate approved UI and contract hardening
This commit is contained in:
@@ -8,7 +8,7 @@ public sealed class RepositoryRulesTests
|
||||
public void Prohibited_source_patterns_are_not_introduced()
|
||||
{
|
||||
var repositoryRoot = FindRepositoryRoot();
|
||||
var sourceFiles = Directory.EnumerateFiles(repositoryRoot, "*.cs", SearchOption.AllDirectories)
|
||||
var sourceFiles = Directory.EnumerateFiles(Path.Combine(repositoryRoot, "src"), "*.cs", SearchOption.AllDirectories)
|
||||
.Where(x => !IsGeneratedOrTestOutput(x))
|
||||
.ToArray();
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed class RepositoryRulesTests
|
||||
public void DateTime_now_must_use_iclock_abstraction()
|
||||
{
|
||||
var repositoryRoot = FindRepositoryRoot();
|
||||
var sourceFiles = Directory.EnumerateFiles(repositoryRoot, "*.cs", SearchOption.AllDirectories)
|
||||
var sourceFiles = Directory.EnumerateFiles(Path.Combine(repositoryRoot, "src"), "*.cs", SearchOption.AllDirectories)
|
||||
.Where(x => !IsGeneratedOrTestOutput(x))
|
||||
.ToArray();
|
||||
|
||||
@@ -114,6 +114,82 @@ public sealed class RepositoryRulesTests
|
||||
"Every module endpoint must declare Roles or Policies: " + string.Join(", ", violations));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Role_declared_endpoints_must_not_allow_anonymous_access()
|
||||
{
|
||||
var root = FindRepositoryRoot();
|
||||
var endpoints = Directory.EnumerateFiles(
|
||||
Path.Combine(root, "src"),
|
||||
"*.cs",
|
||||
SearchOption.AllDirectories)
|
||||
.Where(path => Path.GetFileName(path).Contains("Endpoint", StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
|
||||
var violations = endpoints.Where(path =>
|
||||
{
|
||||
var text = File.ReadAllText(path);
|
||||
return text.Contains("Roles(", StringComparison.Ordinal)
|
||||
&& text.Contains("AllowAnonymous()", StringComparison.Ordinal)
|
||||
&& !text.Contains("/test/", StringComparison.Ordinal);
|
||||
}).ToArray();
|
||||
|
||||
Assert.True(violations.Length == 0,
|
||||
"Role-declared endpoints must not also allow anonymous access: " + string.Join(", ", violations));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unapproved_reconciliation_endpoints_must_remain_unregistered()
|
||||
{
|
||||
var root = FindRepositoryRoot();
|
||||
var endpointFile = Path.Combine(root, "src", "KArtSell.Modules.ModelOperations", "PortfolioReconciliation", "Endpoints.cs");
|
||||
var text = File.ReadAllText(endpointFile);
|
||||
|
||||
Assert.Equal(4, CountOccurrences(text, "[DontRegister]"));
|
||||
Assert.Equal(4, CountOccurrences(text, "AllowAnonymous()"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KIS_trade_endpoints_must_remain_unregistered_while_capability_is_off()
|
||||
{
|
||||
var root = FindRepositoryRoot();
|
||||
var endpointFile = Path.Combine(
|
||||
root, "src", "KArtSell.Modules.ModelOperations", "TradeExecution", "TradeEndpoints.cs");
|
||||
var text = File.ReadAllText(endpointFile);
|
||||
|
||||
Assert.Equal(2, CountOccurrences(text, "[DontRegister]"));
|
||||
Assert.Equal(2, CountOccurrences(text, "AllowAnonymous()"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Job_run_repository_columns_exist_in_authoritative_baseline_schema()
|
||||
{
|
||||
var root = FindRepositoryRoot();
|
||||
var repository = File.ReadAllText(Path.Combine(
|
||||
root, "src", "KArtSell.BuildingBlocks", "Reliability", "DapperJobRunRepository.cs"));
|
||||
var schema = File.ReadAllText(Path.Combine(root, "db", "migrations", "0000_building_blocks.sql"));
|
||||
|
||||
var requiredColumns = new[]
|
||||
{
|
||||
"job_run_id", "job_type", "job_version", "scope_key", "idempotency_key",
|
||||
"watermark", "app_version", "model_version", "config_version", "data_version",
|
||||
"contract_version", "status", "input_hash", "output_hash", "started_at",
|
||||
"heartbeat_at", "finished_at", "error_code", "trace_id"
|
||||
};
|
||||
|
||||
var missingFromSchema = requiredColumns
|
||||
.Where(column => !schema.Contains(column, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
Assert.Empty(missingFromSchema);
|
||||
|
||||
var insertSection = repository[(repository.IndexOf("insert into", StringComparison.OrdinalIgnoreCase))..];
|
||||
var referencedInRepository = requiredColumns
|
||||
.Where(column => insertSection.Contains(column, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
Assert.Equal(
|
||||
requiredColumns.OrderBy(x => x),
|
||||
referencedInRepository.OrderBy(x => x));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Accidental_placeholder_files_are_not_committed()
|
||||
{
|
||||
@@ -179,6 +255,9 @@ public sealed class RepositoryRulesTests
|
||||
Assert.True(violations.Length == 0, message + " " + string.Join(", ", violations));
|
||||
}
|
||||
|
||||
private static int CountOccurrences(string text, string value) =>
|
||||
text.Split(value, StringSplitOptions.None).Length - 1;
|
||||
|
||||
private static bool IsGeneratedOrTestOutput(string path)
|
||||
=> path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
|
||||
|| path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using KArtSell.Host.Infrastructure;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace KArtSell.Integration.Tests;
|
||||
|
||||
public sealed class CorrelationIdMiddlewareTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Preserves_valid_header_and_exposes_one_boundary_identifier()
|
||||
{
|
||||
var expected = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Headers["X-Correlation-Id"] = expected.ToString("D");
|
||||
var nextCalled = false;
|
||||
var middleware = new CorrelationIdMiddleware(_ =>
|
||||
{
|
||||
nextCalled = true;
|
||||
return Task.CompletedTask;
|
||||
}, NullLogger<CorrelationIdMiddleware>.Instance);
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
Assert.True(nextCalled);
|
||||
Assert.Equal(expected.ToString("D"), context.TraceIdentifier);
|
||||
Assert.Equal(expected, Assert.IsType<Guid>(context.Items["CorrelationId"]));
|
||||
Assert.Equal(expected.ToString("D"), context.Response.Headers["X-Correlation-Id"].ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Replaces_invalid_or_empty_header_without_reusing_client_value()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Headers["X-Correlation-Id"] = "not-a-guid";
|
||||
var middleware = new CorrelationIdMiddleware(_ => Task.CompletedTask, NullLogger<CorrelationIdMiddleware>.Instance);
|
||||
|
||||
await middleware.InvokeAsync(context);
|
||||
|
||||
var correlationId = Assert.IsType<Guid>(context.Items["CorrelationId"]);
|
||||
Assert.NotEqual(Guid.Empty, correlationId);
|
||||
Assert.Equal(correlationId.ToString("D"), context.TraceIdentifier);
|
||||
Assert.Equal(correlationId.ToString("D"), context.Response.Headers["X-Correlation-Id"].ToString());
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ public sealed class Phase1Phase2LocalExecutionTests
|
||||
Console.WriteLine($" Gate 3 (Cost > 0): {(costPass ? "✅" : "❌")} ({(metrics.TotalReturn * 100):F1}%)");
|
||||
|
||||
Console.WriteLine($"\n{(allGatesPassed ? "✅" : "⚠️")} AllGatesPassed: {allGatesPassed}");
|
||||
Console.WriteLine("="*70);
|
||||
Console.WriteLine(new string('=', 70));
|
||||
|
||||
Console.WriteLine($"\n📋 다음 단계:");
|
||||
if (allGatesPassed)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using KArtSell.Modules.ModelOperations.PortfolioReconciliation;
|
||||
|
||||
namespace KArtSell.ModelOperations.UnitTests;
|
||||
|
||||
public sealed class ReconciliationRequestValidatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Missing_idempotency_key_is_rejected()
|
||||
{
|
||||
Assert.False(ReconcileTradeRequestContract.HasIdempotencyKey(new ReconcileTradeRequest()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Supplied_idempotency_key_is_accepted()
|
||||
{
|
||||
Assert.True(ReconcileTradeRequestContract.HasIdempotencyKey(new ReconcileTradeRequest { IdempotencyKey = "reconcile-20260812-001" }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user