6b65b7e7d0
This change removes the obsolete project member paging code. Previously, we implemented functionality for including project invitations in pages of project members. However, the satellite frontend still expected API responses to use the old paging style, so the related code could not be removed right away. Now that the frontend has been updated, this code is no longer necessary. References #5855 Change-Id: I12fdaaeb869977c4d87a0d50b9a7b11c68552c82
2133 lines
74 KiB
Go
2133 lines
74 KiB
Go
// Copyright (C) 2020 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package console_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"storj.io/common/currency"
|
|
"storj.io/common/macaroon"
|
|
"storj.io/common/memory"
|
|
"storj.io/common/storj"
|
|
"storj.io/common/testcontext"
|
|
"storj.io/common/testrand"
|
|
"storj.io/common/uuid"
|
|
"storj.io/storj/private/blockchain"
|
|
"storj.io/storj/private/testplanet"
|
|
"storj.io/storj/satellite"
|
|
"storj.io/storj/satellite/buckets"
|
|
"storj.io/storj/satellite/console"
|
|
"storj.io/storj/satellite/payments"
|
|
"storj.io/storj/satellite/payments/billing"
|
|
"storj.io/storj/satellite/payments/coinpayments"
|
|
"storj.io/storj/satellite/payments/storjscan"
|
|
"storj.io/storj/satellite/payments/storjscan/blockchaintest"
|
|
"storj.io/storj/satellite/payments/stripe"
|
|
)
|
|
|
|
func TestService(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 3,
|
|
Reconfigure: testplanet.Reconfigure{
|
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
|
config.Payments.StripeCoinPayments.StripeFreeTierCouponID = stripe.MockCouponID1
|
|
},
|
|
},
|
|
},
|
|
func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
service := sat.API.Console.Service
|
|
|
|
up1Proj, err := sat.API.DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID)
|
|
require.NoError(t, err)
|
|
up2Proj, err := sat.API.DB.Console().Projects().Get(ctx, planet.Uplinks[1].Projects[0].ID)
|
|
require.NoError(t, err)
|
|
|
|
require.NotEqual(t, up1Proj.ID, up2Proj.ID)
|
|
require.NotEqual(t, up1Proj.OwnerID, up2Proj.OwnerID)
|
|
|
|
userCtx1, err := sat.UserContext(ctx, up1Proj.OwnerID)
|
|
require.NoError(t, err)
|
|
|
|
userCtx2, err := sat.UserContext(ctx, up2Proj.OwnerID)
|
|
require.NoError(t, err)
|
|
|
|
getOwnerAndCtx := func(ctx context.Context, proj *console.Project) (user *console.User, userCtx context.Context) {
|
|
user, err := sat.API.DB.Console().Users().Get(ctx, proj.OwnerID)
|
|
require.NoError(t, err)
|
|
userCtx, err = sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
return
|
|
}
|
|
|
|
t.Run("GetProject", func(t *testing.T) {
|
|
// Getting own project details should work
|
|
project, err := service.GetProject(userCtx1, up1Proj.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, up1Proj.ID, project.ID)
|
|
|
|
// Getting someone else project details should not work
|
|
project, err = service.GetProject(userCtx1, up2Proj.ID)
|
|
require.Error(t, err)
|
|
require.Nil(t, project)
|
|
})
|
|
|
|
t.Run("GetSalt", func(t *testing.T) {
|
|
// Getting project salt as a member should work
|
|
salt, err := service.GetSalt(userCtx1, up1Proj.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, salt)
|
|
|
|
// Getting project salt with publicID should work
|
|
salt1, err := service.GetSalt(userCtx1, up1Proj.PublicID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, salt1)
|
|
|
|
// project.PublicID salt should be the same as project.ID salt
|
|
require.Equal(t, salt, salt1)
|
|
|
|
// Getting project salt as a non-member should not work
|
|
salt, err = service.GetSalt(userCtx1, up2Proj.ID)
|
|
require.Error(t, err)
|
|
require.Nil(t, salt)
|
|
})
|
|
|
|
t.Run("AddCreditCard fails when payments.CreditCards.Add returns error", func(t *testing.T) {
|
|
// user should be in free tier
|
|
user, userCtx1 := getOwnerAndCtx(ctx, up1Proj)
|
|
require.False(t, user.PaidTier)
|
|
|
|
// stripecoinpayments.TestPaymentMethodsAttachFailure triggers the underlying mock stripe client to return an error
|
|
// when attaching a payment method to a customer.
|
|
_, err = service.Payments().AddCreditCard(userCtx1, stripe.TestPaymentMethodsAttachFailure)
|
|
require.Error(t, err)
|
|
|
|
// user still in free tier
|
|
user, err = service.GetUser(ctx, up1Proj.OwnerID)
|
|
require.NoError(t, err)
|
|
require.False(t, user.PaidTier)
|
|
|
|
cards, err := service.Payments().ListCreditCards(userCtx1)
|
|
require.NoError(t, err)
|
|
require.Len(t, cards, 0)
|
|
})
|
|
|
|
t.Run("AddCreditCard", func(t *testing.T) {
|
|
// user should be in free tier
|
|
user, userCtx1 := getOwnerAndCtx(ctx, up1Proj)
|
|
require.False(t, user.PaidTier)
|
|
|
|
// add a credit card to put the user in the paid tier
|
|
card, err := service.Payments().AddCreditCard(userCtx1, "test-cc-token")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, card)
|
|
// user should be in paid tier
|
|
user, err = service.GetUser(ctx, up1Proj.OwnerID)
|
|
require.NoError(t, err)
|
|
require.True(t, user.PaidTier)
|
|
|
|
cards, err := service.Payments().ListCreditCards(userCtx1)
|
|
require.NoError(t, err)
|
|
require.Len(t, cards, 1)
|
|
})
|
|
|
|
t.Run("CreateProject", func(t *testing.T) {
|
|
// Creating a project with a previously used name should fail
|
|
createdProject, err := service.CreateProject(userCtx1, console.ProjectInfo{
|
|
Name: up1Proj.Name,
|
|
})
|
|
require.Error(t, err)
|
|
require.Nil(t, createdProject)
|
|
})
|
|
|
|
t.Run("CreateProject with placement", func(t *testing.T) {
|
|
uid := planet.Uplinks[2].Projects[0].Owner.ID
|
|
err := sat.API.DB.Console().Users().Update(ctx, uid, console.UpdateUserRequest{
|
|
DefaultPlacement: storj.EU,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
user, err := service.GetUser(ctx, uid)
|
|
require.NoError(t, err)
|
|
|
|
userCtx, err := sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
p, err := service.CreateProject(userCtx, console.ProjectInfo{
|
|
Name: "eu-project",
|
|
Description: "project with eu1 default placement",
|
|
CreatedAt: time.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, storj.EU, p.DefaultPlacement)
|
|
|
|
})
|
|
|
|
t.Run("UpdateProject", func(t *testing.T) {
|
|
updatedName := "newName"
|
|
updatedDescription := "newDescription"
|
|
updatedStorageLimit := memory.Size(100)
|
|
updatedBandwidthLimit := memory.Size(100)
|
|
|
|
_, userCtx1 := getOwnerAndCtx(ctx, up1Proj)
|
|
|
|
// Updating own project should work
|
|
updatedProject, err := service.UpdateProject(userCtx1, up1Proj.ID, console.ProjectInfo{
|
|
Name: updatedName,
|
|
Description: updatedDescription,
|
|
StorageLimit: updatedStorageLimit,
|
|
BandwidthLimit: updatedBandwidthLimit,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, up1Proj.Name, updatedProject.Name)
|
|
require.Equal(t, updatedName, updatedProject.Name)
|
|
require.NotEqual(t, up1Proj.Description, updatedProject.Description)
|
|
require.Equal(t, updatedDescription, updatedProject.Description)
|
|
require.NotEqual(t, *up1Proj.StorageLimit, *updatedProject.StorageLimit)
|
|
require.Equal(t, updatedStorageLimit, *updatedProject.StorageLimit)
|
|
require.NotEqual(t, *up1Proj.BandwidthLimit, *updatedProject.BandwidthLimit)
|
|
require.Equal(t, updatedBandwidthLimit, *updatedProject.BandwidthLimit)
|
|
require.Equal(t, updatedStorageLimit, *updatedProject.UserSpecifiedStorageLimit)
|
|
require.Equal(t, updatedBandwidthLimit, *updatedProject.UserSpecifiedBandwidthLimit)
|
|
|
|
// Updating someone else project details should not work
|
|
updatedProject, err = service.UpdateProject(userCtx1, up2Proj.ID, console.ProjectInfo{
|
|
Name: "newName",
|
|
Description: "TestUpdate",
|
|
StorageLimit: memory.Size(100),
|
|
BandwidthLimit: memory.Size(100),
|
|
})
|
|
require.Error(t, err)
|
|
require.Nil(t, updatedProject)
|
|
|
|
// attempting to update a project with bandwidth or storage limits set to 0 should fail
|
|
size0 := new(memory.Size)
|
|
*size0 = 0
|
|
size100 := new(memory.Size)
|
|
*size100 = memory.Size(100)
|
|
|
|
up1Proj.StorageLimit = size0
|
|
err = sat.DB.Console().Projects().Update(ctx, up1Proj)
|
|
require.NoError(t, err)
|
|
|
|
updateInfo := console.ProjectInfo{
|
|
Name: "a b c",
|
|
Description: "1 2 3",
|
|
StorageLimit: memory.Size(123),
|
|
BandwidthLimit: memory.Size(123),
|
|
}
|
|
updatedProject, err = service.UpdateProject(userCtx1, up1Proj.ID, updateInfo)
|
|
require.Error(t, err)
|
|
require.Nil(t, updatedProject)
|
|
|
|
up1Proj.StorageLimit = size100
|
|
up1Proj.BandwidthLimit = size0
|
|
|
|
err = sat.DB.Console().Projects().Update(ctx, up1Proj)
|
|
require.NoError(t, err)
|
|
|
|
updatedProject, err = service.UpdateProject(userCtx1, up1Proj.ID, updateInfo)
|
|
require.Error(t, err)
|
|
require.Nil(t, updatedProject)
|
|
|
|
up1Proj.StorageLimit = size100
|
|
up1Proj.BandwidthLimit = size100
|
|
err = sat.DB.Console().Projects().Update(ctx, up1Proj)
|
|
require.NoError(t, err)
|
|
|
|
updatedProject, err = service.UpdateProject(userCtx1, up1Proj.ID, updateInfo)
|
|
require.NoError(t, err)
|
|
require.Equal(t, updateInfo.Name, updatedProject.Name)
|
|
require.Equal(t, updateInfo.Description, updatedProject.Description)
|
|
require.NotNil(t, updatedProject.StorageLimit)
|
|
require.NotNil(t, updatedProject.BandwidthLimit)
|
|
require.Equal(t, updateInfo.StorageLimit, *updatedProject.StorageLimit)
|
|
require.Equal(t, updateInfo.BandwidthLimit, *updatedProject.BandwidthLimit)
|
|
|
|
project, err := service.GetProject(userCtx1, up1Proj.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, updateInfo.StorageLimit, *project.StorageLimit)
|
|
require.Equal(t, updateInfo.BandwidthLimit, *project.BandwidthLimit)
|
|
|
|
// attempting to update a project with a previously used name should fail
|
|
updatedProject, err = service.UpdateProject(userCtx1, up2Proj.ID, console.ProjectInfo{
|
|
Name: up1Proj.Name,
|
|
})
|
|
require.Error(t, err)
|
|
require.Nil(t, updatedProject)
|
|
})
|
|
|
|
t.Run("AddProjectMembers", func(t *testing.T) {
|
|
up2User, _ := getOwnerAndCtx(ctx, up2Proj)
|
|
|
|
// Adding members to own project should work
|
|
addedUsers, err := service.AddProjectMembers(userCtx1, up1Proj.ID, []string{up2User.Email})
|
|
require.NoError(t, err)
|
|
require.Len(t, addedUsers, 1)
|
|
require.Contains(t, addedUsers, up2User)
|
|
|
|
// Adding members to someone else project should not work
|
|
addedUsers, err = service.AddProjectMembers(userCtx1, up2Proj.ID, []string{up2User.Email})
|
|
require.Error(t, err)
|
|
require.Nil(t, addedUsers)
|
|
})
|
|
|
|
t.Run("GetProjectMembersAndInvitations", func(t *testing.T) {
|
|
// Getting the project members of an own project that one is a part of should work
|
|
userPage, err := service.GetProjectMembersAndInvitations(userCtx1, up1Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10})
|
|
require.NoError(t, err)
|
|
require.Len(t, userPage.ProjectMembers, 2)
|
|
|
|
// Getting the project members of a foreign project that one is a part of should work
|
|
userPage, err = service.GetProjectMembersAndInvitations(userCtx2, up1Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10})
|
|
require.NoError(t, err)
|
|
require.Len(t, userPage.ProjectMembers, 2)
|
|
|
|
// Getting the project members of a foreign project that one is not a part of should not work
|
|
userPage, err = service.GetProjectMembersAndInvitations(userCtx1, up2Proj.ID, console.ProjectMembersCursor{Page: 1, Limit: 10})
|
|
require.Error(t, err)
|
|
require.Nil(t, userPage)
|
|
})
|
|
|
|
t.Run("DeleteProjectMembersAndInvitations", func(t *testing.T) {
|
|
user1, user1Ctx := getOwnerAndCtx(ctx, up1Proj)
|
|
_, user2Ctx := getOwnerAndCtx(ctx, up2Proj)
|
|
|
|
invitedUser, err := sat.AddUser(ctx, console.CreateUser{
|
|
FullName: "Test User",
|
|
Email: "test@mail.test",
|
|
}, 1)
|
|
require.NoError(t, err)
|
|
|
|
for _, id := range []uuid.UUID{up1Proj.ID, up2Proj.ID} {
|
|
_, err = sat.DB.Console().ProjectInvitations().Insert(ctx, &console.ProjectInvitation{
|
|
ProjectID: id,
|
|
Email: invitedUser.Email,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// You should not be able to remove someone from a project that you aren't a member of.
|
|
err = service.DeleteProjectMembersAndInvitations(user1Ctx, up2Proj.ID, []string{invitedUser.Email})
|
|
require.Error(t, err)
|
|
|
|
// Project owners should not be able to be removed.
|
|
err = service.DeleteProjectMembersAndInvitations(user2Ctx, up1Proj.ID, []string{user1.Email})
|
|
require.Error(t, err)
|
|
|
|
// An invalid email should cause the operation to fail.
|
|
err = service.DeleteProjectMembersAndInvitations(user2Ctx, up2Proj.ID, []string{invitedUser.Email, "nobody@mail.test"})
|
|
require.Error(t, err)
|
|
_, err = sat.DB.Console().ProjectInvitations().Get(ctx, up2Proj.ID, invitedUser.Email)
|
|
require.NoError(t, err)
|
|
|
|
// Members and invitations should be removed.
|
|
err = service.DeleteProjectMembersAndInvitations(user2Ctx, up2Proj.ID, []string{invitedUser.Email, user1.Email})
|
|
require.NoError(t, err)
|
|
|
|
_, err = sat.DB.Console().ProjectInvitations().Get(ctx, up2Proj.ID, invitedUser.Email)
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
|
|
memberships, err := sat.DB.Console().ProjectMembers().GetByMemberID(ctx, user1.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, memberships, 1)
|
|
require.NotEqual(t, up2Proj.ID, memberships[0].ProjectID)
|
|
})
|
|
|
|
t.Run("DeleteProject", func(t *testing.T) {
|
|
// Deleting the own project should not work before deleting the API-Key
|
|
err := service.DeleteProject(userCtx1, up1Proj.ID)
|
|
require.Error(t, err)
|
|
|
|
keys, err := service.GetAPIKeys(userCtx1, up1Proj.ID, console.APIKeyCursor{Page: 1, Limit: 10})
|
|
require.NoError(t, err)
|
|
require.Len(t, keys.APIKeys, 1)
|
|
|
|
err = service.DeleteAPIKeys(userCtx1, []uuid.UUID{keys.APIKeys[0].ID})
|
|
require.NoError(t, err)
|
|
|
|
// Deleting the own project should now work
|
|
err = service.DeleteProject(userCtx1, up1Proj.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Deleting someone else project should not work
|
|
err = service.DeleteProject(userCtx1, up2Proj.ID)
|
|
require.Error(t, err)
|
|
|
|
err = planet.Uplinks[1].CreateBucket(ctx, sat, "testbucket")
|
|
require.NoError(t, err)
|
|
|
|
// deleting a project with a bucket should fail
|
|
err = service.DeleteProject(userCtx2, up2Proj.ID)
|
|
require.Error(t, err)
|
|
require.Equal(t, "console service: project usage: some buckets still exist", err.Error())
|
|
})
|
|
|
|
t.Run("GetProjectUsageLimits", func(t *testing.T) {
|
|
bandwidthLimit := sat.Config.Console.UsageLimits.Bandwidth.Free
|
|
storageLimit := sat.Config.Console.UsageLimits.Storage.Free
|
|
|
|
limits1, err := service.GetProjectUsageLimits(userCtx2, up2Proj.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, limits1)
|
|
|
|
// Get usage limits with publicID
|
|
limits2, err := service.GetProjectUsageLimits(userCtx2, up2Proj.PublicID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, limits2)
|
|
|
|
// limits gotten by ID and publicID should be the same
|
|
require.Equal(t, storageLimit.Int64(), limits1.StorageLimit)
|
|
require.Equal(t, bandwidthLimit.Int64(), limits1.BandwidthLimit)
|
|
require.Equal(t, storageLimit.Int64(), limits2.StorageLimit)
|
|
require.Equal(t, bandwidthLimit.Int64(), limits2.BandwidthLimit)
|
|
|
|
// update project's limits
|
|
updatedStorageLimit := memory.Size(100) + memory.TB
|
|
updatedBandwidthLimit := memory.Size(100) + memory.TB
|
|
up2Proj.StorageLimit = new(memory.Size)
|
|
*up2Proj.StorageLimit = updatedStorageLimit
|
|
up2Proj.BandwidthLimit = new(memory.Size)
|
|
*up2Proj.BandwidthLimit = updatedBandwidthLimit
|
|
err = sat.DB.Console().Projects().Update(ctx, up2Proj)
|
|
require.NoError(t, err)
|
|
|
|
limits1, err = service.GetProjectUsageLimits(userCtx2, up2Proj.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, limits1)
|
|
|
|
// Get usage limits with publicID
|
|
limits2, err = service.GetProjectUsageLimits(userCtx2, up2Proj.PublicID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, limits2)
|
|
|
|
// limits gotten by ID and publicID should be the same
|
|
require.Equal(t, updatedStorageLimit.Int64(), limits1.StorageLimit)
|
|
require.Equal(t, updatedBandwidthLimit.Int64(), limits1.BandwidthLimit)
|
|
require.Equal(t, updatedStorageLimit.Int64(), limits2.StorageLimit)
|
|
require.Equal(t, updatedBandwidthLimit.Int64(), limits2.BandwidthLimit)
|
|
})
|
|
|
|
t.Run("ChangeEmail", func(t *testing.T) {
|
|
const newEmail = "newEmail@example.com"
|
|
|
|
err = service.ChangeEmail(userCtx2, newEmail)
|
|
require.NoError(t, err)
|
|
|
|
user, _, err := service.GetUserByEmailWithUnverified(userCtx2, newEmail)
|
|
require.NoError(t, err)
|
|
require.Equal(t, newEmail, user.Email)
|
|
|
|
err = service.ChangeEmail(userCtx2, newEmail)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("GetAllBucketNames", func(t *testing.T) {
|
|
bucket1 := buckets.Bucket{
|
|
ID: testrand.UUID(),
|
|
Name: "testBucket1",
|
|
ProjectID: up2Proj.ID,
|
|
}
|
|
|
|
bucket2 := buckets.Bucket{
|
|
ID: testrand.UUID(),
|
|
Name: "testBucket2",
|
|
ProjectID: up2Proj.ID,
|
|
}
|
|
|
|
_, err := sat.API.Buckets.Service.CreateBucket(userCtx2, bucket1)
|
|
require.NoError(t, err)
|
|
|
|
_, err = sat.API.Buckets.Service.CreateBucket(userCtx2, bucket2)
|
|
require.NoError(t, err)
|
|
|
|
bucketNames, err := service.GetAllBucketNames(userCtx2, up2Proj.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, bucket1.Name, bucketNames[0])
|
|
require.Equal(t, bucket2.Name, bucketNames[1])
|
|
|
|
bucketNames, err = service.GetAllBucketNames(userCtx2, up2Proj.PublicID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, bucket1.Name, bucketNames[0])
|
|
require.Equal(t, bucket2.Name, bucketNames[1])
|
|
|
|
// Getting someone else buckets should not work
|
|
bucketsForUnauthorizedUser, err := service.GetAllBucketNames(userCtx1, up2Proj.ID)
|
|
require.Error(t, err)
|
|
require.Nil(t, bucketsForUnauthorizedUser)
|
|
})
|
|
|
|
t.Run("DeleteAPIKeyByNameAndProjectID", func(t *testing.T) {
|
|
secret, err := macaroon.NewSecret()
|
|
require.NoError(t, err)
|
|
|
|
key, err := macaroon.NewAPIKey(secret)
|
|
require.NoError(t, err)
|
|
|
|
apikey := console.APIKeyInfo{
|
|
Name: "test",
|
|
ProjectID: up2Proj.ID,
|
|
Secret: secret,
|
|
}
|
|
|
|
createdKey, err := sat.DB.Console().APIKeys().Create(ctx, key.Head(), apikey)
|
|
require.NoError(t, err)
|
|
|
|
info, err := sat.DB.Console().APIKeys().Get(ctx, createdKey.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, info)
|
|
|
|
// Deleting someone else api keys should not work
|
|
err = service.DeleteAPIKeyByNameAndProjectID(userCtx1, apikey.Name, up2Proj.ID)
|
|
require.Error(t, err)
|
|
|
|
err = service.DeleteAPIKeyByNameAndProjectID(userCtx2, apikey.Name, up2Proj.ID)
|
|
require.NoError(t, err)
|
|
|
|
info, err = sat.DB.Console().APIKeys().Get(ctx, createdKey.ID)
|
|
require.Error(t, err)
|
|
require.Nil(t, info)
|
|
|
|
// test deleting by project.publicID
|
|
createdKey, err = sat.DB.Console().APIKeys().Create(ctx, key.Head(), apikey)
|
|
require.NoError(t, err)
|
|
|
|
info, err = sat.DB.Console().APIKeys().Get(ctx, createdKey.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, info)
|
|
|
|
// deleting by project.publicID
|
|
err = service.DeleteAPIKeyByNameAndProjectID(userCtx2, apikey.Name, up2Proj.PublicID)
|
|
require.NoError(t, err)
|
|
|
|
info, err = sat.DB.Console().APIKeys().Get(ctx, createdKey.ID)
|
|
require.Error(t, err)
|
|
require.Nil(t, info)
|
|
})
|
|
t.Run("ApplyFreeTierCoupon", func(t *testing.T) {
|
|
// testplanet applies the free tier coupon first, so we need to change it in order
|
|
// to verify that ApplyFreeTierCoupon really works.
|
|
freeTier := sat.Config.Payments.StripeCoinPayments.StripeFreeTierCouponID
|
|
coupon3, err := service.Payments().ApplyCoupon(userCtx1, stripe.MockCouponID3)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, coupon3)
|
|
require.NotEqual(t, freeTier, coupon3.ID)
|
|
|
|
coupon, err := service.Payments().ApplyFreeTierCoupon(userCtx1)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, coupon)
|
|
require.Equal(t, freeTier, coupon.ID)
|
|
|
|
coupon, err = sat.API.Payments.Accounts.Coupons().GetByUserID(ctx, up1Proj.OwnerID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, freeTier, coupon.ID)
|
|
|
|
})
|
|
t.Run("ApplyFreeTierCoupon fails with unknown user", func(t *testing.T) {
|
|
coupon, err := service.Payments().ApplyFreeTierCoupon(ctx)
|
|
require.Error(t, err)
|
|
require.Nil(t, coupon)
|
|
})
|
|
t.Run("ApplyCoupon", func(t *testing.T) {
|
|
id := stripe.MockCouponID2
|
|
coupon, err := service.Payments().ApplyCoupon(userCtx2, id)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, coupon)
|
|
require.Equal(t, id, coupon.ID)
|
|
|
|
coupon, err = sat.API.Payments.Accounts.Coupons().GetByUserID(ctx, up2Proj.OwnerID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, id, coupon.ID)
|
|
})
|
|
t.Run("ApplyCoupon fails with unknown user", func(t *testing.T) {
|
|
id := stripe.MockCouponID2
|
|
coupon, err := service.Payments().ApplyCoupon(ctx, id)
|
|
require.Error(t, err)
|
|
require.Nil(t, coupon)
|
|
})
|
|
t.Run("ApplyCoupon fails with unknown coupon ID", func(t *testing.T) {
|
|
coupon, err := service.Payments().ApplyCoupon(userCtx2, "unknown_coupon_id")
|
|
require.Error(t, err)
|
|
require.Nil(t, coupon)
|
|
})
|
|
t.Run("UpdatePackage", func(t *testing.T) {
|
|
packagePlan := "package-plan-1"
|
|
purchaseTime := time.Now()
|
|
|
|
check := func() {
|
|
dbPackagePlan, dbPurchaseTime, err := sat.DB.StripeCoinPayments().Customers().GetPackageInfo(ctx, up1Proj.OwnerID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, dbPackagePlan)
|
|
require.NotNil(t, dbPurchaseTime)
|
|
require.Equal(t, packagePlan, *dbPackagePlan)
|
|
require.Equal(t, dbPurchaseTime.Truncate(time.Millisecond), dbPurchaseTime.Truncate(time.Millisecond))
|
|
}
|
|
|
|
require.NoError(t, service.Payments().UpdatePackage(userCtx1, packagePlan, purchaseTime))
|
|
check()
|
|
|
|
// Check values can't be overwritten
|
|
err = service.Payments().UpdatePackage(userCtx1, "different-package-plan", time.Now())
|
|
require.Error(t, err)
|
|
require.True(t, console.ErrAlreadyHasPackage.Has(err))
|
|
|
|
check()
|
|
})
|
|
t.Run("ApplyCredit fails when payments.Balances.ApplyCredit returns an error", func(t *testing.T) {
|
|
require.Error(t, service.Payments().ApplyCredit(userCtx1, 1000, stripe.MockCBTXsNewFailure))
|
|
btxs, err := sat.API.Payments.Accounts.Balances().ListTransactions(ctx, up1Proj.OwnerID)
|
|
require.NoError(t, err)
|
|
require.Zero(t, len(btxs))
|
|
})
|
|
t.Run("ApplyCredit", func(t *testing.T) {
|
|
amount := int64(1000)
|
|
desc := "test"
|
|
require.NoError(t, service.Payments().ApplyCredit(userCtx1, 1000, desc))
|
|
btxs, err := sat.API.Payments.Accounts.Balances().ListTransactions(ctx, up1Proj.OwnerID)
|
|
require.NoError(t, err)
|
|
require.Len(t, btxs, 1)
|
|
require.Equal(t, amount, btxs[0].Amount)
|
|
require.Equal(t, desc, btxs[0].Description)
|
|
|
|
// test same description results in no new credit
|
|
require.NoError(t, service.Payments().ApplyCredit(userCtx1, 1000, desc))
|
|
btxs, err = sat.API.Payments.Accounts.Balances().ListTransactions(ctx, up1Proj.OwnerID)
|
|
require.NoError(t, err)
|
|
require.Len(t, btxs, 1)
|
|
|
|
// test different description results in new credit
|
|
require.NoError(t, service.Payments().ApplyCredit(userCtx1, 1000, "new desc"))
|
|
btxs, err = sat.API.Payments.Accounts.Balances().ListTransactions(ctx, up1Proj.OwnerID)
|
|
require.NoError(t, err)
|
|
require.Len(t, btxs, 2)
|
|
})
|
|
t.Run("ApplyCredit fails with unknown user", func(t *testing.T) {
|
|
require.Error(t, service.Payments().ApplyCredit(ctx, 1000, "test"))
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestPaidTier(t *testing.T) {
|
|
usageConfig := console.UsageLimitsConfig{
|
|
Storage: console.StorageLimitConfig{
|
|
Free: memory.GB,
|
|
Paid: memory.TB,
|
|
},
|
|
Bandwidth: console.BandwidthLimitConfig{
|
|
Free: 2 * memory.GB,
|
|
Paid: 2 * memory.TB,
|
|
},
|
|
Segment: console.SegmentLimitConfig{
|
|
Free: 10,
|
|
Paid: 50,
|
|
},
|
|
Project: console.ProjectLimitConfig{
|
|
Free: 1,
|
|
Paid: 3,
|
|
},
|
|
}
|
|
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
|
|
Reconfigure: testplanet.Reconfigure{
|
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
|
config.Console.UsageLimits = usageConfig
|
|
},
|
|
},
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
service := sat.API.Console.Service
|
|
|
|
// project should have free tier usage limits
|
|
proj1, err := sat.API.DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, usageConfig.Storage.Free, *proj1.StorageLimit)
|
|
require.Equal(t, usageConfig.Bandwidth.Free, *proj1.BandwidthLimit)
|
|
require.Equal(t, usageConfig.Segment.Free, *proj1.SegmentLimit)
|
|
|
|
// user should be in free tier
|
|
user, err := service.GetUser(ctx, proj1.OwnerID)
|
|
require.NoError(t, err)
|
|
require.False(t, user.PaidTier)
|
|
|
|
userCtx, err := sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
// add a credit card to the user
|
|
_, err = service.Payments().AddCreditCard(userCtx, "test-cc-token")
|
|
require.NoError(t, err)
|
|
|
|
// expect user to be in paid tier
|
|
user, err = service.GetUser(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
require.True(t, user.PaidTier)
|
|
require.Equal(t, usageConfig.Project.Paid, user.ProjectLimit)
|
|
|
|
// update auth ctx
|
|
userCtx, err = sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
// expect project to be migrated to paid tier usage limits
|
|
proj1, err = service.GetProject(userCtx, proj1.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, usageConfig.Storage.Paid, *proj1.StorageLimit)
|
|
require.Equal(t, usageConfig.Bandwidth.Paid, *proj1.BandwidthLimit)
|
|
require.Equal(t, usageConfig.Segment.Paid, *proj1.SegmentLimit)
|
|
|
|
// expect new project to be created with paid tier usage limits
|
|
proj2, err := service.CreateProject(userCtx, console.ProjectInfo{Name: "Project 2"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, usageConfig.Storage.Paid, *proj2.StorageLimit)
|
|
})
|
|
}
|
|
|
|
// TestUpdateProjectExceedsLimits ensures that a project with limits manually set above the defaults can be updated.
|
|
func TestUpdateProjectExceedsLimits(t *testing.T) {
|
|
usageConfig := console.UsageLimitsConfig{
|
|
Storage: console.StorageLimitConfig{
|
|
Free: memory.GB,
|
|
Paid: memory.TB,
|
|
},
|
|
Bandwidth: console.BandwidthLimitConfig{
|
|
Free: 2 * memory.GB,
|
|
Paid: 2 * memory.TB,
|
|
},
|
|
Segment: console.SegmentLimitConfig{
|
|
Free: 10,
|
|
Paid: 50,
|
|
},
|
|
Project: console.ProjectLimitConfig{
|
|
Free: 1,
|
|
Paid: 3,
|
|
},
|
|
}
|
|
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
|
|
Reconfigure: testplanet.Reconfigure{
|
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
|
config.Console.UsageLimits = usageConfig
|
|
},
|
|
},
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
service := sat.API.Console.Service
|
|
projectID := planet.Uplinks[0].Projects[0].ID
|
|
|
|
updatedName := "newName"
|
|
updatedDescription := "newDescription"
|
|
updatedStorageLimit := memory.Size(100) + memory.TB
|
|
updatedBandwidthLimit := memory.Size(100) + memory.TB
|
|
|
|
proj, err := sat.API.DB.Console().Projects().Get(ctx, projectID)
|
|
require.NoError(t, err)
|
|
|
|
userCtx1, err := sat.UserContext(ctx, proj.OwnerID)
|
|
require.NoError(t, err)
|
|
|
|
// project should have free tier usage limits
|
|
require.Equal(t, usageConfig.Storage.Free, *proj.StorageLimit)
|
|
require.Equal(t, usageConfig.Bandwidth.Free, *proj.BandwidthLimit)
|
|
require.Equal(t, usageConfig.Segment.Free, *proj.SegmentLimit)
|
|
|
|
// update project name should succeed
|
|
_, err = service.UpdateProject(userCtx1, projectID, console.ProjectInfo{
|
|
Name: updatedName,
|
|
Description: updatedDescription,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// manually set project limits above defaults
|
|
proj1, err := sat.API.DB.Console().Projects().Get(ctx, projectID)
|
|
require.NoError(t, err)
|
|
proj1.StorageLimit = new(memory.Size)
|
|
*proj1.StorageLimit = updatedStorageLimit
|
|
proj1.BandwidthLimit = new(memory.Size)
|
|
*proj1.BandwidthLimit = updatedBandwidthLimit
|
|
err = sat.DB.Console().Projects().Update(ctx, proj1)
|
|
require.NoError(t, err)
|
|
|
|
// try to update project name should succeed
|
|
_, err = service.UpdateProject(userCtx1, projectID, console.ProjectInfo{
|
|
Name: "updatedName",
|
|
Description: "updatedDescription",
|
|
})
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestMFA(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
service := sat.API.Console.Service
|
|
|
|
user, err := sat.AddUser(ctx, console.CreateUser{
|
|
FullName: "MFA Test User",
|
|
Email: "mfauser@mail.test",
|
|
}, 1)
|
|
require.NoError(t, err)
|
|
|
|
updateContext := func() (context.Context, *console.User) {
|
|
userCtx, err := sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
user, err := console.GetUser(userCtx)
|
|
require.NoError(t, err)
|
|
return userCtx, user
|
|
}
|
|
userCtx, user := updateContext()
|
|
|
|
var key string
|
|
t.Run("ResetMFASecretKey", func(t *testing.T) {
|
|
key, err = service.ResetMFASecretKey(userCtx)
|
|
require.NoError(t, err)
|
|
|
|
_, user := updateContext()
|
|
require.NotEmpty(t, user.MFASecretKey)
|
|
})
|
|
|
|
t.Run("EnableUserMFABadPasscode", func(t *testing.T) {
|
|
// Expect MFA-enabling attempt to be rejected when providing stale passcode.
|
|
badCode, err := console.NewMFAPasscode(key, time.Time{}.Add(time.Hour))
|
|
require.NoError(t, err)
|
|
|
|
err = service.EnableUserMFA(userCtx, badCode, time.Time{})
|
|
require.True(t, console.ErrValidation.Has(err))
|
|
|
|
userCtx, _ = updateContext()
|
|
_, err = service.ResetMFARecoveryCodes(userCtx)
|
|
require.True(t, console.ErrUnauthorized.Has(err))
|
|
|
|
_, user = updateContext()
|
|
require.False(t, user.MFAEnabled)
|
|
})
|
|
|
|
t.Run("EnableUserMFAGoodPasscode", func(t *testing.T) {
|
|
// Expect MFA-enabling attempt to succeed when providing valid passcode.
|
|
goodCode, err := console.NewMFAPasscode(key, time.Time{})
|
|
require.NoError(t, err)
|
|
|
|
userCtx, _ = updateContext()
|
|
err = service.EnableUserMFA(userCtx, goodCode, time.Time{})
|
|
require.NoError(t, err)
|
|
|
|
_, user = updateContext()
|
|
require.True(t, user.MFAEnabled)
|
|
require.Equal(t, user.MFASecretKey, key)
|
|
})
|
|
|
|
t.Run("MFAGetToken", func(t *testing.T) {
|
|
request := console.AuthUser{Email: user.Email, Password: user.FullName}
|
|
|
|
// Expect no token due to lack of MFA passcode.
|
|
token, err := service.Token(ctx, request)
|
|
require.True(t, console.ErrMFAMissing.Has(err))
|
|
require.Empty(t, token)
|
|
|
|
// Expect no token due to bad MFA passcode.
|
|
wrongCode, err := console.NewMFAPasscode(key, time.Now().Add(time.Hour))
|
|
require.NoError(t, err)
|
|
|
|
request.MFAPasscode = wrongCode
|
|
token, err = service.Token(ctx, request)
|
|
require.True(t, console.ErrMFAPasscode.Has(err))
|
|
require.Empty(t, token)
|
|
|
|
// Expect token when providing valid passcode.
|
|
goodCode, err := console.NewMFAPasscode(key, time.Now())
|
|
require.NoError(t, err)
|
|
|
|
request.MFAPasscode = goodCode
|
|
token, err = service.Token(ctx, request)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, token)
|
|
})
|
|
|
|
t.Run("MFARecoveryCodes", func(t *testing.T) {
|
|
_, err = service.ResetMFARecoveryCodes(userCtx)
|
|
require.NoError(t, err)
|
|
|
|
_, user = updateContext()
|
|
require.Len(t, user.MFARecoveryCodes, console.MFARecoveryCodeCount)
|
|
|
|
for _, code := range user.MFARecoveryCodes {
|
|
// Ensure code is of the form XXXX-XXXX-XXXX where X is A-Z or 0-9.
|
|
require.Regexp(t, "^([A-Z0-9]{4})((-[A-Z0-9]{4})){2}$", code)
|
|
|
|
// Expect token when providing valid recovery code.
|
|
request := console.AuthUser{Email: user.Email, Password: user.FullName, MFARecoveryCode: code}
|
|
token, err := service.Token(ctx, request)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, token)
|
|
|
|
// Expect no token due to providing previously-used recovery code.
|
|
token, err = service.Token(ctx, request)
|
|
require.True(t, console.ErrMFARecoveryCode.Has(err))
|
|
require.Empty(t, token)
|
|
|
|
_, user = updateContext()
|
|
}
|
|
|
|
userCtx, _ = updateContext()
|
|
_, err = service.ResetMFARecoveryCodes(userCtx)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("DisableUserMFABadPasscode", func(t *testing.T) {
|
|
// Expect MFA-disabling attempt to fail when providing valid passcode.
|
|
badCode, err := console.NewMFAPasscode(key, time.Time{}.Add(time.Hour))
|
|
require.NoError(t, err)
|
|
|
|
userCtx, _ = updateContext()
|
|
err = service.DisableUserMFA(userCtx, badCode, time.Time{}, "")
|
|
require.True(t, console.ErrValidation.Has(err))
|
|
|
|
_, user = updateContext()
|
|
require.True(t, user.MFAEnabled)
|
|
require.NotEmpty(t, user.MFASecretKey)
|
|
require.NotEmpty(t, user.MFARecoveryCodes)
|
|
})
|
|
|
|
t.Run("DisableUserMFAConflict", func(t *testing.T) {
|
|
// Expect MFA-disabling attempt to fail when providing both recovery code and passcode.
|
|
goodCode, err := console.NewMFAPasscode(key, time.Time{})
|
|
require.NoError(t, err)
|
|
|
|
userCtx, user = updateContext()
|
|
err = service.DisableUserMFA(userCtx, goodCode, time.Time{}, user.MFARecoveryCodes[0])
|
|
require.True(t, console.ErrMFAConflict.Has(err))
|
|
|
|
_, user = updateContext()
|
|
require.True(t, user.MFAEnabled)
|
|
require.NotEmpty(t, user.MFASecretKey)
|
|
require.NotEmpty(t, user.MFARecoveryCodes)
|
|
})
|
|
|
|
t.Run("DisableUserMFAGoodPasscode", func(t *testing.T) {
|
|
// Expect MFA-disabling attempt to succeed when providing valid passcode.
|
|
goodCode, err := console.NewMFAPasscode(key, time.Time{})
|
|
require.NoError(t, err)
|
|
|
|
userCtx, _ = updateContext()
|
|
err = service.DisableUserMFA(userCtx, goodCode, time.Time{}, "")
|
|
require.NoError(t, err)
|
|
|
|
userCtx, user = updateContext()
|
|
require.False(t, user.MFAEnabled)
|
|
require.Empty(t, user.MFASecretKey)
|
|
require.Empty(t, user.MFARecoveryCodes)
|
|
})
|
|
|
|
t.Run("DisableUserMFAGoodRecoveryCode", func(t *testing.T) {
|
|
// Expect MFA-disabling attempt to succeed when providing valid recovery code.
|
|
// Enable MFA
|
|
key, err = service.ResetMFASecretKey(userCtx)
|
|
require.NoError(t, err)
|
|
|
|
goodCode, err := console.NewMFAPasscode(key, time.Time{})
|
|
require.NoError(t, err)
|
|
|
|
userCtx, _ = updateContext()
|
|
err = service.EnableUserMFA(userCtx, goodCode, time.Time{})
|
|
require.NoError(t, err)
|
|
|
|
userCtx, _ = updateContext()
|
|
_, err = service.ResetMFARecoveryCodes(userCtx)
|
|
require.NoError(t, err)
|
|
|
|
userCtx, user = updateContext()
|
|
require.True(t, user.MFAEnabled)
|
|
require.NotEmpty(t, user.MFASecretKey)
|
|
require.NotEmpty(t, user.MFARecoveryCodes)
|
|
|
|
// Disable MFA
|
|
err = service.DisableUserMFA(userCtx, "", time.Time{}, user.MFARecoveryCodes[0])
|
|
require.NoError(t, err)
|
|
|
|
_, user = updateContext()
|
|
require.False(t, user.MFAEnabled)
|
|
require.Empty(t, user.MFASecretKey)
|
|
require.Empty(t, user.MFARecoveryCodes)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestResetPassword(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
service := sat.API.Console.Service
|
|
|
|
user, err := sat.AddUser(ctx, console.CreateUser{
|
|
FullName: "Test User",
|
|
Email: "test@mail.test",
|
|
}, 1)
|
|
require.NoError(t, err)
|
|
|
|
newPass := user.FullName
|
|
|
|
getNewResetToken := func() *console.ResetPasswordToken {
|
|
token, err := sat.DB.Console().ResetPasswordTokens().Create(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, token)
|
|
return token
|
|
}
|
|
token := getNewResetToken()
|
|
|
|
// Expect error when providing bad token.
|
|
err = service.ResetPassword(ctx, "badToken", newPass, "", "", token.CreatedAt)
|
|
require.True(t, console.ErrRecoveryToken.Has(err))
|
|
|
|
// Expect error when providing good but expired token.
|
|
err = service.ResetPassword(ctx, token.Secret.String(), newPass, "", "", token.CreatedAt.Add(sat.Config.ConsoleAuth.TokenExpirationTime).Add(time.Second))
|
|
require.True(t, console.ErrTokenExpiration.Has(err))
|
|
|
|
// Expect error when providing good token with bad (too short) password.
|
|
err = service.ResetPassword(ctx, token.Secret.String(), "bad", "", "", token.CreatedAt)
|
|
require.True(t, console.ErrValidation.Has(err))
|
|
|
|
// Expect success when providing good token and good password.
|
|
err = service.ResetPassword(ctx, token.Secret.String(), newPass, "", "", token.CreatedAt)
|
|
require.NoError(t, err)
|
|
|
|
token = getNewResetToken()
|
|
|
|
// Enable MFA.
|
|
userCtx, err := sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
key, err := service.ResetMFASecretKey(userCtx)
|
|
require.NoError(t, err)
|
|
userCtx, err = sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
passcode, err := console.NewMFAPasscode(key, token.CreatedAt)
|
|
require.NoError(t, err)
|
|
|
|
err = service.EnableUserMFA(userCtx, passcode, token.CreatedAt)
|
|
require.NoError(t, err)
|
|
|
|
// Expect error when providing bad passcode.
|
|
badPasscode, err := console.NewMFAPasscode(key, token.CreatedAt.Add(time.Hour))
|
|
require.NoError(t, err)
|
|
err = service.ResetPassword(ctx, token.Secret.String(), newPass, badPasscode, "", token.CreatedAt)
|
|
require.True(t, console.ErrMFAPasscode.Has(err))
|
|
|
|
for _, recoveryCode := range user.MFARecoveryCodes {
|
|
// Expect success when providing bad passcode and good recovery code.
|
|
err = service.ResetPassword(ctx, token.Secret.String(), newPass, badPasscode, recoveryCode, token.CreatedAt)
|
|
require.NoError(t, err)
|
|
token = getNewResetToken()
|
|
|
|
// Expect error when providing bad passcode and already-used recovery code.
|
|
err = service.ResetPassword(ctx, token.Secret.String(), newPass, badPasscode, recoveryCode, token.CreatedAt)
|
|
require.True(t, console.ErrMFARecoveryCode.Has(err))
|
|
}
|
|
|
|
// Expect success when providing good passcode.
|
|
err = service.ResetPassword(ctx, token.Secret.String(), newPass, passcode, "", token.CreatedAt)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestChangePassword(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]
|
|
upl := planet.Uplinks[0]
|
|
newPass := "newPass123!"
|
|
|
|
user, err := sat.DB.Console().Users().GetByEmail(ctx, upl.User[sat.ID()].Email)
|
|
require.NoError(t, err)
|
|
userCtx, err := sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
// generate a password recovery token to test that changing password invalidates it
|
|
passwordRecoveryToken, err := sat.API.Console.Service.GeneratePasswordRecoveryToken(userCtx, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, sat.API.Console.Service.ChangePassword(userCtx, upl.User[sat.ID()].Password, newPass))
|
|
user, err = sat.DB.Console().Users().Get(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
require.NoError(t, bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(newPass)))
|
|
|
|
err = sat.API.Console.Service.ResetPassword(userCtx, passwordRecoveryToken, "aDifferentPassword123!", "", "", time.Now())
|
|
require.Error(t, err)
|
|
require.True(t, console.ErrRecoveryToken.Has(err))
|
|
})
|
|
}
|
|
|
|
func TestGenerateSessionToken(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
|
|
Reconfigure: testplanet.Reconfigure{
|
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
|
config.Console.Session.InactivityTimerEnabled = true
|
|
config.Console.Session.InactivityTimerDuration = 600
|
|
},
|
|
},
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
srv := sat.API.Console.Service
|
|
|
|
user, _, err := srv.GetUserByEmailWithUnverified(ctx, planet.Uplinks[0].User[sat.ID()].Email)
|
|
require.NoError(t, err)
|
|
|
|
userCtx, err := sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
now := time.Now()
|
|
token1, err := srv.GenerateSessionToken(userCtx, user.ID, user.Email, "", "")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, token1)
|
|
|
|
token1Duration := token1.ExpiresAt.Sub(now)
|
|
increase := 10 * time.Minute
|
|
increasedDuration := time.Duration(sat.Config.Console.Session.InactivityTimerDuration)*time.Second + increase
|
|
ptr := &increasedDuration
|
|
require.NoError(t, sat.DB.Console().Users().UpsertSettings(ctx, user.ID, console.UpsertUserSettingsRequest{
|
|
SessionDuration: &ptr,
|
|
}))
|
|
|
|
now = time.Now()
|
|
token2, err := srv.GenerateSessionToken(userCtx, user.ID, user.Email, "", "")
|
|
require.NoError(t, err)
|
|
token2Duration := token2.ExpiresAt.Sub(now)
|
|
require.Greater(t, token2Duration, token1Duration)
|
|
|
|
decrease := -5 * time.Minute
|
|
decreasedDuration := time.Duration(sat.Config.Console.Session.InactivityTimerDuration)*time.Second + decrease
|
|
ptr = &decreasedDuration
|
|
require.NoError(t, sat.DB.Console().Users().UpsertSettings(ctx, user.ID, console.UpsertUserSettingsRequest{
|
|
SessionDuration: &ptr,
|
|
}))
|
|
|
|
now = time.Now()
|
|
token3, err := srv.GenerateSessionToken(userCtx, user.ID, user.Email, "", "")
|
|
require.NoError(t, err)
|
|
token3Duration := token3.ExpiresAt.Sub(now)
|
|
require.Less(t, token3Duration, token1Duration)
|
|
})
|
|
}
|
|
|
|
func TestRefreshSessionToken(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
|
|
Reconfigure: testplanet.Reconfigure{
|
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
|
config.Console.Session.InactivityTimerEnabled = true
|
|
config.Console.Session.InactivityTimerDuration = 600
|
|
},
|
|
},
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
srv := sat.API.Console.Service
|
|
|
|
user, _, err := srv.GetUserByEmailWithUnverified(ctx, planet.Uplinks[0].User[sat.ID()].Email)
|
|
require.NoError(t, err)
|
|
|
|
userCtx, err := sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
now := time.Now()
|
|
token, err := srv.GenerateSessionToken(userCtx, user.ID, user.Email, "", "")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, token)
|
|
|
|
defaultDuration := token.ExpiresAt.Sub(now)
|
|
increase := 10 * time.Minute
|
|
increasedDuration := time.Duration(sat.Config.Console.Session.InactivityTimerDuration)*time.Second + increase
|
|
ptr := &increasedDuration
|
|
require.NoError(t, sat.DB.Console().Users().UpsertSettings(ctx, user.ID, console.UpsertUserSettingsRequest{
|
|
SessionDuration: &ptr,
|
|
}))
|
|
|
|
sessionID, err := uuid.FromBytes(token.Token.Payload)
|
|
require.NoError(t, err)
|
|
|
|
now = time.Now()
|
|
increasedExpiration, err := srv.RefreshSession(userCtx, sessionID)
|
|
require.NoError(t, err)
|
|
require.Greater(t, increasedExpiration.Sub(now), defaultDuration)
|
|
|
|
decrease := -5 * time.Minute
|
|
decreasedDuration := time.Duration(sat.Config.Console.Session.InactivityTimerDuration)*time.Second + decrease
|
|
ptr = &decreasedDuration
|
|
require.NoError(t, sat.DB.Console().Users().UpsertSettings(ctx, user.ID, console.UpsertUserSettingsRequest{
|
|
SessionDuration: &ptr,
|
|
}))
|
|
|
|
now = time.Now()
|
|
decreasedExpiration, err := srv.RefreshSession(userCtx, sessionID)
|
|
require.NoError(t, err)
|
|
require.Less(t, decreasedExpiration.Sub(now), defaultDuration)
|
|
})
|
|
}
|
|
|
|
func TestUserSettings(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]
|
|
srv := sat.API.Console.Service
|
|
userDB := sat.DB.Console().Users()
|
|
|
|
existingUser, _, err := srv.GetUserByEmailWithUnverified(ctx, planet.Uplinks[0].User[sat.ID()].Email)
|
|
require.NoError(t, err)
|
|
|
|
userCtx, err := sat.UserContext(ctx, existingUser.ID)
|
|
require.NoError(t, err)
|
|
|
|
_, err = userDB.GetSettings(userCtx, existingUser.ID)
|
|
require.Error(t, err)
|
|
|
|
// a user that already has a project prior to getting user settings should not go through onboarding again
|
|
// in other words, onboarding start and end should both be true for users who have a project
|
|
settings, err := srv.GetUserSettings(userCtx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, true, settings.OnboardingStart)
|
|
require.Equal(t, true, settings.OnboardingEnd)
|
|
require.Nil(t, settings.OnboardingStep)
|
|
require.Nil(t, settings.SessionDuration)
|
|
|
|
newUser, err := userDB.Insert(ctx, &console.User{
|
|
ID: testrand.UUID(),
|
|
Email: "newuser@example.com",
|
|
PasswordHash: []byte("i am a hash of a password, hello"),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
userCtx, err = sat.UserContext(ctx, newUser.ID)
|
|
require.NoError(t, err)
|
|
|
|
// a brand new user with no project should go through onboarding
|
|
// in other words, onboarding start and end should both be false for users withouut a project
|
|
settings, err = srv.GetUserSettings(userCtx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, false, settings.OnboardingStart)
|
|
require.Equal(t, false, settings.OnboardingEnd)
|
|
require.Nil(t, settings.OnboardingStep)
|
|
require.Nil(t, settings.SessionDuration)
|
|
|
|
onboardingBool := true
|
|
onboardingStep := "Overview"
|
|
sessionDur := time.Duration(rand.Int63()).Round(time.Minute)
|
|
sessionDurPtr := &sessionDur
|
|
settings, err = srv.SetUserSettings(userCtx, console.UpsertUserSettingsRequest{
|
|
SessionDuration: &sessionDurPtr,
|
|
OnboardingStart: &onboardingBool,
|
|
OnboardingEnd: &onboardingBool,
|
|
OnboardingStep: &onboardingStep,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, onboardingBool, settings.OnboardingStart)
|
|
require.Equal(t, onboardingBool, settings.OnboardingEnd)
|
|
require.Equal(t, &onboardingStep, settings.OnboardingStep)
|
|
require.Equal(t, sessionDurPtr, settings.SessionDuration)
|
|
|
|
settings, err = userDB.GetSettings(userCtx, newUser.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, onboardingBool, settings.OnboardingStart)
|
|
require.Equal(t, onboardingBool, settings.OnboardingEnd)
|
|
require.Equal(t, &onboardingStep, settings.OnboardingStep)
|
|
require.Equal(t, sessionDurPtr, settings.SessionDuration)
|
|
|
|
// passing nil should not override existing values
|
|
settings, err = srv.SetUserSettings(userCtx, console.UpsertUserSettingsRequest{
|
|
SessionDuration: nil,
|
|
OnboardingStart: nil,
|
|
OnboardingEnd: nil,
|
|
OnboardingStep: nil,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, onboardingBool, settings.OnboardingStart)
|
|
require.Equal(t, onboardingBool, settings.OnboardingEnd)
|
|
require.Equal(t, &onboardingStep, settings.OnboardingStep)
|
|
require.Equal(t, sessionDurPtr, settings.SessionDuration)
|
|
|
|
settings, err = userDB.GetSettings(userCtx, newUser.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, onboardingBool, settings.OnboardingStart)
|
|
require.Equal(t, onboardingBool, settings.OnboardingEnd)
|
|
require.Equal(t, &onboardingStep, settings.OnboardingStep)
|
|
require.Equal(t, sessionDurPtr, settings.SessionDuration)
|
|
})
|
|
}
|
|
|
|
func TestRESTKeys(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]
|
|
service := sat.API.Console.Service
|
|
|
|
proj1, err := sat.API.DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID)
|
|
require.NoError(t, err)
|
|
|
|
user, err := service.GetUser(ctx, proj1.OwnerID)
|
|
require.NoError(t, err)
|
|
|
|
userCtx, err := sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
now := time.Now()
|
|
expires := 5 * time.Hour
|
|
apiKey, expiresAt, err := service.CreateRESTKey(userCtx, expires)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, apiKey)
|
|
require.True(t, expiresAt.After(now))
|
|
require.True(t, expiresAt.Before(now.Add(expires+time.Hour)))
|
|
|
|
// test revocation
|
|
require.NoError(t, service.RevokeRESTKey(userCtx, apiKey))
|
|
|
|
// test revoke non existent key
|
|
nonexistent := testrand.UUID()
|
|
err = service.RevokeRESTKey(userCtx, nonexistent.String())
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
// TestLockAccount ensures user's gets locked when incorrect credentials are provided.
|
|
func TestLockAccount(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
service := sat.API.Console.Service
|
|
usersDB := sat.DB.Console().Users()
|
|
consoleConfig := sat.Config.Console
|
|
|
|
newUser := console.CreateUser{
|
|
FullName: "token test",
|
|
Email: "token_test@mail.test",
|
|
}
|
|
|
|
user, err := sat.AddUser(ctx, newUser, 1)
|
|
require.NoError(t, err)
|
|
|
|
updateContext := func() (context.Context, *console.User) {
|
|
userCtx, err := sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
user, err := console.GetUser(userCtx)
|
|
require.NoError(t, err)
|
|
return userCtx, user
|
|
}
|
|
|
|
userCtx, _ := updateContext()
|
|
secret, err := service.ResetMFASecretKey(userCtx)
|
|
require.NoError(t, err)
|
|
|
|
goodCode0, err := console.NewMFAPasscode(secret, time.Time{})
|
|
require.NoError(t, err)
|
|
|
|
userCtx, _ = updateContext()
|
|
err = service.EnableUserMFA(userCtx, goodCode0, time.Time{})
|
|
require.NoError(t, err)
|
|
|
|
now := time.Now()
|
|
|
|
goodCode1, err := console.NewMFAPasscode(secret, now)
|
|
require.NoError(t, err)
|
|
|
|
authUser := console.AuthUser{
|
|
Email: newUser.Email,
|
|
Password: newUser.FullName,
|
|
MFAPasscode: goodCode1,
|
|
}
|
|
|
|
// successful login.
|
|
token, err := service.Token(ctx, authUser)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, token)
|
|
|
|
// check if user's account gets locked because of providing wrong password.
|
|
authUser.Password = "qweQWE1@"
|
|
for i := 1; i <= consoleConfig.LoginAttemptsWithoutPenalty; i++ {
|
|
token, err = service.Token(ctx, authUser)
|
|
require.Empty(t, token)
|
|
require.True(t, console.ErrLoginCredentials.Has(err))
|
|
}
|
|
|
|
lockedUser, err := service.GetUser(userCtx, user.ID)
|
|
require.NoError(t, err)
|
|
require.True(t, lockedUser.FailedLoginCount == consoleConfig.LoginAttemptsWithoutPenalty)
|
|
require.True(t, lockedUser.LoginLockoutExpiration.After(now))
|
|
|
|
// lock account once again and check if lockout expiration time increased.
|
|
err = service.UpdateUsersFailedLoginState(userCtx, lockedUser)
|
|
require.NoError(t, err)
|
|
|
|
lockedUser, err = service.GetUser(userCtx, user.ID)
|
|
require.NoError(t, err)
|
|
require.True(t, lockedUser.FailedLoginCount == consoleConfig.LoginAttemptsWithoutPenalty+1)
|
|
|
|
diff := lockedUser.LoginLockoutExpiration.Sub(now)
|
|
require.Greater(t, diff, time.Duration(consoleConfig.FailedLoginPenalty)*time.Minute)
|
|
|
|
// unlock account by successful login
|
|
lockedUser.LoginLockoutExpiration = now.Add(-time.Second)
|
|
lockoutExpirationPtr := &lockedUser.LoginLockoutExpiration
|
|
err = usersDB.Update(userCtx, lockedUser.ID, console.UpdateUserRequest{
|
|
LoginLockoutExpiration: &lockoutExpirationPtr,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
authUser.Password = newUser.FullName
|
|
token, err = service.Token(ctx, authUser)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, token)
|
|
|
|
unlockedUser, err := service.GetUser(userCtx, user.ID)
|
|
require.NoError(t, err)
|
|
require.Zero(t, unlockedUser.FailedLoginCount)
|
|
|
|
// check if user's account gets locked because of providing wrong mfa passcode.
|
|
authUser.MFAPasscode = "000000"
|
|
for i := 1; i <= consoleConfig.LoginAttemptsWithoutPenalty; i++ {
|
|
token, err = service.Token(ctx, authUser)
|
|
require.Empty(t, token)
|
|
require.True(t, console.ErrMFAPasscode.Has(err))
|
|
}
|
|
|
|
lockedUser, err = service.GetUser(userCtx, user.ID)
|
|
require.NoError(t, err)
|
|
require.True(t, lockedUser.FailedLoginCount == consoleConfig.LoginAttemptsWithoutPenalty)
|
|
require.True(t, lockedUser.LoginLockoutExpiration.After(now))
|
|
|
|
// unlock account
|
|
lockedUser.LoginLockoutExpiration = time.Time{}
|
|
lockoutExpirationPtr = &lockedUser.LoginLockoutExpiration
|
|
lockedUser.FailedLoginCount = 0
|
|
err = usersDB.Update(userCtx, lockedUser.ID, console.UpdateUserRequest{
|
|
LoginLockoutExpiration: &lockoutExpirationPtr,
|
|
FailedLoginCount: &lockedUser.FailedLoginCount,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// check if user's account gets locked because of providing wrong mfa recovery code.
|
|
authUser.MFAPasscode = ""
|
|
authUser.MFARecoveryCode = "000000"
|
|
for i := 1; i <= consoleConfig.LoginAttemptsWithoutPenalty; i++ {
|
|
token, err = service.Token(ctx, authUser)
|
|
require.Empty(t, token)
|
|
require.True(t, console.ErrMFARecoveryCode.Has(err))
|
|
}
|
|
|
|
lockedUser, err = service.GetUser(userCtx, user.ID)
|
|
require.NoError(t, err)
|
|
require.True(t, lockedUser.FailedLoginCount == consoleConfig.LoginAttemptsWithoutPenalty)
|
|
require.True(t, lockedUser.LoginLockoutExpiration.After(now))
|
|
})
|
|
}
|
|
|
|
func TestWalletJsonMarshall(t *testing.T) {
|
|
wi := console.WalletInfo{
|
|
Address: blockchain.Address{1, 2, 3},
|
|
Balance: currency.AmountFromBaseUnits(10000, currency.USDollars),
|
|
}
|
|
|
|
out, err := json.Marshal(wi)
|
|
require.NoError(t, err)
|
|
require.Contains(t, string(out), "\"address\":\"0x0102030000000000000000000000000000000000\"")
|
|
require.Contains(t, string(out), "\"balance\":{\"value\":\"100\",\"currency\":\"USD\"}")
|
|
|
|
}
|
|
|
|
func TestSessionExpiration(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
|
Reconfigure: testplanet.Reconfigure{
|
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
|
config.Console.Session.Duration = time.Hour
|
|
},
|
|
},
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
service := sat.API.Console.Service
|
|
|
|
user, err := sat.AddUser(ctx, console.CreateUser{
|
|
FullName: "Test User",
|
|
Email: "test@mail.test",
|
|
}, 1)
|
|
require.NoError(t, err)
|
|
|
|
// Session should be added to DB after token request
|
|
tokenInfo, err := service.Token(ctx, console.AuthUser{Email: user.Email, Password: user.FullName})
|
|
require.NoError(t, err)
|
|
|
|
_, err = service.TokenAuth(ctx, tokenInfo.Token, time.Now())
|
|
require.NoError(t, err)
|
|
|
|
sessionID, err := uuid.FromBytes(tokenInfo.Token.Payload)
|
|
require.NoError(t, err)
|
|
|
|
_, err = sat.DB.Console().WebappSessions().GetBySessionID(ctx, sessionID)
|
|
require.NoError(t, err)
|
|
|
|
// Session should be removed from DB after it has expired
|
|
_, err = service.TokenAuth(ctx, tokenInfo.Token, time.Now().Add(2*time.Hour))
|
|
require.True(t, console.ErrTokenExpiration.Has(err))
|
|
|
|
_, err = sat.DB.Console().WebappSessions().GetBySessionID(ctx, sessionID)
|
|
require.ErrorIs(t, sql.ErrNoRows, err)
|
|
})
|
|
}
|
|
|
|
func TestDeleteAllSessionsByUserIDExcept(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
service := sat.API.Console.Service
|
|
|
|
user, err := sat.AddUser(ctx, console.CreateUser{
|
|
FullName: "Test User",
|
|
Email: "test@mail.test",
|
|
}, 1)
|
|
require.NoError(t, err)
|
|
|
|
// Session should be added to DB after token request
|
|
tokenInfo, err := service.Token(ctx, console.AuthUser{Email: user.Email, Password: user.FullName})
|
|
require.NoError(t, err)
|
|
|
|
_, err = service.TokenAuth(ctx, tokenInfo.Token, time.Now())
|
|
require.NoError(t, err)
|
|
|
|
sessionID, err := uuid.FromBytes(tokenInfo.Token.Payload)
|
|
require.NoError(t, err)
|
|
|
|
_, err = sat.DB.Console().WebappSessions().GetBySessionID(ctx, sessionID)
|
|
require.NoError(t, err)
|
|
|
|
// Session2 should be added to DB after token request
|
|
tokenInfo2, err := service.Token(ctx, console.AuthUser{Email: user.Email, Password: user.FullName})
|
|
require.NoError(t, err)
|
|
|
|
_, err = service.TokenAuth(ctx, tokenInfo2.Token, time.Now())
|
|
require.NoError(t, err)
|
|
|
|
sessionID2, err := uuid.FromBytes(tokenInfo2.Token.Payload)
|
|
require.NoError(t, err)
|
|
|
|
_, err = sat.DB.Console().WebappSessions().GetBySessionID(ctx, sessionID2)
|
|
require.NoError(t, err)
|
|
|
|
// Session2 should be removed from DB after calling DeleteAllSessionByUserIDExcept with Session1
|
|
err = service.DeleteAllSessionsByUserIDExcept(ctx, user.ID, sessionID)
|
|
require.NoError(t, err)
|
|
|
|
_, err = sat.DB.Console().WebappSessions().GetBySessionID(ctx, sessionID2)
|
|
require.ErrorIs(t, sql.ErrNoRows, err)
|
|
})
|
|
}
|
|
|
|
func TestPaymentsWalletPayments(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
|
Reconfigure: testplanet.Reconfigure{
|
|
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
|
config.Payments.BillingConfig.DisableLoop = false
|
|
config.Payments.BonusRate = 10
|
|
},
|
|
},
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
now := time.Now().Truncate(time.Second).UTC()
|
|
|
|
user, err := sat.AddUser(ctx, console.CreateUser{
|
|
FullName: "Test User",
|
|
Email: "test@mail.test",
|
|
Password: "example",
|
|
}, 1)
|
|
require.NoError(t, err)
|
|
|
|
wallet := blockchaintest.NewAddress()
|
|
err = sat.DB.Wallets().Add(ctx, user.ID, wallet)
|
|
require.NoError(t, err)
|
|
|
|
var transactions []stripe.Transaction
|
|
for i := 0; i < 5; i++ {
|
|
tx := stripe.Transaction{
|
|
ID: coinpayments.TransactionID(fmt.Sprintf("%d", i)),
|
|
AccountID: user.ID,
|
|
Address: blockchaintest.NewAddress().Hex(),
|
|
Amount: currency.AmountFromBaseUnits(1000000000, currency.StorjToken),
|
|
Received: currency.AmountFromBaseUnits(1000000000, currency.StorjToken),
|
|
Status: coinpayments.StatusCompleted,
|
|
Key: "key",
|
|
Timeout: 0,
|
|
}
|
|
|
|
createdAt, err := sat.DB.StripeCoinPayments().Transactions().TestInsert(ctx, tx)
|
|
require.NoError(t, err)
|
|
err = sat.DB.StripeCoinPayments().Transactions().TestLockRate(ctx, tx.ID, decimal.NewFromInt(1))
|
|
require.NoError(t, err)
|
|
|
|
tx.CreatedAt = createdAt.UTC()
|
|
transactions = append(transactions, tx)
|
|
}
|
|
|
|
var cachedPayments []storjscan.CachedPayment
|
|
for i := 0; i < 10; i++ {
|
|
cachedPayments = append(cachedPayments, storjscan.CachedPayment{
|
|
From: blockchaintest.NewAddress(),
|
|
To: wallet,
|
|
TokenValue: currency.AmountFromBaseUnits(1000, currency.StorjToken),
|
|
USDValue: currency.AmountFromBaseUnits(1000, currency.USDollarsMicro),
|
|
Status: payments.PaymentStatusConfirmed,
|
|
BlockHash: blockchaintest.NewHash(),
|
|
BlockNumber: int64(i),
|
|
Transaction: blockchaintest.NewHash(),
|
|
LogIndex: 0,
|
|
Timestamp: now.Add(time.Duration(i) * 15 * time.Second),
|
|
})
|
|
}
|
|
err = sat.DB.StorjscanPayments().InsertBatch(ctx, cachedPayments)
|
|
require.NoError(t, err)
|
|
|
|
reqCtx := console.WithUser(ctx, user)
|
|
|
|
sort.Slice(cachedPayments, func(i, j int) bool {
|
|
return cachedPayments[i].BlockNumber > cachedPayments[j].BlockNumber
|
|
})
|
|
sort.Slice(transactions, func(i, j int) bool {
|
|
return transactions[i].CreatedAt.After(transactions[j].CreatedAt)
|
|
})
|
|
|
|
var expected []console.PaymentInfo
|
|
for _, pmnt := range cachedPayments {
|
|
expected = append(expected, console.PaymentInfo{
|
|
ID: fmt.Sprintf("%s#%d", pmnt.Transaction.Hex(), pmnt.LogIndex),
|
|
Type: "storjscan",
|
|
Wallet: pmnt.To.Hex(),
|
|
Amount: pmnt.USDValue,
|
|
Status: string(pmnt.Status),
|
|
Link: console.EtherscanURL(pmnt.Transaction.Hex()),
|
|
Timestamp: pmnt.Timestamp,
|
|
})
|
|
}
|
|
for _, tx := range transactions {
|
|
expected = append(expected, console.PaymentInfo{
|
|
ID: tx.ID.String(),
|
|
Type: "coinpayments",
|
|
Wallet: tx.Address,
|
|
Amount: currency.AmountFromBaseUnits(1000, currency.USDollars),
|
|
Received: currency.AmountFromBaseUnits(1000, currency.USDollars),
|
|
Status: tx.Status.String(),
|
|
Link: coinpayments.GetCheckoutURL(tx.Key, tx.ID),
|
|
Timestamp: tx.CreatedAt,
|
|
})
|
|
}
|
|
|
|
// get billing chore to insert bonuses for transactions.
|
|
sat.Core.Payments.BillingChore.TransactionCycle.TriggerWait()
|
|
|
|
txns, err := sat.DB.Billing().ListSource(ctx, user.ID, billing.StorjScanBonusSource)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, txns)
|
|
|
|
for _, txn := range txns {
|
|
if txn.Source != billing.StorjScanBonusSource {
|
|
continue
|
|
}
|
|
var meta struct {
|
|
ReferenceID string
|
|
Wallet string
|
|
LogIndex int
|
|
}
|
|
err = json.NewDecoder(bytes.NewReader(txn.Metadata)).Decode(&meta)
|
|
require.NoError(t, err)
|
|
|
|
expected = append(expected, console.PaymentInfo{
|
|
ID: fmt.Sprintf("%s#%d", meta.ReferenceID, meta.LogIndex),
|
|
Type: txn.Source,
|
|
Wallet: meta.Wallet,
|
|
Amount: txn.Amount,
|
|
Status: string(txn.Status),
|
|
Link: console.EtherscanURL(meta.ReferenceID),
|
|
Timestamp: txn.Timestamp,
|
|
})
|
|
}
|
|
|
|
walletPayments, err := sat.API.Console.Service.Payments().WalletPayments(reqCtx)
|
|
require.NoError(t, err)
|
|
require.Equal(t, expected, walletPayments.Payments)
|
|
})
|
|
}
|
|
|
|
func TestPaymentsPurchase(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
p := sat.API.Console.Service.Payments()
|
|
user, err := sat.AddUser(ctx, console.CreateUser{
|
|
FullName: "Test User",
|
|
Email: "test@mail.test",
|
|
}, 1)
|
|
require.NoError(t, err)
|
|
|
|
userCtx, err := sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
testDesc := "testDescription"
|
|
testPaymentMethod := "testPaymentMethod"
|
|
|
|
tests := []struct {
|
|
name string
|
|
purchaseDesc string
|
|
paymentMethod string
|
|
shouldErr bool
|
|
ctx context.Context
|
|
}{
|
|
{
|
|
"Purchase returns error with unknown user",
|
|
testDesc,
|
|
testPaymentMethod,
|
|
true,
|
|
ctx,
|
|
},
|
|
{
|
|
"Purchase returns error when underlying payments.Invoices.New returns error",
|
|
stripe.MockInvoicesNewFailure,
|
|
testPaymentMethod,
|
|
true,
|
|
userCtx,
|
|
},
|
|
{
|
|
"Purchase returns error when underlying payments.Invoices.Pay returns error",
|
|
testDesc,
|
|
stripe.MockInvoicesPayFailure,
|
|
true,
|
|
userCtx,
|
|
},
|
|
{
|
|
"Purchase success",
|
|
testDesc,
|
|
testPaymentMethod,
|
|
false,
|
|
userCtx,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := p.Purchase(tt.ctx, 1000, tt.purchaseDesc, tt.paymentMethod)
|
|
if tt.shouldErr {
|
|
require.NotNil(t, err)
|
|
} else {
|
|
require.Nil(t, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
})
|
|
}
|
|
|
|
func TestPaymentsPurchasePreexistingInvoice(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
p := sat.API.Console.Service.Payments()
|
|
|
|
user, err := sat.AddUser(ctx, console.CreateUser{
|
|
FullName: "Test User",
|
|
Email: "test@mail.test",
|
|
}, 1)
|
|
require.NoError(t, err)
|
|
|
|
userCtx, err := sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
|
|
draftInvDesc := "testDraftDescription"
|
|
testPaymentMethod := "testPaymentMethod"
|
|
|
|
invs, err := sat.API.Payments.StripeService.Accounts().Invoices().List(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, invs, 0)
|
|
|
|
// test purchase with draft invoice
|
|
inv, err := sat.API.Payments.StripeService.Accounts().Invoices().Create(ctx, user.ID, 1000, draftInvDesc)
|
|
require.NoError(t, err)
|
|
require.Equal(t, payments.InvoiceStatusDraft, inv.Status)
|
|
|
|
draftInv := inv.ID
|
|
|
|
invs, err = sat.API.Payments.StripeService.Accounts().Invoices().List(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, invs, 1)
|
|
require.Equal(t, draftInv, invs[0].ID)
|
|
|
|
require.NoError(t, p.Purchase(userCtx, 1000, draftInvDesc, stripe.MockInvoicesPaySuccess))
|
|
|
|
invs, err = sat.API.Payments.StripeService.Accounts().Invoices().List(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, invs, 1)
|
|
require.NotEqual(t, draftInv, invs[0].ID)
|
|
require.Equal(t, payments.InvoiceStatusPaid, invs[0].Status)
|
|
|
|
// test purchase with open invoice
|
|
openInvDesc := "testOpenDescription"
|
|
inv, err = sat.API.Payments.StripeService.Accounts().Invoices().Create(ctx, user.ID, 1000, openInvDesc)
|
|
require.NoError(t, err)
|
|
|
|
openInv := inv.ID
|
|
|
|
// attempting to pay a draft invoice changes it to open if payment fails
|
|
_, err = sat.API.Payments.StripeService.Accounts().Invoices().Pay(ctx, inv.ID, stripe.MockInvoicesPayFailure)
|
|
require.Error(t, err)
|
|
|
|
invs, err = sat.API.Payments.StripeService.Accounts().Invoices().List(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, invs, 2)
|
|
var foundInv bool
|
|
for _, inv := range invs {
|
|
if inv.ID == openInv {
|
|
foundInv = true
|
|
require.Equal(t, payments.InvoiceStatusOpen, inv.Status)
|
|
}
|
|
}
|
|
require.True(t, foundInv)
|
|
|
|
require.NoError(t, p.Purchase(userCtx, 1000, openInvDesc, stripe.MockInvoicesPaySuccess))
|
|
|
|
invs, err = sat.API.Payments.StripeService.Accounts().Invoices().List(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, invs, 2)
|
|
foundInv = false
|
|
for _, inv := range invs {
|
|
if inv.ID == openInv {
|
|
foundInv = true
|
|
require.Equal(t, payments.InvoiceStatusPaid, inv.Status)
|
|
}
|
|
}
|
|
require.True(t, foundInv)
|
|
|
|
// purchase with paid invoice skips creating and or paying invoice
|
|
require.NoError(t, p.Purchase(userCtx, 1000, openInvDesc, testPaymentMethod))
|
|
|
|
invs, err = sat.API.Payments.StripeService.Accounts().Invoices().List(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, invs, 2)
|
|
})
|
|
}
|
|
|
|
func TestServiceGenMethods(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 2,
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
s := sat.API.Console.Service
|
|
u0 := planet.Uplinks[0]
|
|
u1 := planet.Uplinks[1]
|
|
user0Ctx, err := sat.UserContext(ctx, u0.Projects[0].Owner.ID)
|
|
require.NoError(t, err)
|
|
user1Ctx, err := sat.UserContext(ctx, u1.Projects[0].Owner.ID)
|
|
require.NoError(t, err)
|
|
|
|
p0ID := u0.Projects[0].ID
|
|
p, err := s.GetProject(user1Ctx, u1.Projects[0].ID)
|
|
require.NoError(t, err)
|
|
p1PublicID := p.PublicID
|
|
|
|
for _, tt := range []struct {
|
|
name string
|
|
ID uuid.UUID
|
|
ctx context.Context
|
|
uplink *testplanet.Uplink
|
|
}{
|
|
{"projectID", p0ID, user0Ctx, u0},
|
|
{"publicID", p1PublicID, user1Ctx, u1},
|
|
} {
|
|
|
|
t.Run("GenUpdateProject with "+tt.name, func(t *testing.T) {
|
|
updatedName := "name " + tt.name
|
|
updatedDescription := "desc " + tt.name
|
|
updatedStorageLimit := memory.Size(100)
|
|
updatedBandwidthLimit := memory.Size(100)
|
|
|
|
info := console.ProjectInfo{
|
|
Name: updatedName,
|
|
Description: updatedDescription,
|
|
StorageLimit: updatedStorageLimit,
|
|
BandwidthLimit: updatedBandwidthLimit,
|
|
}
|
|
updatedProject, err := s.GenUpdateProject(tt.ctx, tt.ID, info)
|
|
require.NoError(t, err.Err)
|
|
if tt.name == "projectID" {
|
|
require.Equal(t, tt.ID, updatedProject.ID)
|
|
} else {
|
|
require.Equal(t, tt.ID, updatedProject.PublicID)
|
|
}
|
|
require.Equal(t, info.Name, updatedProject.Name)
|
|
require.Equal(t, info.Description, updatedProject.Description)
|
|
})
|
|
t.Run("GenCreateAPIKey with "+tt.name, func(t *testing.T) {
|
|
request := console.CreateAPIKeyRequest{
|
|
ProjectID: tt.ID.String(),
|
|
Name: tt.name + " Key",
|
|
}
|
|
apiKey, err := s.GenCreateAPIKey(tt.ctx, request)
|
|
require.NoError(t, err.Err)
|
|
require.Equal(t, tt.ID, apiKey.KeyInfo.ProjectID)
|
|
require.Equal(t, request.Name, apiKey.KeyInfo.Name)
|
|
})
|
|
t.Run("GenGetAPIKeys with "+tt.name, func(t *testing.T) {
|
|
apiKeys, err := s.GenGetAPIKeys(tt.ctx, tt.ID, "", 10, 1, 0, 0)
|
|
require.NoError(t, err.Err)
|
|
require.NotEmpty(t, apiKeys)
|
|
for _, key := range apiKeys.APIKeys {
|
|
require.Equal(t, tt.ID, key.ProjectID)
|
|
}
|
|
})
|
|
|
|
bucket := "testbucket"
|
|
require.NoError(t, tt.uplink.CreateBucket(tt.ctx, sat, bucket))
|
|
require.NoError(t, tt.uplink.Upload(tt.ctx, sat, bucket, "helloworld.txt", []byte("hello world")))
|
|
sat.Accounting.Tally.Loop.TriggerWait()
|
|
|
|
t.Run("GenGetSingleBucketUsageRollup with "+tt.name, func(t *testing.T) {
|
|
rollup, err := s.GenGetSingleBucketUsageRollup(tt.ctx, tt.ID, bucket, time.Now().Add(-24*time.Hour), time.Now())
|
|
require.NoError(t, err.Err)
|
|
require.NotNil(t, rollup)
|
|
require.Equal(t, tt.ID, rollup.ProjectID)
|
|
})
|
|
t.Run("GenGetBucketUsageRollups with "+tt.name, func(t *testing.T) {
|
|
rollups, err := s.GenGetBucketUsageRollups(tt.ctx, tt.ID, time.Now().Add(-24*time.Hour), time.Now())
|
|
require.NoError(t, err.Err)
|
|
require.NotEmpty(t, rollups)
|
|
for _, r := range rollups {
|
|
require.Equal(t, tt.ID, r.ProjectID)
|
|
}
|
|
})
|
|
|
|
// create empty project for easy deletion
|
|
p, err := s.CreateProject(tt.ctx, console.ProjectInfo{
|
|
Name: "foo",
|
|
Description: "bar",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("GenDeleteProject with "+tt.name, func(t *testing.T) {
|
|
var id uuid.UUID
|
|
if tt.name == "projectID" {
|
|
id = p.ID
|
|
} else {
|
|
id = p.PublicID
|
|
}
|
|
httpErr := s.GenDeleteProject(tt.ctx, id)
|
|
require.NoError(t, httpErr.Err)
|
|
p, err := s.GetProject(ctx, id)
|
|
require.Error(t, err)
|
|
require.Nil(t, p)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestProjectInvitations(t *testing.T) {
|
|
testplanet.Run(t, testplanet.Config{
|
|
SatelliteCount: 1,
|
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
|
sat := planet.Satellites[0]
|
|
service := sat.API.Console.Service
|
|
|
|
addUser := func(t *testing.T, ctx context.Context) *console.User {
|
|
user, err := sat.AddUser(ctx, console.CreateUser{
|
|
FullName: "Test User",
|
|
Email: fmt.Sprintf("%s@mail.test", testrand.RandAlphaNumeric(16)),
|
|
}, 1)
|
|
require.NoError(t, err)
|
|
return user
|
|
}
|
|
|
|
getUserAndCtx := func(t *testing.T) (*console.User, context.Context) {
|
|
ctx := testcontext.New(t)
|
|
user := addUser(t, ctx)
|
|
userCtx, err := sat.UserContext(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
return user, userCtx
|
|
}
|
|
|
|
addProject := func(t *testing.T, ctx context.Context) *console.Project {
|
|
owner := addUser(t, ctx)
|
|
project, err := sat.AddProject(ctx, owner.ID, "Test Project")
|
|
require.NoError(t, err)
|
|
return project
|
|
}
|
|
|
|
setInviteDate := func(ctx context.Context, invite *console.ProjectInvitation, createdAt time.Time) *console.ProjectInvitation {
|
|
result, err := sat.DB.Testing().RawDB().ExecContext(ctx,
|
|
"UPDATE project_invitations SET created_at = $1 WHERE project_id = $2 AND email = $3",
|
|
createdAt, invite.ProjectID, strings.ToUpper(invite.Email),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
count, err := result.RowsAffected()
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, count)
|
|
|
|
invite, err = sat.DB.Console().ProjectInvitations().Get(ctx, invite.ProjectID, invite.Email)
|
|
require.NoError(t, err)
|
|
return invite
|
|
}
|
|
|
|
addInvite := func(t *testing.T, ctx context.Context, project *console.Project, email string, createdAt time.Time) *console.ProjectInvitation {
|
|
invite, err := sat.DB.Console().ProjectInvitations().Insert(ctx, &console.ProjectInvitation{
|
|
ProjectID: project.ID,
|
|
Email: email,
|
|
InviterID: &project.OwnerID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return setInviteDate(ctx, invite, createdAt)
|
|
}
|
|
|
|
t.Run("invite users", func(t *testing.T) {
|
|
user, ctx := getUserAndCtx(t)
|
|
user2, ctx2 := getUserAndCtx(t)
|
|
user3, ctx3 := getUserAndCtx(t)
|
|
|
|
project, err := sat.AddProject(ctx, user.ID, "Test Project")
|
|
require.NoError(t, err)
|
|
|
|
invites, err := service.InviteProjectMembers(ctx, project.ID, []string{user2.Email})
|
|
require.NoError(t, err)
|
|
require.Len(t, invites, 1)
|
|
|
|
invites, err = service.GetUserProjectInvitations(ctx2)
|
|
require.NoError(t, err)
|
|
require.Len(t, invites, 1)
|
|
|
|
// adding in a non-existent user should not fail the invitation.
|
|
invites, err = service.InviteProjectMembers(ctx, project.ID, []string{user3.Email, "notauser@mail.com"})
|
|
require.NoError(t, err)
|
|
require.Len(t, invites, 1)
|
|
|
|
invites, err = service.GetUserProjectInvitations(ctx3)
|
|
require.NoError(t, err)
|
|
require.Len(t, invites, 1)
|
|
invite := invites[0]
|
|
|
|
// inviting the same user again should fail if existing invite hasn't expired.
|
|
_, err = service.InviteProjectMembers(ctx, project.ID, []string{user3.Email})
|
|
require.Error(t, err)
|
|
|
|
// expire the invitation.
|
|
setInviteDate(ctx, &invite, time.Now().Add(-sat.Config.Console.ProjectInvitationExpiration))
|
|
|
|
// inviting the same user again should succeed because the existing invite has expired.
|
|
invites, err = service.InviteProjectMembers(ctx, project.ID, []string{user3.Email})
|
|
require.NoError(t, err)
|
|
require.Len(t, invites, 1)
|
|
|
|
// prevent unauthorized users from inviting others (user2 is not a member of the project yet).
|
|
_, err = service.InviteProjectMembers(ctx2, project.ID, []string{"other@mail.com"})
|
|
require.Error(t, err)
|
|
require.True(t, console.ErrNoMembership.Has(err))
|
|
|
|
require.NoError(t, service.RespondToProjectInvitation(ctx2, project.ID, console.ProjectInvitationAccept))
|
|
|
|
// now that user2 is a member, they can invite others.
|
|
_, err = service.InviteProjectMembers(ctx2, project.ID, []string{"other@mail.com"})
|
|
require.NoError(t, err)
|
|
|
|
// inviting a project member should fail.
|
|
_, err = service.InviteProjectMembers(ctx, project.ID, []string{user2.Email})
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("get invitation", func(t *testing.T) {
|
|
user, ctx := getUserAndCtx(t)
|
|
|
|
invites, err := service.GetUserProjectInvitations(ctx)
|
|
require.NoError(t, err)
|
|
require.Empty(t, invites)
|
|
|
|
invite := addInvite(t, ctx, addProject(t, ctx), user.Email, time.Now())
|
|
invites, err = service.GetUserProjectInvitations(ctx)
|
|
require.NoError(t, err)
|
|
require.Len(t, invites, 1)
|
|
require.Equal(t, invite.ProjectID, invites[0].ProjectID)
|
|
require.Equal(t, invite.Email, invites[0].Email)
|
|
require.Equal(t, invite.InviterID, invites[0].InviterID)
|
|
require.WithinDuration(t, invite.CreatedAt, invites[0].CreatedAt, time.Second)
|
|
|
|
setInviteDate(ctx, invite, time.Now().Add(-sat.Config.Console.ProjectInvitationExpiration))
|
|
invites, err = service.GetUserProjectInvitations(ctx)
|
|
require.NoError(t, err)
|
|
require.Empty(t, invites)
|
|
})
|
|
|
|
t.Run("accept invitation", func(t *testing.T) {
|
|
user, ctx := getUserAndCtx(t)
|
|
proj := addProject(t, ctx)
|
|
|
|
addInvite(t, ctx, proj, user.Email, time.Now().Add(-sat.Config.Console.ProjectInvitationExpiration))
|
|
err := service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationAccept)
|
|
require.True(t, console.ErrProjectInviteInvalid.Has(err))
|
|
|
|
addInvite(t, ctx, proj, user.Email, time.Now())
|
|
require.NoError(t, service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationAccept))
|
|
|
|
invites, err := service.GetUserProjectInvitations(ctx)
|
|
require.NoError(t, err)
|
|
require.Empty(t, invites)
|
|
|
|
memberships, err := sat.DB.Console().ProjectMembers().GetByMemberID(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, memberships, 1)
|
|
require.Equal(t, proj.ID, memberships[0].ProjectID)
|
|
|
|
// Ensure that accepting an invitation for a project you are already a member of doesn't return an error.
|
|
// This is because the outcome of the operation is the same as if you weren't a member.
|
|
require.NoError(t, service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationAccept))
|
|
// Ensure that an error is returned if you're a member of a project whose invitation you decline.
|
|
err = service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationDecline)
|
|
require.True(t, console.ErrAlreadyMember.Has(err))
|
|
})
|
|
|
|
t.Run("reject invitation", func(t *testing.T) {
|
|
user, ctx := getUserAndCtx(t)
|
|
proj := addProject(t, ctx)
|
|
|
|
addInvite(t, ctx, proj, user.Email, time.Now())
|
|
require.NoError(t, service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationDecline))
|
|
|
|
invites, err := service.GetUserProjectInvitations(ctx)
|
|
require.NoError(t, err)
|
|
require.Empty(t, invites)
|
|
|
|
memberships, err := sat.DB.Console().ProjectMembers().GetByMemberID(ctx, user.ID)
|
|
require.NoError(t, err)
|
|
require.Empty(t, memberships)
|
|
|
|
// Ensure that declining an invitation for a project you are not a member of doesn't return an error.
|
|
// This is because the outcome of the operation is the same as if you were a member.
|
|
require.NoError(t, service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationDecline))
|
|
// Ensure that an error is returned if you try to accept an invitation that you have already declined or doesn't exist.
|
|
err = service.RespondToProjectInvitation(ctx, proj.ID, console.ProjectInvitationAccept)
|
|
require.True(t, console.ErrProjectInviteInvalid.Has(err))
|
|
})
|
|
})
|
|
}
|