SseAssertions.TUnit 0.5.0

Prefix Reserved
dotnet add package SseAssertions.TUnit --version 0.5.0
                    
NuGet\Install-Package SseAssertions.TUnit -Version 0.5.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.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SseAssertions.TUnit" Version="0.5.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.5.0
                    
#r "nuget: SseAssertions.TUnit, 0.5.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.5.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.5.0
                    
Install as a Cake Addin
#tool nuget:?package=SseAssertions.TUnit&version=0.5.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), AtLeast(n), AtMost(n), Exactly(n)
HasSseEvent(eventName, minCount, cancellationToken) Stream Flat (Task<AssertionResult>); cancellation-bounded partial-buffer reads
HasSseEvent(eventName, minCount, strictContentType, cancellationToken) HttpResponseMessage Flat; default-on Content-Type: text/event-stream validation
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 chain on the string receiver composes WithData(Func<string, bool>) to narrow by data payload and AtLeast / AtMost / Exactly to terminate with a count assertion. The async receivers (Stream, HttpResponseMessage) use a flat-form entry point because composing an async body read with a synchronous chain is awkward in C#; the async-receiver chain is a candidate for a future release.

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.50.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", minCount: 3, cancellationToken: ct);
}

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.5.0 0 6/6/2026
0.4.1 48 6/5/2026
0.4.0 56 6/4/2026
0.3.0 99 6/2/2026
0.2.0 148 5/21/2026
0.1.0 193 5/17/2026
0.0.1 171 5/17/2026

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

Minor release. Adds a value-pinning `HasSseRetryDirectiveFirst(int millis)` overload across all three receivers, so the leading `retry:` directive's position and value can be asserted in a single read of a forward-only response body. Purely additive; the `0.3.0` ApiCompat baseline is preserved.

### Added

- **`HasSseRetryDirectiveFirst(int millis)`** on the `string`, `Stream`, and `HttpResponseMessage` receivers asserts that a `retry:` directive precedes the first data-bearing event *and* that the leading directive's value equals `millis`. Position and value were previously assertable only separately (`HasSseRetryDirectiveFirst()` for position, `HasSseRetryDirective(millis)` for value), which a forward-only `HttpResponseMessage` body cannot satisfy across two calls; this overload pins both in a single read. On a value mismatch the failure names the expected and the observed leading `retry:` value.

### Changed

- The release workflow now publishes the matching `CHANGELOG.md` section as the GitHub release body (`body_path`), so release notes carry the full hand-written detail instead of GitHub's auto-generated commit summary.