SseAssertions.TUnit 0.7.0

Prefix Reserved
dotnet add package SseAssertions.TUnit --version 0.7.0
                    
NuGet\Install-Package SseAssertions.TUnit -Version 0.7.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SseAssertions.TUnit" Version="0.7.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SseAssertions.TUnit" Version="0.7.0" />
                    
Directory.Packages.props
<PackageReference Include="SseAssertions.TUnit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SseAssertions.TUnit --version 0.7.0
                    
#r "nuget: SseAssertions.TUnit, 0.7.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SseAssertions.TUnit@0.7.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SseAssertions.TUnit&version=0.7.0
                    
Install as a Cake Addin
#tool nuget:?package=SseAssertions.TUnit&version=0.7.0
                    
Install as a Cake Tool

SseAssertions.TUnit

NuGet Downloads License: MIT .NET

Scope: Test projects only. Not intended for production code.

TUnit-native Server-Sent Events (SSE) assertions for .NET. Fluent entry points over TUnit's Assert.That(...) pipeline for asserting on SSE event streams from HTTP response bodies, streams, and strings. AOT-compatible, trimmable, no runtime reflection in the assertion path.

Full documentation and roadmap: github.com/JohnVerheij/SseAssertions.TUnit

What ships

Entry point Receiver Shape
HasSseEvent(eventName) string Chain with WithData(predicate), WithDataParsedAs<T>(parse, predicate), WithId(id), WithRetryMillis(predicate), AtLeast(n), AtMost(n), Exactly(n)
HasSseEvent(eventName, cancellationToken) Stream Same chain; cancellation-bounded partial-buffer reads (v0.7.0+)
HasSseEvent(eventName, strictContentType, cancellationToken) HttpResponseMessage Same chain; default-on Content-Type: text/event-stream validation (v0.7.0+)
IsServerSentEventsStream() string Lightweight discriminator.
HasSseContentType(strict) HttpResponseMessage Header-only discriminator (no body read). strict: false (default) matches text/event-stream with any parameters; strict: true requires the bare media type with no parameters.
HasFirstSseEvent(eventName) string Asserts the first parsed frame's event: name. Unlabeled frames match HasFirstSseEvent("message") per the WHATWG default.
HasFirstSseEvent(eventName, cancellationToken) Stream Async; reads to end then asserts first frame.
HasFirstSseEvent(eventName, strictContentType, cancellationToken) HttpResponseMessage Async; validates Content-Type (default-on) then asserts first frame.
HasSseEventsInOrder(eventNames) string Chain with .WithStrictOrdering(). Default permits gaps between named events; strict mode requires contiguous appearance.
HasSseEventsInOrder(eventNames, strictOrdering, cancellationToken) Stream Async flat form. strictOrdering: true requires contiguous.
HasSseEventsInOrder(eventNames, strictOrdering, strictContentType, cancellationToken) HttpResponseMessage Async flat form with Content-Type validation (default-on).
HasSseRetryDirective(millis) string Asserts a retry: directive is present. millis: null accepts any value; millis: <n> requires exact match.
HasSseRetryDirective(millis, cancellationToken) Stream Async; same shape.
HasSseRetryDirective(millis, strictContentType, cancellationToken) HttpResponseMessage Async; validates Content-Type (default-on) then inspects retry directives.
HasSseRetryDirectiveFirst() string / Stream / HttpResponseMessage Asserts a retry: directive precedes the first non-empty data: field, checked at the wire-field level. An empty data: line carries no payload and does not count, so the standard ASP.NET Core control frame (event: retry + empty data: + retry: <ms>) passes. Stream / HttpResponseMessage forms take cancellationToken; HttpResponseMessage takes strictContentType (default-on). Matches the retry: directive field, not an event: retry named event: a stream emitting event: retry + data: with no retry: field fails. (v0.3.0+; empty-data: handling v0.4.1+)
EndsCleanlyOnCancellation(cancellationToken) Stream / HttpResponseMessage Asserts a canceled read tears down via cooperative cancellation rather than a transport exception (IOException / HttpRequestException). HttpResponseMessage form takes strictContentType (default-on) and reads the body via ReadAsStreamAsync. (Stream v0.3.0+; HttpResponseMessage v0.4.0+)

The HasSseEvent(eventName) chain narrows by data payload (WithData(Func<string, bool>), or WithDataParsedAs<T>(Func<string, T> parse, Func<T, bool> predicate) for a caller-supplied reflection-free deserializer), by the per-event id: directive (WithId(string)), or by the per-event retry: directive (WithRetryMillis(Func<int?, bool>)), then terminates with AtLeast / AtMost / Exactly count assertions. The same chain is available on all three receivers; on Stream and HttpResponseMessage the body read happens inside the assertion under the supplied cancellation token.

Install

dotnet add package SseAssertions.TUnit

The framework-agnostic SseAssertions core (defining the SseEvent public record, SseFrameParser, and SseFailureMessage factories) comes transitively.

