Skip to content

Test the farmer and plot endpoints, tenant isolation included - #27

Open
padovan wants to merge 1 commit into
agstack:mainfrom
manaiba:tests/farmer-plot-endpoints
Open

Test the farmer and plot endpoints, tenant isolation included#27
padovan wants to merge 1 commit into
agstack:mainfrom
manaiba:tests/farmer-plot-endpoints

Conversation

@padovan

@padovan padovan commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

A farmer record is the most sensitive thing INATrace stores: a named
smallholder, their phone number, their bank account, and the satellite
coordinates of the land they work. Every farmer and plot endpoint on
CompanyController takes an id straight from the URL or the request body, so
the only thing standing between one cooperative and another's farmer register
is the enrolment check in CompanyService.

This adds 20 tests that hold that line from both sides. Each endpoint is asked
once by someone entitled to the data and once by a stranger.

The refusals are checked for more than a status code

Asserting 403 alone would not catch an endpoint that refused in the header and
still wrote the rows into the body, or an export that streamed its file before
the check ran. So the fixture seeds distinctive markers — a bank account
number, a GeoID, a pair of coordinates — and every refused response is searched
for them.

Covered

Endpoint Entitled Stranger
GET /userCustomers/{companyId}/{type} lists own farmers 403
GET /userCustomers/{id} reads own farmer 403
GET /userCustomers/{companyId}/plots reads own plots 403
GET /userCustomers/{companyId}/exportFarmerData downloads the zip 403
GET /userCustomers/{id}/exportGeoData exports GeoJSON 403
POST /userCustomers/{id}/uploadGeoData 403
POST /userCustomers/add/{companyId} 403
PUT /userCustomers/edit 403
DELETE /userCustomers/{id} 403
POST /userCustomers/{id}/plots/add adds a plot 403
POST /userCustomers/{id}/plots/{plotId}/updateGeoID 403

Each refused write is also checked to have written nothing: the farmer's
surname is unchanged after a rejected edit, the record survives a rejected
delete, and the plot count is unchanged after a rejected plot or GeoJSON upload.

Two cases go past enrolment. A listing is asserted to be scoped rather than
merely gated — the other tenant's farmer must not appear in a response the
caller is entitled to. And a plot id is asserted to resolve against the farmer
named in the path rather than globally: the caller owns both farmers, so a
mismatched farmer/plot pair isolates the lookup rather than the permission, and
must answer 400 rather than updating someone else's plot.

Finally, every endpoint is asked with no session at all and must answer 401.

Notes

  • Tests only. Nothing under src/main is touched.
  • The suite follows MultiTenantIsolationTest: @SpringBootTest with MockMvc
    over the real filter chain, a real JWT from TokenService in the access
    cookie, and a MySQL container via @ServiceConnection.
  • It is @Transactional, and the seed ends with a flush and clear so every
    request loads its entities from the database as production does. That matters
    here: UserCustomer.getAssociations() and its neighbours, unlike
    getPlots(), do not initialise themselves when the field is null, so an
    object left in the session from the seed would fail in the mapper for a
    reason no real request ever meets.

🤖 Generated with Claude Code

https://claude.ai/code/session_016mn32g3xvTVhT79ZVEbfUe

A farmer record is the most sensitive thing INATrace stores: a named
smallholder, their phone number, their bank account and the coordinates
of the land they work. Every farmer and plot endpoint on CompanyController
takes an id straight from the URL or the request body, so the only thing
between one cooperative and another's farmer register is the enrolment
check in CompanyService.

Twenty tests hold that line from both sides. Each endpoint is asked once
by someone entitled to the data and once by a stranger, and the stranger's
response is checked not only for a 403 but for the absence of the data
itself -- the seeded bank account, GeoID and coordinates are distinctive
strings, so an endpoint that refused in the header while writing rows into
the body, or streamed an export before the check ran, would still fail.

Covered: the farmer list and its scoping, a farmer by id, the company
plots list, plot creation, the GeoID refresh, geo-data export and upload,
the farmer data zip, and the add, edit and delete writes. Two cases go
past enrolment: a plot id is asserted to resolve against the farmer in
the path rather than globally, and every endpoint is asked anonymously.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016mn32g3xvTVhT79ZVEbfUe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant