Skip to content

Commit a631f8b

Browse files
committed
feat(tests): add production-grade integration tests with Testcontainers
- Add Integration.Tests project with 57 tests covering all modules: health checks, authentication, user management, roles, groups, multitenancy, auditing, webhooks, and authorization - WebApplicationFactory + Testcontainers PostgreSQL for real database testing - Manual tenant provisioning for deterministic test setup - DetailedTestExceptionHandler for better test diagnostics - Add webhook EF Core migrations (were missing) - Fix IDomainEvent to implement INotification for Mediator compatibility - Add integration test job to CI workflow - Gate publish jobs on integration test results
1 parent ff88424 commit a631f8b

26 files changed

Lines changed: 858 additions & 755 deletions

.github/workflows/ci.yml

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,47 @@ jobs:
133133
path: '**/*.trx'
134134
retention-days: 7
135135

136+
integration-test:
137+
name: Integration Tests
138+
runs-on: ubuntu-latest
139+
needs: build
140+
# Testcontainers requires Docker — ubuntu-latest has it pre-installed.
141+
142+
steps:
143+
- name: Checkout
144+
uses: actions/checkout@v6
145+
146+
- name: Setup .NET SDK
147+
uses: actions/setup-dotnet@v5
148+
with:
149+
dotnet-version: '10.0.x'
150+
dotnet-quality: 'preview'
151+
152+
- name: Cache NuGet packages
153+
uses: actions/cache@v5
154+
with:
155+
path: ~/.nuget/packages
156+
key: ${{ runner.os }}-nuget-${{ hashFiles('**/Directory.Packages.props') }}
157+
restore-keys: |
158+
${{ runner.os }}-nuget-
159+
160+
# Integration tests use WebApplicationFactory + Testcontainers.
161+
# They must build from source (can't use pre-built artifacts).
162+
- name: Run Integration Tests
163+
run: dotnet test src/Tests/Integration.Tests -c Release --verbosity normal --logger "trx;LogFileName=Integration.Tests.trx"
164+
165+
- name: Upload test results
166+
uses: actions/upload-artifact@v7
167+
if: always()
168+
with:
169+
name: test-results-Integration.Tests
170+
path: '**/*.trx'
171+
retention-days: 7
172+
136173
publish-dev-containers:
137174
name: Publish Dev Containers
138175
runs-on: ubuntu-latest
139-
needs: test
176+
needs: [test, integration-test]
140177
if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
141178

142179
steps:
@@ -183,7 +220,7 @@ jobs:
183220
publish-release:
184221
name: Publish Release (NuGet + Containers)
185222
runs-on: ubuntu-latest
186-
needs: test
223+
needs: [test, integration-test]
187224
if: |
188225
(github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch') ||
189226
startsWith(github.ref, 'refs/tags/v')

src/BuildingBlocks/Core/Domain/IDomainEvent.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
namespace FSH.Framework.Core.Domain;
1+
using Mediator;
2+
3+
namespace FSH.Framework.Core.Domain;
24

35
/// <summary>
46
/// Represents a domain event with correlation and tenant context.
7+
/// Extends <see cref="INotification"/> so domain events can be published via Mediator.
58
/// </summary>
6-
public interface IDomainEvent
9+
public interface IDomainEvent : INotification
710
{
811
/// <summary>
912
/// Gets the unique event identifier.

src/Playground/Migrations.PostgreSQL/Migrations.PostgreSQL.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<ProjectReference Include="..\..\Modules\Auditing\Modules.Auditing\Modules.Auditing.csproj" />
1212
<ProjectReference Include="..\..\Modules\Identity\Modules.Identity\Modules.Identity.csproj" />
1313
<ProjectReference Include="..\..\Modules\Multitenancy\Modules.Multitenancy\Modules.Multitenancy.csproj" />
14+
<ProjectReference Include="..\..\Modules\Webhooks\Modules.Webhooks\Modules.Webhooks.csproj" />
1415
</ItemGroup>
1516

1617
<ItemGroup>

src/Playground/Migrations.PostgreSQL/Webhooks/20260403090248_InitialWebhooks.Designer.cs

Lines changed: 107 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
using System;
2+
using Microsoft.EntityFrameworkCore.Migrations;
3+
4+
#nullable disable
5+
6+
namespace FSH.Migrations.PostgreSQL.Webhooks
7+
{
8+
/// <inheritdoc />
9+
public partial class InitialWebhooks : Migration
10+
{
11+
/// <inheritdoc />
12+
protected override void Up(MigrationBuilder migrationBuilder)
13+
{
14+
migrationBuilder.EnsureSchema(
15+
name: "webhooks");
16+
17+
migrationBuilder.CreateTable(
18+
name: "Deliveries",
19+
schema: "webhooks",
20+
columns: table => new
21+
{
22+
Id = table.Column<Guid>(type: "uuid", nullable: false),
23+
SubscriptionId = table.Column<Guid>(type: "uuid", nullable: false),
24+
EventType = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
25+
PayloadJson = table.Column<string>(type: "text", nullable: false),
26+
HttpStatusCode = table.Column<int>(type: "integer", nullable: false),
27+
Success = table.Column<bool>(type: "boolean", nullable: false),
28+
AttemptCount = table.Column<int>(type: "integer", nullable: false),
29+
AttemptedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
30+
ErrorMessage = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true)
31+
},
32+
constraints: table =>
33+
{
34+
table.PrimaryKey("PK_Deliveries", x => x.Id);
35+
});
36+
37+
migrationBuilder.CreateTable(
38+
name: "Subscriptions",
39+
schema: "webhooks",
40+
columns: table => new
41+
{
42+
Id = table.Column<Guid>(type: "uuid", nullable: false),
43+
Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
44+
EventsCsv = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
45+
SecretHash = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
46+
IsActive = table.Column<bool>(type: "boolean", nullable: false),
47+
CreatedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
48+
},
49+
constraints: table =>
50+
{
51+
table.PrimaryKey("PK_Subscriptions", x => x.Id);
52+
});
53+
54+
migrationBuilder.CreateIndex(
55+
name: "IX_Deliveries_AttemptedAtUtc",
56+
schema: "webhooks",
57+
table: "Deliveries",
58+
column: "AttemptedAtUtc");
59+
60+
migrationBuilder.CreateIndex(
61+
name: "IX_Deliveries_SubscriptionId",
62+
schema: "webhooks",
63+
table: "Deliveries",
64+
column: "SubscriptionId");
65+
66+
migrationBuilder.CreateIndex(
67+
name: "IX_Subscriptions_IsActive",
68+
schema: "webhooks",
69+
table: "Subscriptions",
70+
column: "IsActive");
71+
}
72+
73+
/// <inheritdoc />
74+
protected override void Down(MigrationBuilder migrationBuilder)
75+
{
76+
migrationBuilder.DropTable(
77+
name: "Deliveries",
78+
schema: "webhooks");
79+
80+
migrationBuilder.DropTable(
81+
name: "Subscriptions",
82+
schema: "webhooks");
83+
}
84+
}
85+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// <auto-generated />
2+
using System;
3+
using FSH.Modules.Webhooks.Data;
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.EntityFrameworkCore.Infrastructure;
6+
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
7+
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
8+
9+
#nullable disable
10+
11+
namespace FSH.Migrations.PostgreSQL.Webhooks
12+
{
13+
[DbContext(typeof(WebhookDbContext))]
14+
partial class WebhookDbContextModelSnapshot : ModelSnapshot
15+
{
16+
protected override void BuildModel(ModelBuilder modelBuilder)
17+
{
18+
#pragma warning disable 612, 618
19+
modelBuilder
20+
.HasDefaultSchema("webhooks")
21+
.HasAnnotation("ProductVersion", "10.0.5")
22+
.HasAnnotation("Relational:MaxIdentifierLength", 63);
23+
24+
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
25+
26+
modelBuilder.Entity("FSH.Modules.Webhooks.Domain.WebhookDelivery", b =>
27+
{
28+
b.Property<Guid>("Id")
29+
.ValueGeneratedOnAdd()
30+
.HasColumnType("uuid");
31+
32+
b.Property<int>("AttemptCount")
33+
.HasColumnType("integer");
34+
35+
b.Property<DateTime>("AttemptedAtUtc")
36+
.HasColumnType("timestamp with time zone");
37+
38+
b.Property<string>("ErrorMessage")
39+
.HasMaxLength(4096)
40+
.HasColumnType("character varying(4096)");
41+
42+
b.Property<string>("EventType")
43+
.IsRequired()
44+
.HasMaxLength(256)
45+
.HasColumnType("character varying(256)");
46+
47+
b.Property<int>("HttpStatusCode")
48+
.HasColumnType("integer");
49+
50+
b.Property<string>("PayloadJson")
51+
.IsRequired()
52+
.HasColumnType("text");
53+
54+
b.Property<Guid>("SubscriptionId")
55+
.HasColumnType("uuid");
56+
57+
b.Property<bool>("Success")
58+
.HasColumnType("boolean");
59+
60+
b.HasKey("Id");
61+
62+
b.HasIndex("AttemptedAtUtc");
63+
64+
b.HasIndex("SubscriptionId");
65+
66+
b.ToTable("Deliveries", "webhooks");
67+
});
68+
69+
modelBuilder.Entity("FSH.Modules.Webhooks.Domain.WebhookSubscription", b =>
70+
{
71+
b.Property<Guid>("Id")
72+
.ValueGeneratedOnAdd()
73+
.HasColumnType("uuid");
74+
75+
b.Property<DateTime>("CreatedAtUtc")
76+
.HasColumnType("timestamp with time zone");
77+
78+
b.Property<string>("EventsCsv")
79+
.IsRequired()
80+
.HasMaxLength(4096)
81+
.HasColumnType("character varying(4096)");
82+
83+
b.Property<bool>("IsActive")
84+
.HasColumnType("boolean");
85+
86+
b.Property<string>("SecretHash")
87+
.HasMaxLength(512)
88+
.HasColumnType("character varying(512)");
89+
90+
b.Property<string>("Url")
91+
.IsRequired()
92+
.HasMaxLength(2048)
93+
.HasColumnType("character varying(2048)");
94+
95+
b.HasKey("Id");
96+
97+
b.HasIndex("IsActive");
98+
99+
b.ToTable("Subscriptions", "webhooks");
100+
});
101+
#pragma warning restore 612, 618
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)