fix(ci): run the test suite, and repair the tests it was hiding - #1063
Open
rdahis wants to merge 2 commits into
Open
fix(ci): run the test suite, and repair the tests it was hiding#1063rdahis wants to merge 2 commits into
rdahis wants to merge 2 commits into
Conversation
The CI test job has not executed a single test since February 2024. `poetry install --only=test` installs pytest without Django, so pytest aborts with `ModuleNotFoundError: No module named 'django'`, and because the command pipes into `tee`, the step exits 0 and the job reports success. Every PR since has merged against a green check that measured nothing. CI: - install `--only main,test` so Django and the project's apps are present - add a postgres:14 service with the DB_* env the settings expect - `shell: bash` for pipefail, so a pytest failure is no longer swallowed - `if: always()` on the coverage comment so failures still report With CI fixed, 27 of the 31 existing tests failed or errored against code that had moved on beneath them: - `Dataset.organization` became the `organizations` m2m; the conftest fixture still passed the old kwarg, breaking 24 tests downstream - `Table.raw_data_url` was removed from the model but not the fixtures - `Table.full_coverage` and `Dataset.coverage` were replaced by `full_temporal_coverage` / `temporal_coverage`; assertions rewritten against the current shape - `backend.apps.payment` was renamed `account_payment`; patch targets pointed at a module that no longer exists - the GraphQL endpoint is `/api/v1/graphql`, not `/api/graphql/` - `CreateStripeSubscription` in tests.gql queried a `subscription` field the mutation no longer exposes, which invalidated the whole document and with it the login mutation every payment test depends on - the account fixture used `objects.create`, storing the password unhashed so login could never succeed - activation email assertions predate the `is_prd()` guard added in June 2024 New tests, 100 of them, covering logic that had none: - `custom/environment.py` — the production gate gating activation emails - `custom/utils.py` — snake/kebab case validation - `get_spatial_coverage` — turns its documented hierarchy rules into a contract - `Area.clean` — administrative level and parent hierarchy validation - `CloudTable.clean` and the Table slug properties derived from it - `Account` password handling, where a mistake either stores a password in the clear or locks the user out, silently either way Writing the CloudTable tests surfaced a real bug: the dataset id check was guarded by `gcp_project_id` rather than `gcp_dataset_id`, so a malformed dataset id passed validation whenever the project id was blank. Fixed, with a test that fails without it. Also adds `backend/settings/test.py`, which points haystack at the in-memory backend and drops the search signal processor. Without it every `save()` in the suite tries to reach Elasticsearch and fails. Runtime drops from 37s to 27s. Suite: 4 passing -> 131 passing. Coverage: 44% -> 59%.
Second pass on coverage, aimed at the code where a silent wrong answer costs something: authorization decisions, Stripe entitlement routing, and the dataset aggregates the public catalogue counts are built from. - Dataset aggregates (31 tests). Every one of the ~15 rollup properties excludes under_review/excluded tables and the dictionary tables, longhand, in each copy. That rule is now pinned down, along with open/closed data, the direct-download size thresholds and the first_* pointers. - Model validation (40). Coverage, Update, Poll and QualityCheck each reimplement the same "exactly one foreign key" rule; each copy is checked. Plus DateTimeRange, which assembles datetimes out of seven nullable columns. - Account HTTP views (40). Activation, password reset, and Google OAuth, including the two security boundaries: the `state` check against CSRF and the redirect-origin allowlist that keeps a JWT-bearing redirect on a known domain. - account_auth gateway (32). `/auth/` decides whether a reverse proxy lets a request through, so a wrongly returned 200 is an authorization bypass. All six refusal paths and the token expiry/domain rules are covered. - GraphQL decorators (24). owner_required and subscription_member are the authorization boundary for the API. owner_required identifies its target by regex over the raw request body, which is unusual enough to pin down. - Prefect flow-failure webhook (42). Decides whether a pipeline's schedule is switched off; the reactivated_at guard that stops a fixed flow being re-disabled by its own past failures is covered directly. - Stripe entitlements (39). Chatbot-vs-BD-Pro product detection, trial eligibility, and the admin guard that stops a webhook revoking a staff member's chatbot access. - Table/Column relations (25), api/v1 REST endpoints (20), Account subscription state (19), environment allowlist (5). One more bug found and fixed: Token.save() was declared `def save(self)`, so Token.objects.create() raised TypeError on force_insert and no Token could be created through the normal Django API. Three further findings are documented in tests rather than changed, since each alters behaviour beyond a testing PR: - Token.save() regenerates the token on every save, so editing a Token in the admin (setting an expiry, toggling is_active) silently invalidates the value the client is using. - DatasetRedirectView indexes URL_MAPPING directly, so any host outside the four it knows about is a KeyError, not a redirect. - Area similarity compares names by prefix, so an Area saved with a blank name scores as similar to every other area. Suite: 131 -> 448 passing. Coverage: 59% -> 73%.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
The CI test job has not executed a single test since February 2024, and has
reported success the whole time.
poetry install --only=testinstalls pytest without Django, so pytest abortswith
ModuleNotFoundError: No module named 'django'. Because the command pipesinto
tee, the step exits 0 and the job goes green. Every PR since has mergedagainst a check that measured nothing. The most recent run before this PR
(32311142291)
shows the traceback under a passing job.
This PR fixes CI, repairs the tests that were rotting behind it, and adds
coverage for the code where a wrong answer is silent rather than loud.
Suite: 4 → 448 passing. Coverage: 44% → 73%.
Description
CI (
ci-python.yaml)poetry install --only main,test— Django and the project's apps are neededby the suite;
devis excluded to keep the job leanpostgres:14service with theDB_*env the settings expectshell: bashfor pipefail, so a pytest failure is no longer swallowed byteeif: always()on the coverage comment, so failures still reportRepairing the existing suite
With CI fixed, 27 of the 31 existing tests failed or errored against code that
had moved on beneath them:
Dataset.organizationbecame theorganizationsm2m; the conftest fixture still passed the old kwargTable.raw_data_urlremoved from the model, not the fixturesfull_coverage/Dataset.coveragereplaced byfull_temporal_coverage/temporal_coveragebackend.apps.paymentrenamedaccount_payment; patch targets pointed at a dead module/api/v1/graphql, tests used/api/graphql/CreateStripeSubscriptionintests.gqlqueried a field the mutation no longer exposes, invalidating the whole document and with it the login mutation every payment test depends onobjects.create, storing the password unhashed so login could never succeedis_prd()guardAssertions were rewritten against the current model API, not deleted. Where a
data shape changed (
full_coverage→full_temporal_coverage), the newexpectations were derived by observing actual behaviour and checking it still
matched the original tests' intent.
New tests
Aimed at authorization, entitlement routing, and the aggregates behind the
public catalogue counts:
"exclude
under_review/excludedstatus and the dictionary tables" rulelonghand, with nothing checking the copies agree
reimplement the same "exactly one foreign key" rule; plus
DateTimeRange,which assembles datetimes out of seven nullable columns
including the
statecheck against CSRF and the redirect-origin allowlist(that redirect carries a JWT, so an origin leaking onto the list is a token
disclosure)
account_authgateway (32) —/auth/decides whether a reverse proxylets a request through, so a wrongly-returned 200 is an authorization bypass;
all six refusal paths covered
owner_requiredandsubscription_memberarethe authorization boundary for the API
reactivated_atguardthat stops a fixed flow being re-disabled by its own past failures
eligibility, and the admin guard that stops a webhook revoking a staff
member's chatbot access
subscription state (19), environment allowlist (5)
custom/environment.pyaccount_auth/views.pyaccount/views.pyaccount/models.pycustom/graphql_jwt.pyapi/v1/models.pyadmin_data_tools/views.pybackend/settings/test.pyNew test settings module, wired via
pytest.ini. Points haystack at thein-memory backend and drops the search signal processor — without it every
save()in the suite tries to reach Elasticsearch and fails. Also supplies aJWT algorithm and an in-memory email backend. Runtime drops from 37s to 27s.
Bugs found by writing the tests
Fixed (each with a test that fails without the fix):
CloudTable.cleanguarded the dataset-id check withgcp_project_idinstead of
gcp_dataset_id, so a malformed dataset id passed validationwhenever the project id was blank.
Token.save()was declareddef save(self)— no*args, **kwargs— soToken.objects.create()raisedTypeErroronforce_insert. No Tokencould be created through the normal Django API.
Documented in tests, not changed — each alters runtime behaviour beyond
what a testing PR should decide alone:
Token.save()regenerates the token on every save. Editing a Token inthe admin — setting an expiry, toggling
is_active— silently invalidatesthe value the client is already using. Revoking becomes rotating. This is
the one most worth a second opinion.
DatasetRedirectViewindexesURL_MAPPINGdirectly, so any host outsidethe four it knows about is a
KeyErrorand a 500, not a redirect.scores as similar to every area and inflates the related-tables ranking.
Worth checking separately:
GRAPHQL_JWT["JWT_ALGORITHM"]readsDJANGO_JWT_ALGORITHMfrom the environment and falls back toNone, whichmakes django-graphql-jwt sign with
alg="none". Worth confirming everydeployment sets it.
Checklist
Testing and evidence
Run locally against Python 3.11 at the
poetry.lockversions, from an emptydatabase:
ruff checkpasses; all pre-commit hooks pass.The CI change itself was validated by installing a real Poetry and confirming
poetry install --only main,testresolves (main + test, no dev), and that thepytestconsole script importsbackendwithout the project root onsys.path.Next steps
Per the repo's Git Flow this same branch should also be PR'd into
devandstaging— happy to open those on request.The largest remaining gaps are
account_payment/graphql.py(46%) andwebhooks.py(30%): the Stripe mutation and handler bodies, which need realdjstripefixture scaffolding. Getting past ~80% overall means committing tothat; worth doing as a follow-up rather than growing this PR.
🤖 Generated with Claude Code