Help us shape the future of Go! Take the 2026 Go Developer Survey

testutil

package
v3.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 20, 2026 License: MIT Imports: 33 Imported by: 0

Documentation

Overview

Package testutil provides test helpers for gitlab-mcp-server.

It wraps net/http/httptest, the official gitlab.com/gitlab-org/api/client-go client, and shared assertion helpers so every domain test can stand up an isolated MCP server in a few lines. The package is consumed by every tool test under internal/tools and is the only sanctioned way to construct a gitlabclient.Client in tests.

Helper categories

Typical usage

func TestListBranches(t *testing.T) {
    client := testutil.NewTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        testutil.RespondJSON(w, http.StatusOK, `[{"id":1,"name":"main"}]`)
    }))
    // ... call the domain handler with client ...
}

Coverage

This package does not reach the repository's 100% statement rule, and the residue is named here rather than left for the next contributor to rediscover: the testing.T.Fatalf branches of AssertEmbeddedResource, IsolateTempDir and the legacy elicitation client. Every one of them exists to abort the caller's test, so reaching it means arranging for a helper to fail while the test that called it keeps running, and the only way to do that is to route the abort through a package variable a test can replace. That would cost these helpers the guarantee they are used for, since a Fatalf that no longer aborts leaves the code after it running on the value it was refusing, and it would have to be done at every call site to be worth anything. The recording and shape files added for the request inventory are at 100%, and the seams they use ([createShard], [recorderPackage], the requestReporter interface) are the pattern to follow if these are ever closed.

Index

Constants

View Source
const FixtureSentinelRFC3339 = "2001-02-03T04:05:06Z"

FixtureSentinelRFC3339 is FixtureSentinelTime as GitLab sends it.

View Source
const FixtureURLBase = "https://gitlab.example"

FixtureURLBase is the origin every URL-shaped field points at, so a scan can tell a fixture link from any other destination.

View Source
const GFMHintsHeading = "\U0001F4A1 **Next steps:**"

GFMHintsHeading is the heading the server writes over its next-step hints, spelled here so the line model can find the section without importing the package that writes it.

View Source
const GitLabAuthenticatedMaxComplexity = 250

GitLabAuthenticatedMaxComplexity is the complexity above which GitLab refuses a query from an authenticated user who is not an administrator, AUTHENTICATED_MAX_COMPLEXITY in app/graphql/gitlab_schema.rb. Every token this server is handed is held to it unless its user is an administrator (300) or the request carries none (200). GitLab computes the figure from the document and its variables before it runs anything, and refuses the whole query above it, so a document over it fails on every call however small the answer would have been.

View Source
const InventoryDirEnv = "GITLAB_MCP_TEST_INVENTORY_DIR"

InventoryDirEnv names the directory every request a test issues through NewTestClient is recorded into. Recording is off unless it is set, so an ordinary `go test` run pays nothing for it.

The path must be absolute. A test binary runs with its own package directory as the working directory, so a relative path would scatter one shard per package under 178 different directories and the merge would find none of them. A relative value is refused rather than resolved, because resolving it would produce exactly that scattering silently.

It carries the project's prefix but is not one of the settings internal/config resolves: it configures the test harness, cmd/server never reads it, and there is no legacy spelling of it to warn anybody about. That is the same standing the developer-only MODELEVAL_* variables have.

View Source
const MsgErrEmptyProjectID = "expected error for empty project_id, got nil"

MsgErrEmptyProjectID is the canonical assertion message for tests that expect an error when the GitLab tool input omits project_id.

Variables

View Source
var FixtureSentinelTime = time.Date(2001, time.February, 3, 4, 5, 6, 0, time.UTC)

FixtureSentinelTime is the instant every time-shaped field carries, chosen so its rendered forms are unmistakable: a display helper writes "3 Feb 2001 04:05 UTC" and a raw print writes the RFC 3339 text or Go's default form, either of which a scan can look for.

Functions

func AllowInvalidGraphQL

func AllowInvalidGraphQL(tb testing.TB)

AllowInvalidGraphQL exempts tb, and every subtest under it, from GraphQL document validation.

It exists for the test that deliberately sends a malformed document, which is the only honest reason to want it: a document the pinned schema refuses is a document GitLab refuses, so silencing the gate anywhere else silences a real defect. Say in a comment at the call site which malformed document the test is sending and why it has to.

Declare it on the test that calls NewTestClient, not on a subtest under one. The exemption is looked up under the name of the test the validation reports against, and that is the test the client belongs to: with a client built in the parent and driven from subtests, which is the common shape here, a subtest's exemption is never consulted and the refusal is reported against the parent while every subtest passes.

The exemption lasts for tb's lifetime and is released on cleanup.

