refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion. - Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects. - Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management. - Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering. - Improve database initialization by moving it from `internal/model` to `internal/database`. - Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool. - Enhance error handling and security by implementing fail-fast checks for encryption key length during startup. - Clean up unused code, including the `avatar` package and several unused DTOs.
This commit is contained in:
179
internal/service/admin_group_service_test.go
Normal file
179
internal/service/admin_group_service_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"with_you/internal/model"
|
||||
"with_you/internal/repository"
|
||||
)
|
||||
|
||||
// stubGroupRepo 通过嵌入 repository.GroupRepository 接口(保持 nil),
|
||||
// 仅覆盖测试涉及的方法。未覆盖的方法被调用时会 panic——这能在测试中
|
||||
// 提前暴露「Service 调用了非预期 repo 方法」的回归。
|
||||
type stubGroupRepo struct {
|
||||
repository.GroupRepository // nil 嵌入,未覆盖方法 panic
|
||||
|
||||
groupByID map[string]*model.Group
|
||||
groupByIDErr error
|
||||
memberByUser map[string]*model.GroupMember
|
||||
memberErr error
|
||||
transferCalls []transferCall
|
||||
transferErr error
|
||||
}
|
||||
|
||||
type transferCall struct {
|
||||
GroupID, OldOwner, NewOwner string
|
||||
}
|
||||
|
||||
func (s *stubGroupRepo) GetByID(id string) (*model.Group, error) {
|
||||
if s.groupByIDErr != nil {
|
||||
return nil, s.groupByIDErr
|
||||
}
|
||||
g, ok := s.groupByID[id]
|
||||
if !ok {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (s *stubGroupRepo) GetMember(groupID, userID string) (*model.GroupMember, error) {
|
||||
if s.memberErr != nil {
|
||||
return nil, s.memberErr
|
||||
}
|
||||
m, ok := s.memberByUser[userID]
|
||||
if !ok {
|
||||
return nil, errors.New("not member")
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *stubGroupRepo) TransferOwner(groupID, oldOwnerID, newOwnerID string) error {
|
||||
s.transferCalls = append(s.transferCalls, transferCall{
|
||||
GroupID: groupID, OldOwner: oldOwnerID, NewOwner: newOwnerID,
|
||||
})
|
||||
// 模拟真实 DB 语义:事务提交后 owner_id 已变更,后续 GetByID 读到新值
|
||||
if g, ok := s.groupByID[groupID]; ok {
|
||||
g.OwnerID = newOwnerID
|
||||
}
|
||||
return s.transferErr
|
||||
}
|
||||
|
||||
// stubUserRepo 同样仅覆盖 GetByID。
|
||||
type stubUserRepo struct {
|
||||
repository.UserRepository // nil 嵌入
|
||||
|
||||
byID map[string]*model.User
|
||||
}
|
||||
|
||||
func (s *stubUserRepo) GetByID(id string) (*model.User, error) {
|
||||
u, ok := s.byID[id]
|
||||
if !ok {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// TestTransferOwner_Success 正常转让:TransferOwner 应被调用一次,
|
||||
// 参数为 (groupID, 当前 owner, 新 owner)。
|
||||
func TestTransferOwner_Success(t *testing.T) {
|
||||
repo := &stubGroupRepo{
|
||||
groupByID: map[string]*model.Group{
|
||||
"g1": {ID: "g1", OwnerID: "old-owner", Name: "测试群"},
|
||||
},
|
||||
memberByUser: map[string]*model.GroupMember{
|
||||
"old-owner": {UserID: "old-owner", GroupID: "g1", Role: model.GroupRoleOwner},
|
||||
"new-owner": {UserID: "new-owner", GroupID: "g1", Role: model.GroupRoleMember},
|
||||
},
|
||||
}
|
||||
userRepo := &stubUserRepo{
|
||||
byID: map[string]*model.User{
|
||||
"new-owner": {ID: "new-owner", Username: "new", Nickname: "新群主"},
|
||||
},
|
||||
}
|
||||
svc := NewAdminGroupService(repo, userRepo)
|
||||
|
||||
resp, err := svc.TransferOwner(context.Background(), "g1", "new-owner")
|
||||
if err != nil {
|
||||
t.Fatalf("TransferOwner failed: %v", err)
|
||||
}
|
||||
|
||||
// 校验 TransferOwner 恰好被调用一次,参数正确
|
||||
if len(repo.transferCalls) != 1 {
|
||||
t.Fatalf("expected 1 TransferOwner call, got %d", len(repo.transferCalls))
|
||||
}
|
||||
tc := repo.transferCalls[0]
|
||||
if tc.GroupID != "g1" || tc.OldOwner != "old-owner" || tc.NewOwner != "new-owner" {
|
||||
t.Errorf("TransferOwner called with (%s,%s,%s), want (g1,old-owner,new-owner)",
|
||||
tc.GroupID, tc.OldOwner, tc.NewOwner)
|
||||
}
|
||||
|
||||
// 响应应反映新群主
|
||||
if resp == nil {
|
||||
t.Fatal("expected non-nil response")
|
||||
}
|
||||
if resp.OwnerID != "new-owner" {
|
||||
t.Errorf("response OwnerID = %s, want new-owner", resp.OwnerID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransferOwner_NewOwnerNotMember 新群主非成员时应返回错误,
|
||||
// 且 TransferOwner 不应被调用。
|
||||
func TestTransferOwner_NewOwnerNotMember(t *testing.T) {
|
||||
repo := &stubGroupRepo{
|
||||
groupByID: map[string]*model.Group{
|
||||
"g1": {ID: "g1", OwnerID: "old-owner"},
|
||||
},
|
||||
memberByUser: map[string]*model.GroupMember{
|
||||
"old-owner": {UserID: "old-owner", GroupID: "g1", Role: model.GroupRoleOwner},
|
||||
// new-owner 不在成员表
|
||||
},
|
||||
}
|
||||
svc := NewAdminGroupService(repo, &stubUserRepo{})
|
||||
|
||||
_, err := svc.TransferOwner(context.Background(), "g1", "new-owner")
|
||||
if err == nil {
|
||||
t.Fatal("expected error when new owner is not a member")
|
||||
}
|
||||
if len(repo.transferCalls) != 0 {
|
||||
t.Errorf("TransferOwner should not be called, got %d calls", len(repo.transferCalls))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransferOwner_GroupNotFound 群不存在时应返回错误。
|
||||
func TestTransferOwner_GroupNotFound(t *testing.T) {
|
||||
repo := &stubGroupRepo{
|
||||
groupByID: map[string]*model.Group{}, // g1 不存在
|
||||
}
|
||||
svc := NewAdminGroupService(repo, &stubUserRepo{})
|
||||
|
||||
_, err := svc.TransferOwner(context.Background(), "g1", "new-owner")
|
||||
if err == nil {
|
||||
t.Fatal("expected error when group does not exist")
|
||||
}
|
||||
if len(repo.transferCalls) != 0 {
|
||||
t.Errorf("TransferOwner should not be called, got %d calls", len(repo.transferCalls))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransferOwner_AlreadyOwner 新群主已是群主时应拒绝(无操作幂等)。
|
||||
func TestTransferOwner_AlreadyOwner(t *testing.T) {
|
||||
repo := &stubGroupRepo{
|
||||
groupByID: map[string]*model.Group{
|
||||
"g1": {ID: "g1", OwnerID: "same-user"},
|
||||
},
|
||||
memberByUser: map[string]*model.GroupMember{
|
||||
"same-user": {UserID: "same-user", GroupID: "g1", Role: model.GroupRoleOwner},
|
||||
},
|
||||
}
|
||||
svc := NewAdminGroupService(repo, &stubUserRepo{})
|
||||
|
||||
_, err := svc.TransferOwner(context.Background(), "g1", "same-user")
|
||||
if err == nil {
|
||||
t.Fatal("expected error when transferring to existing owner")
|
||||
}
|
||||
if len(repo.transferCalls) != 0 {
|
||||
t.Errorf("TransferOwner should not be called, got %d calls", len(repo.transferCalls))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user