Skip to content

test: start migrating dbauthz tests to mocked db #19257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 8, 2025
Merged

Conversation

Emyrk
Copy link
Member

@Emyrk Emyrk commented Aug 8, 2025

What this does

Starts work on coder/internal#869

DBAuthz tests are taking a long amount of time from the sheer quantity of databases required to be provisioned. This PR adds a framework to move to a mocked db. And therefore massively speed up these tests.

The biggest challenge was to randomly seed data into the various database.<type> structs, as zero values could hide rbac bugs. In the actual db, we use dbgen. Instead of refactoring out or redoing dbgen to allow it to work with mocks, I went with https://github.com/brianvoe/gofakeit to randomly seed data.

Since we are not testing the db or queries themselves, I feel more ok with actual "random" data. The dbgen is hand rolled, but those fake values must confine to our data schema.

Maybe in the future we can annotate our models with faker tags and merge these ideas. At present, I don't think we can add field tags to sqlc output.

Should we be using a real database?

No, not really. The dbauthz tests are just asserting that all database calls have a corresponding authz check. There is no need to test the db layer here. The only reason we used a database, is because we used to have dbmem, and the tooling for setting up fake data was there and easy to use.

An example migrating to a mocked test

- s.Run("GetAPIKeysByUserID", s.Subtest(func(db database.Store, check *expects) {
+ s.Run("GetAPIKeysByUserID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
-	u1 := dbgen.User(s.T(), db, database.User{})
-	u2 := dbgen.User(s.T(), db, database.User{})
+       u1 := testutil.Fake(s.T(), faker, database.User{}) 

-	keyA, _ := dbgen.APIKey(s.T(), db, database.APIKey{UserID: u1.ID, LoginType: database.LoginTypeToken, TokenName: "key-a"})
-	keyB, _ := dbgen.APIKey(s.T(), db, database.APIKey{UserID: u1.ID, LoginType: database.LoginTypeToken, TokenName: "key-b"})
-	_, _ = dbgen.APIKey(s.T(), db, database.APIKey{UserID: u2.ID, LoginType: database.LoginTypeToken})
+       dbm.EXPECT().GetAPIKeysByUserID(gomock.Any(), gomock.Any()).Return(slice.New(keyA, keyB), nil).AnyTimes()

	check.Args(database.GetAPIKeysByUserIDParams{LoginType: database.LoginTypeToken, UserID: u1.ID}).
		Asserts(keyA, policy.ActionRead, keyB, policy.ActionRead).
		Returns(slice.New(keyA, keyB))
}))

Annotated output

// Moved from `s.Subtest` to `s.Mocked`. The mocker comes with a `faker` tool
s.Run("GetAPIKeysByUserID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
	// Instead of dbgen, we use `testutil.Fake(` to make populate random data
	u1 := testutil.Fake(s.T(), faker, database.User{})
	// The test util keeps non-zero values, so we keep our relations of objects
	keyA := testutil.Fake(s.T(), faker, database.APIKey{UserID: u1.ID, LoginType: database.LoginTypeToken, TokenName: "key-a"})
	keyB := testutil.Fake(s.T(), faker, database.APIKey{UserID: u1.ID, LoginType: database.LoginTypeToken, TokenName: "key-b"})

	// We need to add the mocked return and add 'AnyTimes'. The dbauthz package
	// runs multiple tests on this query method, so always use 'AnyTimes'.
	dbm.EXPECT().GetAPIKeysByUserID(gomock.Any(), gomock.Any()).Return(slice.New(keyA, keyB), nil).AnyTimes()
	check.Args(database.GetAPIKeysByUserIDParams{LoginType: database.LoginTypeToken, UserID: u1.ID}).
	Returns(slice.New(keyA, keyB))
}))

DBAuthz tests are taking a long amount of time from the sheer
quantity of databases required to be provisioned. This PR
adds a framework to move to a mocked db.
Comment on lines 11 to 62
// Fake will populate any zero fields in the provided struct with fake data.
// Non-zero fields will remain unchanged.
// Usage:
//
// key := Fake(t, faker, database.APIKey{
// TokenName: "keep-my-name",
// })
func Fake[T any](t *testing.T, faker *gofakeit.Faker, seed T) T {
t.Helper()

var tmp T
err := faker.Struct(&tmp)
require.NoError(t, err, "failed to generate fake data for type %T", tmp)

mergeZero(&seed, tmp)
return seed
}

// mergeZero merges the fields of src into dst, but only if the field in dst is
// currently the zero value.
func mergeZero[T any](dst *T, src T) {
remain := [][2]reflect.Value{
{reflect.ValueOf(dst).Elem(), reflect.ValueOf(src)},
}

// Traverse the struct fields and set them only if they are currently zero.
// This is a breadth-first traversal of the struct fields. Struct definitions
// Should not be that deep, so we should not hit any stack overflow issues.
for {
if len(remain) == 0 {
return
}
dv, sv := remain[0][0], remain[0][1]
remain = remain[1:] //
for i := 0; i < dv.NumField(); i++ {
df := dv.Field(i)
sf := sv.Field(i)
if !df.CanSet() {
continue
}
if reflect.Value.IsZero(df) { // only write if currently zero
df.Set(sf)
continue
}

if dv.Field(i).Kind() == reflect.Struct {
// If the field is a struct, we need to traverse it as well.
remain = append(remain, [2]reflect.Value{df, sf})
}
}
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably the most objectionable add. It adds a new package https://github.com/brianvoe/gofakeit, and some reflect magic.

But the alternative is to refactor dbgen or copy it and make a different fake for each database type. That feels exhausting. This is just so easy to use, and it is in testutil, so it does not need to be "production worthy".

I debated throwing this into dbauthz_test, but thought it might be helpful elsewhere too.

@Emyrk Emyrk requested review from johnstcn and deansheather August 8, 2025 16:29
Copy link
Member

@johnstcn johnstcn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new approach looks fine to me, and I'm not against adding a library for faking data.

@@ -204,14 +207,15 @@ func defaultIPAddress() pqtype.Inet {
}

func (s *MethodTestSuite) TestAPIKey() {
s.Run("DeleteAPIKeyByID", s.Subtest(func(db database.Store, check *expects) {
dbtestutil.DisableForeignKeysAndTriggers(s.T(), db)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌

Copy link
Member

@deansheather deansheather left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉 🎉

Depends on reflect under the hood anyway
@Emyrk Emyrk merged commit ce93565 into main Aug 8, 2025
31 checks passed
@Emyrk Emyrk deleted the stevenmasley/mock_dbauthz branch August 8, 2025 18:46
@github-actions github-actions bot locked and limited conversation to collaborators Aug 8, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy