Skip to main content
  1. Docs/
  2. Module Reference/

garland-http

·659 words·4 mins

garland-http provides HttpTestClient — a configured HTTP client that produces steps for use in pipelines. It holds configuration (base URL, default headers, retry policy) and exposes methods like makeCall, pollingCall, and downloadFile that each return a Step ready to be passed to .then().

<dependency>
    <groupId>dev.garlandframework</groupId>
    <artifactId>garland-http</artifactId>
    <version>1.0.0</version>
    <scope>test</scope>
</dependency>

How a call works
#

A single makeCall step does everything: sends the request, checks the status, deserializes the body, and optionally asserts the response matches an expected value.

flowchart TD
    req["HttpCallRequest\n───────────────\nurl · method · headers\nbody · query params"]
    send["Send HTTP request"]
    status["Assert status code\n e.g. 201 == 201 ✓"]
    deser["Deserialize response body\nJSON → ResponseDto"]
    match["Match against expected\nnull fields ignored · optional"]
    dto["ResponseDto\n✓ returned to pipeline"]

    req -->|"makeCall()"| send
    send --> status
    status --> deser
    deser --> match
    match --> dto

    style req fill:#f5f5f5,stroke:#999
    style dto fill:#d4edda,stroke:#28a745
    style match fill:#fff3cd,stroke:#ffc107

HttpCallRequest
#

HttpCallRequest<T> carries all parameters for a single HTTP call. Build it via static factory methods — not inline in tests:

HttpCallRequest.get("/api/users/123")
HttpCallRequest.post("/api/users", userDto)
HttpCallRequest.put("/api/users/123", updateDto)
HttpCallRequest.delete("/api/users/123")

Query parameters:

// add a single param
request.withQueryParam("page", "0")

// add several at once
request.withQueryParams(Map.of("page", "0", "size", "10"))

HttpCallResponse
#

HttpCallResponse<T> describes the expected response for assertion:

// status + body assertion (no header assertions)
new HttpCallResponse<>(201, Map.of(), expectedDto)

// status + specific response header assertion
new HttpCallResponse<>(201, Map.of("Content-Type", List.of("application/json")), expectedDto)
  • status — asserted as exact match
  • headers — asserted as subset; actual response may contain additional headers
  • dto — null fields ignored during comparison (same as Verify.matching)

makeCall
#

makeCall has two main forms:

// assert status + match response body (null fields in expected are ignored)
.then(http.makeCall(new HttpCallResponse<>(201, Map.of(), expectedDto)))

// assert status only, return deserialized body
.then(http.makeCall(400, ErrorDto.class))

Generic response types (e.g. List<UserDto>) use TypeReference to avoid type erasure:

.then(http.makeCall(200, new TypeReference<List<UserDto>>() {}))

Temporal tolerance for timestamp fields in the response:

.then(http.makeCall(new HttpCallResponse<>(201, Map.of(), expectedDto), Duration.ofMillis(1)))

pollingCall
#

Retries until the response body matches. Use for endpoints that expose eventually-consistent state populated by async consumers — no Thread.sleep needed:

.then(http.pollingCall(200, expectedDto, RetryConfig.of(10, Duration.ofSeconds(1))))

Auth
#

Client-level — token known before the pipeline. Returns a new client instance; the original is not modified:

HttpTestClient authed = http.withBearer(token);
HttpTestClient withKey = http.withApiKey("X-Api-Key", key);
HttpTestClient withCookie = http.withCookie("session", value);

Context-level — token fetched inside the pipeline:

Pipeline.given(loginRequest())
        .then(http.makeCall(200, TokenDto.class))
        .then(HttpTestClient.storeBearer(TokenDto::accessToken))
        .then(__ -> protectedRequest())
        .then(http.makeCall(201, ResultDto.class))  // Authorization injected automatically
        .execute();

Client-level headers always take precedence over context — a suite-wide token set via withBearer cannot be overridden mid-pipeline.

Negative auth tests — send a request without the suite-wide token:

.then(http.withoutHeader("Authorization").makeCall(401, ErrorDto.class))

Client configuration
#

All with* methods return a new client instance — the original is never modified:

Method Description
withBaseUrl(url) Prepends url to any request path starting with /
withTimeout(duration) Per-request timeout — throws if server does not respond in time
withBearer(token) Sets Authorization: Bearer <token> on all requests
withHeader(name, value) Sets a custom default header
withoutHeader(name) Removes a header — useful for negative auth tests
withApiKey(header, key) Sets an API key header
withCookie(name, value) Sets a Cookie header

FormBody
#

For application/x-www-form-urlencoded requests — typically OAuth2 token endpoints and legacy APIs. Content-Type is set automatically:

HttpCallRequest<FormBody> request = new HttpCallRequest<>(
        "/oauth/token",
        "POST",
        List.of(),
        new FormBody()
                .field("grant_type", "client_credentials")
                .field("client_id", "my-client")
                .field("client_secret", "secret")
);

Pipeline.given(request)
        .then(http.makeCall(200, TokenDto.class))
        .execute();

Raw string body
#

Pass a String directly as the body to skip Jackson serialization. Content-Type: application/json is set automatically. Use when you need to send pre-built JSON or test malformed payloads:

HttpCallRequest<String> request = new HttpCallRequest<>(
        "/api/users",
        "POST",
        List.of(),
        "{\"name\": \"Alice\", \"email\": \"[email protected]\"}"
);

uploadFile
#

File uploads use MultipartBody as the request body. Content-Type: multipart/form-data is set automatically:

HttpCallRequest<MultipartBody> request = new HttpCallRequest<>(
        "/api/files/upload",
        "POST",
        List.of(),
        new MultipartBody()
                .field("description", "profile photo")
                .file("photo", Path.of("/tmp/photo.jpg"), "image/jpeg")
);

Pipeline.given(request)
        .then(http.makeCall(200, UploadResultDto.class))
        .execute();

Multiple files and fields can be chained — MultipartBody is immutable, each call returns a new instance:

new MultipartBody()
        .field("type", "report")
        .file("file1", Path.of("/tmp/a.pdf"), "application/pdf")
        .file("file2", Path.of("/tmp/b.pdf"), "application/pdf")

downloadFile
#

Downloads binary response to a local path:

Path file = Pipeline.given(TestFileRequests.downloadReport("123"))
        .then(http.downloadFile(200, Path.of("/tmp/report.pdf")))
        .execute();