func AssertCapturedDecodeFailures

func AssertCapturedDecodeFailures(t *testing.T, cases []CapturedCase)

AssertCapturedDecodeFailures runs each case under a subtest of its own and fails the ones whose handler answered successfully, or with an error that does not name the captured response's decode failure.

Every package that reads a field client-go does not model asserts the same thing about every handler it converted (ADR-0021), so the table lives with the handlers and the assertion lives here.

func AssertEmbeddedResource

func AssertEmbeddedResource(t *testing.T, ctx context.Context, session *mcp.ClientSession, name string, args map[string]any, wantURI string, toggle EmbedToggle)

AssertEmbeddedResource verifies that an MCP tool behaves correctly under both states of the embedded-resource toggle. It invokes the named tool via session twice:

  1. With toggle(true) — expects an *mcp.EmbeddedResource block whose URI matches wantURI and whose MIME type is "application/json".
  2. With toggle(false) — expects no EmbeddedResource blocks.

The toggle is always restored to its enabled (production default) state on test exit via testing.T.Cleanup. The test fails the surrounding run if either subtest fails.

func AssertQueryParam

func AssertQueryParam(t *testing.T, r *http.Request, key, expected string)

AssertQueryParam fails the test if the URL query parameter key does not equal expected. Missing parameters are reported as a mismatch with an empty actual value.

func AssertRequestMethod

func AssertRequestMethod(t *testing.T, r *http.Request, expected string)

AssertRequestMethod fails the test if r.Method does not equal expected. It calls testing.T.Helper so failure lines point at the caller.

func AssertRequestPath

func AssertRequestPath(t *testing.T, r *http.Request, expected string)

AssertRequestPath fails the test if r.URL.Path does not equal expected. It calls testing.T.Helper so failure lines point at the caller.

func AssertUnreadableBodyRefused added in v3.1.0

func AssertUnreadableBodyRefused(t *testing.T, cases []CapturedCase)

AssertUnreadableBodyRefused runs each case under a subtest of its own and fails the ones whose handler answered successfully, or with an error that names neither decoder's failure.

It is AssertCapturedDecodeFailures for a field client-go has since caught up with. Both decoders read the same bytes and the SDK's runs first, so once client-go models the field the capture reads, no body can fail the capture alone and the stricter assertion is unsatisfiable. What survives is the behavior these tests were written for: a handler refuses a body it cannot read rather than publishing a half-filled result.

func CancelOnArrival added in v3.1.0

func CancelOnArrival(tb testing.TB, respond http.HandlerFunc) (context.Context, *gitlabclient.Client)

CancelOnArrival is the fixture for the one thing a handler's cancellation test cannot learn from a context cancelled up front: whether the request the handler builds carries the caller's context at all.

client-go takes that context only as a request option, gl.WithContext, and builds the request from context.Background() without it. A handler that forgets the option compiles, passes every other test and serves correct answers, while neither the action deadline nor an abandoned HTTP POST ever reaches the request it sends. A context that is already cancelled cannot tell the two apart in a handler that checks ctx.Err() before it builds anything, as the handlers this fixture was written for do: that handler returns before any request exists, whether or not it would have passed the context on.

The context returned here is cancelled the moment the first request reaches the mock, and the mock holds every request until the client abandons it or five seconds pass, answering through respond only in the second case. A handler whose first request carried the context therefore returns promptly with an error for which errors.Is(err, context.Canceled) holds; one whose first request did not waits out the hold and returns whatever respond answered, which a test asserting the cancellation reports.

A handler that sends more than one request is not proved by that alone: a later request that carries the context is refused by the transport once the context is cancelled, so a handler whose first call dropped it and whose second passed it would still end in context.Canceled. Every request that sits out the hold is therefore counted, and the test fails at cleanup when any did, naming how many.

The cancel is made through a sync.Once on the handler's own goroutine rather than by a watcher goroutine, since a CancelFunc may be called from anywhere and a goroutine that waited for the arrival would be one more thing a test that never sends a request could leak.

func CancelledCtx

func CancelledCtx(t *testing.T) context.Context

CancelledCtx returns a context.Context that is already cancelled. Use it to test handler cancellation paths without dealing with real timers.

The returned context returns context.Canceled from context.Context.Err immediately. The associated cancel function is intentionally dropped — callers must not attempt to cancel it again.

func CaptureSlog

func CaptureSlog(t *testing.T) *bytes.Buffer

CaptureSlog redirects slog output to an in-memory bytes.Buffer for the duration of the test. The original slog.Default logger is restored via testing.T.Cleanup.

CaptureSlog is NOT safe for testing.T.Parallel: it acquires [captureSlogMu] for the lifetime of the test and would deadlock with another parallel CaptureSlog caller.

func ConnectLegacyElicitationClient

func ConnectLegacyElicitationClient(ctx context.Context, t *testing.T, server *mcp.Server, handler ElicitHandlerFunc, opts LegacyClientOptions) *mcp.ServerSession

ConnectLegacyElicitationClient connects a minimal legacy MCP client (protocol 2025-11-25, elicitation capability advertised) to server and returns the resulting server session. Server-initiated elicitation/create requests are answered by handler; ping requests are acknowledged; all other server-initiated requests fail with MethodNotFound. The session and the fake client are torn down via t.Cleanup.

After the handshake, handler runs on the fake client's serving goroutine, not the test goroutine: report failures inside handler with t.Errorf or by returning an error, never t.Fatal/t.FailNow (which only terminate the calling goroutine).

func FillFixture added in v3.1.0

func FillFixture(t reflect.Type, opts FixtureOptions) reflect.Value

FillFixture builds a value of type t the way a GitLab response would fill it, with values a scan can recognize: a string gets a sentinel of letters and digits made from its field name, a URL-shaped field an address under FixtureURLBase, a time-shaped string the RFC 3339 sentinel, a number 7, a flag true, a time.Time the sentinel instant, a slice two elements, a map one entry, a pointer an allocated value, and the shared pagination shapes the state asked for. An interface stays nil, nesting stops at depth 5, and the zero state returns the zero value.

func FixtureText added in v3.1.0

func FixtureText(path string) string

FixtureText is the sentinel a string field carries: an address for a URL-shaped field, the RFC 3339 sentinel for a time-shaped one, and for the rest the field's own name and a digit, letters and digits only, so the value holds no Markdown and can be found in the render.

func FixtureURLShaped added in v3.1.0

func FixtureURLShaped(name string) bool

FixtureURLShaped reports whether a field name says it holds an address, which is what decides that the field gets a fixture URL, and what keeps a hostile render from putting its payload where only an address can go.

func FollowKeyset added in v3.3.0

func FollowKeyset(r KeysetReporter, listing *KeysetListing, call func(pageToken string) (KeysetPage, error))

FollowKeyset reads a KeysetListing to its end through call, the way a caller follows a keyset list: once with no page_token, then once with the token the first call published. It reports through r unless the first page publishes the listing's next link and says more follow, the second request carries exactly the query of that link, the first request asks for keyset pagination, the second page publishes no token and says nothing follows, and each page carries the rows its body holds.

The second request is held to the whole of the link because GitLab writes every parameter it read into it: one sent beside the token that the link does not carry is a second form of a parameter it does, which Rack refuses.

call is the handler under test, invoked with the page_token to send and returning what its output reported.

func ForbiddenHandler

func ForbiddenHandler(t *testing.T) http.Handler

ForbiddenHandler returns an http.Handler for mocks that must never be called. Each request is counted atomically and answered with a deterministic 500 so the client under test fails loudly, and a testing.T.Cleanup hook asserts on the test goroutine that no request arrived. This is the sanctioned replacement for `t.Fatal("should not be called")` inside handler literals, which the testing package forbids off the test goroutine (see .github/instructions/test-goroutines.instructions.md).

func FormFile added in v3.1.0

func FormFile(form *multipart.Form, name string) (multipart.File, *multipart.FileHeader, error)

FormFile returns the first file of a multipart form field, opened, with its header, the way Request.FormFile answers for a parsed form. A field that carries no file reports http.ErrMissingFile, as Request.FormFile does.

func FormValue added in v3.1.0

func FormValue(form *multipart.Form, name string) string

FormValue returns the first value of a multipart form field, or an empty string when the form carries none, which is what Request.FormValue answers for a parsed form. An absent field is an assertion for the test goroutine to make, not a panic on the server's.

func GFMCells added in v3.1.0

func GFMCells(line string) []string

GFMCells splits one table line into its cells the way GFM does: on every pipe not preceded by a backslash, the entity | left alone, with the one leading and one trailing pipe the house style writes removed.

func GitLabQueryComplexity added in v3.1.0

func GitLabQueryComplexity(document string, variables map[string]any) (int, error)

GitLabQueryComplexity estimates the complexity GitLab computes for document sent with variables, which is the figure it refuses a query on (GitLabAuthenticatedMaxComplexity).

No unit test reaches GitLab and the validating transport judges a document by the schema alone, which says nothing about cost, so a selection that grew past the limit used to pass every test and fail on every instance. This is GitLab's computation, legacy mode of graphql-ruby's QueryComplexity with Types::BaseField's field costs, run over the pinned schema: each field costs its own cost, one unless [gitLabFieldCosts] records otherwise, plus its children; a selection set costs, for each object type its selections can resolve to, the sum of the fields that apply to that type, whether selected on it or on an interface it implements, and the largest of those sums.

