Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/prompts/coreex-test-api.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
mode: agent
description: "Write or update an integration test in a CoreEx *.Test.Api project — OneTimeSetUp seeding, CRUD/ETag/soft-delete scenarios, outbox assertions, and HTTP client mocking."
---

Use the `coreex-test-api` skill to write or update an integration test in this CoreEx `*.Test.Api` project.

Read `.github/skills/coreex-test-api/SKILL.md` first, then follow
`.github/skills/coreex-test-api/references/workflow.md` step by step (seed data → test classes →
per-operation patterns → outbox assertions → HTTP mocking → resources).

User request: ${input}
11 changes: 11 additions & 0 deletions .github/prompts/coreex-test-relay.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
mode: agent
description: "Understand, verify, or extend an Outbox Relay host integration test in a CoreEx *.Test.Relay project — mostly templated plumbing checks, rarely extended per domain."
---

Use the `coreex-test-relay` skill to work with the CoreEx `*.Test.Relay` project.

Read `.github/skills/coreex-test-relay/SKILL.md` first — check what the templated `RelayTests.cs`
already verifies before assuming new scenarios are needed.

User request: ${input}
13 changes: 13 additions & 0 deletions .github/prompts/coreex-test-subscribe.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
mode: agent
description: "Write or update an integration test in a CoreEx *.Test.Subscribe project — simulating broker message receipt, ErrorHandler outcomes, and command/event-sync/event-business-process test scenarios."
---

Use the `coreex-test-subscribe` skill to write or update an integration test in this CoreEx
`*.Test.Subscribe` project.

Read `.github/skills/coreex-test-subscribe/SKILL.md` first, then follow
`.github/skills/coreex-test-subscribe/references/workflow.md` step by step. For the shared DB/cache/
outbox `OneTimeSetUp` mechanics, also see `.github/skills/coreex-test-api/references/workflow.md`.

User request: ${input}
6 changes: 4 additions & 2 deletions .github/skills/coreex-api/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: coreex-api
description: "Add or modify a CoreEx API controller (or Minimal API endpoint) in an *.Api host. USE FOR: scaffolding the MVC controller pair (XxxController + XxxReadController), GET/query/schema endpoints, POST create, PUT + PATCH full-entity update, DELETE, and custom business-action endpoints. Covers both exception-based and Result<T> service styles, and Minimal API as an alternative to MVC. DO NOT USE FOR: Api host setup / Program.cs (use coreex-host-setup), application services (use coreex-app-service), API integration tests (see coreex-tests.instructions.md)."
description: "Add or modify a CoreEx API controller (or Minimal API endpoint) in an *.Api host. USE FOR: scaffolding the MVC controller pair (XxxController + XxxReadController), GET/query/schema endpoints, POST create, PUT + PATCH full-entity update, DELETE, and custom business-action endpoints. Covers both exception-based and Result<T> service styles, and Minimal API as an alternative to MVC. DO NOT USE FOR: Api host setup / Program.cs (use coreex-host-setup), application services (use coreex-app-service), API integration tests (use coreex-test-api)."
argument-hint: "Optional: entity name, operations needed (get/query/create/update/delete/custom), exception-based or Result<T> service style, MVC or Minimal API"
tags: ["api", "controller", "mvc", "minimal-api", "webapi", "routing", "cqrs", "coreex"]
---
Expand All @@ -23,7 +23,7 @@ Guides you through adding or modifying HTTP API endpoints in an `*.Api` host. Co

- Api host setup and `Program.cs` composition — use `coreex-host-setup`
- Application service creation — use `coreex-app-service`
- API integration tests (`WithApiTester<Program>`) — see `coreex-tests.instructions.md`
- API integration tests — use `coreex-test-api` (hand off once the endpoint is implemented)
- Subscriber or relay hosts — controllers do not belong there

