diff options
Diffstat (limited to 'internal/db/users_test.go')
-rw-r--r-- | internal/db/users_test.go | 268 |
1 files changed, 267 insertions, 1 deletions
diff --git a/internal/db/users_test.go b/internal/db/users_test.go index 171b3a88..69f157ea 100644 --- a/internal/db/users_test.go +++ b/internal/db/users_test.go @@ -82,7 +82,11 @@ func TestUsers(t *testing.T) { } t.Parallel() - tables := []any{new(User), new(EmailAddress), new(Repository), new(Follow), new(PullRequest), new(PublicKey)} + tables := []any{ + new(User), new(EmailAddress), new(Repository), new(Follow), new(PullRequest), new(PublicKey), new(OrgUser), + new(Watch), new(Star), new(Issue), new(AccessToken), new(Collaboration), new(Action), new(IssueUser), + new(Access), + } db := &users{ DB: dbtest.NewDB(t, "users", tables...), } @@ -96,6 +100,8 @@ func TestUsers(t *testing.T) { {"Count", usersCount}, {"Create", usersCreate}, {"DeleteCustomAvatar", usersDeleteCustomAvatar}, + {"DeleteByID", usersDeleteByID}, + {"DeleteInactivated", usersDeleteInactivated}, {"GetByEmail", usersGetByEmail}, {"GetByID", usersGetByID}, {"GetByUsername", usersGetByUsername}, @@ -463,6 +469,266 @@ func usersDeleteCustomAvatar(t *testing.T, db *users) { assert.False(t, alice.UseCustomAvatar) } +func usersDeleteByID(t *testing.T, db *users) { + ctx := context.Background() + reposStore := NewReposStore(db.DB) + + t.Run("user still has repository ownership", func(t *testing.T) { + alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOptions{}) + require.NoError(t, err) + + _, err = reposStore.Create(ctx, alice.ID, CreateRepoOptions{Name: "repo1"}) + require.NoError(t, err) + + err = db.DeleteByID(ctx, alice.ID, false) + wantErr := ErrUserOwnRepos{errutil.Args{"userID": alice.ID}} + assert.Equal(t, wantErr, err) + }) + + t.Run("user still has organization membership", func(t *testing.T) { + bob, err := db.Create(ctx, "bob", "bob@exmaple.com", CreateUserOptions{}) + require.NoError(t, err) + + // TODO: Use Orgs.Create to replace SQL hack when the method is available. + org1, err := db.Create(ctx, "org1", "org1@example.com", CreateUserOptions{}) + require.NoError(t, err) + err = db.Exec( + dbutil.Quote("UPDATE %s SET type = ? WHERE id IN (?)", "user"), + UserTypeOrganization, org1.ID, + ).Error + require.NoError(t, err) + + // TODO: Use Orgs.Join to replace SQL hack when the method is available. + err = db.Exec(`INSERT INTO org_user (uid, org_id) VALUES (?, ?)`, bob.ID, org1.ID).Error + require.NoError(t, err) + + err = db.DeleteByID(ctx, bob.ID, false) + wantErr := ErrUserHasOrgs{errutil.Args{"userID": bob.ID}} + assert.Equal(t, wantErr, err) + }) + + cindy, err := db.Create(ctx, "cindy", "cindy@exmaple.com", CreateUserOptions{}) + require.NoError(t, err) + frank, err := db.Create(ctx, "frank", "frank@exmaple.com", CreateUserOptions{}) + require.NoError(t, err) + repo2, err := reposStore.Create(ctx, cindy.ID, CreateRepoOptions{Name: "repo2"}) + require.NoError(t, err) + + testUser, err := db.Create(ctx, "testUser", "testUser@exmaple.com", CreateUserOptions{}) + require.NoError(t, err) + + // Mock watches, stars and follows + err = NewWatchesStore(db.DB).Watch(ctx, testUser.ID, repo2.ID) + require.NoError(t, err) + err = reposStore.Star(ctx, testUser.ID, repo2.ID) + require.NoError(t, err) + followsStore := NewFollowsStore(db.DB) + err = followsStore.Follow(ctx, testUser.ID, cindy.ID) + require.NoError(t, err) + err = followsStore.Follow(ctx, frank.ID, testUser.ID) + require.NoError(t, err) + + // Mock "authorized_keys" file + // TODO: Use PublicKeys.Add to replace SQL hack when the method is available. + publicKey := &PublicKey{ + OwnerID: testUser.ID, + Name: "test-key", + Fingerprint: "12:f8:7e:78:61:b4:bf:e2:de:24:15:96:4e:d4:72:53", + Content: "test-key-content", + } + err = db.DB.Create(publicKey).Error + require.NoError(t, err) + tempSSHRootPath := filepath.Join(os.TempDir(), "usersDeleteByID-tempSSHRootPath") + conf.SetMockSSH(t, conf.SSHOpts{RootPath: tempSSHRootPath}) + err = NewPublicKeysStore(db.DB).RewriteAuthorizedKeys() + require.NoError(t, err) + + // Mock issue assignee + // TODO: Use Issues.Assign to replace SQL hack when the method is available. + issue := &Issue{ + RepoID: repo2.ID, + Index: 1, + PosterID: cindy.ID, + Title: "test-issue", + AssigneeID: testUser.ID, + } + err = db.DB.Create(issue).Error + require.NoError(t, err) + + // Mock random entries in related tables + for _, table := range []any{ + &AccessToken{UserID: testUser.ID}, + &Collaboration{UserID: testUser.ID}, + &Access{UserID: testUser.ID}, + &Action{UserID: testUser.ID}, + &IssueUser{UserID: testUser.ID}, + &EmailAddress{UserID: testUser.ID}, + } { + err = db.DB.Create(table).Error + require.NoError(t, err, "table for %T", table) + } + + // Mock user directory + tempRepositoryRoot := filepath.Join(os.TempDir(), "usersDeleteByID-tempRepositoryRoot") + conf.SetMockRepository(t, conf.RepositoryOpts{Root: tempRepositoryRoot}) + tempUserPath := repoutil.UserPath(testUser.Name) + err = os.MkdirAll(tempUserPath, os.ModePerm) + require.NoError(t, err) + + // Mock user custom avatar + tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "usersDeleteByID-tempPictureAvatarUploadPath") + conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) + err = os.MkdirAll(tempPictureAvatarUploadPath, os.ModePerm) + require.NoError(t, err) + tempCustomAvatarPath := userutil.CustomAvatarPath(testUser.ID) + err = os.WriteFile(tempCustomAvatarPath, []byte("test"), 0600) + require.NoError(t, err) + + // Verify mock data + repo2, err = reposStore.GetByID(ctx, repo2.ID) + require.NoError(t, err) + assert.Equal(t, 2, repo2.NumWatches) // The owner is watching the repo by default. + assert.Equal(t, 1, repo2.NumStars) + + cindy, err = db.GetByID(ctx, cindy.ID) + require.NoError(t, err) + assert.Equal(t, 1, cindy.NumFollowers) + frank, err = db.GetByID(ctx, frank.ID) + require.NoError(t, err) + assert.Equal(t, 1, frank.NumFollowing) + + authorizedKeys, err := os.ReadFile(authorizedKeysPath()) + require.NoError(t, err) + assert.Contains(t, string(authorizedKeys), fmt.Sprintf("key-%d", publicKey.ID)) + assert.Contains(t, string(authorizedKeys), publicKey.Content) + + // TODO: Use Issues.GetByID to replace SQL hack when the method is available. + err = db.DB.First(issue, issue.ID).Error + require.NoError(t, err) + assert.Equal(t, testUser.ID, issue.AssigneeID) + + relatedTables := []any{ + &Watch{UserID: testUser.ID}, + &Star{UserID: testUser.ID}, + &Follow{UserID: testUser.ID}, + &PublicKey{OwnerID: testUser.ID}, + &AccessToken{UserID: testUser.ID}, + &Collaboration{UserID: testUser.ID}, + &Access{UserID: testUser.ID}, + &Action{UserID: testUser.ID}, + &IssueUser{UserID: testUser.ID}, + &EmailAddress{UserID: testUser.ID}, + } + for _, table := range relatedTables { + var count int64 + err = db.DB.Model(table).Where(table).Count(&count).Error + require.NoError(t, err, "table for %T", table) + assert.NotZero(t, count, "table for %T", table) + } + + assert.True(t, osutil.IsExist(tempUserPath)) + assert.True(t, osutil.IsExist(tempCustomAvatarPath)) + + // Pull the trigger + err = db.DeleteByID(ctx, testUser.ID, false) + require.NoError(t, err) + + // Verify after-the-fact data + repo2, err = reposStore.GetByID(ctx, repo2.ID) + require.NoError(t, err) + assert.Equal(t, 1, repo2.NumWatches) // The owner is watching the repo by default. + assert.Equal(t, 0, repo2.NumStars) + + cindy, err = db.GetByID(ctx, cindy.ID) + require.NoError(t, err) + assert.Equal(t, 0, cindy.NumFollowers) + frank, err = db.GetByID(ctx, frank.ID) + require.NoError(t, err) + assert.Equal(t, 0, frank.NumFollowing) + + authorizedKeys, err = os.ReadFile(authorizedKeysPath()) + require.NoError(t, err) + assert.Empty(t, authorizedKeys) + + // TODO: Use Issues.GetByID to replace SQL hack when the method is available. + err = db.DB.First(issue, issue.ID).Error + require.NoError(t, err) + assert.Equal(t, int64(0), issue.AssigneeID) + + for _, table := range []any{ + &Watch{UserID: testUser.ID}, + &Star{UserID: testUser.ID}, + &Follow{UserID: testUser.ID}, + &PublicKey{OwnerID: testUser.ID}, + &AccessToken{UserID: testUser.ID}, + &Collaboration{UserID: testUser.ID}, + &Access{UserID: testUser.ID}, + &Action{UserID: testUser.ID}, + &IssueUser{UserID: testUser.ID}, + &EmailAddress{UserID: testUser.ID}, + } { + var count int64 + err = db.DB.Model(table).Where(table).Count(&count).Error + require.NoError(t, err, "table for %T", table) + assert.Equal(t, int64(0), count, "table for %T", table) + } + + assert.False(t, osutil.IsExist(tempUserPath)) + assert.False(t, osutil.IsExist(tempCustomAvatarPath)) + + _, err = db.GetByID(ctx, testUser.ID) + wantErr := ErrUserNotExist{errutil.Args{"userID": testUser.ID}} + assert.Equal(t, wantErr, err) +} + +func usersDeleteInactivated(t *testing.T, db *users) { + ctx := context.Background() + + // User with repository ownership should be skipped + alice, err := db.Create(ctx, "alice", "alice@exmaple.com", CreateUserOptions{}) + require.NoError(t, err) + reposStore := NewReposStore(db.DB) + _, err = reposStore.Create(ctx, alice.ID, CreateRepoOptions{Name: "repo1"}) + require.NoError(t, err) + + // User with organization membership should be skipped + bob, err := db.Create(ctx, "bob", "bob@exmaple.com", CreateUserOptions{}) + require.NoError(t, err) + // TODO: Use Orgs.Create to replace SQL hack when the method is available. + org1, err := db.Create(ctx, "org1", "org1@example.com", CreateUserOptions{}) + require.NoError(t, err) + err = db.Exec( + dbutil.Quote("UPDATE %s SET type = ? WHERE id IN (?)", "user"), + UserTypeOrganization, org1.ID, + ).Error + require.NoError(t, err) + // TODO: Use Orgs.Join to replace SQL hack when the method is available. + err = db.Exec(`INSERT INTO org_user (uid, org_id) VALUES (?, ?)`, bob.ID, org1.ID).Error + require.NoError(t, err) + + // User activated state should be skipped + _, err = db.Create(ctx, "cindy", "cindy@exmaple.com", CreateUserOptions{Activated: true}) + require.NoError(t, err) + + // User meant to be deleted + david, err := db.Create(ctx, "david", "david@exmaple.com", CreateUserOptions{}) + require.NoError(t, err) + + tempSSHRootPath := filepath.Join(os.TempDir(), "usersDeleteInactivated-tempSSHRootPath") + conf.SetMockSSH(t, conf.SSHOpts{RootPath: tempSSHRootPath}) + + err = db.DeleteInactivated() + require.NoError(t, err) + + _, err = db.GetByID(ctx, david.ID) + wantErr := ErrUserNotExist{errutil.Args{"userID": david.ID}} + assert.Equal(t, wantErr, err) + + users, err := db.List(ctx, 1, 10) + require.NoError(t, err) + require.Len(t, users, 3) +} + func usersGetByEmail(t *testing.T, db *users) { ctx := context.Background() |