// 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("invite tokens", func(t *testing.T) { user, ctx1 := getUserAndCtx(t) _, ctx2 := getUserAndCtx(t) project, err := sat.AddProject(ctx1, user.ID, "Test Project") require.NoError(t, err) _, err = service.CreateInviteToken(ctx2, project.PublicID, email, time.Now()) require.Error(t, err) require.True(t, console.ErrNoMembership.Has(err)) _, err = service.CreateInviteToken(ctx1, testrand.UUID(), email, time.Now()) require.Error(t, err) require.ErrorIs(t, err, sql.ErrNoRows) someToken, err := service.CreateInviteToken(ctx1, project.PublicID, email, time.Now()) require.NoError(t, err) require.NotEmpty(t, someToken) id, mail, err := service.ParseInviteToken(ctx1, someToken) require.NoError(t, err) require.Equal(t, project.PublicID, id) require.Equal(t, email, mail) someToken, err = service.CreateInviteToken(ctx1, project.PublicID, email, time.Now().Add(-360*time.Hour)) require.NoError(t, err) require.NotEmpty(t, someToken) _, _, err = service.ParseInviteToken(ctx, someToken) require.Error(t, err) require.True(t, console.ErrTokenExpiration.Has(err)) }) t.Run("get invite by invite token", func(t *testing.T) { owner, ctx := getUserAndCtx(t) user, _ := getUserAndCtx(t) project, err := sat.AddProject(ctx, owner.ID, "Test Project") require.NoError(t, err) invite := addInvite(t, ctx, project, user.Email, time.Now()) someToken, err := service.CreateInviteToken(ctx, project.PublicID, "some@email.com", invite.CreatedAt) require.NoError(t, err) inviteFromToken, err := service.GetInviteByToken(ctx, someToken) require.Error(t, err) require.True(t, console.ErrProjectInviteInvalid.Has(err)) require.Nil(t, inviteFromToken) inviteToken, err := service.CreateInviteToken(ctx, project.PublicID, user.Email, invite.CreatedAt) require.NoError(t, err) inviteFromToken, err = service.GetInviteByToken(ctx, inviteToken) require.NoError(t, err) require.NotNil(t, inviteFromToken) require.Equal(t, invite, inviteFromToken) setInviteDate(ctx, invite, time.Now().Add(-sat.Config.Console.ProjectInvitationExpiration)) invites, err := service.GetUserProjectInvitations(ctx) require.NoError(t, err) require.Empty(t, invites) _, err = service.GetInviteByToken(ctx, inviteToken) require.Error(t, err) require.True(t, console.ErrProjectInviteInvalid.Has(err)) // invalid project id. GetInviteByToken supports only public ids. someToken, err = service.CreateInviteToken(ctx, project.ID, user.Email, invite.CreatedAt) require.NoError(t, err) _, err = service.GetInviteByToken(ctx, someToken) require.Error(t, err) require.True(t, console.ErrProjectInviteInvalid.Has(err)) }) 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)) }) }) }