Skip to content

Verify the open API surface with live contract tests that need no credentials #13

Description

Every test in the module runs against a mocked transport, because no Lovdata API key is available to the repository yet. That keeps CI green and fast, but it also means nothing in the pipeline proves the module can actually talk to Lovdata. The shape of several responses is documented as free-form in the OpenAPI document, so the field names the module maps are informed guesses until a real response confirms them. A user is the first one to find out if a guess was wrong.

Request

User impact

A change that breaks the module against the live API — a renamed field, a changed status code, a different error shape — currently ships green. Contract coverage against the real service turns that into a failed build instead of a bug report.

Acceptance criteria

  • A suite of integration tests exercises the module against the live Lovdata API
  • The suite runs in CI when an API key is available as a repository secret
  • The suite is skipped, not failed, when no key is available, so forks and contributors without a key still get a green pipeline
  • The tests assert the response contract the module depends on, not just that a call succeeded
  • The key is never written to logs, test output, or artifacts
  • Running the unit suite locally still needs no key and no network

References

  • Keys are issued by Lovdata to users holding the api role; contact api@lovdata.no
  • PowerShell module standard covers shared test infrastructure and the tests/BeforeAll.ps1 and tests/AfterAll.ps1 phases

Technical decisions

Separation: Integration tests live in their own discovered test entry under tests/, so a failure names the live suite rather than being mixed into unit results. The unit suites keep mocking the transport and stay the default local experience.

Gating: Skip on the absence of the key rather than failing. A -Skip condition on the container is preferred over an early return, so skipped tests are reported as skipped and the gap is visible in the run summary.

Secret plumbing: The key reaches the workflow through the TestData secret input on the Process-PSModule workflow, the same mechanism PSModule/GitHub uses for its test credentials.

Scope: Read-only endpoints only. Nothing in the live suite may create, change, or delete anything in Lovdata.

Contract assertions: Assert the fields the module maps — that a legal source carries an identifier and a description, that the error body for a rejected key carries a message — rather than asserting on data values that Lovdata may legitimately change.

Unauthenticated coverage: /ping and /version need no key, so the live checks that use them can run unconditionally once #8 lands. That gives the pipeline some real-network coverage even with no secret configured.

Depends on: the load-bearing core in #2, and an API key being provisioned as a repository secret. The pull request targets build-lovdata-module.


Implementation plan

Core changes

  • Provision the Lovdata API key as a repository secret
  • Pass the secret through the workflow's TestData input in .github/workflows/Process-PSModule.yml
  • Add an integration test entry under tests/ that skips when the key is absent
  • Assert the response contract for the legal source list, including the field names the module maps

Verification

  • Confirm the suite is reported as skipped, not failed, when no key is configured
  • Confirm the key does not appear in any log or artifact