## Quick Reference
Expand All @@ -49,6 +49,7 @@ Guides you through adding or modifying HTTP API endpoints in an `*.Api` host. Co
- `Result<T>` service → `WithResult` variants (`GetWithResultAsync`, `PostWithResultAsync`, `PutWithResultAsync`, …)
- No business logic in controllers — delegate immediately to the application service
- `[Query(supportsOrderBy: true), Paging(supportsCount: true)]` + `[HttpGet("$query")]` schema endpoint for query operations
- Once the endpoint is implemented, hand off to `coreex-test-api` to add/update its integration test

For full workflow and code examples see [`references/workflow.md`](references/workflow.md).

Expand All @@ -58,3 +59,4 @@ For full workflow and code examples see [`references/workflow.md`](references/wo
- [`/samples/src/Contoso.Products.Api/Controllers/`](/samples/src/Contoso.Products.Api/Controllers/) — `ProductController` + `ProductReadController` (exception-based, full CRUD + query + $query)
- [`/samples/src/Contoso.Shopping.Api/Controllers/`](/samples/src/Contoso.Shopping.Api/Controllers/) — `BasketController` + `BasketReadController` (Result&lt;T&gt; style, custom business actions, cross-tagged nested route)
- [`/samples/src/Contoso.Orders.Api/Controllers/`](/samples/src/Contoso.Orders.Api/Controllers/) — `OrderController` (exception-based, custom orchestration action returning 202 Accepted)
- `coreex-test-api` — the integration-test workflow for the endpoints this skill scaffolds
7 changes: 3 additions & 4 deletions .github/skills/coreex-subscriber/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: coreex-subscriber
description: "Add or modify an event/command subscriber in a CoreEx Subscribe host. USE FOR: command subscriber (owns the contract, delegates to app service), event-data-sync subscriber (delegates to IXxxSyncAdapter), event-business-process subscriber (choreography step, delegates to app service). Covers SubscribedBase, SubscribedBase<T>, ValueValidator, ErrorHandler, subject naming, and Subscribe-test integration tests. DO NOT USE FOR: API controllers (use coreex-api), application services (use coreex-app-service), replication adapter implementations (use coreex-adapter), Subscribe host Program.cs setup (see coreex-host-setup.instructions.md)."
description: "Add or modify an event/command subscriber in a CoreEx Subscribe host. USE FOR: command subscriber (owns the contract, delegates to app service), event-data-sync subscriber (delegates to IXxxSyncAdapter), event-business-process subscriber (choreography step, delegates to app service). Covers SubscribedBase, SubscribedBase<T>, ValueValidator, ErrorHandler, and subject naming. DO NOT USE FOR: API controllers (use coreex-api), application services (use coreex-app-service), replication adapter implementations (use coreex-adapter), Subscribe host Program.cs setup (see coreex-host-setup.instructions.md), Subscribe-test integration tests (use coreex-test-subscribe)."
argument-hint: "Optional: subscriber scenario (command / event-sync / event-process), subject string, payload type, whether ErrorHandler is needed"
tags: ["subscriber", "messaging", "service-bus", "event-handling", "choreography", "saga", "coreex"]
---
Expand Down Expand Up @@ -41,7 +41,7 @@ There are three distinct subscriber scenarios — determine which applies before
- Subject format: `{solution}.{domain}.{entity}.{action}[.v{n}]` — include `.v{n}` only when the message carries a payload
- `EventData.CreateCommand(...)` for commands; `EventData.CreateEvent(...)` / `new EventData().WithTitle(...)` for events
- `ErrorHandler` for graceful not-found and retry/dead-letter control — share the same static instance across related subscribers
- Tests use `WithApiTester<Program>` (Subscribe host); simulate receipt via `ServiceBusSubscribedSubscriber.ReceiveAsync(sbm)`
- Integration tests use `WithApiTester<Program>` (Subscribe host); simulate receipt via `ServiceBusSubscribedSubscriber.ReceiveAsync(sbm)` — see `coreex-test-subscribe` for the full test workflow

For full workflow and code examples see [`references/workflow.md`](references/workflow.md).

Expand All @@ -51,7 +51,6 @@ For full workflow and code examples see [`references/workflow.md`](references/wo
- `samples/src/Contoso.Products.Subscribe/Subscribers/ReservationCancelSubscriber.cs` — command subscriber sharing an `ErrorHandler`
- `samples/src/Contoso.Shopping.Subscribe/Subscribers/ProductModifySubscriber.cs` — typed event-sync subscriber with `ValueValidator`
- `samples/src/Contoso.Shopping.Subscribe/Subscribers/ProductDeleteSubscriber.cs` — untyped event-sync subscriber (key-only delete)
- `samples/tests/Contoso.Products.Test.Subscribe/SubscriberTests.ReservationConfirm.cs` — command subscriber test (outbox assertion + ErrorHandler)
- `samples/tests/Contoso.Shopping.Test.Subscribe/SubscriberTests.ProductModify.cs` — event-sync subscriber test (state assertion)
- `coreex-test-subscribe` — full Subscribe-test integration test workflow (test class shape, simulating message receipt, command/event-sync/event-business-process test patterns, unsubscribed-subject test)
- `.github/instructions/coreex-event-subscribers.instructions.md` — full subscriber conventions reference
- `.github/instructions/coreex-host-setup.instructions.md` — Subscribe host `Program.cs` shape
154 changes: 6 additions & 148 deletions .github/skills/coreex-subscriber/references/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,154 +227,12 @@ builder.Services

## Step 5 — Integration Tests

Subscriber tests use `WithApiTester<Program>` (from the Subscribe host's `Program` class). The
`ServiceBusSubscribedSubscriber` simulates broker message receipt without a live Service Bus
connection.

### Test class shape

```csharp
// Tests/SubscriberTests.{Scenario}.cs (partial class — split by scenario)
namespace {Domain}.Test.Subscribe;

public partial class SubscriberTests : WithApiTester<{Domain}.Subscribe.Program>
{
[OneTimeSetUp]
public async Task OneTimeSetUpAsync()
{
// Seed + migrate: always specify the seed file explicitly.
// Read tests → "read-data.seed.yaml", mutate/subscribe tests → "mutate-data.seed.yaml",
// schema-only tests (health, relay) → "no-data.seed.yaml".
// SQL Server: MigrateSqlServerDataAsync; PostgreSQL: MigratePostgresDataAsync.
await Test.MigrateSqlServerDataAsync<TestData>(["mutate-data.seed.yaml"], DbMigration.ConfigureMigrationArgs).ConfigureAwait(false);
await Test.ClearFusionCacheAsync().ConfigureAwait(false);

// Register the outbox publisher expectation for the domain's database engine.
Test.UseExpectedSqlServerOutboxPublisher(); // SQL Server
// Test.UseExpectedPostgresOutboxPublisher(); // PostgreSQL
}
}
```

### Simulating message receipt

```csharp
// Build the event and convert to a ServiceBusReceivedMessage
var ed = EventData.CreateCommand("products", "reservation", "confirm").WithKey(referenceId);
// For events with payload:
// var ed = new EventData().WithTitle("contoso.products.product.updated.v1").WithValue(product);

var ce = Test.CreateCloudEventFrom(ed);
var sbm = ce.ToServiceBusReceivedMessage();

var sbs = test.Services.GetRequiredService<ServiceBusSubscribedSubscriber>();
var r = await sbs.ReceiveAsync(sbm);
r.IsSuccess.Should().BeTrue();
```

### Command subscriber test (outbox assertion + `ErrorHandler`)

```csharp
[Test]
public void {Entity}{Action}_NotFound() => Test.Scoped(test =>
{
var ed = EventData.CreateCommand("{domain}", "{entity}", "{action}").WithKey("missing-id");
var ce = Test.CreateCloudEventFrom(ed);
var sbm = ce.ToServiceBusReceivedMessage();

test.Run(async _ =>
{
var sbs = test.Services.GetRequiredService<ServiceBusSubscribedSubscriber>();
var r = await sbs.ReceiveAsync(sbm);

r.IsFailure.Should().BeTrue();
var e = r.Error.Should().BeOfType<EventSubscriberHandledException>().Subject;
e.ErrorHandling.Should().Be(ErrorHandling.CompleteAsInformation);
e.InnerException.Should().BeOfType<NotFoundException>().Which.ErrorCode.Should().Be("{entity}-not-found");
}).AssertSuccess();
});

[Test]
public void {Entity}{Action}_Success() => Test.Scoped(async test =>
{
// Arrange — use a service or repository to confirm pre-conditions.
var referenceId = 1000.ToGuid().ToString();
var svc = test.Services.GetRequiredService<I{Entity}ReadService>();
var items = await svc.GetAsync(referenceId);
items.Should().HaveCount(3);

// Act — simulate the command message.
test.ExpectSqlServerOutboxEvents(e => e.AssertCount(3))
.Run(async _ =>
{
var ed = EventData.CreateCommand("{domain}", "{entity}", "{action}").WithKey(referenceId);
var ce = Test.CreateCloudEventFrom(ed);
var sbm = ce.ToServiceBusReceivedMessage();

var sbs = test.Services.GetRequiredService<ServiceBusSubscribedSubscriber>();
var r = await sbs.ReceiveAsync(sbm);
r.IsSuccess.Should().BeTrue();
}).AssertSuccess();

// Assert — verify state change.
items = await svc.GetAsync(referenceId);
items.Should().AllSatisfy(i => i.StatusCode.Should().Be({Status}.Confirmed));
});
```

### Event-sync subscriber test (state assertion)

```csharp
[Test]
public void {Entity}Modify_Success() => Test.Scoped(async test =>
{
// Arrange — get current state and apply a change.
var pa = test.Services.GetRequiredService<I{External}Adapter>();
var result = await pa.GetAsync(knownId);
result.IsSuccess.Should().BeTrue();
var entity = result.Value;
entity.Name += " modified";

var ed = new EventData().WithTitle("{solution}.{external}.{entity}.updated.v1").WithValue(entity);
var ce = Test.CreateCloudEventFrom(ed);
var sbm = ce.ToServiceBusReceivedMessage();

// Act.
test.Run(async _ =>
{
var sbs = test.Services.GetRequiredService<ServiceBusSubscribedSubscriber>();
var r = await sbs.ReceiveAsync(sbm);
r.IsSuccess.Should().BeTrue();
}).AssertSuccess();

// Assert — verify the local store reflects the change.
result = await pa.GetAsync(knownId);
result.Value.Name.Should().EndWith(" modified");
});
```

**Unsubscribed subject test** — always include one to confirm unknown subjects are consumed silently:

```csharp
[Test]
public void Unsubscribed_Error() => Test.Scoped(test =>
{
var ed = EventData.CreateEvent("test", "not-subscribed").WithKey("abc");
var ce = Test.CreateCloudEventFrom(ed);
var sbm = ce.ToServiceBusReceivedMessage();

test.Run(async _ =>
{
var sbs = test.Services.GetRequiredService<ServiceBusSubscribedSubscriber>();
var r = await sbs.ReceiveAsync(sbm);

r.IsFailure.Should().BeTrue();
var e = r.Error.Should().BeOfType<EventSubscriberHandledException>().Subject;
e.ErrorHandling.Should().Be(ErrorHandling.CompleteAsSilent);
e.InnerException.Message.Should().Be("No subscriber matched the event.");
}).AssertSuccess();
});
```
Always provide a test for a new or modified subscriber. Full workflow, test-class shape, message-receipt
simulation, and per-scenario test patterns (command / event-data-sync / event-business-process, plus the
required "unsubscribed subject" test) now live in the dedicated **`coreex-test-subscribe`** skill — see
[`../coreex-test-subscribe/references/workflow.md`](../coreex-test-subscribe/references/workflow.md).
That skill also links back to `coreex-test-api` for the shared DB/cache/outbox `OneTimeSetUp` mechanics
(seed authoring, provider-specific outbox helpers) common to both API and Subscribe host tests.

---

Expand Down
Loading
Loading