// Copyright (C) 2023 Storj Labs, Inc. // See LICENSE for copying information. package satellitedb_test import ( "database/sql" "strings" "testing" "time" "github.com/stretchr/testify/require" "storj.io/common/testcontext" "storj.io/common/testrand" "storj.io/storj/satellite" "storj.io/storj/satellite/console" "storj.io/storj/satellite/satellitedb/satellitedbtest" ) func TestProjectInvitations(t *testing.T) { satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) { invitesDB := db.Console().ProjectInvitations() projectsDB := db.Console().Projects() inviterID := testrand.UUID() projID := testrand.UUID() projID2 := testrand.UUID() email := "user@mail.test" email2 := "user2@mail.test" _, err := projectsDB.Insert(ctx, &console.Project{ID: projID}) require.NoError(t, err) _, err = projectsDB.Insert(ctx, &console.Project{ID: projID2}) require.NoError(t, err) invite := &console.ProjectInvitation{ ProjectID: projID, Email: email, InviterID: &inviterID, } inviteSameEmail := &console.ProjectInvitation{ ProjectID: projID2, Email: email, } inviteSameProject := &console.ProjectInvitation{ ProjectID: projID, Email: email2, } if !t.Run("insert invitations", func(t *testing.T) { // Expect failure because no user with inviterID exists. _, err = invitesDB.Insert(ctx, invite) require.Error(t, err) _, err = db.Console().Users().Insert(ctx, &console.User{ ID: inviterID, PasswordHash: testrand.Bytes(8), }) require.NoError(t, err) invite, err = invitesDB.Insert(ctx, invite) require.NoError(t, err) require.WithinDuration(t, time.Now(), invite.CreatedAt, time.Minute) require.Equal(t, projID, invite.ProjectID) require.Equal(t, strings.ToUpper(email), invite.Email) // Duplicate invitations should be rejected. _, err = invitesDB.Insert(ctx, invite) require.Error(t, err) inviteSameEmail, err = invitesDB.Insert(ctx, inviteSameEmail) require.NoError(t, err) inviteSameProject, err = invitesDB.Insert(ctx, inviteSameProject) require.NoError(t, err) }) { // None of the following subtests will pass if invitation insertion failed. return } t.Run("get invitation", func(t *testing.T) { ctx := testcontext.New(t) other, err := invitesDB.Get(ctx, projID, "nobody@mail.test") require.ErrorIs(t, err, sql.ErrNoRows) require.Nil(t, other) other, err = invitesDB.Get(ctx, projID, email) require.NoError(t, err) require.Equal(t, invite, other) }) t.Run("get invitations by email", func(t *testing.T) { ctx := testcontext.New(t) invites, err := invitesDB.GetByEmail(ctx, "nobody@mail.test") require.NoError(t, err) require.Empty(t, invites) invites, err = invitesDB.GetByEmail(ctx, "uSeR@mAiL.tEsT") require.NoError(t, err) require.ElementsMatch(t, invites, []console.ProjectInvitation{*invite, *inviteSameEmail}) }) t.Run("get invitations by project ID", func(t *testing.T) { ctx := testcontext.New(t) invites, err := invitesDB.GetByProjectID(ctx, testrand.UUID()) require.NoError(t, err) require.Empty(t, invites) invites, err = invitesDB.GetByProjectID(ctx, projID) require.NoError(t, err) require.ElementsMatch(t, invites, []console.ProjectInvitation{*invite, *inviteSameProject}) }) t.Run("ensure inviter removal nullifies inviter ID", func(t *testing.T) { ctx := testcontext.New(t) require.NoError(t, db.Console().Users().Delete(ctx, inviterID)) invite, err := invitesDB.Get(ctx, projID, email) require.NoError(t, err) require.Nil(t, invite.InviterID) }) t.Run("update invitation", func(t *testing.T) { ctx := testcontext.New(t) req := console.UpdateProjectInvitationRequest{} newCreatedAt := invite.CreatedAt.Add(time.Hour) req.CreatedAt = &newCreatedAt newInvite, err := invitesDB.Update(ctx, projID, email, req) require.NoError(t, err) require.Equal(t, newCreatedAt, newInvite.CreatedAt) inviter, err := db.Console().Users().Insert(ctx, &console.User{ ID: testrand.UUID(), PasswordHash: testrand.Bytes(8), }) require.NoError(t, err) req.InviterID = &inviter.ID newInvite, err = invitesDB.Update(ctx, projID, email, req) require.NoError(t, err) require.Equal(t, inviter.ID, *newInvite.InviterID) }) t.Run("delete invitation", func(t *testing.T) { ctx := testcontext.New(t) require.NoError(t, invitesDB.Delete(ctx, projID, email)) invites, err := invitesDB.GetByEmail(ctx, email) require.NoError(t, err) require.Equal(t, invites, []console.ProjectInvitation{*inviteSameEmail}) }) t.Run("ensure project removal deletes invitations", func(t *testing.T) { ctx := testcontext.New(t) require.NoError(t, projectsDB.Delete(ctx, projID)) invites, err := invitesDB.GetByProjectID(ctx, projID) require.NoError(t, err) require.Empty(t, invites) invites, err = invitesDB.GetByEmail(ctx, email) require.NoError(t, err) require.Equal(t, invites, []console.ProjectInvitation{*inviteSameEmail}) }) }) } func TestDeleteBefore(t *testing.T) { maxAge := time.Hour now := time.Now() expiration := now.Add(-maxAge) satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) { invitesDB := db.Console().ProjectInvitations() // Only positive page sizes should be allowed. require.Error(t, invitesDB.DeleteBefore(ctx, time.Time{}, 0, 0)) require.Error(t, invitesDB.DeleteBefore(ctx, time.Time{}, 0, -1)) createInvite := func() *console.ProjectInvitation { projID := testrand.UUID() _, err := db.Console().Projects().Insert(ctx, &console.Project{ID: projID}) require.NoError(t, err) invite, err := invitesDB.Insert(ctx, &console.ProjectInvitation{ProjectID: projID}) require.NoError(t, err) return invite } newInvite := createInvite() oldInvite := createInvite() oldCreatedAt := expiration.Add(-time.Second) oldInvite, err := invitesDB.Update(ctx, oldInvite.ProjectID, oldInvite.Email, console.UpdateProjectInvitationRequest{ CreatedAt: &oldCreatedAt, }) require.NoError(t, err) require.NoError(t, invitesDB.DeleteBefore(ctx, expiration, 0, 1)) // Ensure that the old invitation record was deleted and the other remains. _, err = invitesDB.Get(ctx, oldInvite.ProjectID, oldInvite.Email) require.ErrorIs(t, err, sql.ErrNoRows) _, err = invitesDB.Get(ctx, newInvite.ProjectID, newInvite.Email) require.NoError(t, err) }) }