Activity

  1. MariusStorhaug commented on Aug 2, 2026

    @MariusStorhaug
    MemberAuthor

    Partly unblocked. The endpoints in the first release need no credential, so a live suite covering /ping, /version, and /v1/publicData/list can run in CI unconditionally, with no secret to provision and nothing to skip. That is worth doing on its own, and it gives the repository a real-network contract check from the first release.

    The gated part — a key in a repository secret, skipped when absent — applies only to the authenticated surface and belongs with #15. Worth splitting this issue along that line when it is picked up.

  2. MariusStorhaug commented on Aug 2, 2026

    @MariusStorhaug
    MemberAuthor

    Concrete evidence for this, surfaced during PR #16 (the key-free 0.1.0 surface).

    A LastModified timezone fix passed the unit suite the whole time yet was worse against the live API. Cause: ConvertFrom-Json in PowerShell 7 already deserializes an ISO-8601 Z timestamp into a [datetime] with Kind=Utc, so by the time the command sees the field it is a DateTime, not a string. The mock returned a raw wire string the transport never actually produces, so the test encoded an assumption about the boundary instead of the boundary's real behaviour. Only a live call caught it (fixed in 0f4d628 on build-lovdata-module).

    Generalised lesson worth baking into the live suite's rationale: when a mock stands in for a deserialization boundary, it must return what the deserializer actually returns, not what the wire format looks like.

    Two things that lower the bar for landing at least part of this now:

    • The entire 0.1.0 surface is key-free. Test-LovdataConnection (/ping), Get-LovdataApiVersion (/version), and Get-/Save-LovdataPublicDataset (/v1/publicData/*) all work with no credential, so a live contract suite over them can run in CI unconditionally, with no secret, not just as the skipped-when-absent path described above. The key-gated portion can stay deferred to the authenticated surface (Add the authenticated Lovdata API surface in a later release #15).
    • End-to-end already verified by hand against the live service during the 🚀 [Feature]: Norwegian legal data, no account needed #16 review: /ping -> true; /version -> lovdata-api 2026-07-31-1613; all four packages listed with Kind=Utc; a real download of lovtidend-avd1-2026.tar.bz2 produced 1,315,035 bytes matching the reported SizeBytes; overwrite guard refused the second attempt. That is the contract a key-free live suite would lock in.
  3. MariusStorhaug commented on Aug 2, 2026

    @MariusStorhaug
    MemberAuthor

    Why this matters, with evidence rather than principle

    The case for this issue was originally "the module has no live coverage, which is a gap". Building 0.1.0 produced something stronger: three defects shipped past a green test suite, and all three failed the same way. In each case a mock encoded what the boundary was expected to produce rather than what it actually produces, so the test passed while the code was wrong.

    1. LastModified lost its UTC offset. ConvertFrom-Json in PowerShell 7 already deserializes an ISO-8601 Z timestamp into a [datetime] with Kind=Utc. The command cast it to a string first, which rendered it through the current culture and dropped the offset, so the reparse produced Kind=Unspecified. The same package then reported a different day depending on the runner's timezone.

    The mock returned a raw JSON string — a wire format the transport never emits, because deserialization happens before the command sees the value. The test asserted Should -BeOfType [datetime], which passed either way. Only a live call exposed it, and the value was worse after the "fix" than before it.

    2. Change references carried the wrong law's date. The parser computed one date for the whole lastChangedBy field and stamped it on every reference in it. A law amended by two others recorded the first amendment's date against the second, and dropped the per-reference fra <date> entirely. That is wrong date information about legal amendments, in published data, in a corpus consulted precisely because Lovdata is authoritative.

    The fixture had a single-reference field. The corpus has 33 multi-reference ones in the acts alone. The defect was invisible to the test because with one reference the field-wide date coincides with the correct answer.

    3. The same defect silently affected a second field. changesToDocuments shares that parser. It never showed the symptom, because those fields are bare RefID lists with no free-text date for the old code to latch onto — so the computed field-wide date usually happened to be right. Same latent bug, masked by the data shape, and it would have surfaced the first time Lovdata changed that field's format.

    The generalisable rule

    A mock standing in for a deserialization boundary must return what the deserializer returns, not what the wire format looks like. Every one of the three above is an instance of violating that.

    Unit tests cannot enforce it, because the mock is the assumption. Only something that touches the real boundary can. That is what this issue provides.

    And it costs nothing to run

    Every endpoint in 0.1.0 is key-free: /ping, /version, /v1/publicData/list, /v1/publicData/get/{filename}. A live contract suite over those needs no secret, no repository configuration, and nothing to skip — it can run unconditionally in CI, on forks, and on contributor pull requests. The gated portion applies only to the authenticated surface in #15 and should be split along that line.

    What such a suite should assert

    Contract, not data values, so it does not break when Lovdata publishes an ordinary update:

    • /v1/publicData/list returns entries carrying filename, description, sizeBytes and lastModified, and lastModified deserializes to a UTC [datetime] — the exact property defect 1 violated.
    • sizeBytes arrives as a string and converts to a whole number.
    • A downloaded package's byte length equals the sizeBytes the listing reported. This was verified by hand during 0.1.0 — lovtidend-avd1-2026.tar.bz2 returned exactly 1 315 035 bytes — and it is the single strongest end-to-end assertion available without a key.
    • /ping answers, and /version returns name, timestamp and revision.
    • Rate-limit headers are present on an unauthenticated response, since the module surfaces them.

    Verified during 0.1.0 by hand; the point of this issue is that none of it is verified by anything that runs on its own.

  4. changed the title [-]Add live API integration tests gated behind a repository secret[/-] [+]Verify the open API surface with live contract tests that need no credentials[/+] on Aug 2, 2026
  5. MariusStorhaug commented on Aug 2, 2026

    @MariusStorhaug
    MemberAuthor

    Retitled from "Add live API integration tests gated behind a repository secret".

    The original title contradicted what this issue turned into. It described a suite blocked on provisioning a secret, which would invite deferring the work until someone arranges credentials. That blocker does not exist: every endpoint in 0.1.0 is key-free, so the suite that would have caught all three defects recorded above runs unconditionally — no secret, no repository configuration, nothing to skip, and green on forks and contributor pull requests.

    Scope after the retitle:

    • This issue covers live contract tests over the open surface: /ping, /version, /v1/publicData/list, /v1/publicData/get/{filename}. No credentials, so nothing gates it.
    • The credential-gated portion moves to Add the authenticated Lovdata API surface in a later release #15, alongside the authenticated surface it would test. There is nothing to gate until that surface exists.

    The title is what gets scanned in a backlog, and the old one implied a dependency that was never real.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    NoReleaseDocumentation, maintenance or CI/CD - no version bumpfeatureNew feature

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions