contracttest

package
v0.47.0 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package contracttest provides a reusable test harness for verifying that platform backends satisfy the behavioral contracts defined by the provider.Provider interface.

Each backend's test suite imports this package and calls Run with a backend-specific Harness. This ensures that list/pagination, error classification, retry behavior, webhook validation, and context cancellation are consistent across every platform the SDK supports.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Run

func Run(t *testing.T, h Harness)

Run executes the full contract suite against h. Each subtest is independent.

func RunIssuesSuite added in v0.39.0

func RunIssuesSuite(t *testing.T, h IssuesHarness)

RunIssuesSuite executes the issue-management contract suite. The mock routes by method and path shape so platform-specific paths don't matter: GET ending /issues/{alphanumeric} → GetResponse (issue numbers are not always numeric — Gitee's are alphanumeric); GET containing comments|notes → CommentsResponse; GET containing labels (not issues) → LabelsResponse; other GET → ListResponse; POST → 201 + MutateResponse (a POST to a labels path — adding labels to an issue — returns LabelsResponse, since GitHub-shaped APIs answer with the label array); PATCH/PUT → MutateResponse; DELETE → 204. Requests are recorded for wire assertions.

func RunLabelsSuite added in v0.38.0

func RunLabelsSuite(t *testing.T, h LabelsHarness)

RunLabelsSuite executes the label-management contract suite. The mock server routes by HTTP method so platform-specific paths don't matter: GET returns ListResponse, POST returns 201 + MutateResponse, PATCH/PUT return 200 + MutateResponse, DELETE returns 204. The mock also records every request (method, path+query, decoded body) so the subtests can assert the wire behavior behind each operation: pagination parameters on list, the verb and body on create/update/delete, and that update does not clobber fields the caller left nil.

func RunMilestonesSuite added in v0.40.0

func RunMilestonesSuite(t *testing.T, h MilestonesHarness)

RunMilestonesSuite executes the milestone-management contract suite. The mock server is dedicated to milestone paths (route by "milestones" in the path so platform-specific prefixes don't matter) and routes by HTTP method: GET → ListResponse (array); POST → 201 + MutateResponse; PATCH/PUT → MutateResponse (GitLab updates with PUT, the rest with PATCH); DELETE → 204. Requests are recorded for wire assertions.

func RunReleaseSuite added in v0.40.0

func RunReleaseSuite(t *testing.T, h ReleaseHarness)

RunReleaseSuite executes the release-management contract suite. The mock routes by method and path shape so platform-specific paths don't matter: GET ending at a releases collection (…/releases) returns a one-element array wrapping ByTagResponse (list-shaped); every other GET returns the ByTagResponse object — this covers both the native by-tag fetches (…/releases/tags/{tag} on GitHub/Gitea/Forgejo/Gitee/GitCode, …/releases/{tag} on GitLab/TencentCode) and the tag→id resolution fetches ID-addressed platforms make before update/delete, which go through the same single-release endpoints; PATCH/PUT return UpdateResponse; DELETE returns 200 + ByTagResponse — GitLab's delete responds with the deleted release object and its SDK decodes it, while the body-agnostic platforms ignore it (the shared transport skips decoding for nil results). Requests are recorded for wire assertions.

func RunReviewsSuite added in v0.40.0

func RunReviewsSuite(t *testing.T, h ReviewsHarness)

RunReviewsSuite executes the review-management contract suite. The mock routes by method and path shape so platform-specific paths don't matter: GET ending /reviews/{digits} or /notes/{digits} → GetResponse; other GET → ListResponse; POST → 201 + MutateResponse; PUT/PATCH → MutateResponse (GitHub's dismiss is a PUT to a .../dismissals sub-resource); DELETE → 204. Requests are recorded for wire assertions.

func RunSearchSuite added in v0.40.0

func RunSearchSuite(t *testing.T, h SearchHarness)

