// Copyright 2022 The Gogs Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package db import ( "context" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gogs.io/gogs/internal/conf" "gogs.io/gogs/internal/dbtest" "gogs.io/gogs/internal/errutil" ) func TestOrganizations(t *testing.T) { if testing.Short() { t.Skip() } t.Parallel() ctx := context.Background() tables := []any{ new(User), new(EmailAddress), new(OrgUser), new(Team), new(TeamUser), new(Repository), new(Watch), new(Star), new(Follow), new(Issue), new(PublicKey), new(AccessToken), new(Collaboration), new(Access), new(Action), new(IssueUser), } db := &organizations{ DB: dbtest.NewDB(t, "orgs", tables...), } for _, tc := range []struct { name string test func(t *testing.T, ctx context.Context, db *organizations) }{ {"Create", orgsCreate}, {"GetByName", orgsGetByName}, {"SearchByName", orgsSearchByName}, {"List", orgsList}, {"CountByUser", orgsCountByUser}, {"Count", orgsCount}, {"DeleteByID", orgsDeleteByID}, {"AddMember", orgsAddMember}, } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { err := clearTables(t, db.DB, tables...) require.NoError(t, err) }) tc.test(t, ctx, db) }) if t.Failed() { break } } } func orgsCreate(t *testing.T, ctx context.Context, db *organizations) { usersStore := NewUsersStore(db.DB) alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) t.Run("name not allowed", func(t *testing.T) { _, err := db.Create(ctx, "-", alice.ID, CreateOrganizationOptions{}) wantErr := ErrNameNotAllowed{ args: errutil.Args{ "reason": "reserved", "name": "-", }, } assert.Equal(t, wantErr, err) }) // Users and organizations share the same namespace for names. t.Run("name already exists", func(t *testing.T) { _, err := db.Create(ctx, alice.Name, alice.ID, CreateOrganizationOptions{}) wantErr := ErrOrganizationAlreadyExist{ args: errutil.Args{ "name": alice.Name, }, } assert.Equal(t, wantErr, err) }) tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsCreate-tempPictureAvatarUploadPath") conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) org, err := db.Create( ctx, "acme", alice.ID, CreateOrganizationOptions{ FullName: "Acme Corp", Email: "admin@acme.com", Location: "Earth", Website: "acme.com", Description: "A popcorn company", }, ) require.NoError(t, err) got, err := db.GetByName(ctx, org.Name) require.NoError(t, err) assert.Equal(t, org.Name, got.Name) assert.Equal(t, org.FullName, got.FullName) assert.Equal(t, org.Email, got.Email) assert.Equal(t, org.Location, got.Location) assert.Equal(t, org.Website, got.Website) assert.Equal(t, org.Description, got.Description) assert.Equal(t, -1, got.MaxRepoCreation) assert.Equal(t, 1, got.NumTeams) assert.Equal(t, 1, got.NumMembers) assert.Equal(t, db.NowFunc().Format(time.RFC3339), got.Created.UTC().Format(time.RFC3339)) assert.Equal(t, db.NowFunc().Format(time.RFC3339), got.Updated.UTC().Format(time.RFC3339)) } func orgsGetByName(t *testing.T, ctx context.Context, db *organizations) { t.Run("correct user type", func(t *testing.T) { tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "usersGetByUsername-tempPictureAvatarUploadPath") conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) org1, err := db.Create(ctx, "org1", 1, CreateOrganizationOptions{}) require.NoError(t, err) got, err := db.GetByName(ctx, org1.Name) require.NoError(t, err) assert.Equal(t, org1.Name, got.Name) _, err = db.GetByName(ctx, "bad_name") wantErr := ErrOrganizationNotExist{args: errutil.Args{"name": "bad_name"}} assert.Equal(t, wantErr, err) }) t.Run("wrong user type", func(t *testing.T) { alice, err := NewUsersStore(db.DB).Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) _, err = db.GetByName(ctx, alice.Name) wantErr := ErrOrganizationNotExist{args: errutil.Args{"name": alice.Name}} assert.Equal(t, wantErr, err) }) } func orgsList(t *testing.T, ctx context.Context, db *organizations) { usersStore := NewUsersStore(db.DB) alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) bob, err := usersStore.Create(ctx, "bob", "bob@example.com", CreateUserOptions{}) require.NoError(t, err) tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsList-tempPictureAvatarUploadPath") conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) org1, err := db.Create(ctx, "org1-alice-owned", alice.ID, CreateOrganizationOptions{}) require.NoError(t, err) org2, err := db.Create(ctx, "org2-alice-owned", alice.ID, CreateOrganizationOptions{}) require.NoError(t, err) err = db.SetMemberVisibility(ctx, org2.ID, alice.ID, true) require.NoError(t, err) err = db.AddMember(ctx, org2.ID, bob.ID) require.NoError(t, err) org3, err := db.Create(ctx, "org3-bob-owned", bob.ID, CreateOrganizationOptions{}) require.NoError(t, err) tests := []struct { name string opts ListOrganizationsOptions wantOrgNames []string }{ { name: "only public memberships for a user", opts: ListOrganizationsOptions{ MemberID: alice.ID, IncludePrivateMembers: false, }, wantOrgNames: []string{org2.Name}, }, { name: "all memberships for a user", opts: ListOrganizationsOptions{ MemberID: alice.ID, IncludePrivateMembers: true, }, wantOrgNames: []string{org1.Name, org2.Name}, }, { name: "only public ownership for a user", opts: ListOrganizationsOptions{ OwnerID: alice.ID, IncludePrivateMembers: false, }, wantOrgNames: []string{org2.Name}, }, { name: "all ownership for a user", opts: ListOrganizationsOptions{ OwnerID: alice.ID, IncludePrivateMembers: true, }, wantOrgNames: []string{org1.Name, org2.Name}, }, { name: "no membership for a non-existent user", opts: ListOrganizationsOptions{ MemberID: 404, IncludePrivateMembers: true, }, wantOrgNames: []string{}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, err := db.List(ctx, test.opts) require.NoError(t, err) gotOrgNames := make([]string, len(got)) for i := range got { gotOrgNames[i] = got[i].Name } assert.Equal(t, test.wantOrgNames, gotOrgNames) }) } t.Run("pagination", func(t *testing.T) { got, err := db.List(ctx, ListOrganizationsOptions{Page: 1, PageSize: 1}) require.NoError(t, err) require.Len(t, got, 1) assert.Equal(t, org1.ID, got[0].ID) got, err = db.List(ctx, ListOrganizationsOptions{Page: 2, PageSize: 1}) require.NoError(t, err) require.Len(t, got, 1) assert.Equal(t, org2.ID, got[0].ID) got, err = db.List(ctx, ListOrganizationsOptions{Page: 1, PageSize: 4}) require.NoError(t, err) require.Len(t, got, 3) assert.Equal(t, org1.ID, got[0].ID) assert.Equal(t, org2.ID, got[1].ID) assert.Equal(t, org3.ID, got[2].ID) }) } func orgsSearchByName(t *testing.T, ctx context.Context, db *organizations) { tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsSearchByName-tempPictureAvatarUploadPath") conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) tempRepositoryRoot := filepath.Join(os.TempDir(), "orgsSearchByName-tempRepositoryRoot") conf.SetMockRepository(t, conf.RepositoryOpts{Root: tempRepositoryRoot}) org1, err := db.Create(ctx, "org1", 1, CreateOrganizationOptions{FullName: "Acme Corp"}) require.NoError(t, err) org2, err := db.Create(ctx, "org2", 1, CreateOrganizationOptions{FullName: "Acme Corp 2"}) require.NoError(t, err) t.Run("search for username org1", func(t *testing.T) { orgs, count, err := db.SearchByName(ctx, "G1", 1, 1, "") require.NoError(t, err) require.Len(t, orgs, int(count)) assert.Equal(t, int64(1), count) assert.Equal(t, org1.ID, orgs[0].ID) }) t.Run("search for username org2", func(t *testing.T) { orgs, count, err := db.SearchByName(ctx, "G2", 1, 1, "") require.NoError(t, err) require.Len(t, orgs, int(count)) assert.Equal(t, int64(1), count) assert.Equal(t, org2.ID, orgs[0].ID) }) t.Run("search for full name acme", func(t *testing.T) { orgs, count, err := db.SearchByName(ctx, "ACME", 1, 10, "") require.NoError(t, err) require.Len(t, orgs, int(count)) assert.Equal(t, int64(2), count) }) t.Run("search for full name acme ORDER BY id DESC LIMIT 1", func(t *testing.T) { orgs, count, err := db.SearchByName(ctx, "ACME", 1, 1, "id DESC") require.NoError(t, err) require.Len(t, orgs, 1) assert.Equal(t, int64(2), count) assert.Equal(t, org2.ID, orgs[0].ID) }) } func orgsCountByUser(t *testing.T, ctx context.Context, db *organizations) { usersStore := NewUsersStore(db.DB) alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) bob, err := usersStore.Create(ctx, "bob", "bob@example.com", CreateUserOptions{}) require.NoError(t, err) tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsCountByUser-tempPictureAvatarUploadPath") conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) org1, err := db.Create(ctx, "org1", alice.ID, CreateOrganizationOptions{}) require.NoError(t, err) err = db.AddMember(ctx, org1.ID, bob.ID) require.NoError(t, err) got, err := db.CountByUser(ctx, alice.ID) require.NoError(t, err) assert.Equal(t, int64(1), got) got, err = db.CountByUser(ctx, 404) require.NoError(t, err) assert.Equal(t, int64(0), got) } func orgsCount(t *testing.T, db *organizations) { ctx := context.Background() // Has no organization initially got := db.Count(ctx) assert.Equal(t, int64(0), got) tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "usersCount-tempPictureAvatarUploadPath") conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) _, err := db.Create(ctx, "org1", 1, CreateOrganizationOptions{}) require.NoError(t, err) // Create a user shouldn't count _, err = NewUsersStore(db.DB).Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) got = db.Count(ctx) assert.Equal(t, int64(1), got) } func orgsDeleteByID(t *testing.T, db *organizations) { ctx := context.Background() tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsDeleteByID-tempPictureAvatarUploadPath") conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) t.Run("organization still has repository ownership", func(t *testing.T) { org1, err := db.Create(ctx, "org1", 404, CreateOrganizationOptions{}) require.NoError(t, err) _, err = NewRepositoriesStore(db.DB).Create(ctx, org1.ID, CreateRepoOptions{Name: "repo1"}) require.NoError(t, err) err = db.DeleteByID(ctx, org1.ID) wantErr := ErrOrganizationOwnRepos{errutil.Args{"orgID": org1.ID}} assert.Equal(t, wantErr, err) }) alice, err := NewUsersStore(db.DB).Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) org2, err := db.Create(ctx, "org2", alice.ID, CreateOrganizationOptions{}) require.NoError(t, err) // Mock team membership // TODO: Use Organizations.CreateTeam to replace SQL hack when the method is available. team1 := &Team{ OrgID: org2.ID, LowerName: "team1", Name: "team1", NumMembers: 1, } err = db.DB.Create(team1).Error require.NoError(t, err) // TODO: Use Organizations.AddTeamMember to replace SQL hack when the method is available. err = db.DB.Create( &TeamUser{ OrgID: org2.ID, TeamID: team1.ID, UID: alice.ID, }, ).Error require.NoError(t, err) // Pull the trigger err = db.DeleteByID(ctx, org2.ID) require.NoError(t, err) // Verify after-the-fact data for _, table := range []any{ &Team{OrgID: org2.ID}, &TeamUser{OrgID: org2.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) } _, err = db.GetByName(ctx, org2.Name) wantErr := ErrOrganizationNotExist{errutil.Args{"name": org2.Name}} assert.Equal(t, wantErr, err) } func orgsAddMember(t *testing.T, db *organizations) { ctx := context.Background() usersStore := NewUsersStore(db.DB) alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) bob, err := usersStore.Create(ctx, "bob", "bob@exmaple.com", CreateUserOptions{}) require.NoError(t, err) tempPictureAvatarUploadPath := filepath.Join(os.TempDir(), "orgsAddMember-tempPictureAvatarUploadPath") conf.SetMockPicture(t, conf.PictureOpts{AvatarUploadPath: tempPictureAvatarUploadPath}) org1, err := db.Create(ctx, "org1", alice.ID, CreateOrganizationOptions{}) require.NoError(t, err) // Not yet a member got, err := db.List(ctx, ListOrganizationsOptions{MemberID: bob.ID, IncludePrivateMembers: true}) require.NoError(t, err) assert.Len(t, got, 0) // Add member err = db.AddMember(ctx, org1.ID, bob.ID) require.NoError(t, err) // Now a member got, err = db.List(ctx, ListOrganizationsOptions{MemberID: bob.ID, IncludePrivateMembers: true}) require.NoError(t, err) assert.Len(t, got, 1) assert.Equal(t, org1.ID, got[0].ID) // Add member again shouldn't fail err = db.AddMember(ctx, org1.ID, bob.ID) require.NoError(t, err) }