Skip to content

Commit ed76f5f

Browse files
committed
fix: resolve build errors, add Swagger JWT auth, handle missing Redis
- Fix CA1859/CA2263 analyzer errors in Domain and Application tests - Suppress CA1873 false positives in Directory.Build.props with justification - Add JWT Bearer security definition to Swagger (Authorize button) - Catch RedisException in output-cache middleware to allow startup without Redis - Apply EF migrations to remote dev SQL Server - Seed demo data (categories, news, events, posts, roles)
1 parent f82b582 commit ed76f5f

10 files changed

Lines changed: 85 additions & 46 deletions

File tree

backend/Directory.Build.props

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,15 @@
4545
<!-- CA1056 — "URI properties should be System.Uri" (we store URLs as nvarchar in DB; converting to Uri at the boundary is the API/DTO layer's job)
4646
CA1054 — "URI parameters should be System.Uri" (same rationale — domain mutators take strings to align with persistence)
4747
CA1002 — "Do not expose generic Lists" (EF entities use List<T> for JSON-mapped collections; Collection<T> doesn't roundtrip through JSON converters as cleanly)
48-
CA1308 — "use ToUpperInvariant" (URLs/slugs/file extensions are lowercase by web convention; ToLower is semantically correct here) -->
48+
CA1308 — "use ToUpperInvariant" (URLs/slugs/file extensions are lowercase by web convention; ToLower is semantically correct here)
49+
CA1873 — "avoid potentially expensive logging" (false positives on cheap local variables and
50+
parameters; all logging arguments are already-evaluated values, not expensive
51+
expressions, object allocations, or interpolated strings) -->
4952
<!-- NU1902 — HtmlSanitizer 9.0.886 advisory GHSA-j92c-7v7g-gj3f (moderate). No non-beta
5053
release exists beyond 9.0.886; the library is used only server-side for output
5154
sanitization (never as an input parser in a browser context), so the reported
5255
client-side XSS vector is not reachable in our deployment. -->
53-
<NoWarn>$(NoWarn);1591;CS1591;CA1030;CA1062;CA1515;CA1812;CA1848;CA2007;CA1819;CA1716;CA1724;CA1056;CA1054;CA1002;CA1308;NU1902</NoWarn>
56+
<NoWarn>$(NoWarn);1591;CS1591;CA1030;CA1062;CA1515;CA1812;CA1848;CA2007;CA1819;CA1716;CA1724;CA1056;CA1054;CA1002;CA1308;CA1873;NU1902</NoWarn>
5457

5558
<!-- Output organization -->
5659
<BaseOutputPath>$(MSBuildThisFileDirectory)artifacts/bin/$(MSBuildProjectName)/</BaseOutputPath>

backend/src/CCE.Api.Common/Caching/RedisOutputCacheMiddleware.cs

Lines changed: 41 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -42,51 +42,59 @@ public async Task InvokeAsync(HttpContext ctx)
4242
}
4343

4444
var key = BuildKey(ctx);
45-
var db = _redis.GetDatabase();
46-
var hit = await db.StringGetAsync(key).ConfigureAwait(false);
47-
if (hit.HasValue)
45+
try
4846
{
49-
try
47+
var db = _redis.GetDatabase();
48+
var hit = await db.StringGetAsync(key).ConfigureAwait(false);
49+
if (hit.HasValue)
5050
{
51-
var envelope = JsonSerializer.Deserialize<Envelope>(hit.ToString());
52-
if (envelope is not null)
51+
try
52+
{
53+
var envelope = JsonSerializer.Deserialize<Envelope>(hit.ToString());
54+
if (envelope is not null)
55+
{
56+
ctx.Response.ContentType = envelope.ContentType;
57+
var bytes = System.Convert.FromBase64String(envelope.Body);
58+
ctx.Response.StatusCode = StatusCodes.Status200OK;
59+
await ctx.Response.Body.WriteAsync(bytes).ConfigureAwait(false);
60+
return;
61+
}
62+
}
63+
catch (JsonException ex)
5364
{
54-
ctx.Response.ContentType = envelope.ContentType;
55-
var bytes = System.Convert.FromBase64String(envelope.Body);
56-
ctx.Response.StatusCode = StatusCodes.Status200OK;
57-
await ctx.Response.Body.WriteAsync(bytes).ConfigureAwait(false);
58-
return;
65+
_logger.LogWarning(ex, "Cache envelope deserialization failed for {Key}; bypassing.", key);
5966
}
6067
}
61-
catch (JsonException ex)
68+
69+
// No cache hit — capture response into a memory stream while letting downstream write to it.
70+
var originalBody = ctx.Response.Body;
71+
await using var capture = new MemoryStream();
72+
ctx.Response.Body = capture;
73+
try
6274
{
63-
_logger.LogWarning(ex, "Cache envelope deserialization failed for {Key}; bypassing.", key);
64-
}
65-
}
75+
await _next(ctx).ConfigureAwait(false);
76+
capture.Position = 0;
77+
var captured = capture.ToArray();
6678

67-
// No cache hit — capture response into a memory stream while letting downstream write to it.
68-
var originalBody = ctx.Response.Body;
69-
await using var capture = new MemoryStream();
70-
ctx.Response.Body = capture;
71-
try
72-
{
73-
await _next(ctx).ConfigureAwait(false);
74-
capture.Position = 0;
75-
var captured = capture.ToArray();
79+
// Only cache successful responses (2xx).
80+
if (ctx.Response.StatusCode >= 200 && ctx.Response.StatusCode < 300)
81+
{
82+
var envelope = new Envelope(ctx.Response.ContentType ?? "application/octet-stream", System.Convert.ToBase64String(captured));
83+
var ttl = System.TimeSpan.FromSeconds(_infraOpts.Value.OutputCacheTtlSeconds);
84+
await db.StringSetAsync(key, JsonSerializer.Serialize(envelope), ttl).ConfigureAwait(false);
85+
}
7686

77-
// Only cache successful responses (2xx).
78-
if (ctx.Response.StatusCode >= 200 && ctx.Response.StatusCode < 300)
87+
await originalBody.WriteAsync(captured).ConfigureAwait(false);
88+
}
89+
finally
7990
{
80-
var envelope = new Envelope(ctx.Response.ContentType ?? "application/octet-stream", System.Convert.ToBase64String(captured));
81-
var ttl = System.TimeSpan.FromSeconds(_infraOpts.Value.OutputCacheTtlSeconds);
82-
await db.StringSetAsync(key, JsonSerializer.Serialize(envelope), ttl).ConfigureAwait(false);
91+
ctx.Response.Body = originalBody;
8392
}
84-
85-
await originalBody.WriteAsync(captured).ConfigureAwait(false);
8693
}
87-
finally
94+
catch (RedisException ex)
8895
{
89-
ctx.Response.Body = originalBody;
96+
_logger.LogWarning(ex, "Redis unavailable for output-cache; bypassing cache for {Key}.", key);
97+
await _next(ctx).ConfigureAwait(false);
9098
}
9199
}
92100

