Documentation
¶
Overview ¶
Package storetest is the contract every glabs-web store implementation has to satisfy, written once and run against each of them.
It exists because web/db had no tests at all when the move from MongoDB to PostgreSQL began. Porting a database layer without one means testing the new implementation against one's own recollection of the old one rather than against what production actually does — and the parts most easily misremembered (does a delete of someone else's course fail or silently succeed? does an absent secret come back as an error or as nil?) are exactly the parts the app above relies on.
So the suite is written against the MongoDB implementation FIRST, while it is still the one in production, and only then pointed at PostgreSQL. Every assertion here is derived from reading web/db/*.go, not from taste. Where the old behaviour is arguably wrong it is still pinned, and the comment says so; changing it is a separate decision from moving the data.
Two things it deliberately does NOT assert:
- the order of records sharing a timestamp. MongoDB leaves those in an arbitrary order; PostgreSQL breaks the tie on the identity column. The suite has to be green against both, so it only ever compares sets there.
- whether an empty result is a nil slice or an empty one. The callers in web/app all use len(), the two drivers differ, and pinning it would freeze a detail nobody depends on.
Index ¶
Constants ¶
const ActivityLimit = 200
ActivityLimit is the cap the two GUI-facing activity reads apply. It mirrors the unexported activityLimit in web/db; the suite has to know the number to assert that the cap is there at all.
Variables ¶
This section is empty.
Functions ¶
func FullCourseSource ¶
func FullCourseSource() *config.CourseSource
FullCourseSource is a course source with EVERY field of config.CourseSource and its sub-structs set to a non-zero value, including both states of each pointer field — set where absent and present differ, nil where that is the interesting case.
It is the fixture for the round-trip test, whose whole point is that a field which is not stored shows up as a difference. A fixture that leaves fields at their zero value cannot do that: zero survives being dropped.
It is hand-written and therefore goes stale the moment a field is added to config/source.go. The reflection-based check that catches exactly that is a separate test over the json tags; this one is here to prove the storage path carries the values, not just the names.
func Run ¶
Run executes the whole contract against one implementation.
The groups mirror the files in web/db, so a failure names the file to open.
func SummerInstant ¶
SummerInstant and WinterInstant are the two timestamps the suite writes: one on each side of the DST boundary, +02:00 in summer and +01:00 in winter. A store that hands back UTC passes a test that only uses one of them, because the two are indistinguishable when the offset is never compared.
FUNCTIONS, not variables, and that is the whole point. A package-level variable is initialised before TestMain runs, so it would capture whatever time.Local happened to be at process start -- Europe/Berlin in the dev container, which has TZ set, and UTC on a GitHub runner, which does not. The suite then compared a UTC instant against a correctly Berlin-zoned result and failed on the runner only. Reading time.Local at call time makes the fixtures agree with TestMain wherever they run.
func WinterInstant ¶
Types ¶
type NewStore ¶
NewStore hands back a store with no data in it. It is called once per subtest, so nothing a case writes can reach the next one — the alternative, cleaning up afterwards, leaves the suite's correctness depending on every case remembering to do so.
type Store ¶
type Store interface {
CoursesOf(ctx context.Context, owner string) ([]*db.StoredCourse, error)
CourseOf(ctx context.Context, owner, name string) (*db.StoredCourse, error)
SaveCourse(ctx context.Context, course *db.StoredCourse) error
DeleteCourse(ctx context.Context, owner, name string) error
GetUserSecret(ctx context.Context, owner string) (*db.UserSecret, error)
SaveUserGitLabToken(ctx context.Context, owner string, sealed secrets.SealedValue, updatedAt time.Time) error
DeleteUserGitLabToken(ctx context.Context, owner string) error
RecordActivity(ctx context.Context, e *db.ActivityEntry) error
ActivityFor(ctx context.Context, owner, course, assignment string) ([]*db.ActivityEntry, error)
CourseActivityFor(ctx context.Context, owner, course string) ([]*db.ActivityEntry, error)
AllActivityFor(ctx context.Context, owner string) ([]*db.ActivityEntry, error)
SaveJob(ctx context.Context, job *db.ScheduledJob) error
CancelJob(ctx context.Context, owner, id string) (*db.ScheduledJob, error)
JobsOf(ctx context.Context, owner string, statuses []string) ([]*db.ScheduledJob, error)
JobOf(ctx context.Context, owner, id string) (*db.ScheduledJob, error)
ClaimDueJob(ctx context.Context, workerID string, now time.Time) (*db.ScheduledJob, error)
FinishJob(ctx context.Context, id, status, logText, errText string) error
UnnotifiedTerminalJobs(ctx context.Context) ([]*db.ScheduledJob, error)
MarkNotified(ctx context.Context, id string) error
RecordEvent(ctx context.Context, e *db.Event) error
EventsBetween(ctx context.Context, since, until time.Time) ([]*db.Event, error)
RecentEvents(ctx context.Context, since time.Time, limit int64) ([]*db.Event, error)
SystemState(ctx context.Context) (*db.SystemState, error)
SetSummarySentAt(ctx context.Context, at time.Time) error
}
Store is the slice of the database the application uses. It is a copy of the unexported `store` interface in web/app — deliberately a copy rather than a shared type, because that one is the app's statement of what it needs and this one is the storage layer's statement of what it promises. They happen to coincide today.
ReapExpired is NOT part of it: MongoDB does that with a TTL index rather than a method, so only the PostgreSQL implementation has one. It is tested separately, by the tests of that implementation.