271 рядки
8.4 KiB
Go
271 рядки
8.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"git.entityseven.com/rilaxik/pllist/internal/domain"
|
|
)
|
|
|
|
type fakeBotAccessRepository struct {
|
|
allowedIDs map[int64]bool
|
|
usernameIDs map[string]int64
|
|
claimCalls []claimCall
|
|
allowIDCalls []int64
|
|
allowNameCalls []string
|
|
revokeIDCalls []int64
|
|
revokeNameCall []string
|
|
revokeResult bool
|
|
}
|
|
|
|
type fakeSharedAccessCleaner struct {
|
|
telegramIDs []int64
|
|
usernames []string
|
|
}
|
|
|
|
func (f *fakeSharedAccessCleaner) RemoveSharedAccess(
|
|
_ context.Context, telegramID int64, username string,
|
|
) error {
|
|
f.telegramIDs = append(f.telegramIDs, telegramID)
|
|
f.usernames = append(f.usernames, username)
|
|
return nil
|
|
}
|
|
|
|
type claimCall struct {
|
|
telegramID int64
|
|
username string
|
|
}
|
|
|
|
func (f *fakeBotAccessRepository) AllowTelegramID(_ context.Context, telegramID int64) (bool, error) {
|
|
f.allowIDCalls = append(f.allowIDCalls, telegramID)
|
|
return true, nil
|
|
}
|
|
|
|
func (f *fakeBotAccessRepository) AllowUsername(_ context.Context, username string) (bool, error) {
|
|
f.allowNameCalls = append(f.allowNameCalls, username)
|
|
return true, nil
|
|
}
|
|
|
|
func (f *fakeBotAccessRepository) IsAllowed(_ context.Context, telegramID int64) (bool, error) {
|
|
return f.allowedIDs[telegramID], nil
|
|
}
|
|
|
|
func (f *fakeBotAccessRepository) TelegramIDForUsername(
|
|
_ context.Context, username string,
|
|
) (int64, bool, error) {
|
|
id, ok := f.usernameIDs[username]
|
|
return id, ok, nil
|
|
}
|
|
|
|
func (f *fakeBotAccessRepository) Claim(
|
|
_ context.Context, telegramID int64, username string,
|
|
) error {
|
|
f.claimCalls = append(f.claimCalls, claimCall{telegramID: telegramID, username: username})
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeBotAccessRepository) RevokeTelegramID(
|
|
_ context.Context, telegramID int64,
|
|
) (bool, error) {
|
|
f.revokeIDCalls = append(f.revokeIDCalls, telegramID)
|
|
return f.revokeResult, nil
|
|
}
|
|
|
|
func (f *fakeBotAccessRepository) RevokeUsername(_ context.Context, username string) (bool, error) {
|
|
f.revokeNameCall = append(f.revokeNameCall, username)
|
|
return f.revokeResult, nil
|
|
}
|
|
|
|
func TestBotAccessEnsureAllowed(t *testing.T) {
|
|
t.Run("owner bypasses the grant table", func(t *testing.T) {
|
|
repo := &fakeBotAccessRepository{}
|
|
access := NewBotAccessService(repo, 1, nil)
|
|
|
|
allowed, err := access.EnsureAllowed(context.Background(), 1, "")
|
|
if err != nil || !allowed {
|
|
t.Fatalf("EnsureAllowed() = %v, %v, want true, nil", allowed, err)
|
|
}
|
|
if len(repo.claimCalls) != 0 {
|
|
t.Fatalf("owner made claim calls: %v", repo.claimCalls)
|
|
}
|
|
})
|
|
|
|
t.Run("claims username before checking", func(t *testing.T) {
|
|
repo := &fakeBotAccessRepository{allowedIDs: map[int64]bool{2: true}}
|
|
access := NewBotAccessService(repo, 1, nil)
|
|
|
|
allowed, err := access.EnsureAllowed(context.Background(), 2, "@Alice_1")
|
|
if err != nil || !allowed {
|
|
t.Fatalf("EnsureAllowed() = %v, %v, want true, nil", allowed, err)
|
|
}
|
|
want := claimCall{telegramID: 2, username: "alice_1"}
|
|
if len(repo.claimCalls) != 1 || repo.claimCalls[0] != want {
|
|
t.Fatalf("claim calls = %v, want %v", repo.claimCalls, want)
|
|
}
|
|
})
|
|
|
|
t.Run("denied user", func(t *testing.T) {
|
|
repo := &fakeBotAccessRepository{}
|
|
access := NewBotAccessService(repo, 1, nil)
|
|
|
|
allowed, err := access.EnsureAllowed(context.Background(), 2, "")
|
|
if err != nil || allowed {
|
|
t.Fatalf("EnsureAllowed() = %v, %v, want false, nil", allowed, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBotAccessAllow(t *testing.T) {
|
|
t.Run("rejects non-owner", func(t *testing.T) {
|
|
repo := &fakeBotAccessRepository{}
|
|
access := NewBotAccessService(repo, 1, nil)
|
|
|
|
if _, _, err := access.Allow(context.Background(), 2, "alice_1"); !errors.Is(err, domain.ErrForbidden) {
|
|
t.Fatalf("Allow() error = %v, want ErrForbidden", err)
|
|
}
|
|
})
|
|
|
|
t.Run("normalizes username", func(t *testing.T) {
|
|
repo := &fakeBotAccessRepository{}
|
|
access := NewBotAccessService(repo, 1, nil)
|
|
|
|
created, target, err := access.Allow(context.Background(), 1, "@Alice_1")
|
|
if err != nil || !created {
|
|
t.Fatalf("Allow() = %v, %v, want true, nil", created, err)
|
|
}
|
|
if target.Username != "alice_1" {
|
|
t.Fatalf("target username = %q, want alice_1", target.Username)
|
|
}
|
|
want := []string{"alice_1"}
|
|
if len(repo.allowNameCalls) != 1 || repo.allowNameCalls[0] != want[0] {
|
|
t.Fatalf("AllowUsername calls = %v, want %v", repo.allowNameCalls, want)
|
|
}
|
|
})
|
|
|
|
t.Run("numeric id", func(t *testing.T) {
|
|
repo := &fakeBotAccessRepository{}
|
|
access := NewBotAccessService(repo, 1, nil)
|
|
|
|
if _, target, err := access.Allow(context.Background(), 1, "12345"); err != nil {
|
|
t.Fatalf("Allow() error = %v", err)
|
|
} else if target.TelegramID != 12345 {
|
|
t.Fatalf("target ID = %d, want 12345", target.TelegramID)
|
|
}
|
|
if len(repo.allowIDCalls) != 1 || repo.allowIDCalls[0] != 12345 {
|
|
t.Fatalf("AllowTelegramID calls = %v, want [12345]", repo.allowIDCalls)
|
|
}
|
|
})
|
|
|
|
t.Run("owner is already allowed", func(t *testing.T) {
|
|
repo := &fakeBotAccessRepository{}
|
|
access := NewBotAccessService(repo, 1, nil)
|
|
|
|
created, _, err := access.Allow(context.Background(), 1, "1")
|
|
if err != nil || created {
|
|
t.Fatalf("Allow() = %v, %v, want false, nil", created, err)
|
|
}
|
|
if len(repo.allowIDCalls) != 0 {
|
|
t.Fatalf("owner grant was written: %v", repo.allowIDCalls)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBotAccessKick(t *testing.T) {
|
|
t.Run("rejects non-owner", func(t *testing.T) {
|
|
repo := &fakeBotAccessRepository{}
|
|
access := NewBotAccessService(repo, 1, nil)
|
|
|
|
if _, _, err := access.Kick(context.Background(), 2, "alice_1"); !errors.Is(err, domain.ErrForbidden) {
|
|
t.Fatalf("Kick() error = %v, want ErrForbidden", err)
|
|
}
|
|
})
|
|
|
|
t.Run("refuses to kick the owner", func(t *testing.T) {
|
|
repo := &fakeBotAccessRepository{}
|
|
access := NewBotAccessService(repo, 1, nil)
|
|
|
|
_, _, err := access.Kick(context.Background(), 1, "1")
|
|
if !errors.Is(err, domain.ErrInvalidInput) {
|
|
t.Fatalf("Kick() error = %v, want ErrInvalidInput", err)
|
|
}
|
|
if len(repo.revokeIDCalls) != 0 {
|
|
t.Fatalf("owner revoke was attempted: %v", repo.revokeIDCalls)
|
|
}
|
|
})
|
|
|
|
t.Run("revokes by username and removes shared lists", func(t *testing.T) {
|
|
repo := &fakeBotAccessRepository{
|
|
revokeResult: true,
|
|
usernameIDs: map[string]int64{"alice_1": 12345},
|
|
}
|
|
cleaner := &fakeSharedAccessCleaner{}
|
|
access := NewBotAccessService(repo, 1, cleaner)
|
|
|
|
revoked, _, err := access.Kick(context.Background(), 1, "@Alice_1")
|
|
if err != nil || !revoked {
|
|
t.Fatalf("Kick() = %v, %v, want true, nil", revoked, err)
|
|
}
|
|
if len(repo.revokeNameCall) != 1 || repo.revokeNameCall[0] != "alice_1" {
|
|
t.Fatalf("RevokeUsername calls = %v, want [alice_1]", repo.revokeNameCall)
|
|
}
|
|
if len(cleaner.telegramIDs) != 1 || cleaner.telegramIDs[0] != 12345 {
|
|
t.Fatalf("cleaner telegram IDs = %v, want [12345]", cleaner.telegramIDs)
|
|
}
|
|
if len(cleaner.usernames) != 1 || cleaner.usernames[0] != "alice_1" {
|
|
t.Fatalf("cleaner usernames = %v, want [alice_1]", cleaner.usernames)
|
|
}
|
|
})
|
|
|
|
t.Run("revokes by id and removes shared lists", func(t *testing.T) {
|
|
repo := &fakeBotAccessRepository{revokeResult: true}
|
|
cleaner := &fakeSharedAccessCleaner{}
|
|
access := NewBotAccessService(repo, 1, cleaner)
|
|
|
|
if _, _, err := access.Kick(context.Background(), 1, "12345"); err != nil {
|
|
t.Fatalf("Kick() error = %v", err)
|
|
}
|
|
if len(repo.revokeIDCalls) != 1 || repo.revokeIDCalls[0] != 12345 {
|
|
t.Fatalf("RevokeTelegramID calls = %v, want [12345]", repo.revokeIDCalls)
|
|
}
|
|
if len(cleaner.telegramIDs) != 1 || cleaner.telegramIDs[0] != 12345 {
|
|
t.Fatalf("cleaner telegram IDs = %v, want [12345]", cleaner.telegramIDs)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestParseGrantTarget(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
raw string
|
|
wantID int64
|
|
wantName string
|
|
wantErr bool
|
|
}{
|
|
{name: "username with at", raw: "@Alice_1", wantName: "alice_1"},
|
|
{name: "username without at", raw: "alice_1", wantName: "alice_1"},
|
|
{name: "numeric id", raw: "12345", wantID: 12345},
|
|
{name: "empty", raw: "", wantErr: true},
|
|
{name: "zero id", raw: "0", wantErr: true},
|
|
{name: "negative id", raw: "-5", wantErr: true},
|
|
{name: "short username", raw: "abcd", wantErr: true},
|
|
{name: "space in username", raw: "alice one", wantErr: true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
target, err := parseGrantTarget(tt.raw, "usage")
|
|
if (err != nil) != tt.wantErr {
|
|
t.Fatalf("parseGrantTarget() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
if tt.wantErr {
|
|
return
|
|
}
|
|
if target.TelegramID != tt.wantID || target.Username != tt.wantName {
|
|
t.Fatalf("parseGrantTarget() = %+v, want id %d name %q",
|
|
target, tt.wantID, tt.wantName)
|
|
}
|
|
})
|
|
}
|
|
}
|