It is an estimate because the field costs live in GitLab's Ruby rather than in its schema, and the table holds only those a measurement has needed. A caller therefore holds the estimate equal to a figure GitLab reported for the same document, so that a change to the selection changes the estimate and sends whoever made it to measure again.

It refuses what it does not model rather than guessing: a document that GitLab would not accept, one defining other than one operation, a selection carrying a directive (skip and include change what is counted), a response key selected twice on one type, and a page size that is not a number.

func GraphQLHandler

func GraphQLHandler(handlers map[string]http.HandlerFunc) http.Handler

GraphQLHandler returns an http.Handler that routes GraphQL POST requests by matching the request's query string against handler keys. The first key that appears as a substring of the query wins; keys are therefore evaluated longest-first so specific mutation names take precedence over shorter operation roots (e.g. "vulnerabilityDismiss" matches before "vulnerability"), and keys of one length in lexical order.

The request body is parsed into a [graphqlRequest], then reattached to r so downstream handlers can read it again. Non-POST requests are rejected with 405 Method Not Allowed, malformed JSON with 400 Bad Request, and queries that match no key with 400 Bad Request.

Keys should be GraphQL operation identifiers (field names, type names, or mutation names) that uniquely identify the operation, for example:

testutil.GraphQLHandler(map[string]http.HandlerFunc{
    "vulnerabilities":      handleListVulnerabilities,
    "vulnerabilityDismiss": handleDismissVulnerability,
})

func IsolateTempDir

func IsolateTempDir(t *testing.T, dir string)

IsolateTempDir points os.TempDir at dir for the duration of the test.

Setting TMPDIR alone is a POSIX-only instruction. On Windows os.TempDir never reads it, so a test that sets only TMPDIR keeps the real temporary directory and its isolation silently does nothing.

That silence is expensive in exactly the tests this helper exists for. The upload, download and import allow-lists treat the OS temporary directory as an always-allowed root, so a fixture built with testing.T.TempDir and meant to sit outside every allowed root instead sat inside one. Every assertion that such a path is refused then passed a path that was correctly accepted, and the suite reported that containment worked while testing nothing. The allow-list itself was never wrong.

It cannot be used from a parallel test, because testing.T.Setenv cannot.

func NewOAuthTestClient added in v3.3.0

func NewOAuthTestClient(tb testing.TB, handler http.Handler) *gitlabclient.Client

NewOAuthTestClient is NewTestClient for an oauth HTTP mode session: the client is the one gitlabclient.NewOAuthClientWithToken builds for every credential that mode admits, so "test-token" travels as "Authorization: Bearer" and never as PRIVATE-TOKEN, and gitlabclient.Client.SendsBearer is true. It is for a handler whose answer depends on which of the two schemes its session sends. The validation and the recording are NewTestClient's; retries are the constructor's own and are not disabled, so a test should not answer it with a status the client retries.

func NewTestClient

func NewTestClient(tb testing.TB, handler http.Handler) *gitlabclient.Client

NewTestClient creates a gitlabclient.Client pointed at a fresh httptest.Server backed by handler. The server is automatically torn down when the test finishes via testing.T.Cleanup, so callers never need to manage its lifecycle.

The client uses a static token ("test-token"), TLS verification disabled, and retries disabled — sufficient for most handler unit tests but not for exercising the transport retry policy. NewTestClient calls testing.T.Helper and testing.T.Fatalf if client construction fails.

Every GraphQL document sent through the returned client is validated against the pinned GitLab schema before handler sees it, so a mock can no longer answer what GitLab would refuse. The request proceeds either way and a refusal is reported with testing.TB.Errorf; AllowInvalidGraphQL is the opt-out for a test that sends a malformed document on purpose.

That validation is the reason to build a test's client here rather than wiring httptest.NewServer to gitlabclient.NewClient by hand. A handful of tests still do the latter, none of them sending GraphQL today, and any GraphQL written against one of those seams would be judged by nobody, which is the state this helper exists to end. The same goes for the recording below: a client built by hand is a request nothing observes.

Every request is also recorded, when InventoryDirEnv names a directory to record into, so the suite can answer what this server actually sends GitLab. Recording is off in an ordinary run.

The returned client is safe for concurrent use; the httptest.Server that backs it is goroutine-safe by construction.

func NewTestClientUnderRoot added in v3.3.0

func NewTestClientUnderRoot(tb testing.TB, root string, handler http.Handler) *gitlabclient.Client

NewTestClientUnderRoot is NewTestClient for an instance served under a relative URL root such as "/gitlab". The client's base URL carries the root, as GITLAB_URL does on such an instance, so a handler that derives the instance's web URL from the client sees the root; the root is taken off every request before the handler, the validation and the recording see it, so each reads the paths it reads for any other instance and the request inventory gains no row of its own for the root.