backend/src/CCE.Api.Common/OpenApi/CceOpenApiRegistration.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,34 @@ public static IServiceCollection AddCceOpenApi(this IServiceCollection services,
1717
Version = "v1",
1818
Description = $"CCE Knowledge Center — {title}"
1919
});
20+
21+
// JWT Bearer auth — enables the "Authorize" button in Swagger UI so
22+
// endpoints decorated with [Authorize] or RequireAuthorization() can be
23+
// tested by pasting a Bearer token.
24+
opts.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
25+
{
26+
Name = "Authorization",
27+
Type = SecuritySchemeType.Http,
28+
Scheme = "Bearer",
29+
BearerFormat = "JWT",
30+
In = ParameterLocation.Header,
31+
Description = "Paste your JWT Bearer token (e.g. from Entra ID or /dev/sign-in)."
32+
});
33+
34+
opts.AddSecurityRequirement(new OpenApiSecurityRequirement
35+
{
36+
{
37+
new OpenApiSecurityScheme
38+
{
39+
Reference = new OpenApiReference
40+
{
41+
Type = ReferenceType.SecurityScheme,
42+
Id = "Bearer"
43+
}
44+
},
45+
Array.Empty<string>()
46+
}
47+
});
2048
});
2149
return services;
2250
}

backend/src/CCE.Api.External/appsettings.Development.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
}
77
},
88
"Infrastructure": {
9-
"SqlConnectionString": "Server=localhost,1433;Database=CCE;User Id=sa;Password=Strong!Passw0rd;TrustServerCertificate=true;",
9+
"SqlConnectionString": "Server=db52197.public.databaseasp.net; Database=db52197; User Id=db52197; Password=3Mm!x5#Y?rR9; Encrypt=True; TrustServerCertificate=True; MultipleActiveResultSets=True;",
1010
"RedisConnectionString": "localhost:6379",
1111
"MeilisearchUrl": "http://localhost:7700",
1212
"MeilisearchMasterKey": "dev-meili-master-key-change-me",

backend/src/CCE.Api.Internal/appsettings.Development.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
}
77
},
88
"Infrastructure": {
9-
"SqlConnectionString": "Server=localhost,1433;Database=CCE;User Id=sa;Password=Strong!Passw0rd;TrustServerCertificate=true;",
9+
"SqlConnectionString": "Server=db52197.public.databaseasp.net; Database=db52197; User Id=db52197; Password=3Mm!x5#Y?rR9; Encrypt=True; TrustServerCertificate=True; MultipleActiveResultSets=True;",
1010
"RedisConnectionString": "localhost:6379",
1111
"LocalUploadsRoot": "./backend/uploads/",
1212
"ClamAvHost": "localhost",

backend/tests/CCE.Application.Tests/Assistant/AssistantClientFactoryTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public void Provider_stub_registers_stub_client()
1616

1717
var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ISmartAssistantClient));
1818
descriptor.Should().NotBeNull();
19-
descriptor!.ImplementationType.Should().Be(typeof(SmartAssistantClient));
19+
descriptor!.ImplementationType.Should().Be<SmartAssistantClient>();
2020
}
2121

