Файли
2026-11-08 03:05:46 +00:00

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)
}
})
}
}