RunSearchSuite executes the search contract suite. The mock server is dedicated to search requests and routes pragmatically, because the platforms disagree on both paths and encoding:

  • GitLab sends every scope to the same path (/search) with a scope query param, so a scope param wins when present: projects -> repos, issues -> issues, users -> users.
  • Otherwise the path is matched: "repositories" (GitHub/Gitee/GitCode /search/repositories) or "projects" -> repos; "issues" (including Gitea/Forgejo's /repos/issues/search) -> issues; "users" -> users.
  • Anything else falls back to the repos fixture, which covers Gitea/Forgejo's bare /repos/search path.

Subtests: each search parses its fixture (repos by full_name, issues by title + string number, users by login) and the query keyword reaches the wire under some query parameter; the repo-scoped subtest additionally verifies SearchIssuesOptions.Repo takes a wire route that reflects the repo (path-embedded or query-carried, per platform).

func VersionProxy

func VersionProxy(baseURL, versionBody string) *httptest.Server

VersionProxy returns a test server that responds to /api/v1/version with versionBody and reverse-proxies every other path to baseURL. Gitea/Forgejo SDKs require the version endpoint at client init; this wrapper lets the contract suite target those backends with a plain mock server.

Types

type CommitStatusHarnessConfig added in v0.45.0

type CommitStatusHarnessConfig struct{}

CommitStatusHarnessConfig mounts the commit-status suite. The suite is self-driving: it records requests, invokes CreateCommitStatus, and asserts exactly one status-reporting request reached the wire.

type Harness

type Harness struct {
	// Name is the human-readable platform identifier (e.g. "GitHub").
	Name string
	// Platform is the provider.Platform constant for this backend.
	Platform provider.Platform
	// NewProvider builds a provider.Provider from the given config. The harness
	// fills in BaseURL (and, for the retry subtest, RetryConfig) before calling,
	// so the function should forward any supplied RetryConfig/Hooks rather than
	// discarding them.
	NewProvider func(t *testing.T, cfg provider.Config) provider.Provider
	// EmptyListResponse is the JSON body the mock returns for empty lists.
	EmptyListResponse string
	// NonEmptyListResponse is the JSON body the mock returns for a non-empty
	// list, with at least one item that maps to a valid repo.
	NonEmptyListResponse string
	// Labels, when non-nil, auto-mounts the label-management suite inside
	// Run. Run enforces both directions: a platform declaring
	// Capabilities().Labels must provide this config, and a config must not
	// be provided by a platform that does not declare the capability.
	Labels *LabelsHarnessConfig
	// Issues, when non-nil, auto-mounts the issue-management suite inside
	// Run, with the same bidirectional drift checks as Labels.
	Issues *IssuesHarnessConfig
	// Reviews, when non-nil, auto-mounts the review-management suite inside
	// Run, with the same bidirectional drift checks as Labels and Issues.
	Reviews *ReviewsHarnessConfig
	// Milestones, when non-nil, auto-mounts the milestone-management suite
	// inside Run, with the same bidirectional drift checks as Labels,
	// Issues, and Reviews.
	Milestones *MilestonesHarnessConfig
	// Search, when non-nil, auto-mounts the search suite inside Run, with
	// the same bidirectional drift checks as Labels, Issues, Reviews, and
	// Milestones.
	Search *SearchHarnessConfig
	// CommitStatus, when non-nil, auto-mounts the commit-status suite inside
	// Run, with the same bidirectional drift checks as Labels, Issues,
	// Reviews, Milestones, and Search.
	CommitStatus *CommitStatusHarnessConfig
	// Releases auto-mounts the release-management suite inside Run. Unlike
	// the fields above, ReleaseManager is a core interface composed into
	// provider.Provider (every backend implements it), so there is no
	// capability-declaration drift to check — the config is mandatory and
	// the suite fails the run when it is missing.
	Releases *ReleasesHarnessConfig
}

Harness bundles the inputs needed to run the contract suite against a backend.

type IssuesHarness added in v0.39.0

type IssuesHarness struct {
	Name        string
	Platform    provider.Platform
	NewProvider func(t *testing.T, cfg provider.Config) provider.Provider
	IssuesHarnessConfig
}

IssuesHarness is the full harness RunIssuesSuite consumes; auto-mounting builds it from the enclosing Harness plus IssuesHarnessConfig.

type IssuesHarnessConfig added in v0.39.0

type IssuesHarnessConfig struct {
	// ListResponse is the JSON array for issue-list GETs. First item: number
	// 1, title "bug", state "open", milestone {number:1, title:"v1"}.
	ListResponse string
	// GetResponse is the JSON object for single-issue GETs (number 1, title
	// "bug", state "open", milestone {number:1, title:"v1"}).
	GetResponse string
	// MutateResponse is the JSON object for POST/PATCH/PUT (same shape as
	// GetResponse).
	MutateResponse string
	// CommentsResponse is the JSON array for issue-comment GETs. First item:
	// id 1, body "a comment".
	CommentsResponse string
	// LabelsResponse is the JSON array for repository-label GETs. First
	// item: id 1, name "bug", color "#4cc917".
	LabelsResponse string
	// CreateIssueAssigneesByID declares that the backend resolves issue
	// assignee usernames through a users lookup (GitLab's /users?username=
	// exact-match filter, Tencent 工蜂's /users/{username} endpoint) and
	// writes numeric assignee IDs. When set, the suite asserts the
	// lookup-then-write wire shape on CreateIssue; when unset, the create
	// subtest exercises the plain title-only wire (the default).
	CreateIssueAssigneesByID bool
}

IssuesHarnessConfig carries the fixtures a backend's main Harness needs to auto-mount the issue-management suite via Harness.Issues.

type LabelsHarness added in v0.38.0

type LabelsHarness struct {
	// Name is the human-readable platform identifier (e.g. "GitHub").
	Name string
	// Platform is the provider.Platform constant for this backend.
	Platform provider.Platform
	// NewProvider builds a provider.Provider; the harness fills in BaseURL.
	// It must construct the provider the same way the platform's main
	// harness does (including any VersionProxy wrapping for Gitea/Forgejo).
	NewProvider func(t *testing.T, cfg provider.Config) provider.Provider
	// ListResponse is the JSON array the mock returns for GET requests
	// (label listings and name→ID resolution lookups). Its first item must
	// have name "bug" and color "#4cc917" so the suite can assert color
	// normalization.
	ListResponse string
	// MutateResponse is the JSON object the mock returns for POST/PATCH
	// requests. It must have name "bug" and color "#4cc917".
	MutateResponse string
	// IgnoresListPagination declares that the backend's list endpoint does
	// not accept pagination parameters, so the suite skips its wire-level
	// page/per-page assertion. Currently only GitCode sets this: its SDK's
	// ListIssueLabels exposes no page/page-size parameters, so ListLabels
	// silently drops ListLabelsOptions on the wire. That is a known contract
	// gap, not something the other five backends share.
	IgnoresListPagination bool
}

LabelsHarness bundles the inputs needed to run the label-management contract suite against a backend that implements provider.LabelManager.

type LabelsHarnessConfig added in v0.38.1

type LabelsHarnessConfig struct {
	// ListResponse is the JSON array the mock returns for GET requests. Its
	// first item must have name "bug" and color "#4cc917".
	ListResponse string
	// MutateResponse is the JSON object the mock returns for POST/PATCH/PUT
	// requests. It must have name "bug" and color "#4cc917".
	MutateResponse string
	// IgnoresListPagination opts the backend out of the wire-level
	// pagination assertion (see LabelsHarness.IgnoresListPagination).
	IgnoresListPagination bool
}

LabelsHarnessConfig carries the fixtures a backend's main Harness needs to auto-mount the label-management suite via Harness.Labels. Name, Platform, and NewProvider are reused from the enclosing Harness.

type MilestonesHarness added in v0.40.0

type MilestonesHarness struct {
	Name        string
	Platform    provider.Platform
	NewProvider func(t *testing.T, cfg provider.Config) provider.Provider
	MilestonesHarnessConfig
}

MilestonesHarness is the full harness RunMilestonesSuite consumes; auto-mounting builds it from the enclosing Harness plus MilestonesHarnessConfig.

type MilestonesHarnessConfig added in v0.40.0

type MilestonesHarnessConfig struct {
	// ListResponse is the JSON array for milestone-list GETs. First item:
	// the platform's addressing identifier ("number" on GitHub and Gitee,
	// "id" on GitLab, Gitea, Forgejo, GitCode, and Tencent Code) rendering
	// as the string "1", title "v1", state "open" (GitLab's wire state
	// "active" is also accepted — the backend normalizes it).
	ListResponse string
	// MutateResponse is the JSON object for POST/PATCH/PUT (same shape as
	// the first list item).
	MutateResponse string
}

MilestonesHarnessConfig carries the fixtures a backend's main Harness needs to auto-mount the milestone-management suite via Harness.Milestones.

type ReleaseHarness added in v0.40.0

type ReleaseHarness struct {
	Name        string
	Platform    provider.Platform
	NewProvider func(t *testing.T, cfg provider.Config) provider.Provider
	ReleasesHarnessConfig
}

ReleaseHarness is the full harness RunReleaseSuite consumes; auto-mounting builds it from the enclosing Harness plus ReleasesHarnessConfig.

type ReleasesHarnessConfig added in v0.40.0

type ReleasesHarnessConfig struct {
	// ByTagResponse is the JSON object for single-release fetches. Required
	// shape: id 1, tag_name "v1.0.0", name "v1.0.0", body "release notes"
	// (the suite asserts the parsed TagName).
	ByTagResponse string
	// UpdateResponse is the JSON object for PATCH/PUT release updates (same
	// shape as ByTagResponse).
	UpdateResponse string
	// UpdateSendsDescriptionOnly declares that the platform's update
	// endpoint cannot carry a release name (TencentCode's update surface
	// only accepts a description): the update wire subtest then asserts the
	// "description" key instead of "name".
	UpdateSendsDescriptionOnly bool
}

ReleasesHarnessConfig carries the fixtures a backend's main Harness needs to auto-mount the release-management suite via Harness.Releases.

Unlike Labels/Issues/Reviews, ReleaseManager is a core interface composed into provider.Provider and implemented by every backend, so there is no capability-declaration drift to enforce: the config is mandatory for all seven platforms and testReleaseSuite fails the run when it is missing.

type ReviewsHarness added in v0.40.0

type ReviewsHarness struct {
	Name        string
	Platform    provider.Platform
	NewProvider func(t *testing.T, cfg provider.Config) provider.Provider
	ReviewsHarnessConfig
	// IgnoresRequestReviewers declares that the backend's RequestReviewers is
	// a registered ignore: the platform's reviewer API needs inputs the SDK
	// surface cannot supply (GitLab's reviewer_ids want user IDs, but the SDK
	// addresses reviewers by username and exposes no Users API to resolve
	// them), so the method documents the ignore and performs no request. The
	// wire subtest then only asserts a silent, error-free no-op — anything
	// reaching the network would mean the registration drifted.
	IgnoresRequestReviewers bool
	// IgnoresDismissal declares that the backend's DismissReview is a
	// registered stub: the platform's review surface has no dismissal
	// endpoint at all (Tencent 工蜂 review notes expose no dismiss verb), so
	// the method documents the gap and returns a provider error wrapping
	// provider.ErrNotImplemented without touching the wire. The subtest then
	// asserts exactly that registration — a successful call or any recorded
	// request would mean the stub drifted.
	IgnoresDismissal bool
	// ListStateIsCommented declares that the backend's review reads carry no
	// verdict state: the SDK model behind ListReviews/GetReview has no state
	// field (Tencent 工蜂 review notes — the verdict travels only on the
	// create/update writes as reviewer_state and never comes back), so every
	// read review normalizes to provider.ReviewStateCommented, a registered
	// limitation. The List subtest then asserts that commented normalization
	// instead of the default approved one.
	ListStateIsCommented bool
}

ReviewsHarness is the full harness RunReviewsSuite consumes; auto-mounting builds it from the enclosing Harness plus ReviewsHarnessConfig.

type ReviewsHarnessConfig added in v0.40.0

type ReviewsHarnessConfig struct {
	// ListResponse is the JSON array for review-list GETs. First item: id 1,
	// user login "dev", state "APPROVED" (the suite asserts normalization to
	// provider.ReviewStateApproved).
	ListResponse string
	// GetResponse is the JSON object for single-review GETs (id 1, user
	// "dev", state "APPROVED").
	GetResponse string
	// MutateResponse is the JSON object for POST/PUT/PATCH (same shape as
	// GetResponse).
	MutateResponse string
	// CreateEvent is the wire value the platform's create must carry under
	// the "event" key for the suite's APPROVE-verdict create. The suite
	// calls CreateReview with Event "APPROVE": GitHub and GitCode forward
	// the option verbatim ("APPROVE"), Gitea/Forgejo translate it to their
	// SDK's review state ("APPROVED"). Empty means the platform's create
	// carries no verdict on the wire (GitLab's note-based create) and the
	// event-key assertion is skipped — the body assertion still runs.
	CreateEvent string
	// IgnoresRequestReviewers opts the backend out of the RequestReviewers
	// wire assertion (see ReviewsHarness.IgnoresRequestReviewers).
	IgnoresRequestReviewers bool
	// RequestReviewersByID declares that the backend resolves reviewer
	// usernames to numeric user IDs through a /users lookup before writing
	// them (GitLab resolves usernames to IDs first): the wire subtest then
	// asserts the lookup GET and an ID-carrying update body instead of the
	// username-carrying create (see assertRequestReviewersByIDWire).
	RequestReviewersByID bool
	// IgnoresDismissal opts the backend out of the DismissReview verb
	// assertion (see ReviewsHarness.IgnoresDismissal). It is for platforms
	// whose ONLY dismissal gap is a registered stub: the platform exposes no
	// dismiss surface at all, so DismissReview returns a wrapped
	// provider.ErrNotImplemented and the subtest asserts that registration
	// instead of a state-changing verb.
	IgnoresDismissal bool
	// ListStateIsCommented declares that the platform's review reads cannot
	// carry a verdict state (see ReviewsHarness.ListStateIsCommented).
	ListStateIsCommented bool
}

ReviewsHarnessConfig carries the fixtures a backend's main Harness needs to auto-mount the review-management suite via Harness.Reviews.

type SearchHarness added in v0.40.0

type SearchHarness struct {
	Name        string
	Platform    provider.Platform
	NewProvider func(t *testing.T, cfg provider.Config) provider.Provider
	SearchHarnessConfig
}

SearchHarness is the full harness RunSearchSuite consumes; auto-mounting builds it from the enclosing Harness plus SearchHarnessConfig.

type SearchHarnessConfig added in v0.40.0

type SearchHarnessConfig struct {
	// ReposResponse is the JSON body for repository-search GETs, in the
	// platform's wire shape (GitHub wraps items in {"total_count":..,
	// "items":[..]}, Gitea/Forgejo in {"data":[..]}, the rest are bare
	// arrays).
	ReposResponse string
	// ReposTotalCount is the server-side total the repos fixture reports
	// (GitHub's envelope carries total_count; the suite then asserts
	// SearchRepos returns exactly this total). Zero means the platform
	// reports no server-side total — the backend returns nil for total and
	// the suite keeps the weaker total == nil || *total >= len(results)
	// assertion.
	ReposTotalCount int
	// IssuesResponse is the JSON body for issue-search GETs, same per-
	// platform wrapping rules as ReposResponse.
	IssuesResponse string
	// UsersResponse is the JSON body for user-search GETs, same per-
	// platform wrapping rules as ReposResponse.
	UsersResponse string
}

SearchHarnessConfig carries the fixtures a backend's main Harness needs to auto-mount the search suite via Harness.Search. Every fixture's first item must render as: repos — full_name "owner/repo"; issues — number "1" (the platform's wire encoding of the identifier, parsed to the string "1"), title "found"; users — login "dev".

Jump to

Keyboard shortcuts

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