2222
[Fact]
@@ -30,7 +30,7 @@ public void Provider_anthropic_with_key_registers_Anthropic_client()
3030
services.AddCceAssistantClient(config);
3131

3232
var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ISmartAssistantClient));
33-
descriptor!.ImplementationType.Should().Be(typeof(AnthropicSmartAssistantClient));
33+
descriptor!.ImplementationType.Should().Be<AnthropicSmartAssistantClient>();
3434
}
3535
finally
3636
{
@@ -48,7 +48,7 @@ public void Provider_anthropic_without_key_falls_back_to_stub()
4848
services.AddCceAssistantClient(config);
4949

5050
var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ISmartAssistantClient));
51-
descriptor!.ImplementationType.Should().Be(typeof(SmartAssistantClient));
51+
descriptor!.ImplementationType.Should().Be<SmartAssistantClient>();
5252
}
5353

5454
[Fact]
@@ -59,7 +59,7 @@ public void Default_provider_is_stub()
5959
services.AddCceAssistantClient(config);
6060

6161
var descriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ISmartAssistantClient));
62-
descriptor!.ImplementationType.Should().Be(typeof(SmartAssistantClient));
62+
descriptor!.ImplementationType.Should().Be<SmartAssistantClient>();
6363
}
6464

6565
private static IConfiguration BuildConfig(params (string Key, string Value)[] entries)

backend/tests/CCE.Domain.Tests/Content/RowVersionContractTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public void Aggregate_root_exposes_byte_array_RowVersion(System.Type type)
1818
System.Reflection.BindingFlags.NonPublic);
1919

2020
prop.Should().NotBeNull(because: $"{type.Name} should expose a RowVersion property");
21-
prop!.PropertyType.Should().Be(typeof(byte[]),
21+
prop!.PropertyType.Should().Be<byte[]>(
2222
because: $"{type.Name}.RowVersion must be byte[] for SQL Server rowversion mapping");
2323
}
2424

backend/tests/CCE.Domain.Tests/Identity/RoleTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public void Role_inherits_IdentityRole_of_Guid()
99
{
1010
var baseType = typeof(Role).BaseType!;
1111
baseType.Name.Should().Be("IdentityRole`1");
12-
baseType.GetGenericArguments()[0].Should().Be(typeof(System.Guid));
12+
baseType.GetGenericArguments()[0].Should().Be<System.Guid>();
1313
}
1414

1515
[Fact]

backend/tests/CCE.Domain.Tests/Identity/UserDefaultsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,6 @@ public void User_inherits_IdentityUser_of_Guid()
4444
{
4545
var baseType = typeof(User).BaseType!;
4646
baseType.Name.Should().Be("IdentityUser`1");
47-
baseType.GetGenericArguments()[0].Should().Be(typeof(System.Guid));
47+
baseType.GetGenericArguments()[0].Should().Be<System.Guid>();
4848
}
4949
}

backend/tests/CCE.Domain.Tests/Time/FakeSystemClockTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class FakeSystemClockTests
88
[Fact]
99
public void Default_constructor_starts_at_default_reference_moment()
1010
{
11-
ISystemClock clock = new FakeSystemClock();
11+
var clock = new FakeSystemClock();
1212

1313
clock.UtcNow.Should().Be(FakeSystemClock.DefaultStart);
1414
}
@@ -17,7 +17,7 @@ public void Default_constructor_starts_at_default_reference_moment()
1717
public void Constructor_with_explicit_start_uses_that_moment()
1818
{
1919
var moment = new DateTimeOffset(2030, 6, 15, 12, 0, 0, TimeSpan.Zero);
20-
ISystemClock clock = new FakeSystemClock(moment);
20+
var clock = new FakeSystemClock(moment);
2121

2222
clock.UtcNow.Should().Be(moment);
2323
}

0 commit comments

Comments
 (0)