Help us shape the future of Go! Take the 2026 Go Developer Survey

storetest

package
v3.49.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 24, 2026 License: BSD-3-Clause Imports: 10 Imported by: 0

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

View Source
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

func Run(t *testing.T, newStore NewStore)

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

func SummerInstant() time.Time

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

func WinterInstant() time.Time

Types

type NewStore

type NewStore func(t *testing.T) Store

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL