464 lines
14 KiB
Go
464 lines
14 KiB
Go
//go:build unit
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type userRepoStub struct {
|
|
user *User
|
|
getErr error
|
|
createErr error
|
|
deleteErr error
|
|
exists bool
|
|
existsErr error
|
|
nextID int64
|
|
created []*User
|
|
deletedIDs []int64
|
|
}
|
|
|
|
func (s *userRepoStub) Create(ctx context.Context, user *User) error {
|
|
if s.createErr != nil {
|
|
return s.createErr
|
|
}
|
|
if s.nextID != 0 && user.ID == 0 {
|
|
user.ID = s.nextID
|
|
}
|
|
s.created = append(s.created, user)
|
|
return nil
|
|
}
|
|
|
|
func (s *userRepoStub) GetByID(ctx context.Context, id int64) (*User, error) {
|
|
if s.getErr != nil {
|
|
return nil, s.getErr
|
|
}
|
|
if s.user == nil {
|
|
return nil, ErrUserNotFound
|
|
}
|
|
return s.user, nil
|
|
}
|
|
|
|
func (s *userRepoStub) GetByEmail(ctx context.Context, email string) (*User, error) {
|
|
panic("unexpected GetByEmail call")
|
|
}
|
|
|
|
func (s *userRepoStub) GetFirstAdmin(ctx context.Context) (*User, error) {
|
|
panic("unexpected GetFirstAdmin call")
|
|
}
|
|
|
|
func (s *userRepoStub) Update(ctx context.Context, user *User) error {
|
|
panic("unexpected Update call")
|
|
}
|
|
|
|
func (s *userRepoStub) Delete(ctx context.Context, id int64) error {
|
|
s.deletedIDs = append(s.deletedIDs, id)
|
|
return s.deleteErr
|
|
}
|
|
|
|
func (s *userRepoStub) List(ctx context.Context, params pagination.PaginationParams) ([]User, *pagination.PaginationResult, error) {
|
|
panic("unexpected List call")
|
|
}
|
|
|
|
func (s *userRepoStub) ListWithFilters(ctx context.Context, params pagination.PaginationParams, status, role, search string) ([]User, *pagination.PaginationResult, error) {
|
|
panic("unexpected ListWithFilters call")
|
|
}
|
|
|
|
func (s *userRepoStub) UpdateBalance(ctx context.Context, id int64, amount float64) error {
|
|
panic("unexpected UpdateBalance call")
|
|
}
|
|
|
|
func (s *userRepoStub) DeductBalance(ctx context.Context, id int64, amount float64) error {
|
|
panic("unexpected DeductBalance call")
|
|
}
|
|
|
|
func (s *userRepoStub) UpdateConcurrency(ctx context.Context, id int64, amount int) error {
|
|
panic("unexpected UpdateConcurrency call")
|
|
}
|
|
|
|
func (s *userRepoStub) ExistsByEmail(ctx context.Context, email string) (bool, error) {
|
|
if s.existsErr != nil {
|
|
return false, s.existsErr
|
|
}
|
|
return s.exists, nil
|
|
}
|
|
|
|
func (s *userRepoStub) RemoveGroupFromAllowedGroups(ctx context.Context, groupID int64) (int64, error) {
|
|
panic("unexpected RemoveGroupFromAllowedGroups call")
|
|
}
|
|
|
|
type groupRepoStub struct {
|
|
affectedUserIDs []int64
|
|
deleteErr error
|
|
deleteCalls []int64
|
|
}
|
|
|
|
func (s *groupRepoStub) Create(ctx context.Context, group *Group) error {
|
|
panic("unexpected Create call")
|
|
}
|
|
|
|
func (s *groupRepoStub) GetByID(ctx context.Context, id int64) (*Group, error) {
|
|
panic("unexpected GetByID call")
|
|
}
|
|
|
|
func (s *groupRepoStub) Update(ctx context.Context, group *Group) error {
|
|
panic("unexpected Update call")
|
|
}
|
|
|
|
func (s *groupRepoStub) Delete(ctx context.Context, id int64) error {
|
|
panic("unexpected Delete call")
|
|
}
|
|
|
|
func (s *groupRepoStub) DeleteCascade(ctx context.Context, id int64) ([]int64, error) {
|
|
s.deleteCalls = append(s.deleteCalls, id)
|
|
return s.affectedUserIDs, s.deleteErr
|
|
}
|
|
|
|
func (s *groupRepoStub) List(ctx context.Context, params pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error) {
|
|
panic("unexpected List call")
|
|
}
|
|
|
|
func (s *groupRepoStub) ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status string, isExclusive *bool) ([]Group, *pagination.PaginationResult, error) {
|
|
panic("unexpected ListWithFilters call")
|
|
}
|
|
|
|
func (s *groupRepoStub) ListActive(ctx context.Context) ([]Group, error) {
|
|
panic("unexpected ListActive call")
|
|
}
|
|
|
|
func (s *groupRepoStub) ListActiveByPlatform(ctx context.Context, platform string) ([]Group, error) {
|
|
panic("unexpected ListActiveByPlatform call")
|
|
}
|
|
|
|
func (s *groupRepoStub) ExistsByName(ctx context.Context, name string) (bool, error) {
|
|
panic("unexpected ExistsByName call")
|
|
}
|
|
|
|
func (s *groupRepoStub) GetAccountCount(ctx context.Context, groupID int64) (int64, error) {
|
|
panic("unexpected GetAccountCount call")
|
|
}
|
|
|
|
func (s *groupRepoStub) DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error) {
|
|
panic("unexpected DeleteAccountGroupsByGroupID call")
|
|
}
|
|
|
|
type proxyRepoStub struct {
|
|
deleteErr error
|
|
deletedIDs []int64
|
|
}
|
|
|
|
func (s *proxyRepoStub) Create(ctx context.Context, proxy *Proxy) error {
|
|
panic("unexpected Create call")
|
|
}
|
|
|
|
func (s *proxyRepoStub) GetByID(ctx context.Context, id int64) (*Proxy, error) {
|
|
panic("unexpected GetByID call")
|
|
}
|
|
|
|
func (s *proxyRepoStub) Update(ctx context.Context, proxy *Proxy) error {
|
|
panic("unexpected Update call")
|
|
}
|
|
|
|
func (s *proxyRepoStub) Delete(ctx context.Context, id int64) error {
|
|
s.deletedIDs = append(s.deletedIDs, id)
|
|
return s.deleteErr
|
|
}
|
|
|
|
func (s *proxyRepoStub) List(ctx context.Context, params pagination.PaginationParams) ([]Proxy, *pagination.PaginationResult, error) {
|
|
panic("unexpected List call")
|
|
}
|
|
|
|
func (s *proxyRepoStub) ListWithFilters(ctx context.Context, params pagination.PaginationParams, protocol, status, search string) ([]Proxy, *pagination.PaginationResult, error) {
|
|
panic("unexpected ListWithFilters call")
|
|
}
|
|
|
|
func (s *proxyRepoStub) ListActive(ctx context.Context) ([]Proxy, error) {
|
|
panic("unexpected ListActive call")
|
|
}
|
|
|
|
func (s *proxyRepoStub) ListActiveWithAccountCount(ctx context.Context) ([]ProxyWithAccountCount, error) {
|
|
panic("unexpected ListActiveWithAccountCount call")
|
|
}
|
|
|
|
func (s *proxyRepoStub) ExistsByHostPortAuth(ctx context.Context, host string, port int, username, password string) (bool, error) {
|
|
panic("unexpected ExistsByHostPortAuth call")
|
|
}
|
|
|
|
func (s *proxyRepoStub) CountAccountsByProxyID(ctx context.Context, proxyID int64) (int64, error) {
|
|
panic("unexpected CountAccountsByProxyID call")
|
|
}
|
|
|
|
type redeemRepoStub struct {
|
|
deleteErrByID map[int64]error
|
|
deletedIDs []int64
|
|
}
|
|
|
|
func (s *redeemRepoStub) Create(ctx context.Context, code *RedeemCode) error {
|
|
panic("unexpected Create call")
|
|
}
|
|
|
|
func (s *redeemRepoStub) CreateBatch(ctx context.Context, codes []RedeemCode) error {
|
|
panic("unexpected CreateBatch call")
|
|
}
|
|
|
|
func (s *redeemRepoStub) GetByID(ctx context.Context, id int64) (*RedeemCode, error) {
|
|
panic("unexpected GetByID call")
|
|
}
|
|
|
|
func (s *redeemRepoStub) GetByCode(ctx context.Context, code string) (*RedeemCode, error) {
|
|
panic("unexpected GetByCode call")
|
|
}
|
|
|
|
func (s *redeemRepoStub) Update(ctx context.Context, code *RedeemCode) error {
|
|
panic("unexpected Update call")
|
|
}
|
|
|
|
func (s *redeemRepoStub) Delete(ctx context.Context, id int64) error {
|
|
s.deletedIDs = append(s.deletedIDs, id)
|
|
if s.deleteErrByID != nil {
|
|
if err, ok := s.deleteErrByID[id]; ok {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *redeemRepoStub) Use(ctx context.Context, id, userID int64) error {
|
|
panic("unexpected Use call")
|
|
}
|
|
|
|
func (s *redeemRepoStub) List(ctx context.Context, params pagination.PaginationParams) ([]RedeemCode, *pagination.PaginationResult, error) {
|
|
panic("unexpected List call")
|
|
}
|
|
|
|
func (s *redeemRepoStub) ListWithFilters(ctx context.Context, params pagination.PaginationParams, codeType, status, search string) ([]RedeemCode, *pagination.PaginationResult, error) {
|
|
panic("unexpected ListWithFilters call")
|
|
}
|
|
|
|
func (s *redeemRepoStub) ListByUser(ctx context.Context, userID int64, limit int) ([]RedeemCode, error) {
|
|
panic("unexpected ListByUser call")
|
|
}
|
|
|
|
type subscriptionInvalidateCall struct {
|
|
userID int64
|
|
groupID int64
|
|
}
|
|
|
|
type billingCacheStub struct {
|
|
invalidations chan subscriptionInvalidateCall
|
|
}
|
|
|
|
func newBillingCacheStub(buffer int) *billingCacheStub {
|
|
return &billingCacheStub{invalidations: make(chan subscriptionInvalidateCall, buffer)}
|
|
}
|
|
|
|
func (s *billingCacheStub) GetUserBalance(ctx context.Context, userID int64) (float64, error) {
|
|
panic("unexpected GetUserBalance call")
|
|
}
|
|
|
|
func (s *billingCacheStub) SetUserBalance(ctx context.Context, userID int64, balance float64) error {
|
|
panic("unexpected SetUserBalance call")
|
|
}
|
|
|
|
func (s *billingCacheStub) DeductUserBalance(ctx context.Context, userID int64, amount float64) error {
|
|
panic("unexpected DeductUserBalance call")
|
|
}
|
|
|
|
func (s *billingCacheStub) InvalidateUserBalance(ctx context.Context, userID int64) error {
|
|
panic("unexpected InvalidateUserBalance call")
|
|
}
|
|
|
|
func (s *billingCacheStub) GetSubscriptionCache(ctx context.Context, userID, groupID int64) (*SubscriptionCacheData, error) {
|
|
panic("unexpected GetSubscriptionCache call")
|
|
}
|
|
|
|
func (s *billingCacheStub) SetSubscriptionCache(ctx context.Context, userID, groupID int64, data *SubscriptionCacheData) error {
|
|
panic("unexpected SetSubscriptionCache call")
|
|
}
|
|
|
|
func (s *billingCacheStub) UpdateSubscriptionUsage(ctx context.Context, userID, groupID int64, cost float64) error {
|
|
panic("unexpected UpdateSubscriptionUsage call")
|
|
}
|
|
|
|
func (s *billingCacheStub) InvalidateSubscriptionCache(ctx context.Context, userID, groupID int64) error {
|
|
s.invalidations <- subscriptionInvalidateCall{userID: userID, groupID: groupID}
|
|
return nil
|
|
}
|
|
|
|
func waitForInvalidations(t *testing.T, ch <-chan subscriptionInvalidateCall, expected int) []subscriptionInvalidateCall {
|
|
t.Helper()
|
|
calls := make([]subscriptionInvalidateCall, 0, expected)
|
|
timeout := time.After(2 * time.Second)
|
|
for len(calls) < expected {
|
|
select {
|
|
case call := <-ch:
|
|
calls = append(calls, call)
|
|
case <-timeout:
|
|
t.Fatalf("timeout waiting for %d invalidations, got %d", expected, len(calls))
|
|
}
|
|
}
|
|
return calls
|
|
}
|
|
|
|
func TestAdminService_DeleteUser_Success(t *testing.T) {
|
|
repo := &userRepoStub{user: &User{ID: 7, Role: RoleUser}}
|
|
svc := &adminServiceImpl{userRepo: repo}
|
|
|
|
err := svc.DeleteUser(context.Background(), 7)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []int64{7}, repo.deletedIDs)
|
|
}
|
|
|
|
func TestAdminService_DeleteUser_NotFound(t *testing.T) {
|
|
repo := &userRepoStub{getErr: ErrUserNotFound}
|
|
svc := &adminServiceImpl{userRepo: repo}
|
|
|
|
err := svc.DeleteUser(context.Background(), 404)
|
|
require.ErrorIs(t, err, ErrUserNotFound)
|
|
require.Empty(t, repo.deletedIDs)
|
|
}
|
|
|
|
func TestAdminService_DeleteUser_AdminGuard(t *testing.T) {
|
|
repo := &userRepoStub{user: &User{ID: 1, Role: RoleAdmin}}
|
|
svc := &adminServiceImpl{userRepo: repo}
|
|
|
|
err := svc.DeleteUser(context.Background(), 1)
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "cannot delete admin user")
|
|
require.Empty(t, repo.deletedIDs)
|
|
}
|
|
|
|
func TestAdminService_DeleteUser_DeleteError(t *testing.T) {
|
|
deleteErr := errors.New("delete failed")
|
|
repo := &userRepoStub{
|
|
user: &User{ID: 9, Role: RoleUser},
|
|
deleteErr: deleteErr,
|
|
}
|
|
svc := &adminServiceImpl{userRepo: repo}
|
|
|
|
err := svc.DeleteUser(context.Background(), 9)
|
|
require.ErrorIs(t, err, deleteErr)
|
|
require.Equal(t, []int64{9}, repo.deletedIDs)
|
|
}
|
|
|
|
func TestAdminService_DeleteGroup_Success_WithCacheInvalidation(t *testing.T) {
|
|
cache := newBillingCacheStub(2)
|
|
repo := &groupRepoStub{affectedUserIDs: []int64{11, 12}}
|
|
svc := &adminServiceImpl{
|
|
groupRepo: repo,
|
|
billingCacheService: &BillingCacheService{cache: cache},
|
|
}
|
|
|
|
err := svc.DeleteGroup(context.Background(), 5)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []int64{5}, repo.deleteCalls)
|
|
|
|
calls := waitForInvalidations(t, cache.invalidations, 2)
|
|
require.ElementsMatch(t, []subscriptionInvalidateCall{
|
|
{userID: 11, groupID: 5},
|
|
{userID: 12, groupID: 5},
|
|
}, calls)
|
|
}
|
|
|
|
func TestAdminService_DeleteGroup_NotFound(t *testing.T) {
|
|
repo := &groupRepoStub{deleteErr: ErrGroupNotFound}
|
|
svc := &adminServiceImpl{groupRepo: repo}
|
|
|
|
err := svc.DeleteGroup(context.Background(), 99)
|
|
require.ErrorIs(t, err, ErrGroupNotFound)
|
|
}
|
|
|
|
func TestAdminService_DeleteGroup_Error(t *testing.T) {
|
|
deleteErr := errors.New("delete failed")
|
|
repo := &groupRepoStub{deleteErr: deleteErr}
|
|
svc := &adminServiceImpl{groupRepo: repo}
|
|
|
|
err := svc.DeleteGroup(context.Background(), 42)
|
|
require.ErrorIs(t, err, deleteErr)
|
|
}
|
|
|
|
func TestAdminService_DeleteProxy_Success(t *testing.T) {
|
|
repo := &proxyRepoStub{}
|
|
svc := &adminServiceImpl{proxyRepo: repo}
|
|
|
|
err := svc.DeleteProxy(context.Background(), 7)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []int64{7}, repo.deletedIDs)
|
|
}
|
|
|
|
func TestAdminService_DeleteProxy_Idempotent(t *testing.T) {
|
|
repo := &proxyRepoStub{}
|
|
svc := &adminServiceImpl{proxyRepo: repo}
|
|
|
|
err := svc.DeleteProxy(context.Background(), 404)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []int64{404}, repo.deletedIDs)
|
|
}
|
|
|
|
func TestAdminService_DeleteProxy_Error(t *testing.T) {
|
|
deleteErr := errors.New("delete failed")
|
|
repo := &proxyRepoStub{deleteErr: deleteErr}
|
|
svc := &adminServiceImpl{proxyRepo: repo}
|
|
|
|
err := svc.DeleteProxy(context.Background(), 33)
|
|
require.ErrorIs(t, err, deleteErr)
|
|
}
|
|
|
|
func TestAdminService_DeleteRedeemCode_Success(t *testing.T) {
|
|
repo := &redeemRepoStub{}
|
|
svc := &adminServiceImpl{redeemCodeRepo: repo}
|
|
|
|
err := svc.DeleteRedeemCode(context.Background(), 10)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []int64{10}, repo.deletedIDs)
|
|
}
|
|
|
|
func TestAdminService_DeleteRedeemCode_Idempotent(t *testing.T) {
|
|
repo := &redeemRepoStub{}
|
|
svc := &adminServiceImpl{redeemCodeRepo: repo}
|
|
|
|
err := svc.DeleteRedeemCode(context.Background(), 999)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []int64{999}, repo.deletedIDs)
|
|
}
|
|
|
|
func TestAdminService_DeleteRedeemCode_Error(t *testing.T) {
|
|
deleteErr := errors.New("delete failed")
|
|
repo := &redeemRepoStub{deleteErrByID: map[int64]error{1: deleteErr}}
|
|
svc := &adminServiceImpl{redeemCodeRepo: repo}
|
|
|
|
err := svc.DeleteRedeemCode(context.Background(), 1)
|
|
require.ErrorIs(t, err, deleteErr)
|
|
require.Equal(t, []int64{1}, repo.deletedIDs)
|
|
}
|
|
|
|
func TestAdminService_BatchDeleteRedeemCodes_Success(t *testing.T) {
|
|
repo := &redeemRepoStub{}
|
|
svc := &adminServiceImpl{redeemCodeRepo: repo}
|
|
|
|
deleted, err := svc.BatchDeleteRedeemCodes(context.Background(), []int64{1, 2, 3})
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(3), deleted)
|
|
require.Equal(t, []int64{1, 2, 3}, repo.deletedIDs)
|
|
}
|
|
|
|
func TestAdminService_BatchDeleteRedeemCodes_PartialFailures(t *testing.T) {
|
|
repo := &redeemRepoStub{
|
|
deleteErrByID: map[int64]error{
|
|
2: errors.New("db error"),
|
|
},
|
|
}
|
|
svc := &adminServiceImpl{redeemCodeRepo: repo}
|
|
|
|
deleted, err := svc.BatchDeleteRedeemCodes(context.Background(), []int64{1, 2, 3})
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(2), deleted)
|
|
require.Equal(t, []int64{1, 2, 3}, repo.deletedIDs)
|
|
}
|