This commit is contained in:
Yvan
2026-09-23 17:42:55 +08:00
parent 53e8599bdd
commit 0a8ecbe09b
75 changed files with 2299 additions and 276 deletions
+40 -3
View File
@@ -2,10 +2,14 @@ package gift
import (
"context"
kratoserrors "github.com/go-kratos/kratos/v3/errors"
"fmt"
"sort"
"strings"
"time"
kratoserrors "github.com/go-kratos/kratos/v3/errors"
bizlive "kra/internal/biz/live"
)
var (
@@ -17,6 +21,24 @@ var (
ErrSendNotAllowed = kratoserrors.BadRequest("GIFT_SEND_NOT_ALLOWED", "gift cannot be sent")
)
// InsufficientBalanceError preserves the wallet snapshot needed by clients
// to decide whether to open the recharge flow. It unwraps to the stable
// ErrInsufficientBalance reason so existing errors.Is checks keep working.
type InsufficientBalanceError struct {
Required int64
Balance int64
Missing int64
}
func (e *InsufficientBalanceError) Error() string {
if e == nil {
return ErrInsufficientBalance.Error()
}
return fmt.Sprintf("insufficient diamond balance: need %d, have %d, missing %d", e.Required, e.Balance, e.Missing)
}
func (e *InsufficientBalanceError) Unwrap() error { return ErrInsufficientBalance }
type Gift struct {
ID uint
Name, Icon, Description string
@@ -72,13 +94,21 @@ type Repo interface {
type Notifier interface {
GiftSent(context.Context, *Send)
}
type roomAvailability interface {
IsLive(context.Context, uint) (bool, error)
}
type Usecase struct {
repo Repo
notifier Notifier
rooms roomAvailability
}
func NewUsecase(repo Repo, notifier Notifier) *Usecase {
return &Usecase{repo: repo, notifier: notifier}
func NewUsecase(repo Repo, notifier Notifier, rooms *bizlive.RoomUsecase) *Usecase {
return newUsecase(repo, notifier, rooms)
}
func newUsecase(repo Repo, notifier Notifier, rooms roomAvailability) *Usecase {
return &Usecase{repo: repo, notifier: notifier, rooms: rooms}
}
func (uc *Usecase) List(ctx context.Context, f Filter, o ListOptions) ([]*Gift, int64, error) {
f.Name = strings.TrimSpace(f.Name)
@@ -115,6 +145,13 @@ func (uc *Usecase) Send(ctx context.Context, userID, roomID, giftID uint, quanti
if userID == 0 || roomID == 0 || giftID == 0 || quantity < 1 || quantity > 9999 {
return nil, ErrSendNotAllowed
}
if uc.rooms == nil {
return nil, ErrSendNotAllowed
}
live, err := uc.rooms.IsLive(ctx, roomID)
if err != nil || !live {
return nil, ErrSendNotAllowed
}
result, err := uc.repo.Send(ctx, userID, roomID, giftID, quantity)
if err != nil {
return nil, err
+49
View File
@@ -0,0 +1,49 @@
package gift
import (
"context"
"errors"
"testing"
)
type sendRepoStub struct {
Repo
called bool
}
func (r *sendRepoStub) Send(_ context.Context, userID, roomID, giftID uint, quantity int) (*Send, error) {
r.called = true
return &Send{UserID: userID, RoomID: &roomID, GiftID: &giftID, Quantity: quantity}, nil
}
type availabilityStub struct {
live bool
err error
}
func (s availabilityStub) IsLive(context.Context, uint) (bool, error) { return s.live, s.err }
func TestSendRejectsOfflineRoomBeforeWalletMutation(t *testing.T) {
repo := &sendRepoStub{}
uc := newUsecase(repo, nil, availabilityStub{live: false})
if _, err := uc.Send(context.Background(), 1, 7, 3, 2); !errors.Is(err, ErrSendNotAllowed) {
t.Fatalf("offline send error = %v", err)
}
if repo.called {
t.Fatal("offline room reached gift repository")
}
}
func TestSendAcceptsCustomQuantityForLiveRoom(t *testing.T) {
repo := &sendRepoStub{}
uc := newUsecase(repo, nil, availabilityStub{live: true})
item, err := uc.Send(context.Background(), 1, 7, 3, 37)
if err != nil {
t.Fatal(err)
}
if !repo.called || item.Quantity != 37 {
t.Fatalf("custom quantity result = %+v called=%v", item, repo.called)
}
}
+91 -23
View File
@@ -2,6 +2,8 @@ package live
import (
"context"
"fmt"
"sort"
"strings"
"time"
@@ -22,6 +24,7 @@ var (
ErrCategoryDisabled = kratoserrors.BadRequest("LIVE_CATEGORY_DISABLED", "category is disabled")
ErrRoomCategoryLocked = kratoserrors.BadRequest("LIVE_ROOM_CATEGORY_LOCKED", "live room category cannot be changed while broadcasting")
ErrRoomInvalidArgument = kratoserrors.BadRequest("LIVE_ROOM_INVALID_ARGUMENT", "invalid live room data")
ErrRoomNotLive = kratoserrors.BadRequest("LIVE_ROOM_NOT_LIVE", "live room is not broadcasting")
ErrAnchorNotFound = kratoserrors.BadRequest("LIVE_ANCHOR_NOT_FOUND", "anchor user not found")
)
@@ -37,23 +40,26 @@ type Category struct {
}
type Room struct {
ID uint
Name string
CategoryID uint
CategoryName string
CoverURL string
Intro string
Sort int
AnchorUserID uint
AnchorName string
AnchorAvatar string
StreamURL string
ReplayURL string
Enabled bool
Status string
ViewerCount int
CreatedAt time.Time
UpdatedAt time.Time
ID uint
Name string
CategoryID uint
CategoryName string
CoverURL string
Intro string
Sort int
AnchorUserID uint
AnchorName string
AnchorAvatar string
StreamURL string
ReplayURL string
Enabled bool
Status string
ViewerCount int
ViewerCountText string
LikeCount int
StartedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
type CategoryFilter struct {
@@ -62,12 +68,13 @@ type CategoryFilter struct {
}
type RoomFilter struct {
Name string
AnchorName string
CategoryID uint
Status string
Enabled *bool
CreatedAt []time.Time
Name string
AnchorName string
CategoryID uint
Status string
Enabled *bool
CategoryEnabled *bool
CreatedAt []time.Time
}
type ListOptions struct {
@@ -174,6 +181,17 @@ func (uc *RoomUsecase) listByRuntimeStatus(ctx context.Context, filter RoomFilte
matched = append(matched, room)
}
}
if strings.EqualFold(options.OrderBy, "viewer_count_asc") || strings.EqualFold(options.OrderBy, "viewer_count_desc") {
sort.SliceStable(matched, func(i, j int) bool {
if matched[i].ViewerCount == matched[j].ViewerCount {
return matched[i].ID < matched[j].ID
}
if strings.EqualFold(options.OrderBy, "viewer_count_asc") {
return matched[i].ViewerCount < matched[j].ViewerCount
}
return matched[i].ViewerCount > matched[j].ViewerCount
})
}
total := int64(len(matched))
if options.Offset >= len(matched) {
return []*Room{}, total, nil
@@ -196,6 +214,32 @@ func (uc *RoomUsecase) Get(ctx context.Context, id uint) (*Room, error) {
return uc.attachRuntime(ctx, []*Room{room})[0], nil
}
func (uc *RoomUsecase) GetPublic(ctx context.Context, id uint) (*Room, error) {
room, err := uc.Get(ctx, id)
if err != nil {
return nil, err
}
if !room.Enabled || room.Status != RoomStatusLive {
return nil, ErrRoomNotLive
}
category, err := uc.repo.GetCategory(ctx, room.CategoryID)
if err != nil {
return nil, err
}
if !category.Enabled {
return nil, ErrRoomNotLive
}
return room, nil
}
func (uc *RoomUsecase) IsLive(ctx context.Context, id uint) (bool, error) {
room, err := uc.Get(ctx, id)
if err != nil {
return false, err
}
return room.Enabled && room.Status == RoomStatusLive, nil
}
func (uc *RoomUsecase) Create(ctx context.Context, room *Room) (*Room, error) {
if err := validateRoom(room); err != nil {
return nil, err
@@ -258,17 +302,41 @@ func (uc *RoomUsecase) attachRuntime(ctx context.Context, rooms []*Room) []*Room
for _, room := range rooms {
if !room.Enabled {
room.Status = RoomStatusDisabled
room.ViewerCountText = formatViewerCount(room.ViewerCount)
continue
}
room.Status = RoomStatusOffline
if stream := byID[roomStreamID(room.ID)]; stream != nil && stream.PublisherOnline {
room.Status = RoomStatusLive
room.ViewerCount = stream.SubscriberCount
room.LikeCount = stream.LikeCount
if !stream.StartedAt.IsZero() {
startedAt := stream.StartedAt
room.StartedAt = &startedAt
}
}
room.ViewerCountText = formatViewerCount(room.ViewerCount)
}
return rooms
}
func formatViewerCount(value int) string {
if value < 1000 {
if value < 0 {
value = 0
}
return formatUint(uint(value))
}
if value >= 10000 {
return formatDecimal(float64(value)/10000, "w")
}
return formatDecimal(float64(value)/1000, "k")
}
func formatDecimal(value float64, suffix string) string {
return fmt.Sprintf("%.1f%s", value, suffix)
}
func roomStreamID(id uint) string { return "room-" + formatUint(id) }
func formatUint(value uint) string {
+160
View File
@@ -0,0 +1,160 @@
package live
import (
"context"
"errors"
"testing"
)
type catalogRepoStub struct {
categories []*Category
rooms []*Room
}
func (*catalogRepoStub) ListCategories(context.Context, CategoryFilter, ListOptions) ([]*Category, int64, error) {
return nil, 0, nil
}
func (r *catalogRepoStub) GetCategory(_ context.Context, id uint) (*Category, error) {
for _, category := range r.categories {
if category.ID == id {
copy := *category
return &copy, nil
}
}
return nil, ErrCategoryNotFound
}
func (*catalogRepoStub) CreateCategory(context.Context, *Category) (*Category, error) {
return nil, nil
}
func (*catalogRepoStub) UpdateCategory(context.Context, *Category) (*Category, error) {
return nil, nil
}
func (*catalogRepoStub) DeleteCategory(context.Context, uint) error { return nil }
func (r *catalogRepoStub) ListRooms(_ context.Context, filter RoomFilter, options ListOptions) ([]*Room, int64, error) {
filtered := make([]*Room, 0, len(r.rooms))
for _, room := range r.rooms {
if filter.CategoryID > 0 && room.CategoryID != filter.CategoryID {
continue
}
if filter.Enabled != nil && room.Enabled != *filter.Enabled {
continue
}
if filter.CategoryEnabled != nil {
category, err := r.GetCategory(context.Background(), room.CategoryID)
if err != nil || category.Enabled != *filter.CategoryEnabled {
continue
}
}
copy := *room
filtered = append(filtered, &copy)
}
total := int64(len(filtered))
if options.Offset >= len(filtered) {
return []*Room{}, total, nil
}
end := options.Offset + options.Limit
if end > len(filtered) {
end = len(filtered)
}
return filtered[options.Offset:end], total, nil
}
func (r *catalogRepoStub) GetRoom(_ context.Context, id uint) (*Room, error) {
for _, room := range r.rooms {
if room.ID == id {
copy := *room
return &copy, nil
}
}
return nil, ErrRoomNotFound
}
func (*catalogRepoStub) CreateRoom(context.Context, *Room) (*Room, error) { return nil, nil }
func (*catalogRepoStub) UpdateRoom(context.Context, *Room) (*Room, error) { return nil, nil }
func (*catalogRepoStub) DeleteRoom(context.Context, uint) error { return nil }
func (*catalogRepoStub) UpdateRoomEnabled(context.Context, uint, bool) error {
return nil
}
func (*catalogRepoStub) SaveRoomSort(context.Context, map[uint]int) error { return nil }
func TestRoomUsecaseFiltersSortsAndPaginatesLiveRooms(t *testing.T) {
enabled := true
catalog := &catalogRepoStub{rooms: []*Room{
{ID: 1, CategoryID: 1, Enabled: true},
{ID: 2, CategoryID: 1, Enabled: true},
{ID: 3, CategoryID: 1, Enabled: true},
{ID: 4, CategoryID: 2, Enabled: true},
}}
streams := NewStreamUsecase(&testRepo{streams: map[string]*Stream{
"room-1": {StreamID: "room-1", PublisherOnline: true, SubscriberCount: 100},
"room-2": {StreamID: "room-2", PublisherOnline: true, SubscriberCount: 5600},
"room-4": {StreamID: "room-4", PublisherOnline: true, SubscriberCount: 10000},
}})
rooms := NewRoomUsecase(catalog, streams)
items, total, err := rooms.List(context.Background(), RoomFilter{CategoryID: 1, Status: RoomStatusLive, Enabled: &enabled}, ListOptions{Offset: 1, Limit: 1, OrderBy: "viewer_count_desc"})
if err != nil {
t.Fatal(err)
}
if total != 2 || len(items) != 1 || items[0].ID != 1 {
t.Fatalf("live page total=%d items=%+v", total, items)
}
if items[0].ViewerCount != 100 || items[0].ViewerCountText != "100" || items[0].Status != RoomStatusLive {
t.Fatalf("runtime fields = %+v", items[0])
}
}
func TestRoomUsecaseIsLiveRequiresEnabledOnlinePublisher(t *testing.T) {
catalog := &catalogRepoStub{rooms: []*Room{
{ID: 1, Enabled: true},
{ID: 2, Enabled: true},
{ID: 3, Enabled: false},
}}
streams := NewStreamUsecase(&testRepo{streams: map[string]*Stream{
"room-1": {StreamID: "room-1", PublisherOnline: true},
"room-3": {StreamID: "room-3", PublisherOnline: true},
}})
rooms := NewRoomUsecase(catalog, streams)
for roomID, want := range map[uint]bool{1: true, 2: false, 3: false} {
got, err := rooms.IsLive(context.Background(), roomID)
if err != nil {
t.Fatalf("IsLive(%d) error = %v", roomID, err)
}
if got != want {
t.Errorf("IsLive(%d) = %v, want %v", roomID, got, want)
}
}
}
func TestRoomUsecaseGetPublicRequiresLiveRoomAndEnabledCategory(t *testing.T) {
catalog := &catalogRepoStub{
categories: []*Category{{ID: 1, Enabled: true}, {ID: 2, Enabled: false}},
rooms: []*Room{
{ID: 1, CategoryID: 1, Enabled: true},
{ID: 2, CategoryID: 1, Enabled: true},
{ID: 3, CategoryID: 1, Enabled: false},
{ID: 4, CategoryID: 2, Enabled: true},
},
}
streams := NewStreamUsecase(&testRepo{streams: map[string]*Stream{
"room-1": {StreamID: "room-1", PublisherOnline: true},
"room-3": {StreamID: "room-3", PublisherOnline: true},
"room-4": {StreamID: "room-4", PublisherOnline: true},
}})
rooms := NewRoomUsecase(catalog, streams)
item, err := rooms.GetPublic(context.Background(), 1)
if err != nil || item.ID != 1 || item.Status != RoomStatusLive {
t.Fatalf("GetPublic(1) item=%+v err=%v", item, err)
}
for _, roomID := range []uint{2, 3, 4} {
if _, err = rooms.GetPublic(context.Background(), roomID); !errors.Is(err, ErrRoomNotLive) {
t.Errorf("GetPublic(%d) error=%v, want %v", roomID, err, ErrRoomNotLive)
}
if _, err = rooms.Get(context.Background(), roomID); err != nil {
t.Errorf("admin Get(%d) error=%v", roomID, err)
}
}
}
+58
View File
@@ -0,0 +1,58 @@
package live
import (
"context"
"errors"
"time"
kratoserrors "github.com/go-kratos/kratos/v3/errors"
)
var (
ErrFollowNotFound = kratoserrors.NotFound("LIVE_FOLLOW_NOT_FOUND", "follow relation not found")
ErrFollowInvalid = kratoserrors.BadRequest("LIVE_FOLLOW_INVALID_ARGUMENT", "invalid follow argument")
)
type Follow struct {
ID uint
UserID uint
RoomID uint
AnchorUserID uint
CreatedAt time.Time
}
type FollowRepo interface {
Get(context.Context, uint, uint) (*Follow, error)
Create(context.Context, *Follow) (*Follow, error)
Delete(context.Context, uint, uint) error
}
type FollowUsecase struct{ repo FollowRepo }
func NewFollowUsecase(repo FollowRepo) *FollowUsecase { return &FollowUsecase{repo: repo} }
func (uc *FollowUsecase) Get(ctx context.Context, userID, roomID uint) (*Follow, error) {
if userID == 0 || roomID == 0 {
return nil, ErrFollowInvalid
}
return uc.repo.Get(ctx, userID, roomID)
}
func (uc *FollowUsecase) Follow(ctx context.Context, userID, roomID uint) (*Follow, error) {
if userID == 0 || roomID == 0 {
return nil, ErrFollowInvalid
}
if existing, err := uc.repo.Get(ctx, userID, roomID); err == nil {
return existing, nil
} else if !errors.Is(err, ErrFollowNotFound) {
return nil, err
}
return uc.repo.Create(ctx, &Follow{UserID: userID, RoomID: roomID})
}
func (uc *FollowUsecase) Unfollow(ctx context.Context, userID, roomID uint) error {
if userID == 0 || roomID == 0 {
return ErrFollowInvalid
}
return uc.repo.Delete(ctx, userID, roomID)
}
+1 -1
View File
@@ -2,4 +2,4 @@ package live
import "github.com/google/wire"
var ProviderSet = wire.NewSet(NewStreamUsecase, NewCategoryUsecase, NewRoomUsecase)
var ProviderSet = wire.NewSet(NewStreamUsecase, NewCategoryUsecase, NewRoomUsecase, NewFollowUsecase)
+11
View File
@@ -27,6 +27,8 @@ type Stream struct {
PublisherID uint
PublisherOnline bool
SubscriberCount int
LikeCount int
StartedAt time.Time
UpdatedAt time.Time
}
@@ -37,6 +39,7 @@ type StreamRepo interface {
UnregisterPublisher(context.Context, string, uint) error
RegisterSubscriber(context.Context, string, uint) (*Stream, error)
UnregisterSubscriber(context.Context, string, uint) error
Like(context.Context, string) (int, error)
}
type StreamUsecase struct{ repo StreamRepo }
@@ -79,6 +82,14 @@ func (uc *StreamUsecase) UnregisterSubscriber(ctx context.Context, streamID stri
return uc.repo.UnregisterSubscriber(ctx, normalizeStreamID(streamID), userID)
}
func (uc *StreamUsecase) Like(ctx context.Context, streamID string) (int, error) {
streamID = normalizeStreamID(streamID)
if streamID == "" {
return 0, ErrInvalidStreamID
}
return uc.repo.Like(ctx, streamID)
}
func normalizeStreamID(value string) string {
return strings.TrimSpace(value)
}
+10
View File
@@ -5,6 +5,15 @@ import (
"testing"
)
func TestFormatViewerCount(t *testing.T) {
tests := map[int]string{0: "0", 999: "999", 1000: "1.0k", 5600: "5.6k", 10000: "1.0w", 12500: "1.2w"}
for input, want := range tests {
if got := formatViewerCount(input); got != want {
t.Fatalf("formatViewerCount(%d) = %q, want %q", input, got, want)
}
}
}
type testRepo struct {
streams map[string]*Stream
}
@@ -41,6 +50,7 @@ func (r *testRepo) RegisterSubscriber(context.Context, string, uint) (*Stream, e
return nil, nil
}
func (r *testRepo) UnregisterSubscriber(context.Context, string, uint) error { return nil }
func (r *testRepo) Like(context.Context, string) (int, error) { return 0, nil }
func TestStreamUsecaseValidatesAndDelegates(t *testing.T) {
repo := &testRepo{streams: make(map[string]*Stream)}
+8
View File
@@ -53,6 +53,7 @@ type Repo interface {
ListRecent(context.Context, uint, int) ([]*Message, error)
CreateChat(context.Context, uint, uint, string) (*Message, error)
CreateGift(context.Context, *GiftSnapshot) (*Message, error)
Clear(context.Context, uint) error
}
type Usecase struct{ repo Repo }
@@ -83,3 +84,10 @@ func (uc *Usecase) RecordGift(ctx context.Context, gift *GiftSnapshot) (*Message
}
return uc.repo.CreateGift(ctx, gift)
}
func (uc *Usecase) Clear(ctx context.Context, roomID uint) error {
if roomID == 0 {
return ErrRoomNotFound
}
return uc.repo.Clear(ctx, roomID)
}
+8 -2
View File
@@ -36,7 +36,7 @@ type Account struct {
}
type RegisterInput struct {
Username, Password, ConfirmPassword string
Username, Nickname, Password, ConfirmPassword string
}
type LoginInput struct {
@@ -84,6 +84,12 @@ func (uc *Usecase) Register(ctx context.Context, in *RegisterInput, ip, agent st
if n := len([]rune(in.Username)); n < 3 || n > 32 || in.Password == "" || in.Password != in.ConfirmPassword {
return nil, ErrInvalidAccount
}
in.Nickname = strings.TrimSpace(in.Nickname)
if in.Nickname == "" {
in.Nickname = in.Username
} else if n := len([]rune(in.Nickname)); n < 2 || n > 16 {
return nil, ErrInvalidAccount
}
config, err := uc.security.Current(ctx)
if err != nil {
return nil, err
@@ -95,7 +101,7 @@ func (uc *Usecase) Register(ctx context.Context, in *RegisterInput, ip, agent st
if err != nil {
return nil, err
}
account, err := uc.repo.Register(ctx, &Account{Username: in.Username, Nickname: in.Username, AuthorityID: ClientAuthorityID, Enabled: true}, string(hash), InitialDiamonds)
account, err := uc.repo.Register(ctx, &Account{Username: in.Username, Nickname: in.Nickname, AuthorityID: ClientAuthorityID, Enabled: true}, string(hash), InitialDiamonds)
if err != nil {
return nil, err
}
+106
View File
@@ -0,0 +1,106 @@
package liveclient
import (
"context"
"errors"
"testing"
"time"
"kra/internal/biz/system"
)
type registerRepoStub struct {
Repo
account *Account
}
func (r *registerRepoStub) Register(_ context.Context, account *Account, _ string, _ int64) (*Account, error) {
copy := *account
copy.UserID = 1
copy.UUID = "client-uuid"
copy.CreatedAt = time.Now()
r.account = &copy
return &copy, nil
}
type registerSecurityRepoStub struct{ system.SecurityRepo }
func (*registerSecurityRepoStub) SecurityConfig(context.Context) (*system.SecurityConfig, error) {
return &system.SecurityConfig{PwdMinLength: 8}, nil
}
type registerSettingsStub struct{ system.RuntimeSettings }
func (*registerSettingsStub) UseMultipoint() bool { return false }
type registerIssuerStub struct{ system.TokenIssuer }
func (*registerIssuerStub) IssueToken(*system.User, uint, bool, time.Duration) (*system.IssuedToken, error) {
return &system.IssuedToken{Value: "token", ExpiresAt: time.Now().Add(time.Hour), TTL: time.Hour}, nil
}
func (*registerIssuerStub) ReissueToken(claims *system.AuthClaims, _ uint) (*system.IssuedToken, error) {
return &system.IssuedToken{Value: "token", ExpiresAt: claims.ExpiresAt, TTL: time.Until(claims.ExpiresAt)}, nil
}
func (*registerIssuerStub) ParseToken(string) (*system.AuthClaims, error) { return nil, nil }
func newRegisterUsecase(repo *registerRepoStub) *Usecase {
security := system.NewSecurityUsecase(&registerSecurityRepoStub{}, nil, &registerSettingsStub{}, nil)
auth := system.NewAuthenticationUsecase(nil, security, &registerIssuerStub{}, nil)
return NewUsecase(repo, security, auth, nil)
}
func TestRegisterUsesTrimmedNickname(t *testing.T) {
repo := &registerRepoStub{}
uc := newRegisterUsecase(repo)
result, err := uc.Register(context.Background(), &RegisterInput{
Username: "viewer",
Nickname: " 直播用户 ",
Password: "password",
ConfirmPassword: "password",
}, "", "")
if err != nil {
t.Fatal(err)
}
if result.Account.Nickname != "直播用户" || repo.account.Nickname != "直播用户" {
t.Fatalf("nickname = %q, repo nickname = %q", result.Account.Nickname, repo.account.Nickname)
}
}
func TestRegisterDefaultsNicknameToUsername(t *testing.T) {
repo := &registerRepoStub{}
uc := newRegisterUsecase(repo)
result, err := uc.Register(context.Background(), &RegisterInput{
Username: "viewer",
Password: "password",
ConfirmPassword: "password",
}, "", "")
if err != nil {
t.Fatal(err)
}
if result.Account.Nickname != "viewer" {
t.Fatalf("nickname = %q, want username fallback", result.Account.Nickname)
}
}
func TestRegisterRejectsNicknameOutsideRange(t *testing.T) {
for _, nickname := range []string{"A", "12345678901234567"} {
t.Run(nickname, func(t *testing.T) {
repo := &registerRepoStub{}
uc := newRegisterUsecase(repo)
_, err := uc.Register(context.Background(), &RegisterInput{
Username: "viewer",
Nickname: nickname,
Password: "password",
ConfirmPassword: "password",
}, "", "")
if !errors.Is(err, ErrInvalidAccount) {
t.Fatalf("error = %v, want ErrInvalidAccount", err)
}
if repo.account != nil {
t.Fatal("repository called for invalid nickname")
}
})
}
}