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

344 рядки
10 KiB
Go

package service
import (
"context"
"errors"
"testing"
"git.entityseven.com/rilaxik/pllist/internal/domain"
)
type fakeUserLookup struct {
users map[int64]domain.User
}
func (f *fakeUserLookup) GetByTelegramID(
_ context.Context, telegramID int64,
) (domain.User, bool, error) {
user, ok := f.users[telegramID]
return user, ok, nil
}
type fakeShareRepository struct {
shares []domain.Share
userUpserts []shareUserUpsert
pendingIDUpserts []sharePendingIDUpsert
pendingUsernameUpserts []sharePendingUsernameUpsert
setUserRoles []shareRoleUpdate
setPendingRoles []shareRoleUpdate
revokedUserShares []shareRef
revokedPendingShares []shareRef
resolveCalls []resolveCall
}
type shareUserUpsert struct {
listID int64
userID int64
role domain.Role
}
type sharePendingIDUpsert struct {
listID int64
createdBy int64
telegramID int64
role domain.Role
}
type sharePendingUsernameUpsert struct {
listID int64
createdBy int64
username string
role domain.Role
}
type shareRoleUpdate struct {
listID int64
shareID int64
role domain.Role
}
type shareRef struct {
listID int64
shareID int64
}
type resolveCall struct {
userID int64
telegramID int64
username string
}
func (f *fakeShareRepository) ListByList(
_ context.Context, _ int64,
) ([]domain.Share, error) {
return f.shares, nil
}
func (f *fakeShareRepository) UpsertUser(
_ context.Context, listID, userID int64, role domain.Role,
) error {
f.userUpserts = append(f.userUpserts, shareUserUpsert{listID, userID, role})
return nil
}
func (f *fakeShareRepository) UpsertPendingTelegramID(
_ context.Context, listID, createdByID, telegramID int64, role domain.Role,
) error {
f.pendingIDUpserts = append(f.pendingIDUpserts,
sharePendingIDUpsert{listID, createdByID, telegramID, role})
return nil
}
func (f *fakeShareRepository) UpsertPendingUsername(
_ context.Context, listID, createdByID int64, username string, role domain.Role,
) error {
f.pendingUsernameUpserts = append(f.pendingUsernameUpserts,
sharePendingUsernameUpsert{listID, createdByID, username, role})
return nil
}
func (f *fakeShareRepository) SetUserRole(
_ context.Context, listID, shareID int64, role domain.Role,
) error {
f.setUserRoles = append(f.setUserRoles, shareRoleUpdate{listID, shareID, role})
return nil
}
func (f *fakeShareRepository) SetPendingRole(
_ context.Context, listID, shareID int64, role domain.Role,
) error {
f.setPendingRoles = append(f.setPendingRoles, shareRoleUpdate{listID, shareID, role})
return nil
}
func (f *fakeShareRepository) RevokeUser(_ context.Context, listID, shareID int64) error {
f.revokedUserShares = append(f.revokedUserShares, shareRef{listID, shareID})
return nil
}
func (f *fakeShareRepository) RevokePending(_ context.Context, listID, shareID int64) error {
f.revokedPendingShares = append(f.revokedPendingShares, shareRef{listID, shareID})
return nil
}
func (f *fakeShareRepository) ResolvePending(
_ context.Context, userID, telegramID int64, username string,
) error {
f.resolveCalls = append(f.resolveCalls, resolveCall{userID, telegramID, username})
return nil
}
func (f *fakeShareRepository) RemoveSharedAccess(
_ context.Context, _ int64, _ string,
) error {
return nil
}
func newTestShareService(
shares *fakeShareRepository,
users *fakeUserLookup,
bot *fakeBotAccessRepository,
lists *fakeAccessRepository,
) *ShareService {
return NewShareService(
shares, users, NewAccessService(lists),
NewBotAccessService(bot, 100, nil),
)
}
func TestShareServiceShare(t *testing.T) {
owner := domain.User{ID: 1, TelegramID: 100}
lists := &fakeAccessRepository{
lists: map[int64]domain.List{
10: {ID: 10, OwnerID: 1, Title: "Groceries"},
},
}
t.Run("resolved user by numeric id", func(t *testing.T) {
shares := &fakeShareRepository{}
users := &fakeUserLookup{users: map[int64]domain.User{
200: {ID: 2, TelegramID: 200},
}}
bot := &fakeBotAccessRepository{allowedIDs: map[int64]bool{200: true}}
service := newTestShareService(shares, users, bot, lists)
err := service.Share(context.Background(), owner, 10, "200", domain.RoleEditor)
if err != nil {
t.Fatalf("Share() error = %v", err)
}
want := shareUserUpsert{listID: 10, userID: 2, role: domain.RoleEditor}
if len(shares.userUpserts) != 1 || shares.userUpserts[0] != want {
t.Fatalf("user upserts = %v, want %v", shares.userUpserts, want)
}
})
t.Run("pending username", func(t *testing.T) {
shares := &fakeShareRepository{}
bot := &fakeBotAccessRepository{usernameIDs: map[string]int64{"alice_1": 0}}
service := newTestShareService(shares, &fakeUserLookup{}, bot, lists)
err := service.Share(context.Background(), owner, 10, "@Alice_1", domain.RoleReader)
if err != nil {
t.Fatalf("Share() error = %v", err)
}
want := sharePendingUsernameUpsert{
listID: 10, createdBy: 1, username: "alice_1", role: domain.RoleReader,
}
if len(shares.pendingUsernameUpserts) != 1 ||
shares.pendingUsernameUpserts[0] != want {
t.Fatalf("pending username upserts = %v, want %v",
shares.pendingUsernameUpserts, want)
}
})
t.Run("rejects user without bot access", func(t *testing.T) {
service := newTestShareService(
&fakeShareRepository{}, &fakeUserLookup{}, &fakeBotAccessRepository{}, lists,
)
err := service.Share(context.Background(), owner, 10, "@alice_1", domain.RoleEditor)
if !errors.Is(err, domain.ErrInvalidInput) {
t.Fatalf("Share() error = %v, want ErrInvalidInput", err)
}
})
t.Run("rejects sharing with the owner", func(t *testing.T) {
bot := &fakeBotAccessRepository{allowedIDs: map[int64]bool{100: true}}
users := &fakeUserLookup{users: map[int64]domain.User{
100: {ID: 1, TelegramID: 100},
}}
service := newTestShareService(&fakeShareRepository{}, users, bot, lists)
err := service.Share(context.Background(), owner, 10, "100", domain.RoleEditor)
if !errors.Is(err, domain.ErrInvalidInput) {
t.Fatalf("Share() error = %v, want ErrInvalidInput", err)
}
})
t.Run("rejects non-owner", func(t *testing.T) {
readerLists := &fakeAccessRepository{
lists: map[int64]domain.List{
10: {ID: 10, OwnerID: 1, Title: "Groceries"},
},
roles: map[[2]int64]domain.Role{{10, 2}: domain.RoleReader},
}
service := newTestShareService(
&fakeShareRepository{}, &fakeUserLookup{}, &fakeBotAccessRepository{}, readerLists,
)
err := service.Share(context.Background(),
domain.User{ID: 2, TelegramID: 200}, 10, "300", domain.RoleEditor)
if !errors.Is(err, domain.ErrForbidden) {
t.Fatalf("Share() error = %v, want ErrForbidden", err)
}
})
t.Run("rejects invalid role", func(t *testing.T) {
service := newTestShareService(
&fakeShareRepository{}, &fakeUserLookup{}, &fakeBotAccessRepository{}, lists,
)
err := service.Share(context.Background(), owner, 10, "200", domain.RoleOwner)
if !errors.Is(err, domain.ErrInvalidInput) {
t.Fatalf("Share() error = %v, want ErrInvalidInput", err)
}
})
}
func TestShareServicePendingTargets(t *testing.T) {
owner := domain.User{ID: 1, TelegramID: 100}
lists := &fakeAccessRepository{
lists: map[int64]domain.List{
10: {ID: 10, OwnerID: 1, Title: "Groceries"},
},
}
t.Run("pending telegram id", func(t *testing.T) {
shares := &fakeShareRepository{}
bot := &fakeBotAccessRepository{allowedIDs: map[int64]bool{200: true}}
service := newTestShareService(shares, &fakeUserLookup{}, bot, lists)
err := service.Share(context.Background(), owner, 10, "200", domain.RoleReader)
if err != nil {
t.Fatalf("Share() error = %v", err)
}
want := sharePendingIDUpsert{
listID: 10, createdBy: 1, telegramID: 200, role: domain.RoleReader,
}
if len(shares.pendingIDUpserts) != 1 || shares.pendingIDUpserts[0] != want {
t.Fatalf("pending id upserts = %v, want %v", shares.pendingIDUpserts, want)
}
})
t.Run("pending username resolves to a known id", func(t *testing.T) {
shares := &fakeShareRepository{}
bot := &fakeBotAccessRepository{usernameIDs: map[string]int64{"alice_1": 200}}
service := newTestShareService(shares, &fakeUserLookup{}, bot, lists)
if _, err := service.Prepare(context.Background(), owner, 10, "alice_1"); err != nil {
t.Fatalf("Prepare() error = %v", err)
}
err := service.Share(context.Background(), owner, 10, "alice_1", domain.RoleEditor)
if err != nil {
t.Fatalf("Share() error = %v", err)
}
want := sharePendingIDUpsert{
listID: 10, createdBy: 1, telegramID: 200, role: domain.RoleEditor,
}
if len(shares.pendingIDUpserts) != 1 || shares.pendingIDUpserts[0] != want {
t.Fatalf("pending id upserts = %v, want %v", shares.pendingIDUpserts, want)
}
})
}
func TestShareServiceSetRoleAndRevoke(t *testing.T) {
lists := &fakeAccessRepository{
lists: map[int64]domain.List{
10: {ID: 10, OwnerID: 1, Title: "Groceries"},
},
}
shares := &fakeShareRepository{}
service := newTestShareService(shares, &fakeUserLookup{}, &fakeBotAccessRepository{}, lists)
if err := service.SetRole(context.Background(), 1, 10, 55, false, domain.RoleReader); err != nil {
t.Fatalf("SetRole() error = %v", err)
}
if len(shares.setUserRoles) != 1 ||
shares.setUserRoles[0] != (shareRoleUpdate{10, 55, domain.RoleReader}) {
t.Fatalf("set user roles = %v", shares.setUserRoles)
}
if err := service.SetRole(context.Background(), 1, 10, 56, true, domain.RoleEditor); err != nil {
t.Fatalf("SetRole() error = %v", err)
}
if len(shares.setPendingRoles) != 1 ||
shares.setPendingRoles[0] != (shareRoleUpdate{10, 56, domain.RoleEditor}) {
t.Fatalf("set pending roles = %v", shares.setPendingRoles)
}
if err := service.Revoke(context.Background(), 1, 10, 57, false); err != nil {
t.Fatalf("Revoke() error = %v", err)
}
if len(shares.revokedUserShares) != 1 ||
shares.revokedUserShares[0] != (shareRef{10, 57}) {
t.Fatalf("revoked user shares = %v", shares.revokedUserShares)
}
}
func TestShareServiceResolvePending(t *testing.T) {
shares := &fakeShareRepository{}
service := newTestShareService(
shares, &fakeUserLookup{}, &fakeBotAccessRepository{},
&fakeAccessRepository{},
)
if err := service.ResolvePending(context.Background(), 2, 200, "@Alice_1"); err != nil {
t.Fatalf("ResolvePending() error = %v", err)
}
want := resolveCall{userID: 2, telegramID: 200, username: "alice_1"}
if len(shares.resolveCalls) != 1 || shares.resolveCalls[0] != want {
t.Fatalf("resolve calls = %v, want %v", shares.resolveCalls, want)
}
}