func ParseGraphQLVariables

func ParseGraphQLVariables(r *http.Request) (map[string]any, error)

ParseGraphQLVariables reads r's body and returns the Variables map from the GraphQL request, or an error if the body cannot be read or is not valid JSON. The body is restored on r so subsequent handlers can re-read it. The returned map is nil when the request omits a variables field.

ParseGraphQLVariables is intended for assertions inside test handlers — production code should rely on the client-go GraphQL transport.

func ReadMultipartForm added in v3.1.0

func ReadMultipartForm(r *http.Request, maxMemory int64) (*multipart.Form, error)

ReadMultipartForm parses the multipart body of a request a test handler received and hands back the form, which carries the same Value and File maps that Request.MultipartForm would after Request.ParseMultipartForm.

It reads through Request.MultipartReader rather than ParseMultipartForm on purpose. The two parse the same body to the same form; the difference is that ParseMultipartForm is the call gosec's G120 names as where an unbounded upload exhausts a real handler's memory, and a taint rule cannot tell a fixture body from a client's, so every test handler that called it carried a directive saying so. Reading the form here needs none, and maxMemory still bounds what is held in memory before the rest spills to disk, exactly as it does for ParseMultipartForm.

What a caller loses is Request.FormValue and Request.FormFile, which read the form ParseMultipartForm stored on the request: FormValue and FormFile answer the same questions of the returned form.

func RespondGraphQL

func RespondGraphQL(w http.ResponseWriter, status int, data string)

RespondGraphQL writes a GraphQL JSON envelope response with the given data payload. It wraps data in the standard {"data": ...} envelope expected by the GitLab GraphQL API client. For example:

testutil.RespondGraphQL(w, http.StatusOK, `{"project":{"name":"foo"}}`)

produces the body {"data":{"project":{"name":"foo"}}}. Callers remain responsible for escaping any quotes embedded in data.

func RespondGraphQLError

func RespondGraphQLError(w http.ResponseWriter, status int, message string)

RespondGraphQLError writes a GraphQL error envelope with the given message. It sets data to null and emits a single error entry, mirroring the shape returned by GitLab when a query partially or fully fails. For example:

testutil.RespondGraphQLError(w, http.StatusOK, "not found")

produces {"data":null,"errors":[{"message":"not found"}]}. The message is interpolated directly into JSON — callers must escape any embedded quotes.

func RespondJSON

func RespondJSON(w http.ResponseWriter, status int, body string)

RespondJSON writes a JSON response with the given HTTP status and raw body. It sets Content-Type to "application/json". body is written verbatim — callers are responsible for producing valid JSON. Write errors are intentionally ignored because the only writer in tests is an httptest.ResponseRecorder, which never fails.

func RespondJSONWithPagination

func RespondJSONWithPagination(w http.ResponseWriter, status int, body string, p PaginationHeaders)

RespondJSONWithPagination writes a JSON response with GitLab pagination headers attached. Headers whose PaginationHeaders field is empty are omitted, matching GitLab's behavior on pages without a next/previous pointer.

func ZeroField added in v3.1.0

func ZeroField(v reflect.Value, path []int) bool

ZeroField returns v with the field at path set to its zero value, walking through pointers, or false when the path crosses a nil pointer the fixture left unallocated.

Types

type CapturedCase

type CapturedCase struct {
	// Name says which handler the call drives, and names the subtest.
	Name string
	// Call runs the handler and returns the error it reported, if any.
	Call func() error
}

CapturedCase is one handler call in a table asserting the failure a captured response adds: a body the output type cannot hold.

type ElicitHandlerFunc

type ElicitHandlerFunc func(context.Context, *mcp.ElicitParams) (*mcp.ElicitResult, error)

ElicitHandlerFunc handles one server-initiated elicitation request.

type EmbedToggle

type EmbedToggle func(bool)

EmbedToggle is the signature of the [toolutil.EnableEmbeddedResources] setter. testutil reproduces the type locally so callers can drive the embedded-resource global flag without importing toolutil (which would introduce an import cycle through the tool sub-packages).

type FixtureField added in v3.1.0

type FixtureField struct {
	Path []int
	Name string
}

FixtureField is one leaf field a fixture can leave absent: the path of struct field indexes from the root, and its name for a report.

func OptionalFields added in v3.1.0

func OptionalFields(t reflect.Type) []FixtureField

OptionalFields lists the fields of t a GitLab response can omit, by the struct's own declaration: a pointer, a slice, a map, or a json tag with omitempty, walked through embedded and nested structs and through pointers to them. It is the oracle of the absent-value rule: a field the type says may be empty must render as nothing rather than as a glyph.

