Skip to content

Test against a forge

Three tools, for three different jobs. Picking the wrong one is the usual reason forge-related tests are slow, flaky, or prove nothing.

You are testing Use
Your consumer logic: update flows, version comparison, policy the test package's Source
A specific interaction: that you called Push, that you handled an error the published mocks
Your own provider implementation the conformance harness

A real provider, with no network

Source is a complete, in-memory Provider. Because it is a real implementation rather than a mock, code under test exercises the real contract, including the fallback paths that mocks usually paper over:

import forgetest "gitlab.com/phpboyscout/go/forge/test"

src := forgetest.New(
    forgetest.WithRelease("v1.2.3", forgetest.TarGzAsset("mytool", "mytool", "#!/bin/sh\n")),
    forgetest.WithLatestTag("v1.2.3"),
)

updater := myapp.NewUpdater(src)

Options cover the cases worth testing deliberately:

Option Sets up
WithRelease(tag, assets...) A release and its assets
WithLatestTag(tag) Which release is "latest"
WithMissingTag(tag) A tag that must not resolve
WithChecksumManifest(b) Opts into ChecksumProvider
WithSignatureManifest(b) Opts into SignatureProvider
WithDownloadError(err) A download that fails

Asset builders

They produce real bytes (a genuine gzipped tarball, a matching manifest, a real OpenPGP signature) so verification logic is tested against material that actually verifies:

Builder Produces
AssetName(tool) The conventional asset filename for this OS/arch
TarGzAsset(tool, bin, body) A real gzipped tarball containing bin
Manifest(corrupt, assets...) checksums.txt bytes
ChecksumsAsset(corrupt, assets...) A checksums.txt asset
SignatureAsset(entity, manifest, bad) A checksums.txt.sig asset

Test that verification REJECTS a bad artefact

The corrupt and bad flags are the point of these builders, and the half most worth testing. Verifying that a good artefact passes proves little, because a verifier that accepts everything also passes. What matters is that a tampered one is refused:

binary := forgetest.TarGzAsset("mytool", "mytool", "#!/bin/sh\n")

// checksums.txt that NAMES the binary but hashes different bytes
src := forgetest.New(
    forgetest.WithRelease("v1.2.3",
        binary,
        forgetest.ChecksumsAsset(true, binary),
    ),
    forgetest.WithLatestTag("v1.2.3"),
)

_, err := myapp.NewUpdater(src).Update(ctx)
require.Error(t, err, "a mismatched checksum must abort the update")

SignatureAsset(entity, manifest, true) does the same for signatures: it signs different bytes, so the signature is well-formed and verifies against nothing. That distinguishes "rejected because the signature is invalid" from "rejected because the file was missing", which are different failures worth telling apart.

None of the builders take a *testing.T, so the same fixtures work outside a test binary. That is deliberate: it lets a tool build a stub release server for manual or end-to-end use.

Opt-in capabilities are off by default

A bare Source returns ErrNotSupported from both optional methods, so consumers exercise the asset-by-name fallback unless you opt in. That is the path most releases take in production, so it is the right default to test against.

Mocks for interaction tests

When the assertion is that you made a call, rather than what came back, use the published mocks:

import forgemocks "gitlab.com/phpboyscout/go/forge/mocks"

provider := forgemocks.NewMockProvider(t)
provider.EXPECT().
    GetLatestRelease(mock.Anything, "acme", "tool").
    Return(nil, forge.ErrReleaseNotFound).
    Once()

Alias the import. A bare mocks collides the moment a test needs mocks from two modules, and the same goes for test. The module path already says which project these belong to, so the leaf stays a plain noun and the alias supplies the reading context. NewMockProvider(t) registers cleanup, so unmet expectations fail the test on their own.

Twelve mocks are published, covering the contract and the optional capabilities:

MockProvider MockRelease MockReleaseAsset
MockChecksumProvider MockSignatureProvider MockConfig
MockRepositories MockContents MockSites
MockIssues MockIssueFiler MockSnippets

The optional-capability mocks are the useful ones: they drive the opt-in branches without you building a provider that implements them.

Authenticator, KeyManager and Prompter are not mocked. They are small enough to hand-write, and Prompter is a single method your CLI implements anyway.

Prefer the real thing for behaviour

A mock proves you called the API. Only a real provider proves you called it correctly. Reach for mocks when the forge interaction is incidental to what you are testing, and for Source when it is the point.