Requirements: TUnit 1.54.0 or later, .NET 10. AOT-compatible, trimmable, no runtime reflection in the assertion path.

Quick start

[Test]
public async Task TickEndpoint_EmitsThreeTicks()
{
    const string body = "event: tick\ndata: 1\n\nevent: tick\ndata: 2\n\nevent: tick\ndata: 3\n\n";

    await Assert.That(body).HasSseEvent("tick").Exactly(3);
}

[Test]
public async Task NotificationEndpoint_PublishesAtLeastThreeTicks(CancellationToken ct)
{
    using var response = await _client.GetAsync("/notifications/ticks?take=3", ct);

    await Assert.That(response).HasSseEvent("tick", cancellationToken: ct).AtLeast(3);
}

See the full README for the Wire-format syntax reference, Failure diagnostics catalog, Cookbook (5 patterns), Design notes, and Out-of-scope caveats.

License

MIT. Copyright (c) 2026 John Verheij.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.7.0 334 6/16/2026
0.6.0 1,546 6/14/2026
0.5.1 146 6/11/2026
0.5.0 128 6/6/2026
0.4.1 155 6/5/2026
0.4.0 143 6/4/2026
0.3.0 276 6/2/2026
0.2.0 214 5/21/2026
0.1.0 225 5/17/2026
0.0.1 202 5/17/2026

View the rendered release notes: https://github.com/JohnVerheij/SseAssertions.TUnit/releases/tag/v0.7.0

Minor release. The frame narrowers that the `string` receiver already had (`WithData`, `WithDataParsedAs<T>`, `WithId`, `WithRetryMillis`) now reach the `Stream` and `HttpResponseMessage` receivers, which is the realistic shape for testing a live SSE endpoint: subscribe to an open response, drive activity, and assert per-frame payload under a cancellation-bounded partial read. Achieving this with one coherent `HasSseEvent(eventName)` surface across all three receivers means the flat `HasSseEvent(eventName, minCount, ...)` methods are replaced by the chainable form, which is a breaking change.

### BREAKING

- **The flat `HasSseEvent(eventName, minCount, ...)` methods on `Stream` and `HttpResponseMessage` are replaced by a chainable `HasSseEvent(eventName)`** matching the `string` receiver. The minimum-count argument moves to the `.AtLeast(n)` terminator, and the narrowers and other terminators (`AtMost`, `Exactly`) are now available on the streaming receivers.
 - Migration: `HasSseEvent("x", minCount: 2)` becomes `HasSseEvent("x").AtLeast(2)`; `HasSseEvent("x")` (default minimum of 1) is unchanged. The `strictContentType` and `cancellationToken` arguments stay on the entry method (`HasSseEvent("x", strictContentType: false, cancellationToken: ct)`).
- **A null `Stream` / `HttpResponseMessage` receiver now fails the assertion** ("the receiver was null") instead of throwing `ArgumentNullException`, matching the long-standing behavior of the `string` receiver. A null `eventName` still throws `ArgumentNullException`, and a negative count still throws `ArgumentOutOfRangeException` (now from the terminator).

### Added

- **Frame narrowers on the `Stream` and `HttpResponseMessage` receivers.** `WithData`, `WithDataParsedAs<T>`, `WithId`, and `WithRetryMillis` constrain which frames count within the cancellation-bounded read, producing the same per-narrower diagnostics as the `string` chain. `WithDataParsedAs<T>` takes a caller-supplied (source-generated) parser so the assertion stays AOT-compatible; a throwing parser fails naming the deserializer exception and the offending data. When the read is cancellation-truncated and the expectation is unmet, the cancellation-cut diagnostic still takes precedence over a count or narrower miss. All three receivers share one internal matching engine, so their diagnostics are identical. No public surface was added to the `SseAssertions` core.

### Fixed

- **A foreign cancellation during a streaming read is no longer reported as a cancellation-cut.** The shared response-read helper caught every `OperationCanceledException` as a caller-driven partial read; an `OperationCanceledException` from a different source (for example an `HttpClient` timeout) was therefore masked by the cancellation-cut diagnostic. It is now captured as a partial read only when the supplied token is the one that fired, otherwise it propagates, matching the distinction `EndsCleanlyOnCancellation` already made in 0.5.1. This applies to every `HttpResponseMessage` read path (`HasSseEvent`, `HasFirstSseEvent`, `HasSseEventsInOrder`, `HasSseRetryDirective`, `HasSseRetryDirectiveFirst`).
- **Stream acquisition no longer pre-empts the cancellation-cut diagnostic.** The `HttpResponseMessage` read paths passed the cancellation token to `ReadAsStreamAsync`, so a token that fired during stream acquisition threw out of the assertion instead of surfacing as the cancellation-cut diagnostic. Acquisition is now uncancellable; the token bounds only the body read, where cancellation is captured.