skip to content

In MockServer, how do you match only requests that are missing an `X-Sweep-Tenant` header?

level: middleimportance: must knowfreq 57%

answer

  1. negation, not an absence matcher
  2. NottableString carries names and values
  3. negate the name for missing
  4. negate the value for present-but-wrong
  5. in JSON it is a bang prefix

basics

~20 s

Negate the header name. MockServer holds request names and values as NottableString values, so a header built from not("X-Sweep-Tenant") matches only calls that carry no such header. Negating the value instead matches a header that is present but different.

solid answer

~40 s

MockServer has no separate absence matcher; it has negation, and where you put it decides what you get. Names and values on MockServer's request side are `NottableString`s, so its `header(not("X-Sweep-Tenant"))` means *no header of that name is present*, while its `header(string("X-Sweep-Tenant"), not("acme\\.co"))` means *the header is there and its value is something else*. Both go on MockServer's `request().withHeaders(...)`, and the same construction works for cookies and query-string parameters. Omitting the header does neither job, because a MockServer expectation constrains only what it names. In a MockServer JSON expectation the negation is a `!` prefix rather than a call. The distinction earns its keep on the chimney-sweep scheduling stub: an untenanted call should get 401 and an unknown tenant 403, and only the placement of MockServer's `not(...)` separates the two.

code

java · 21 lines
java
import static org.mockserver.model.Header.header;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
import static org.mockserver.model.NottableString.not;
import static org.mockserver.model.NottableString.string;

// No X-Sweep-Tenant header at all -> 401.
client.when(
        request()
            .withMethod("GET")
            .withPath("/v1/sweeps")
            .withHeaders(header(not("X-Sweep-Tenant"))))
    .respond(response().withStatusCode(401));

// Header present, but not the tenant this stub was seeded for -> 403.
client.when(
        request()
            .withMethod("GET")
            .withPath("/v1/sweeps")
            .withHeaders(header(string("X-Sweep-Tenant"), not("acme\\.co"))))
    .respond(response().withStatusCode(403));

go deeper

for a junior

Know that MockServer can match on a header not being there, and that it is expressed by negating the header's name rather than by leaving the header out of the expectation.

for a middle

Be able to explain both placements of MockServer's negation and what each one asserts about the incoming call, and to say why silence in an expectation is a third, weaker thing entirely.

for a senior

Show that you use it to make client branches reachable. A MockServer stub that cannot distinguish an untenanted call from an authorised one cannot prove the unauthenticated path was ever exercised.

for a principal

Decide how far stub definitions should encode authorisation shapes at all, and how a team keeps the negative expectations honest as the real service's header contract moves.

## MockServer has negation, not an absence matcher There is no "absent" matcher on MockServer's request side. What there is instead is a general negation, carried by MockServer's `NottableString`, and where you apply it decides which question you are asking of the incoming call. MockServer's `NottableString.not("X-Sweep-Tenant")` produces a negated **name**, while its `NottableString.not("acme\\.co")` produces a negated **value**. Both are ordinary arguments to the same MockServer builders, so the mechanism is one idea rather than two. That matters because a MockServer expectation is silent by default: entries you do not mention place no constraint on the call at all. "The tenant header must be missing" is therefore a claim you have to make, not one you get by leaving the header out of the expectation. ## Where the negation goes | MockServer construction | what it matches | |---|---| | MockServer `header(not("X-Sweep-Tenant"))` | calls carrying no header of that name | | MockServer `header(string("X-Sweep-Tenant"), not("acme\\.co"))` | the header is present, with some other value | | MockServer `header("X-Sweep-Tenant", ".*")` | the header is present, with any value | | MockServer `header("X-Sweep-Tenant", "acme\\.co")` | the header is present with that value | | no header entry at all in the MockServer expectation | anything, tenanted or not | The last row is the one that trips people. Omitting the header from a MockServer expectation is not a way of saying it must be absent; it is a way of saying you were not asking. ## Writing it on the chimney-sweep scheduling stub The scheduling stub needs three different answers on `GET /v1/sweeps`, and each is a different placement of MockServer's negation: - **No tenant header at all — 401.** MockServer's `request().withPath("/v1/sweeps").withHeaders(header(not("X-Sweep-Tenant")))` matches only untenanted calls, which is the case the client's "you are not signed in" branch has to handle. - **Tenant header present but not ours — 403.** MockServer's `withHeaders(header(string("X-Sweep-Tenant"), not("acme\\.co")))` keeps the name plain and negates the value, so it fires for a real but unauthorised tenant. - **Our tenant — 200 with the booking list.** The ordinary MockServer `withHeader("X-Sweep-Tenant", "acme\\.co")` covers the happy path. Written that way, all three client branches are reachable from one MockServer instance, and the difference between the 401 case and the 403 case is nothing but where MockServer's `not(...)` sits. ## The traps - **Silence is not absence.** This is the single most common mistake. A MockServer expectation that never mentions the tenant header answers tenanted and untenanted calls alike. - **Treat "missing" and "present but wrong" as two expectations.** The two placements ask genuinely different questions of the call, so a MockServer stub that must answer 401 for one and 403 for the other needs both written out. - **Negation applies everywhere on the request side.** MockServer uses the same `NottableString` for headers, cookies and query-string parameters, so a negated parameter name expresses "this parameter must not be present" just as a negated header name does. - **A negated value is still a pattern.** MockServer compares literally first and falls back to a regular expression, so the escaping you would apply to a positive value applies unchanged inside `not(...)`: MockServer reads `not("acme\\.co")` and `not("acme.co")` as different predicates. - **In JSON it is a prefix, not a call.** The same MockServer negation appears as a `!` in front of a name or a value in a JSON expectation, which is what you will see in a definition file rather than a `not(...)` call. ## The same intent in the other stub servers In WireMock the missing-header case has a dedicated matcher rather than a negated name, since WireMock's `withHeader("X-Sweep-Tenant", absent())` expresses absence on the value side. Mountebank splits the idea across two of its eleven predicates. Mountebank's `exists` predicate takes a boolean per field, so a Mountebank `headers` object mapping the tenant header to false matches calls that do not carry it, and any Mountebank predicate at all can be wrapped in Mountebank's `not` to invert it. ## Choosing the shape of your tenant expectations 1. Decide which client branches the test must reach. Untenanted, wrong tenant and correct tenant are three branches, not one. 2. For each, write down whether the header must be **absent**, **present and different**, or **present and equal** — that phrase maps one-to-one onto the placement of MockServer's `not(...)`. 3. Add the negated-name MockServer expectation deliberately rather than relying on the absence of a positive one, so a later edit adding a tenant constraint elsewhere cannot silently change which call the 401 stub answers. 4. Keep the escaping consistent between the positive and negated forms, since MockServer runs both through the same matcher. Getting this right is what makes a MockServer-backed suite able to prove the unauthenticated path at all. Without an explicit negated name, the untenanted call and the authorised call are indistinguishable to the stub, and the test that claims to cover the 401 branch is really just exercising the happy path with a different assertion.

  • Why is leaving the tenant header out of the expectation not the same as negating its name?
    Because a MockServer expectation constrains only what it names. Omitting the header means the call may carry it with any value or not carry it at all, so the same expectation answers both the tenanted and the untenanted case. MockServer's `header(not("X-Sweep-Tenant"))` is the narrower claim that no header of that name is present, which is what a test of the unauthenticated branch actually needs.
  • Does the same negation work on a query-string parameter or a cookie in MockServer?
    Yes. The negation lives on MockServer's `NottableString`, and MockServer uses `NottableString` for names and values across headers, cookies and query-string parameters alike. So a negated parameter name expresses "this parameter must not be present" exactly as a negated header name does, and a negated value expresses "present, but something else" in each of the three places.

saying these in an interview costs you the question

  • Thinks omitting a header from the expectation makes it required to be absent
  • Looks for a MockServer absent matcher instead of negating the name
  • Negates the value when the intent is that the header is missing
  • Forgets that a negated value is still compared as a pattern
  • Writes one expectation and expects it to cover missing and wrong alike