type FixtureOptions added in v3.1.0

type FixtureOptions struct {
	State FixtureState
	// Text, when set, supplies every string value from the field's path
	// instead of the sentinel, which is how a hostile render is built.
	Text func(path string) string
}

FixtureOptions configures FillFixture.

type FixtureState added in v3.1.0

type FixtureState int

FixtureState is the pagination a filled fixture carries, which is the one part of a list output the rendering rules read differently: a multi-page response must name the total, a single page names what it holds, and a keyset page knows no total at all.

const (
	// FixtureZero is the zero value of the type, every field absent.
	FixtureZero FixtureState = iota
	// FixtureMultiPage is a populated value on page 1 of 3, 45 items in all.
	FixtureMultiPage
	// FixtureSinglePage is a populated value on the only page.
	FixtureSinglePage
	// FixtureKeyset is a populated value with no total: a keyset page with a
	// next page, which is what GitLab sends past its counting cutoff.
	FixtureKeyset
)

func (FixtureState) String added in v3.1.0

func (s FixtureState) String() string

String names the state for a subtest.

type GFMBareLine added in v3.1.0

type GFMBareLine struct {
	// Line is the 1-based line the text sits on.
	Line int
	// Text is that line as written, which is what a finding quotes.
	Text string
	// Bare is the line with every code span and its delimiters replaced by a
	// placeholder, which is what a finding scans.
	Bare string
}

GFMBareLine is one line as the raw-tag rule must read it: the line as it was written, and the same line with every inline code span elided, since a span makes its contents literal and HTML-escaped.

type GFMDocument added in v3.1.0

type GFMDocument struct {
	Lines    []string
	Tables   []GFMTable
	Hints    []GFMHintSection
	Findings []GFMFinding
	// Headings, Items, Rows, Cells and Links are the structure the hostile
	// comparison holds constant: the ATX headings, the top-level list items,
	// the table body rows and their cells, and the link destinations, all
	// outside fences and quotes. Content is every line outside a fence or a
	// quote.
	Headings []string
	Items    []string
	Rows     int
	Cells    int
	Links    []string
	Content  []string
	// Bare is where a raw tag is a live tag rather than text: every line
	// outside a fence, its code spans elided. It carries quote lines, which
	// Content does not, because a blockquote contains structure and not tags
	// and a renderer takes an anchor inside one exactly as it takes one
	// outside.
	Bare []GFMBareLine
}

GFMDocument is what the line model read out of one rendered document.

func ScanGFM added in v3.1.0

func ScanGFM(md string) *GFMDocument

ScanGFM reads a rendered Markdown document with a line model of the GFM block rules and reports where the structure the client renders is not the one the formatter wrote.

The model is deliberately small: fences (a backtick run of three or more at the start of a line, closed by a run at least as long) and blockquote lines are skipped as content; list items, ATX headings, thematic breaks and indented continuations are recognized by how their line opens; a table is a pipe line followed by a delimiter row, and its cells split on every pipe not preceded by a backslash, because GFM splits inside code spans too. It is a test oracle for the shapes this server writes, not a renderer.

Inline content is read in two places only, and each says which rule it serves: a link is a bracketed label whose brackets are escaped or matched, and a bare line is a line whose code spans have been elided, since what a span holds is literal.

func (*GFMDocument) Rules added in v3.1.0

func (d *GFMDocument) Rules() []string

Rules lists the rules the findings hit, each once, in rule order.

type GFMFinding added in v3.1.0

type GFMFinding struct {
	// Rule names the rule: T1 to T4 and T3b for tables, B1 to B3 for block
	// separation, H1 for the guidance section.
	Rule string
	// Line is the 1-based line the finding sits on.
	Line int
	// Text is that line as written.
	Text string
	// Detail says what the renderer makes of it.
	Detail string
}

GFMFinding is one place a rendered Markdown document does not have the structure its formatter meant to write, judged by the block rules a GFM renderer applies to the bytes the client receives.

type GFMHintSection added in v3.1.0

type GFMHintSection struct {
	Line    int
	Bullets []string
}

GFMHintSection is one guidance section: its heading line and the bullets directly under it.

type GFMTable added in v3.1.0

type GFMTable struct {
	// Header and Delimiter are 0-based line indexes.
	Header, Delimiter int
	// Columns is the header's cell count.
	Columns int
	// Rows are the 0-based indexes of the body rows.
	Rows []int
}

GFMTable is one table the model recognized: a pipe line followed by a delimiter row, and the body rows that follow.

type KeysetListing added in v3.3.0

