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 ¶
- func Run(t *testing.T, h Harness)
- func RunIssuesSuite(t *testing.T, h IssuesHarness)
- func RunLabelsSuite(t *testing.T, h LabelsHarness)
- func RunMilestonesSuite(t *testing.T, h MilestonesHarness)
- func RunReleaseSuite(t *testing.T, h ReleaseHarness)
- func RunReviewsSuite(t *testing.T, h ReviewsHarness)
- func RunSearchSuite(t *testing.T, h SearchHarness)
- func VersionProxy(baseURL, versionBody string) *httptest.Server
- type Harness
- type IssuesHarness
- type IssuesHarnessConfig
- type LabelsHarness
- type LabelsHarnessConfig
- type MilestonesHarness
- type MilestonesHarnessConfig
- type ReleaseHarness
- type ReleasesHarnessConfig
- type ReviewsHarness
- type ReviewsHarnessConfig
- type SearchHarness
- type SearchHarnessConfig
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
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/{digits} → GetResponse; 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} → 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 ¶
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 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
// 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
}
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, and GitCode) 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
}
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
// IgnoresRequestReviewers opts the backend out of the RequestReviewers
// wire assertion (see ReviewsHarness.IgnoresRequestReviewers).
IgnoresRequestReviewers 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
// 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".