Add a Test
A step-by-step guide to adding a new test to Http11Probe. Every test touches five places: the suite file, the docs URL map (sometimes), a documentation page, the category index, and the RFC Requirement Dashboard.
1. Define the test case
Pick the suite that matches your test's category and add a yield return new TestCase block.
| Category | File |
|---|---|
| Compliance | src/Http11Probe/TestCases/Suites/ComplianceSuite.cs |
| Smuggling | src/Http11Probe/TestCases/Suites/SmugglingSuite.cs |
| Malformed Input | src/Http11Probe/TestCases/Suites/MalformedInputSuite.cs |
| Normalization | src/Http11Probe/TestCases/Suites/NormalizationSuite.cs |
| Cookies | src/Http11Probe/TestCases/Suites/CookieSuite.cs |
| WebSockets | src/Http11Probe/TestCases/Suites/WebSocketsSuite.cs |
| Capabilities | src/Http11Probe/TestCases/Suites/CapabilitiesSuite.cs |
CapabilitiesSuite holds multi-step sequence tests and yields SequenceTestCase instead of TestCase; follow the existing entries in that file if your test needs several requests on one connection.
yield return new TestCase
{
Id = "COMP-MY-TEST",
Description = "Description of what the test checks",
Category = TestCategory.Compliance,
RfcLevel = RfcLevel.Must, // Must (default) | Should | May | OughtTo | NotApplicable
RfcReference = "RFC 9112 Β§X.X",
PayloadFactory = ctx => MakeRequest(
$"GET / HTTP/1.1\r\nHost: {ctx.HostHeader}\r\n\r\n"
),
Expected = new ExpectedBehavior
{
ExpectedStatus = StatusCodeRange.Exact(400),
AllowConnectionClose = true,
},
Scored = true,
};
Test ID naming
| Prefix | Suite |
|---|---|
COMP- |
Compliance |
SMUG- |
Smuggling |
MAL- |
Malformed Input |
NORM- |
Normalization |
COOK- |
Cookies |
WS- |
WebSockets |
CAP- |
Capabilities |
RFC9112-... or RFC9110-... |
Compliance (when the test maps directly to a specific RFC section) |
Validation options
Simple status check:
Expected = new ExpectedBehavior
{
ExpectedStatus = StatusCodeRange.Exact(400),
}
Allow connection close as alternative:
Expected = new ExpectedBehavior
{
ExpectedStatus = StatusCodeRange.Exact(400),
AllowConnectionClose = true,
}
Custom validator (takes priority over ExpectedStatus):
Expected = new ExpectedBehavior
{
CustomValidator = (response, state) =>
{
if (response?.StatusCode == 400) return TestVerdict.Pass;
if (response?.StatusCode >= 200 && response.StatusCode < 300)
return TestVerdict.Warn;
return TestVerdict.Fail;
},
Description = "400 = pass, 2xx = warn"
}
Key conventions
- Set
RfcLevelto match the RFC 2119 keyword for the requirement being tested. The default isMustβ only set it explicitly for non-Must tests. Available values:Must,Should,May,OughtTo,NotApplicable. Check the RFC Requirement Dashboard for classification guidance. - Build payloads with the
MakeRequesthelper each suite defines, and always usectx.HostHeaderrather than a hardcoded host. - Tests are auto-discovered β the
yield returnis the whole registration, there is no list to update. - Use
Exact(400)with noAllowConnectionClosefor strict MUST-400 requirements (SP-BEFORE-COLON, MISSING-HOST, DUPLICATE-HOST, OBS-FOLD, CR-ONLY). - Set
AllowConnectionClose = trueonly when connection close is an acceptable alternative to a status code. - Set
Scored = falsefor MAY-level or informational tests. - Use
"RFC 9112 Β§5.1"format forRfcReference(section sign, not "Section"). - Give the doc page a readable
title(e.g."My Test β HTTP/1.1 Compliance"), not the raw test ID β it's rendered as the page heading and browser tab title. Write adescriptionthat's a specific, one-sentence summary of the request and its RFC basis, not a generic placeholder β it's used as the page's meta description for search results.
2. Add a docs URL mapping (if needed)
File: src/Http11Probe.Cli/Reporting/DocsUrlMap.cs
Tests prefixed with SMUG-, MAL-, NORM-, COOK-, or WS- are auto-mapped to their doc URL based on the ID. For example, SMUG-CL-TE-BOTH maps to smuggling/cl-te-both.
Every other prefix β COMP-, RFC*, CAP- β needs an explicit entry in the ComplianceSlugs dictionary, otherwise the test result won't link to its documentation:
["COMP-MY-TEST"] = "headers/my-test",
If the slug doesn't follow the standard pattern (e.g. the filename differs from the ID), add it to SpecialSlugs instead.
3. Create the documentation page
File: docs/content/docs/{category}/{test-slug}.md
The {category} folder is the slug the doc URL map points at. Compliance tests are split across several folders by topic:
| Category | Folder |
|---|---|
| Compliance (line endings) | line-endings |
| Compliance (request line) | request-line |
| Compliance (headers) | headers |
| Compliance (host header) | host-header |
| Compliance (content-length) | content-length |
| Compliance (body) | body |
| Smuggling | smuggling |
| Malformed Input | malformed-input |
| Normalization | normalization |
| Cookies | cookies |
| WebSockets | websockets |
| Capabilities | caching |
Compliance folders are chosen by topic, so pick the one the requirement belongs to rather than deriving it from the test ID.
Use this template:
---
title: "My Test β HTTP/1.1 Compliance"
description: "One or two sentences describing the request and what makes it non-conforming, ideally ending with the RFC section it's tested against."
weight: 1
---
| | |
|---|---|
| **Test ID** | `COMP-MY-TEST` |
| **Category** | Compliance |
| **RFC** | [RFC 9112 Β§X.X](https://www.rfc-editor.org/rfc/rfc9112#section-X.X) |
| **Requirement** | MUST |
| **Expected** | `400` or close |
## What it sends
Description of the request and what makes it non-conforming.
## What the RFC says
> "Exact quote from the RFC." -- RFC 9112 Section X.X
## Why it matters
Security and compatibility implications.
## Sources
- [RFC 9112 Β§X.X](https://www.rfc-editor.org/rfc/rfc9112#section-X.X)
4. Add a card to the category index
File: docs/content/docs/{category}/_index.md
Add a card entry in the appropriate section (scored or unscored):
The link value is the filename without .md. Place scored tests before unscored ones.
5. Add a row to the RFC Requirement Dashboard
File: docs/content/docs/rfc-requirement-dashboard.md
This page classifies every test by its RFC 2119 requirement level, so a new test needs a row and the surrounding counts need updating.
Pick the table by requirement level:
| Level | Table |
|---|---|
MUST / MUST NOT |
"MUST-Level Requirements" β use the "Reject with 400" sub-table when the RFC explicitly mandates 400, otherwise "Reject (400 or Connection Close Acceptable)" |
SHOULD / SHOULD NOT |
"SHOULD-Level Requirements" |
MAY |
"MAY-Level Requirements" |
Any level, Scored = false |
"Unscored Tests" |
The row carries the test ID, suite name, RFC link, and an exact RFC quote with the keyword bolded (**MUST**). Then update every count the new row affects:
- The summary table at the top
- The total test count, in both the
descriptionfrontmatter and the "Total: N tests" line - The "Requirement Level by Suite" section
- The "RFC Section Cross-Reference" table β increment the existing section or add a row
6. Verify
Build and run the probe locally:
dotnet build Http11Probe.slnx -c Release
dotnet run --project src/Http11Probe.Cli -- --host localhost --port 8080
Check that:
- Your test appears in the JSON output with the correct ID
- The verdict makes sense against a known server
- The documentation page renders and is linked from its category index
To preview the site, build it from web/:
cd web && npm ci && node build.mjs
No changes are needed in the CI workflow -- new tests are discovered automatically.