Testing
The layers, what belongs in each, and how to run them.
The thing being protected is data somebody cannot regenerate. That sets the priority: a test earns its place by covering a workflow where a person could lose a file or see one that is not theirs, not by moving a coverage number.
Running everything
cd server && go test -race -count=1 ./...
cd localdrive && flutter analyze && flutter test
cd landing && npm run buildThe race detector is not optional on the server. Uploads are resumable and
clients retry, so requests genuinely do arrive concurrently, and a failure that
appears only under -race is a real bug rather than a flaky test.
The landing build is the documentation test. It parses every file in docs/
and fails on an internal link that does not resolve.
The layers
Unit
One package, no database, no network. The rules of a thing in isolation.
Examples in the repository: pkg/pathsafe for path handling, and
internal/config for secret generation.
Reach for this when the logic has interesting edges. Path handling has a great many.
Integration
Several packages together, against a real SQLite database and the real HTTP
surface. These live in server/internal/app, built on the harness in
harness_test.go.
This is the right place for most server behaviour, because it exercises what a client would actually receive: routing, middleware, the service layer and the error envelope together.
Permission tests belong here. For any endpoint that returns an object, there should be a test asserting that a different account is refused. That is the single most valuable kind of test in this project.
API
The HTTP contract itself: status codes, the shape of the error envelope, field names. Also written as integration tests, but the assertion is about the contract rather than the behaviour behind it.
A response shape is a public contract. A test that pins it is what turns an accidental rename into a failing build instead of a broken client.
Widget and unit, on the client
localdrive/test/. Widget tests pump a widget and assert on what is rendered.
Useful patterns already in the repository: rendering rules, routing tables, selection behaviour, local database access, and a test that enforces a minimum touch target size.
When you fix a UI bug, the regression test is usually a widget test.
End to end
A built server and a built client, driven as a user would drive them. This is the layer the repository does not have yet. See Where end-to-end tests go.
Regression
Not a separate place. Every bug fix adds a test at whichever layer reproduces it, and that test stays. If a bug was worth fixing, it is worth not having twice.
Security
Not a separate place either. Permission tests are integration tests. What makes them security tests is that they assert a refusal rather than a success.
What must not break
Roughly in order. If time is short, this is the order to spend it in.
- A user does not lose a file. Upload, download, versions, trash, restore.
- A user cannot read another user's file. Every endpoint that names an object.
- An interrupted upload can be resumed, and cannot be resumed by someone else.
- A backup restores.
- An update that fails rolls back, and one that succeeds comes back on the new version.
- Authentication holds: sessions, device approval, the second factor.
What not to test
- Getters, and code with no decision in it.
- Generated code.
*.g.dartis the generator's responsibility. - Behaviour that does not exist yet. A test for a planned feature is a failing build, not a specification.
- Third party libraries. Test how this project uses them, not that they work.
Where end-to-end tests go
There is no end-to-end suite yet. When one is added, it goes in e2e/ at the
repository root, outside both server/ and localdrive/, because it exercises
them together and belongs to neither.
The intended shape:
e2e/
fixtures/ files and accounts a run starts from
scenarios/ one directory per workflow under test
README.md how to start an environment and run against itTwo properties matter more than the choice of tool:
- A run starts from a known, disposable environment. A fresh install, a fresh database, fixed fixtures. A suite that depends on the state left by the last run is a suite that will be deleted within a year.
- It runs against built artifacts, the server binary and a built client, not against source. That is the point of the layer: it tests what is shipped.
Where it sits in the order:
unit -> integration -> api -> build -> environment -> end to end -> releaseEnd-to-end runs after a build has produced something to run, and before a release is treated as good. It is the slowest and least specific layer, so it covers whole workflows rather than edge cases. Edge cases stay in the layers above, where a failure points at a line instead of a screen.
Whatever tool is chosen, the repository must remain complete without it. The project does not depend on a testing vendor, and a contributor who cannot run the end-to-end suite must still be able to run everything else.
Writing a test that is worth having
- Make it fail first. A test that was never red proves nothing.
- Name the behaviour, not the function.
TestTOTPLetsANewDeviceInWithoutApprovalsays what is protected.TestLogin2does not. - Assert on what a user or a client would see, not on internal state.
- One reason to fail. A test that covers four things tells you least when it goes red.
- No sleeps. Wait for a condition.
When a test fails
A failing test is a finding. Never delete it, skip it, or loosen its assertion to get a green build. If it is genuinely wrong, fix the test in its own change and say why it was wrong.