storj/satellite/console/consoleweb/consoleapi/projects_test.go

471 lines
15 KiB
Go
Raw Normal View History

// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
package consoleapi_test
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"storj.io/common/memory"
"storj.io/common/storj"
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/common/uuid"
"storj.io/storj/private/testplanet"
"storj.io/storj/satellite"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/console/consoleweb/consoleapi"
)
func createTestMembers(ctx context.Context, t *testing.T, db console.DB, p uuid.UUID, owner *uuid.UUID) (_ map[uuid.UUID]console.User, _ map[string]console.User) {
members := make(map[uuid.UUID]console.User)
invitees := make(map[string]console.User)
for i := 0; i < 3; i++ {
memberID := testrand.UUID()
member, err := db.Users().Insert(ctx, &console.User{
ID: memberID,
FullName: fmt.Sprintf("Member FullName%c", rune('A'+i)),
ShortName: fmt.Sprintf("Member ShortName%c", rune('A'+i)),
Email: fmt.Sprintf("member%d@storj.test", i),
ProjectLimit: 1,
ProjectStorageLimit: (memory.GB * 150).Int64(),
ProjectBandwidthLimit: (memory.GB * 150).Int64(),
PasswordHash: []byte("test"),
})
require.NoError(t, err)
members[memberID] = *member
status := console.UserStatus(1)
err = db.Users().Update(ctx, memberID, console.UpdateUserRequest{
Status: &status,
})
require.NoError(t, err)
_, err = db.ProjectMembers().Insert(ctx, member.ID, p)
require.NoError(t, err)
inviteeID := testrand.UUID()
inviteeEmail := fmt.Sprintf("invitee%d@storj.test", i)
invitee, err := db.Users().Insert(ctx, &console.User{
ID: inviteeID,
FullName: fmt.Sprintf("Invitee FullName%c", rune('A'+i)),
ShortName: fmt.Sprintf("Invitee ShortName%c", rune('A'+i)),
Email: inviteeEmail,
ProjectLimit: 1,
ProjectStorageLimit: (memory.GB * 150).Int64(),
ProjectBandwidthLimit: (memory.GB * 150).Int64(),
PasswordHash: []byte("test"),
})
require.NoError(t, err)
invitees[inviteeEmail] = *invitee
_, err = db.ProjectInvitations().Upsert(ctx, &console.ProjectInvitation{
ProjectID: p,
Email: inviteeEmail,
InviterID: owner,
CreatedAt: time.Now(),
})
require.NoError(t, err)
}
return members, invitees
}
func TestGetProjectMembersAndInvitationsOrdering(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
p := planet.Uplinks[0].Projects[0].ID
user, err := sat.DB.Console().Users().GetByEmail(ctx, planet.Uplinks[0].User[sat.ID()].Email)
require.NoError(t, err)
members, invitees := createTestMembers(ctx, t, sat.DB.Console(), p, &user.ID)
members[user.ID] = *user
tests := []struct {
order, orderDir int
}{
{ // ascending by name
order: 1,
orderDir: 1,
},
{ // descending by name
order: 1,
orderDir: 2,
},
{ // ascending by email
order: 2,
orderDir: 1,
},
{ // descending by email
order: 2,
orderDir: 2,
},
{ // ascending by created at
order: 3,
orderDir: 1,
},
{ // descending by created at
order: 3,
orderDir: 2,
},
}
for _, tt := range tests {
endpoint := fmt.Sprintf("projects/%s/members?limit=100&page=1&order=%d&order-direction=%d", p.String(), tt.order, tt.orderDir)
body, status, err := doRequestWithAuth(ctx, t, sat, user, http.MethodGet, endpoint, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
var membersAndInvitations consoleapi.ProjectMembersPage
require.NoError(t, json.Unmarshal(body, &membersAndInvitations))
respMembers := membersAndInvitations.Members
respInvitees := membersAndInvitations.Invitations
for i := 1; i < len(respMembers); i++ {
switch tt.orderDir {
case int(console.Ascending):
switch tt.order {
case int(console.Name):
require.Less(t, members[respMembers[i-1].User.ID].FullName, members[respMembers[i].User.ID].FullName)
case int(console.Email):
require.Less(t, members[respMembers[i-1].User.ID].Email, members[respMembers[i].User.ID].Email)
case int(console.Created):
require.Less(t, members[respMembers[i-1].User.ID].CreatedAt, members[respMembers[i].User.ID].CreatedAt)
default:
t.Error("invalid order", tt.order)
}
case int(console.Descending):
switch tt.order {
case int(console.Name):
require.Greater(t, members[respMembers[i-1].User.ID].FullName, members[respMembers[i].User.ID].FullName)
case int(console.Email):
require.Greater(t, members[respMembers[i-1].User.ID].Email, members[respMembers[i].User.ID].Email)
case int(console.Created):
require.Greater(t, members[respMembers[i-1].User.ID].CreatedAt, members[respMembers[i].User.ID].CreatedAt)
default:
t.Error("invalid order", tt.order)
}
default:
t.Error("invalid order direction", tt.orderDir)
}
}
for i := 1; i < len(respInvitees); i++ {
switch tt.orderDir {
case int(console.Ascending):
switch tt.order {
case int(console.Name):
require.Less(t, invitees[respInvitees[i-1].Email].FullName, invitees[respInvitees[i].Email].FullName)
case int(console.Email):
require.Less(t, invitees[respInvitees[i-1].Email].Email, invitees[respInvitees[i].Email].Email)
case int(console.Created):
require.Less(t, invitees[respInvitees[i-1].Email].CreatedAt, invitees[respInvitees[i].Email].CreatedAt)
default:
t.Error("invalid order", tt.order)
}
case int(console.Descending):
switch tt.order {
case int(console.Name):
require.Greater(t, invitees[respInvitees[i-1].Email].FullName, invitees[respInvitees[i].Email].FullName)
case int(console.Email):
require.Greater(t, invitees[respInvitees[i-1].Email].Email, invitees[respInvitees[i].Email].Email)
case int(console.Created):
require.Greater(t, invitees[respInvitees[i-1].Email].CreatedAt, invitees[respInvitees[i].Email].CreatedAt)
default:
t.Error("invalid order", tt.order)
}
default:
t.Error("invalid order direction", tt.orderDir)
}
}
}
})
}
func TestGetProjectMembersAndInvitationsSearch(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
p := planet.Uplinks[0].Projects[0].ID
user, err := sat.DB.Console().Users().GetByEmail(ctx, planet.Uplinks[0].User[sat.ID()].Email)
require.NoError(t, err)
members, invitees := createTestMembers(ctx, t, sat.DB.Console(), p, &user.ID)
members[user.ID] = *user
tests := []struct {
search string
expectedMembers, expectedInvitees int
}{
{ // all members and invitees
"",
4,
3,
},
{ // zero members zero invitees
"asdf",
0,
0,
},
{ // one member one invitee by email
"1",
1,
1,
},
{ // three members by full name
"Member FullName",
3,
0,
},
{ // three invitees by email
"invitee",
0,
3,
},
{ // one member by short name
"Member ShortNameA",
1,
0,
},
}
for _, tt := range tests {
endpoint := fmt.Sprintf("projects/%s/members?limit=100&page=1&order=1&order-direction=1&", p.String())
params := url.Values{}
params.Add("search", tt.search)
endpoint += params.Encode()
body, status, err := doRequestWithAuth(ctx, t, sat, user, http.MethodGet, endpoint, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
var membersAndInvitations consoleapi.ProjectMembersPage
require.NoError(t, json.Unmarshal(body, &membersAndInvitations))
respMembers := membersAndInvitations.Members
respInvitees := membersAndInvitations.Invitations
require.Equal(t, tt.expectedMembers, len(respMembers))
require.Equal(t, tt.expectedInvitees, len(respInvitees))
if tt.search != "" {
for _, m := range respMembers {
containsSearch := strings.Contains(members[m.User.ID].Email, tt.search) || strings.Contains(members[m.User.ID].FullName, tt.search) || strings.Contains(members[m.User.ID].ShortName, tt.search)
require.True(t, containsSearch)
}
for _, inv := range respInvitees {
containsSearch := strings.Contains(inv.Email, tt.search) || strings.Contains(invitees[inv.Email].FullName, tt.search) || strings.Contains(invitees[inv.Email].ShortName, tt.search)
require.True(t, containsSearch)
}
}
}
})
}
func TestGetProjectMembersAndInvitationsLimitAndPage(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
p := planet.Uplinks[0].Projects[0].ID
user, err := sat.DB.Console().Users().GetByEmail(ctx, planet.Uplinks[0].User[sat.ID()].Email)
require.NoError(t, err)
members, _ := createTestMembers(ctx, t, sat.DB.Console(), p, &user.ID)
members[user.ID] = *user
limit := 1
page := 1
var previousResult console.ProjectMembersPage
for i := 0; i < 2; i++ {
endpoint := fmt.Sprintf("projects/%s/members?order=1&order-direction=1&", p.String())
params := url.Values{}
params.Add("limit", fmt.Sprint(limit))
params.Add("page", fmt.Sprint(page))
endpoint += params.Encode()
body, status, err := doRequestWithAuth(ctx, t, sat, user, http.MethodGet, endpoint, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
var membersAndInvitations console.ProjectMembersPage
require.NoError(t, json.Unmarshal(body, &membersAndInvitations))
respMembers := membersAndInvitations.ProjectMembers
respInvitees := membersAndInvitations.ProjectInvitations
length := len(respMembers) + len(respInvitees)
require.Equal(t, limit, length)
require.Equal(t, page, int(membersAndInvitations.CurrentPage))
if i != 0 {
require.NotEqual(t, previousResult, membersAndInvitations)
}
previousResult = membersAndInvitations
limit++
page++
}
})
}
func TestDeleteProjectMembers(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
p := planet.Uplinks[0].Projects[0].ID
user, err := sat.DB.Console().Users().GetByEmail(ctx, planet.Uplinks[0].User[sat.ID()].Email)
require.NoError(t, err)
members, invitees := createTestMembers(ctx, t, sat.DB.Console(), p, &user.ID)
var emails string
var firstAppendDone bool
for _, m := range members {
if firstAppendDone {
emails += ","
} else {
firstAppendDone = true
}
emails += m.Email
}
for e := range invitees {
if len(members) > 0 {
emails += ","
}
emails += e
}
endpoint := fmt.Sprintf("projects/%s/members?", p.String())
params := url.Values{}
params.Add("emails", emails)
endpoint += params.Encode()
body, status, err := doRequestWithAuth(ctx, t, sat, user, http.MethodDelete, endpoint, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
require.NotContains(t, string(body), "error")
page, err := sat.DB.Console().ProjectMembers().GetPagedWithInvitationsByProjectID(ctx, p, console.ProjectMembersCursor{Limit: 1, Page: 1})
require.NoError(t, err)
require.Len(t, page.ProjectMembers, 1)
require.Equal(t, user.ID, page.ProjectMembers[0].MemberID)
// test error
endpoint = fmt.Sprintf("projects/%s/members?", p.String())
params = url.Values{}
params.Add("emails", "nonmember@storj.test")
endpoint += params.Encode()
body, status, err = doRequestWithAuth(ctx, t, sat, user, http.MethodDelete, endpoint, nil)
require.NoError(t, err)
require.Equal(t, http.StatusInternalServerError, status)
require.Contains(t, string(body), "error")
})
}
func TestEdgeURLOverrides(t *testing.T) {
var (
noOverridePlacementID storj.PlacementConstraint
partialOverridePlacementID storj.PlacementConstraint = 1
fullOverridePlacementID storj.PlacementConstraint = 2
authServiceURL = "auth.storj.io"
publicLinksharingURL = "public-link.storj.io"
internalLinksharingURL = "link.storj.io"
)
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1, UplinkCount: 1,
Reconfigure: testplanet.Reconfigure{
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
err := config.Console.PlacementEdgeURLOverrides.Set(
fmt.Sprintf(
`{
"%d": {"authService": "%s"},
"%d": {
"authService": "%s",
"publicLinksharing": "%s",
"internalLinksharing": "%s"
}
}`,
partialOverridePlacementID, authServiceURL,
fullOverridePlacementID, authServiceURL, publicLinksharingURL, internalLinksharingURL,
),
)
require.NoError(t, err)
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
project, err := sat.DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID)
require.NoError(t, err)
user, err := sat.API.Console.Service.GetUser(ctx, project.OwnerID)
require.NoError(t, err)
for _, tt := range []struct {
name string
placement *storj.PlacementConstraint
expectedEdgeURLs *console.EdgeURLOverrides
}{
{"nil placement", nil, nil},
{"placement with no overrides", &noOverridePlacementID, nil},
{
"placement with partial override",
&partialOverridePlacementID,
&console.EdgeURLOverrides{AuthService: authServiceURL},
}, {
"placement with full override",
&fullOverridePlacementID,
&console.EdgeURLOverrides{
AuthService: authServiceURL,
PublicLinksharing: publicLinksharingURL,
InternalLinksharing: internalLinksharingURL,
},
},
} {
t.Run(tt.name, func(t *testing.T) {
result, err := sat.DB.Testing().RawDB().ExecContext(ctx,
"UPDATE projects SET default_placement = $1 WHERE id = $2",
tt.placement, project.ID,
)
require.NoError(t, err)
count, err := result.RowsAffected()
require.NoError(t, err)
require.EqualValues(t, 1, count)
body, status, err := doRequestWithAuth(ctx, t, sat, user, http.MethodGet, "projects", nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)
var infos []console.ProjectInfo
require.NoError(t, json.Unmarshal(body, &infos))
require.NotEmpty(t, infos)
if tt.expectedEdgeURLs == nil {
require.Nil(t, infos[0].EdgeURLOverrides)
return
}
require.NotNil(t, infos[0].EdgeURLOverrides)
require.Equal(t, *tt.expectedEdgeURLs, *infos[0].EdgeURLOverrides)
})
}
})
}