type KeysetListing struct {
	Path   string // request path, such as /api/v4/projects
	Param  string // the parameter the endpoint pages by: id_after, cursor, page_token or last
	Value  string // the position the first page's link names
	First  string // JSON array the first page answers with
	Second string // JSON array the second page answers with

	// Arrays names the parameters of the listing GitLab declares as an Array
	// of strings, such as labels, not[labels] or topic. client-go sends one
	// comma-separated under its bare name, GitLab writes it into the next
	// link in Rack's bracket form (labels[]=a&labels[]=b), and Rack refuses
	// with 400 a query that carries both forms of one, before the API runs.
	Arrays []string
	// contains filtered or unexported fields
}

KeysetListing mocks one list GitLab serves by keyset pagination, two pages long. The first page, asked for with no position, answers First with a Link header naming the next page by Param=Value and none of the offset headers, which is how GitLab answers every keyset page that has a successor. The second, asked for with that position, answers Second with no Link, which is how the last page reads. A request for another path is answered 404 and one carrying another position 400, so a handler that loses the continuation fails rather than reading the first page again.

Every request's query is recorded, so a test can read what the handler sent after the fact instead of asserting off the test goroutine.

func (k *KeysetListing) NextLink() string

NextLink returns the link the first page names in its Link header, written the way GitLab writes one: every parameter of the first request as GitLab read it, an KeysetListing.Arrays parameter in its bracket form, with the listing's position beside them. Before any request it carries the position alone.

func (*KeysetListing) Queries added in v3.3.0

func (k *KeysetListing) Queries() []url.Values

Queries returns the query of every request served so far, in order.

func (*KeysetListing) ServeHTTP added in v3.3.0

func (k *KeysetListing) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP serves the listing and records the query of each request.

Separate ifs rather than a tagless switch: a case expression carries no statement counter, so the mutation gate reports every mutant of one as not covered whatever the tests assert.

type KeysetPage added in v3.3.0

type KeysetPage struct {
	Token   string
	HasMore bool
	Rows    int
}

KeysetPage is what one call of a keyset listing reported: the continuation its pagination block published, whether it said more follow, and how many rows it carried.

type KeysetReporter added in v3.3.0

type KeysetReporter interface {
	// Helper marks the caller as a helper, as [testing.T.Helper] does.
	Helper()
	// Errorf reports a failure and lets the test go on, as [testing.T.Errorf]
	// does.
	Errorf(format string, args ...any)
}

KeysetReporter is the part of testing.T FollowKeyset reports through, an interface so its failure branches can be tested with a recorder.

type LegacyClientOptions

type LegacyClientOptions struct {
	// URLElicitation advertises support for URL-mode elicitation in
	// addition to form elicitation.
	URLElicitation bool
}

LegacyClientOptions configures the fake legacy client's advertised capabilities.

type PaginationHeaders

type PaginationHeaders struct {
	Page       string // X-Page — current page number.
	PerPage    string // X-Per-Page — items per page in this response.
	Total      string // X-Total — total items across all pages.
	TotalPages string // X-Total-Pages — total page count.
	NextPage   string // X-Next-Page — next page number, if any.
	PrevPage   string // X-Prev-Page — previous page number, if any.
}

PaginationHeaders is the set of GitLab pagination response headers returned with list endpoints. Empty fields are omitted from the response so handlers can populate only the headers a given scenario exercises.

type QueryRecorder added in v3.3.0

type QueryRecorder struct {
	// contains filtered or unexported fields
}

QueryRecorder is a stand-in GitLab that answers every request with one body and keeps the query of the last request it received, for a test that holds a list filter to the query parameter it becomes.

func RecordQueries added in v3.3.0

func RecordQueries(tb testing.TB, answer string) (*gitlabclient.Client, *QueryRecorder)

RecordQueries returns a client on a QueryRecorder that answers every request with answer and 200. The client is built by NewTestClient, so the request is still recorded into the inventory.

func (*QueryRecorder) AssertUserIDFilter added in v3.3.0

func (r *QueryRecorder) AssertUserIDFilter(t filterReporter, name string, c UserIDFilterCase, err error)

AssertUserIDFilter reports a call whose outcome for the named filter differs from the case: a refusal that did not happen, came after a request, or does not name None, or a query that does not carry the value the case wants.

func (*QueryRecorder) Param added in v3.3.0

func (r *QueryRecorder) Param(name string) (string, bool)

Param returns the named query parameter of the last request and whether it carried it at all.

func (*QueryRecorder) Requests added in v3.3.0

func (r *QueryRecorder) Requests() int

Requests returns how many requests reached the recorder.

type UserIDFilterCase added in v3.3.0

type UserIDFilterCase struct {
	// Name names the case as a subtest.
	Name string
	// Value is what the caller sends, nil for a filter left out.
	Value any
	// Want is the query value GitLab must receive.
	Want string
	// Sent reports whether the query carries the parameter at all.
	Sent bool
	// Refused reports whether the handler refuses the value before any
	// request, naming None.
	Refused bool
}

UserIDFilterCase is one value of a filter GitLab reads as a user ID or one of None and Any, such as assignee_id and reviewer_id, with what the query must carry for it.

func UserIDFilterCases added in v3.3.0

func UserIDFilterCases() []UserIDFilterCase

UserIDFilterCases returns the values every user ID filter is held to: a numeric ID, None, any in lower case, 0, which is refused with a pointer to None because no user has it, and the filter left out.

func (UserIDFilterCase) Params added in v3.3.0

func (c UserIDFilterCase) Params(base map[string]any, name string) map[string]any

Params returns base with the case's value set under name, or base alone for a filter left out. base is copied, never changed.

type WriteRecorder added in v3.3.0

type WriteRecorder struct {
	// contains filtered or unexported fields
}

WriteRecorder is a stand-in GitLab that answers every read with one body and every write with another, and keeps the JSON body of the last write it received. It is the fixture for the question issue 1306 kept asking of an update handler: whether the value a caller set to empty a field, "" or [], reached GitLab at all.

func RecordWrites added in v3.3.0

func RecordWrites(tb testing.TB, read, write string) (*gitlabclient.Client, *WriteRecorder)

RecordWrites returns a client on a WriteRecorder that answers a GET or HEAD with read and any other method with write, both 200. The client is built by NewTestClient, so a GraphQL document is still validated and the request is still recorded into the inventory.

func (*WriteRecorder) AssertMembers added in v3.3.0

func (r *WriteRecorder) AssertMembers(t memberReporter, want map[string]string)

AssertMembers reports every member of want whose encoding in the last write's body differs from the one given, a member the body did not carry included.

func (*WriteRecorder) Member added in v3.3.0

func (r *WriteRecorder) Member(name string) (string, bool)

Member returns the named member of the last write's body as it was encoded, and whether the body carried it at all. A member sent as null reads "null", which is how a field only null clears is told from one left out.

Directories

Path Synopsis
Package e2ecalls declares the record the end-to-end suite writes down while it runs, and the coverage audit reads back afterwards: what a test asked the server to do, what the server dispatched, and on which runtime, surface and mode.
Package e2ecalls declares the record the end-to-end suite writes down while it runs, and the coverage audit reads back afterwards: what a test asked the server to do, what the server dispatched, and on which runtime, surface and mode.
Package e2erelease decides what an end-to-end run does on the GitLab release it found, measured against the release the fine-grained action table was recorded from (issue 1155).
Package e2erelease decides what an end-to-end run does on the GitLab release it found, measured against the release the fine-grained action table was recorded from (issue 1155).
Package hints collects the canonical action IDs a package's Markdown invites a model to call, by rendering the package's own formatters rather than by reading its constants: a hint spelled as a literal at the call site is published exactly like one behind a constant, and a test that reads only the constants cannot see it.
Package hints collects the canonical action IDs a package's Markdown invites a model to call, by rendering the package's own formatters rather than by reading its constants: a hint spelled as a literal at the call site is published exactly like one behind a constant, and a test that reads only the constants cannot see it.
Package modelcorpus is the model evaluation corpus: for each case, the stimulus a model is given and the key that stimulus is scored against, held apart so that no code which produces a stimulus can read a key.
Package modelcorpus is the model evaluation corpus: for each case, the stimulus a model is given and the key that stimulus is scored against, held apart so that no code which produces a stimulus can read a key.
Package modelrecord declares what one model evaluation run writes down: the stimulus a model was given, every request that went to a provider, every tool call the model made, what the server dispatched for it, and what GitLab answered.
Package modelrecord declares what one model evaluation run writes down: the stimulus a model was given, every request that went to a provider, every tool call the model made, what the server dispatched for it, and what GitLab answered.
Package modelscore turns one attempt's record into the verdict a published row is made of.
Package modelscore turns one attempt's record into the verdict a published row is made of.
Package serialtypecheck makes golang.org/x/tools/go/packages type-check one package at a time in a race build, and does nothing in any other build.
Package serialtypecheck makes golang.org/x/tools/go/packages type-check one package at a time in a race build, and does nothing in any other build.
Package shardio is the shard mechanism the records written by a test process and read back by a command are built on: one shard file per process, one JSON line per record, a directory tree read in one pass, and a line nobody can read reported rather than dropped.
Package shardio is the shard mechanism the records written by a test process and read back by a command are built on: one shard file per process, one JSON line per record, a directory tree read in one pass, and a line nobody can read reported rather than dropped.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL