feat: test case
This commit is contained in:
parent
0e84699df7
commit
d36d57692c
20
Makefile
20
Makefile
@ -3,6 +3,7 @@ MODULE := spark-server
|
||||
BIN := bin/spark-server
|
||||
LINT ?= golangci-lint
|
||||
CONFIG ?= configs/config.yaml
|
||||
LOGIC_COVER_MIN ?= 65
|
||||
|
||||
.PHONY: all build test lint fmt vet tidy migrate start run deploy
|
||||
|
||||
@ -11,8 +12,27 @@ all: lint test build
|
||||
build:
|
||||
$(GO) build -o $(BIN) ./cmd/server
|
||||
|
||||
# All unit tests, then each module logic package must be >= LOGIC_COVER_MIN.
|
||||
test:
|
||||
$(GO) test ./...
|
||||
@echo "==> logic Service coverage (>= $(LOGIC_COVER_MIN)%)"
|
||||
@covout=$$($(GO) test -cover ./internal/module/*/logic); \
|
||||
status=$$?; \
|
||||
printf '%s\n' "$$covout"; \
|
||||
if [ $$status -ne 0 ]; then exit $$status; fi; \
|
||||
printf '%s\n' "$$covout" | awk -v min=$(LOGIC_COVER_MIN) ' \
|
||||
BEGIN { bad=0 } \
|
||||
/\[no test files\]/ { printf "FAIL %s: no test files\n", $$2; bad=1; next } \
|
||||
{ \
|
||||
cov=""; \
|
||||
for (i = 1; i <= NF; i++) { \
|
||||
if ($$i == "coverage:") { cov=$$(i+1); break } \
|
||||
} \
|
||||
if (cov == "" || cov == "[no") next; \
|
||||
gsub("%", "", cov); \
|
||||
if (cov+0 < min+0) { printf "FAIL %s: coverage %s%% < %s%%\n", $$2, cov, min; bad=1 } \
|
||||
} \
|
||||
END { if (bad) exit 1 }'
|
||||
|
||||
vet:
|
||||
$(GO) vet ./...
|
||||
|
||||
@ -29,3 +29,20 @@ func TestAdminJWTRejectsWrongType(t *testing.T) {
|
||||
t.Fatal("expected invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminJWTShortSecretAndNil(t *testing.T) {
|
||||
s := NewTokenService("short")
|
||||
tok, err := s.CreateToken(1, "ops", "SUPER_ADMIN", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.Parse(tok); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := (*TokenService)(nil).Parse("x"); err == nil {
|
||||
t.Fatal("nil parse")
|
||||
}
|
||||
if _, err := s.Parse(""); err == nil {
|
||||
t.Fatal("empty")
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,7 +121,7 @@ func userView(r adminmodel.UserRow) adminentity.UserView {
|
||||
v := adminentity.UserView{
|
||||
ID: r.ID, Email: r.Email, Nickname: r.Nickname, SparkID: r.SparkID,
|
||||
AvatarURL: r.Avatar, Channel: r.Platform, Status: r.Status,
|
||||
Banned: r.Status == int(userentity.StatusDisabled),
|
||||
Banned: r.Status == int(userentity.StatusDisabled),
|
||||
CreatedAt: r.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
if r.DeviceID > 0 {
|
||||
|
||||
63
internal/module/admin/logic/map_test.go
Normal file
63
internal/module/admin/logic/map_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
adminmodel "spark-server/internal/module/admin/model"
|
||||
msgentity "spark-server/internal/module/message/entity"
|
||||
)
|
||||
|
||||
func TestAdminMessageHelpers(t *testing.T) {
|
||||
if msgTypeName(msgentity.TypeText) != "text" || msgTypeName(msgentity.Type(99)) != "type_99" {
|
||||
t.Fatal("type name")
|
||||
}
|
||||
if roleName(1) != "owner" || roleName(2) != "admin" || roleName(3) != "member" {
|
||||
t.Fatal("role")
|
||||
}
|
||||
if reportReasonName(1) != "spam" || reportReasonName(2) != "harassment" || reportReasonName(3) != "fake" || reportReasonName(0) != "other" {
|
||||
t.Fatal("reason name")
|
||||
}
|
||||
if parseReportReason("spam") != 1 || parseReportReason("骚扰") != 2 || parseReportReason("3") != 3 || parseReportReason("x") != 4 {
|
||||
t.Fatal("parse reason")
|
||||
}
|
||||
mustPreview(t, msgentity.NewText("hi"), msgentity.TypeText, "hi")
|
||||
mustPreview(t, msgentity.NewImage(&msgentity.Image{Key: "chat/20260917/1/99.jpg", Mime: "image/jpeg"}), msgentity.TypeImage, "[图片]")
|
||||
mustPreview(t, msgentity.NewVoice(&msgentity.Voice{Key: "chat/20260917/1/a.m4a", DurationMS: 1000}), msgentity.TypeVoice, "[语音]")
|
||||
mustPreview(t, msgentity.NewVideo(&msgentity.Video{Key: "chat/20260917/1/a.mp4"}), msgentity.TypeVideo, "[视频]")
|
||||
mustPreview(t, msgentity.NewEmoji(&msgentity.Emoji{Pack: "default", ID: "smile"}), msgentity.TypeEmoji, "[表情]")
|
||||
mustPreview(t, msgentity.NewGIF(&msgentity.GIF{Key: "chat/20260917/1/a.gif"}), msgentity.TypeGIF, "[GIF]")
|
||||
mustPreview(t, msgentity.NewFile(&msgentity.File{Key: "chat/20260917/1/a.pdf", Name: "a.pdf", Size: 1}), msgentity.TypeFile, "[文件] a.pdf")
|
||||
mustPreview(t, msgentity.NewLocation(&msgentity.Location{Lat: 1, Lng: 2, Name: "cafe"}), msgentity.TypeLocation, "[位置] cafe")
|
||||
mustPreview(t, msgentity.NewSystem(&msgentity.System{Kind: "join", Body: "joined"}), msgentity.TypeSystem, "joined")
|
||||
if previewText([]byte("not-json"), msgentity.TypeText) != "" {
|
||||
t.Fatal("bad content")
|
||||
}
|
||||
v := messageView(adminmodel.MessageRow{
|
||||
ID: 42, SenderID: 7, ReceiverID: 8, GroupID: 9, MessageType: int16(msgentity.TypeText),
|
||||
Content: mustMarshal(t, msgentity.NewText("needle")), Status: 2, CreatedAt: time.Unix(1, 0).UTC(), ChatType: "private",
|
||||
})
|
||||
if !v.Revoked || !messageMatch(v, "needle") || !messageMatch(v, "42") || !messageMatch(v, "text") || !messageMatch(v, "7") {
|
||||
t.Fatalf("%+v", v)
|
||||
}
|
||||
if messageMatch(v, "zzz") {
|
||||
t.Fatal("no match")
|
||||
}
|
||||
}
|
||||
|
||||
func mustMarshal(t *testing.T, c *msgentity.Content) []byte {
|
||||
t.Helper()
|
||||
raw, err := c.Marshal()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func mustPreview(t *testing.T, c *msgentity.Content, typ msgentity.Type, want string) {
|
||||
t.Helper()
|
||||
got := previewText(mustMarshal(t, c), typ)
|
||||
if got != want {
|
||||
t.Fatalf("preview %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
@ -2,17 +2,21 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
adminentity "spark-server/internal/module/admin/entity"
|
||||
adminmodel "spark-server/internal/module/admin/model"
|
||||
friendentity "spark-server/internal/module/friend/entity"
|
||||
grouplogic "spark-server/internal/module/group/logic"
|
||||
userentity "spark-server/internal/module/user/entity"
|
||||
)
|
||||
|
||||
var ErrNotReady = errors.New("admin: repository not ready")
|
||||
|
||||
// Service orchestrates admin use-cases. SQL lives in model.Repo.
|
||||
type Service struct {
|
||||
repo *adminmodel.Repo
|
||||
repo Repository
|
||||
groups *grouplogic.Service
|
||||
ids IDGen
|
||||
}
|
||||
@ -21,7 +25,32 @@ type IDGen interface {
|
||||
NextID() (int64, error)
|
||||
}
|
||||
|
||||
func NewService(repo *adminmodel.Repo, groups *grouplogic.Service, ids IDGen) *Service {
|
||||
// Repository is admin read/write over V3 tables. Model implements it.
|
||||
type Repository interface {
|
||||
ListUsers(ctx context.Context, emailLike string, page, size int) ([]adminmodel.UserRow, int64, error)
|
||||
UsersByEmail(ctx context.Context, email string) ([]adminmodel.UserRow, error)
|
||||
UserByID(ctx context.Context, id int64) (*adminmodel.UserRow, error)
|
||||
UsersByNickname(ctx context.Context, nickname string) ([]adminmodel.UserRow, error)
|
||||
UsersByDevice(ctx context.Context, deviceID string, deviceIDNum uint64) ([]adminmodel.UserRow, error)
|
||||
UsersByIP(ctx context.Context, ip string) ([]adminmodel.UserRow, error)
|
||||
SetUserStatusByEmail(ctx context.Context, email string, status userentity.Status) error
|
||||
SetNicknameByEmail(ctx context.Context, email, nickname string) error
|
||||
ClearAvatarByEmail(ctx context.Context, email string) error
|
||||
SameIPRank(ctx context.Context, limit int) ([]adminmodel.SameIPRankRow, error)
|
||||
LookupUserIDByEmail(ctx context.Context, email string) (int64, error)
|
||||
ListGroups(ctx context.Context, q string) ([]adminmodel.GroupRow, error)
|
||||
GroupByID(ctx context.Context, groupID int64) (*adminmodel.GroupRow, error)
|
||||
ActiveGroupOwner(ctx context.Context, groupID int64) (int64, error)
|
||||
ListGroupMembers(ctx context.Context, groupID int64) ([]adminmodel.GroupMemberRow, error)
|
||||
ListGroupMessages(ctx context.Context, groupID int64, limit int) ([]adminmodel.MessageRow, error)
|
||||
ListComplaints(ctx context.Context, limit int) ([]adminmodel.ComplaintRow, error)
|
||||
InsertComplaint(ctx context.Context, r *friendentity.Report) error
|
||||
ImStats(ctx context.Context) (*adminentity.ImStats, error)
|
||||
ListRecentMessages(ctx context.Context, fetchLimit int) ([]adminmodel.MessageRow, error)
|
||||
ListUserConversations(ctx context.Context, userID int64) ([]adminmodel.ConversationRow, error)
|
||||
}
|
||||
|
||||
func NewService(repo Repository, groups *grouplogic.Service, ids IDGen) *Service {
|
||||
return &Service{repo: repo, groups: groups, ids: ids}
|
||||
}
|
||||
|
||||
|
||||
322
internal/module/admin/logic/service_test.go
Normal file
322
internal/module/admin/logic/service_test.go
Normal file
@ -0,0 +1,322 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
adminentity "spark-server/internal/module/admin/entity"
|
||||
adminmodel "spark-server/internal/module/admin/model"
|
||||
friendentity "spark-server/internal/module/friend/entity"
|
||||
groupentity "spark-server/internal/module/group/entity"
|
||||
msgentity "spark-server/internal/module/message/entity"
|
||||
userentity "spark-server/internal/module/user/entity"
|
||||
"spark-server/internal/pkg/errs"
|
||||
)
|
||||
|
||||
type seqID struct{ n int64 }
|
||||
|
||||
func (s *seqID) NextID() (int64, error) {
|
||||
s.n++
|
||||
return s.n, nil
|
||||
}
|
||||
|
||||
type memoryAdmin struct {
|
||||
users []adminmodel.UserRow
|
||||
emails map[string]int64
|
||||
groups map[int64]adminmodel.GroupRow
|
||||
members map[int64][]adminmodel.GroupMemberRow
|
||||
groupMsgs map[int64][]adminmodel.MessageRow
|
||||
msgs []adminmodel.MessageRow
|
||||
convs []adminmodel.ConversationRow
|
||||
complaints []adminmodel.ComplaintRow
|
||||
stats *adminentity.ImStats
|
||||
ips []adminmodel.SameIPRankRow
|
||||
owners map[int64]int64
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) ListUsers(_ context.Context, emailLike string, page, size int) ([]adminmodel.UserRow, int64, error) {
|
||||
start := page * size
|
||||
if start >= len(m.users) {
|
||||
return nil, int64(len(m.users)), nil
|
||||
}
|
||||
end := start + size
|
||||
if end > len(m.users) {
|
||||
end = len(m.users)
|
||||
}
|
||||
_ = emailLike
|
||||
return m.users[start:end], int64(len(m.users)), nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) UsersByEmail(_ context.Context, email string) ([]adminmodel.UserRow, error) {
|
||||
out := make([]adminmodel.UserRow, 0)
|
||||
for _, u := range m.users {
|
||||
if u.Email == email {
|
||||
out = append(out, u)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) UserByID(_ context.Context, id int64) (*adminmodel.UserRow, error) {
|
||||
for i := range m.users {
|
||||
if m.users[i].ID == id {
|
||||
cp := m.users[i]
|
||||
return &cp, nil
|
||||
}
|
||||
}
|
||||
return nil, errs.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) UsersByNickname(_ context.Context, nickname string) ([]adminmodel.UserRow, error) {
|
||||
out := make([]adminmodel.UserRow, 0)
|
||||
for _, u := range m.users {
|
||||
if u.Nickname == nickname {
|
||||
out = append(out, u)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) UsersByDevice(_ context.Context, _ string, deviceIDNum uint64) ([]adminmodel.UserRow, error) {
|
||||
out := make([]adminmodel.UserRow, 0)
|
||||
for _, u := range m.users {
|
||||
if u.DeviceID == deviceIDNum {
|
||||
out = append(out, u)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) UsersByIP(_ context.Context, _ string) ([]adminmodel.UserRow, error) {
|
||||
return m.users, nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) SetUserStatusByEmail(_ context.Context, email string, status userentity.Status) error {
|
||||
for i := range m.users {
|
||||
if m.users[i].Email == email {
|
||||
m.users[i].Status = int(status)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errs.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) SetNicknameByEmail(_ context.Context, email, nickname string) error {
|
||||
for i := range m.users {
|
||||
if m.users[i].Email == email {
|
||||
m.users[i].Nickname = nickname
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errs.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) ClearAvatarByEmail(_ context.Context, email string) error {
|
||||
for i := range m.users {
|
||||
if m.users[i].Email == email {
|
||||
m.users[i].Avatar = ""
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errs.ErrNotFound
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) SameIPRank(_ context.Context, _ int) ([]adminmodel.SameIPRankRow, error) {
|
||||
return m.ips, nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) LookupUserIDByEmail(_ context.Context, email string) (int64, error) {
|
||||
if m.emails == nil {
|
||||
return 0, nil
|
||||
}
|
||||
return m.emails[email], nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) ListGroups(_ context.Context, _ string) ([]adminmodel.GroupRow, error) {
|
||||
out := make([]adminmodel.GroupRow, 0, len(m.groups))
|
||||
for id := range m.groups {
|
||||
out = append(out, m.groups[id])
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) GroupByID(_ context.Context, groupID int64) (*adminmodel.GroupRow, error) {
|
||||
g, ok := m.groups[groupID]
|
||||
if !ok {
|
||||
return nil, errs.ErrNotFound
|
||||
}
|
||||
return &g, nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) ActiveGroupOwner(_ context.Context, groupID int64) (int64, error) {
|
||||
id, ok := m.owners[groupID]
|
||||
if !ok {
|
||||
return 0, errs.ErrNotFound
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) ListGroupMembers(_ context.Context, groupID int64) ([]adminmodel.GroupMemberRow, error) {
|
||||
return m.members[groupID], nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) ListGroupMessages(_ context.Context, groupID int64, _ int) ([]adminmodel.MessageRow, error) {
|
||||
return m.groupMsgs[groupID], nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) ListComplaints(_ context.Context, _ int) ([]adminmodel.ComplaintRow, error) {
|
||||
return m.complaints, nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) InsertComplaint(_ context.Context, r *friendentity.Report) error {
|
||||
if r == nil {
|
||||
return errs.ErrInvalidArgument
|
||||
}
|
||||
m.complaints = append(m.complaints, adminmodel.ComplaintRow{
|
||||
ID: r.ID, UserID: r.UserID, TargetID: r.TargetID, Reason: int(r.Reason), Detail: r.Detail, CreatedAt: r.CreatedAt,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) ImStats(context.Context) (*adminentity.ImStats, error) {
|
||||
return m.stats, nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) ListRecentMessages(_ context.Context, _ int) ([]adminmodel.MessageRow, error) {
|
||||
return m.msgs, nil
|
||||
}
|
||||
|
||||
func (m *memoryAdmin) ListUserConversations(_ context.Context, _ int64) ([]adminmodel.ConversationRow, error) {
|
||||
return m.convs, nil
|
||||
}
|
||||
|
||||
func TestAdminServiceNotReady(t *testing.T) {
|
||||
s := &Service{}
|
||||
if _, _, err := s.ListUsers(t.Context(), "", 0, 0); !errors.Is(err, ErrNotReady) {
|
||||
t.Fatalf("list %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminServiceOps(t *testing.T) {
|
||||
now := time.Unix(1_700_000_000, 0).UTC()
|
||||
active := now
|
||||
repo := &memoryAdmin{
|
||||
users: []adminmodel.UserRow{{
|
||||
ID: 11, Email: "a@spark.com", Nickname: "Ada", SparkID: "ada_ok01", Status: 1,
|
||||
DeviceID: 7, Platform: "ios", CreatedAt: now, LastActiveAt: &active,
|
||||
}},
|
||||
emails: map[string]int64{"a@spark.com": 11, "b@spark.com": 12},
|
||||
groups: map[int64]adminmodel.GroupRow{
|
||||
3: {ID: 3, Name: "g", OwnerID: 11, Status: int(groupentity.StatusActive), MemberCount: 2, CreatedAt: now, UpdatedAt: now},
|
||||
},
|
||||
members: map[int64][]adminmodel.GroupMemberRow{
|
||||
3: {{UserID: 11, Role: int(groupentity.RoleOwner), Nickname: "", UNick: "Ada", Email: "a@spark.com"}},
|
||||
},
|
||||
groupMsgs: map[int64][]adminmodel.MessageRow{
|
||||
3: {{ID: 99, SenderID: 11, MessageType: int16(msgentity.TypeText), CreatedAt: now}},
|
||||
},
|
||||
msgs: []adminmodel.MessageRow{
|
||||
{ID: 1, SenderID: 11, ReceiverID: 12, MessageType: int16(msgentity.TypeText), Content: []byte(`{"t":1,"text":{"body":"hi"}}`), CreatedAt: now, ChatType: "private"},
|
||||
},
|
||||
convs: []adminmodel.ConversationRow{
|
||||
{ID: 4, Type: 1, UserSmall: 11, UserBig: 12, LastMsgID: 1, LastMsgAt: now},
|
||||
{ID: 5, Type: 2, GroupID: 3, LastMsgID: 99, LastMsgAt: now},
|
||||
},
|
||||
complaints: []adminmodel.ComplaintRow{{ID: 1, Email: "a@spark.com", TargetID: 12, Reason: 1, CreatedAt: now}},
|
||||
stats: &adminentity.ImStats{MessageCount: 2},
|
||||
ips: []adminmodel.SameIPRankRow{{IP: "1.1.1.1", UserCount: 2, DeviceCount: 3}},
|
||||
owners: map[int64]int64{3: 11},
|
||||
}
|
||||
svc := NewService(repo, nil, &seqID{n: 100})
|
||||
ctx := t.Context()
|
||||
|
||||
list, total, err := svc.ListUsers(ctx, "", -1, 0)
|
||||
if err != nil || total != 1 || len(list) != 1 || list[0].Email != "a@spark.com" {
|
||||
t.Fatalf("list %+v %d %v", list, total, err)
|
||||
}
|
||||
byMail, err := svc.UserByEmail(ctx, "A@spark.com")
|
||||
if err != nil || byMail["email"] != "a@spark.com" {
|
||||
t.Fatalf("email %+v %v", byMail, err)
|
||||
}
|
||||
if _, err := svc.UserByEmail(ctx, " "); !errors.Is(err, errs.ErrInvalidArgument) {
|
||||
t.Fatalf("empty email %v", err)
|
||||
}
|
||||
u, err := svc.UserByID(ctx, 11)
|
||||
if err != nil || u.ID != 11 || u.DeviceID != "7" {
|
||||
t.Fatalf("by id %+v %v", u, err)
|
||||
}
|
||||
nicks, err := svc.UsersByNickname(ctx, "Ada")
|
||||
if err != nil || len(nicks) != 1 {
|
||||
t.Fatalf("nick %+v %v", nicks, err)
|
||||
}
|
||||
devs, err := svc.UsersByDevice(ctx, "7")
|
||||
if err != nil || len(devs) != 1 {
|
||||
t.Fatalf("dev %+v %v", devs, err)
|
||||
}
|
||||
ips, err := svc.UsersByIP(ctx, "1.1.1.1")
|
||||
if err != nil || len(ips) != 1 {
|
||||
t.Fatalf("ip %+v %v", ips, err)
|
||||
}
|
||||
if err := svc.BanByEmail(ctx, "a@spark.com", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if repo.users[0].Status != int(userentity.StatusDisabled) {
|
||||
t.Fatal("ban")
|
||||
}
|
||||
if err := svc.ResetNicknameByEmail(ctx, "a@spark.com", ""); err != nil || repo.users[0].Nickname != "用户" {
|
||||
t.Fatalf("nick %v %s", err, repo.users[0].Nickname)
|
||||
}
|
||||
if err := svc.ResetAvatarByEmail(ctx, "a@spark.com"); err != nil || repo.users[0].Avatar != "" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rank, err := svc.SameIPRank(ctx, 0)
|
||||
if err != nil || len(rank) != 1 || rank[0].Count != 2 {
|
||||
t.Fatalf("rank %+v %v", rank, err)
|
||||
}
|
||||
|
||||
groups, err := svc.ListGroups(ctx, "")
|
||||
if err != nil || len(groups) != 1 || groups[0].GroupID != "3" {
|
||||
t.Fatalf("groups %+v %v", groups, err)
|
||||
}
|
||||
detail, err := svc.GroupDetail(ctx, "3")
|
||||
if err != nil || detail == nil || len(detail.Members) != 1 || detail.Members[0].Role != "owner" {
|
||||
t.Fatalf("detail %+v %v", detail, err)
|
||||
}
|
||||
if _, err := svc.GroupDetail(ctx, "x"); !errors.Is(err, errs.ErrNotFound) {
|
||||
t.Fatalf("bad gid %v", err)
|
||||
}
|
||||
if err := svc.DissolveGroup(ctx, "3"); !errors.Is(err, ErrNotReady) {
|
||||
t.Fatalf("dissolve %v", err)
|
||||
}
|
||||
|
||||
stats, err := svc.ImStats(ctx)
|
||||
if err != nil || stats.MessageCount != 2 {
|
||||
t.Fatalf("stats %+v %v", stats, err)
|
||||
}
|
||||
msgs, err := svc.ListMessages(ctx, "hi", 0)
|
||||
if err != nil || len(msgs) != 1 {
|
||||
t.Fatalf("msgs %+v %v", msgs, err)
|
||||
}
|
||||
convs, gviews, st, err := svc.ListConversations(ctx, 11)
|
||||
if err != nil || st != nil || len(convs) != 2 || len(gviews) != 1 {
|
||||
t.Fatalf("convs %+v %+v %v", convs, gviews, err)
|
||||
}
|
||||
_, _, st, err = svc.ListConversations(ctx, 0)
|
||||
if err != nil || st == nil {
|
||||
t.Fatalf("global stats %v", err)
|
||||
}
|
||||
|
||||
cs, err := svc.ListComplaints(ctx)
|
||||
if err != nil || len(cs) != 1 || cs[0].Target != "12" {
|
||||
t.Fatalf("complaints %+v %v", cs, err)
|
||||
}
|
||||
added, err := svc.AddComplaint(ctx, "a@spark.com", "b@spark.com", "spam", "ads")
|
||||
if err != nil || added == nil || added.Reason != "spam" {
|
||||
t.Fatalf("add %+v %v", added, err)
|
||||
}
|
||||
if _, err := svc.AddComplaint(ctx, "", "12", "other", ""); !errors.Is(err, errs.ErrInvalidArgument) {
|
||||
t.Fatalf("bad complaint %v", err)
|
||||
}
|
||||
}
|
||||
@ -232,3 +232,27 @@ func TestMemoryDenylistExpiry(t *testing.T) {
|
||||
t.Fatalf("expired still present: %v %v", ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisStoresRequireClient(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
d := NewRedisDenylist(nil, "")
|
||||
if _, err := d.Has(ctx, "j"); err == nil {
|
||||
t.Fatal("denylist has")
|
||||
}
|
||||
if err := d.Add(ctx, "j", time.Second); err == nil {
|
||||
t.Fatal("denylist add")
|
||||
}
|
||||
p := NewRedisPairs(nil, "")
|
||||
if err := p.Put(ctx, 1, 1, TokenPair{Access: "a"}, time.Second); err == nil {
|
||||
t.Fatal("pairs put")
|
||||
}
|
||||
if _, err := p.Get(ctx, 1, 1); err == nil {
|
||||
t.Fatal("pairs get")
|
||||
}
|
||||
if err := p.Delete(ctx, 1, 1); err == nil {
|
||||
t.Fatal("pairs del")
|
||||
}
|
||||
if _, err := p.ListByUser(ctx, 1); err == nil {
|
||||
t.Fatal("pairs list")
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +41,65 @@ func TestLoginServiceValidatesAndIssues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginServiceSendRefreshLogout(t *testing.T) {
|
||||
j, err := NewJWT(&Options{Secret: "test-secret-test-secret", Issuer: "spark-im"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
users := StaticUser{Email: "test01@spark.com", UserID: 11, SparkID: "test01"}
|
||||
kicker := &kickRec{}
|
||||
svc := NewLoginService(j, users)
|
||||
svc.Sessions = kicker
|
||||
ctx := reqctx.With(t.Context(), &reqctx.RequestContext{RealIP: "203.0.113.10"})
|
||||
sent, err := svc.SendEmailCode(ctx, SendCodeInput{Email: "test01@spark.com"})
|
||||
if err != nil || sent == nil || sent.ExpiresIn != userlogic.EmailCodeTTLSeconds {
|
||||
t.Fatalf("code %+v %v", sent, err)
|
||||
}
|
||||
tok, err := svc.Login(ctx, &LoginInput{Email: "test01@spark.com", VerifyCode: "any", DeviceID: 7, DeviceType: "ios"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
refreshed, err := svc.Refresh(ctx, RefreshInput{RefreshToken: tok.RefreshToken})
|
||||
if err != nil || refreshed.UserID != 11 {
|
||||
t.Fatalf("refresh %+v %v", refreshed, err)
|
||||
}
|
||||
p, err := j.Authenticate(ctx, refreshed.AccessToken)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outCtx := reqctx.With(ctx, &reqctx.RequestContext{UserID: p.UserID, DeviceID: p.DeviceID, JTI: p.JTI, TokenExpires: p.ExpiresAt})
|
||||
if err := svc.Logout(outCtx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if kicker.n != 1 {
|
||||
t.Fatalf("kick %d", kicker.n)
|
||||
}
|
||||
if _, err := svc.SendEmailCode(ctx, SendCodeInput{}); err == nil {
|
||||
t.Fatal("empty email")
|
||||
}
|
||||
if _, err := (*LoginService)(nil).Refresh(ctx, RefreshInput{RefreshToken: "x"}); !errors.Is(err, errs.ErrUnauthorized) {
|
||||
t.Fatalf("nil refresh %v", err)
|
||||
}
|
||||
if err := svc.Logout(ctx); !errors.Is(err, errs.ErrUnauthorized) {
|
||||
t.Fatalf("logout ctx %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticAuthenticate(t *testing.T) {
|
||||
s := Static{Token: "abc", UserID: 1, DeviceID: 2}
|
||||
p, err := s.Authenticate(t.Context(), "abc")
|
||||
if err != nil || p.UserID != 1 || p.DeviceID != 2 {
|
||||
t.Fatalf("%+v %v", p, err)
|
||||
}
|
||||
if _, err := s.Authenticate(t.Context(), "no"); !errors.Is(err, errs.ErrUnauthorized) {
|
||||
t.Fatalf("bad token %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type kickRec struct{ n int }
|
||||
|
||||
func (k *kickRec) KickDevice(int64, uint64) { k.n++ }
|
||||
|
||||
func TestLoginServiceUnauthorized(t *testing.T) {
|
||||
j, err := NewJWT(&Options{Secret: "test-secret-test-secret"})
|
||||
if err != nil {
|
||||
|
||||
106
internal/module/conversation/logic/p2p_test.go
Normal file
106
internal/module/conversation/logic/p2p_test.go
Normal file
@ -0,0 +1,106 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"spark-server/internal/infrastructure/id"
|
||||
"spark-server/internal/module/conversation/entity"
|
||||
"spark-server/internal/module/conversation/model"
|
||||
"spark-server/internal/pkg/errs"
|
||||
)
|
||||
|
||||
func TestEnsureP2PAndUpdateLastMsg(t *testing.T) {
|
||||
ids, err := id.New(1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repo := model.NewMemoryP2P()
|
||||
svc := NewP2PService(repo, ids)
|
||||
ctx := t.Context()
|
||||
if _, err := svc.EnsureP2P(ctx, 0, 1); !errors.Is(err, errs.ErrInvalidArgument) {
|
||||
t.Fatalf("invalid %v", err)
|
||||
}
|
||||
if _, err := svc.EnsureP2P(ctx, 1, 1); !errors.Is(err, errs.ErrInvalidArgument) {
|
||||
t.Fatalf("same user %v", err)
|
||||
}
|
||||
cid, err := svc.EnsureP2P(ctx, 1, 2)
|
||||
if err != nil || cid <= 0 {
|
||||
t.Fatalf("create %d %v", cid, err)
|
||||
}
|
||||
again, err := svc.EnsureP2P(ctx, 2, 1)
|
||||
if err != nil || again != cid {
|
||||
t.Fatalf("idempotent %d %v", again, err)
|
||||
}
|
||||
if err := svc.UpdateLastMsg(ctx, cid, 99, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := repo.Get(ctx, cid)
|
||||
if err != nil || got.LastMsgID != 99 {
|
||||
t.Fatalf("last %+v %v", got, err)
|
||||
}
|
||||
if err := (*P2PService)(nil).UpdateLastMsg(ctx, cid, 1, 1); !errors.Is(err, errs.ErrAvailable) {
|
||||
t.Fatalf("nil svc %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type stubP2P struct {
|
||||
existing *entity.Conversation
|
||||
getErr error
|
||||
createErr error
|
||||
}
|
||||
|
||||
func (s stubP2P) GetP2P(context.Context, int64, int64) (*entity.Conversation, error) {
|
||||
if s.getErr != nil {
|
||||
return nil, s.getErr
|
||||
}
|
||||
if s.existing != nil {
|
||||
return s.existing, nil
|
||||
}
|
||||
return nil, errs.ErrNotFound
|
||||
}
|
||||
|
||||
func (s stubP2P) CreateP2P(context.Context, *entity.Conversation, *entity.P2P, []*entity.Member) error {
|
||||
return s.createErr
|
||||
}
|
||||
|
||||
func (s stubP2P) UpdateLastMsg(context.Context, int64, int64, int64) error { return nil }
|
||||
|
||||
type dupP2P struct {
|
||||
gets int
|
||||
id int64
|
||||
}
|
||||
|
||||
func (d *dupP2P) GetP2P(context.Context, int64, int64) (*entity.Conversation, error) {
|
||||
d.gets++
|
||||
if d.gets == 1 {
|
||||
return nil, errs.ErrNotFound
|
||||
}
|
||||
return &entity.Conversation{ID: d.id}, nil
|
||||
}
|
||||
|
||||
func (d *dupP2P) CreateP2P(context.Context, *entity.Conversation, *entity.P2P, []*entity.Member) error {
|
||||
return errs.ErrDuplicate
|
||||
}
|
||||
|
||||
func (d *dupP2P) UpdateLastMsg(context.Context, int64, int64, int64) error { return nil }
|
||||
|
||||
func TestEnsureP2PDuplicateAndErrors(t *testing.T) {
|
||||
ids, err := id.New(1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := t.Context()
|
||||
dup := NewP2PService(&dupP2P{id: 9}, ids)
|
||||
id9, err := dup.EnsureP2P(ctx, 1, 2)
|
||||
if err != nil || id9 != 9 {
|
||||
t.Fatalf("dup %d %v", id9, err)
|
||||
}
|
||||
if _, err := NewP2PService(stubP2P{getErr: errs.ErrAvailable}, ids).EnsureP2P(ctx, 1, 2); !errors.Is(err, errs.ErrAvailable) {
|
||||
t.Fatalf("get %v", err)
|
||||
}
|
||||
if _, err := NewP2PService(stubP2P{createErr: errs.ErrInner}, ids).EnsureP2P(ctx, 1, 2); !errors.Is(err, errs.ErrInner) {
|
||||
t.Fatalf("create %v", err)
|
||||
}
|
||||
}
|
||||
@ -138,3 +138,68 @@ func TestFavoriteForbidsNonMember(t *testing.T) {
|
||||
t.Fatalf("member %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFavoriteListDeletePurge(t *testing.T) {
|
||||
svc, msgs, convs, objs, ids := testFav(t)
|
||||
ctx := reqctx.With(t.Context(), &reqctx.RequestContext{UserID: 1, Language: "zh-CN"})
|
||||
p2p := convlogic.NewP2PService(convs, ids)
|
||||
convID, err := p2p.EnsureP2P(ctx, 1, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
src := "chat/20260917/1/list.jpg"
|
||||
objs.keys[src] = true
|
||||
raw, err := msgentity.NewImage(&msgentity.Image{Key: src, Mime: "image/jpeg"}).Marshal()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
msgID, err := ids.NextID()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := msgs.Create(ctx, &msgentity.Message{
|
||||
ID: msgID, ConversationID: convID, SenderID: 1, ClientMsgID: "list1",
|
||||
Type: msgentity.TypeImage, Content: raw, Status: msgentity.StatusNormal,
|
||||
CreatedAtMS: time.Now().UnixMilli(),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
item, err := svc.Create(ctx, 1, &CreateInput{MsgID: msgID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listed, err := svc.List(ctx, 1, 20)
|
||||
if err != nil || listed == nil || len(listed.Items) != 1 || listed.Items[0].ID != item.ID {
|
||||
t.Fatalf("list %+v %v", listed, err)
|
||||
}
|
||||
if _, err := svc.List(ctx, 0, 20); !errors.Is(err, errs.ErrUnauthorized) {
|
||||
t.Fatalf("list auth %v", err)
|
||||
}
|
||||
if err := svc.Delete(ctx, 2, item.ID); !errors.Is(err, errs.ErrForbidden) {
|
||||
t.Fatalf("forbid %v", err)
|
||||
}
|
||||
if err := svc.Delete(ctx, 1, item.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listed, err = svc.List(ctx, 1, 20)
|
||||
if err != nil || len(listed.Items) != 0 {
|
||||
t.Fatalf("after delete %+v %v", listed, err)
|
||||
}
|
||||
item2, err := svc.Create(ctx, 1, &CreateInput{MsgID: msgID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := svc.PurgeUser(ctx, 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.List(ctx, 1, 20); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listed, _ = svc.List(ctx, 1, 20)
|
||||
if listed != nil && len(listed.Items) != 0 {
|
||||
t.Fatalf("purged %+v id=%d", listed, item2.ID)
|
||||
}
|
||||
if err := svc.Delete(ctx, 1, 0); !errors.Is(err, errs.ErrInvalidArgument) {
|
||||
t.Fatalf("del zero %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
29
internal/module/inbox/logic/store_test.go
Normal file
29
internal/module/inbox/logic/store_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"spark-server/internal/module/inbox/entity"
|
||||
)
|
||||
|
||||
type nopStore struct{}
|
||||
|
||||
func (nopStore) TouchUnread(context.Context, int64, int64, int64) error { return nil }
|
||||
func (nopStore) TouchRead(context.Context, int64, int64, int64) error { return nil }
|
||||
func (nopStore) IncrUnread(context.Context, int64, int64, int64) error { return nil }
|
||||
func (nopStore) SetPreview(context.Context, int64, []byte) error { return nil }
|
||||
func (nopStore) ListUpdatedSince(context.Context, int64, int64, int) ([]entity.DialogHit, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (nopStore) LoadDialogs(context.Context, int64, []entity.DialogHit) ([]entity.Dialog, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (nopStore) PurgeUser(context.Context, int64) error { return nil }
|
||||
|
||||
func TestStoreContract(t *testing.T) {
|
||||
var s Store = nopStore{}
|
||||
if err := s.PurgeUser(t.Context(), 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@ -238,6 +238,27 @@ func TestP2PRejectsGroupMentions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageGuards(t *testing.T) {
|
||||
svc, _, _, _ := testMessageSvc(t)
|
||||
ctx := t.Context()
|
||||
if _, err := svc.GetHistory(ctx, 0, HistoryRequest{}); !errors.Is(err, errs.ErrUnauthorized) {
|
||||
t.Fatalf("hist %v", err)
|
||||
}
|
||||
if err := svc.DeleteMessage(ctx, 0, 1, 1); !errors.Is(err, errs.ErrUnauthorized) {
|
||||
t.Fatalf("del %v", err)
|
||||
}
|
||||
if err := svc.RecallMessage(ctx, 0, 1, 1); !errors.Is(err, errs.ErrUnauthorized) {
|
||||
t.Fatalf("recall %v", err)
|
||||
}
|
||||
_, err := svc.SendMessage(ctx, 1, 1, &SendInput{ClientMsgID: "x", Content: json.RawMessage(`{"t":1,"text":{"body":"x"}}`)})
|
||||
if !errors.Is(err, errs.ErrInvalidArgument) {
|
||||
t.Fatalf("no dest %v", err)
|
||||
}
|
||||
if err := svc.DeleteMessage(ctx, 1, 1, 0); !errors.Is(err, errs.ErrInvalidArgument) && !errors.Is(err, errs.ErrUnauthorized) {
|
||||
t.Fatalf("del zero %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func takeCmd(ctx context.Context, q *delivermodel.MemoryQueue, cmd uint16, msgID int64) (*deliverentity.Job, error) {
|
||||
for i := 0; i < 8; i++ {
|
||||
job, err := q.Take(ctx, 50*time.Millisecond)
|
||||
|
||||
@ -156,6 +156,57 @@ func TestLikeCommentNoticeAndHide(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnlikeCommentSettingsAndPurge(t *testing.T) {
|
||||
svc, friends, users := testMoment(t)
|
||||
a, b := twoAccounts(t, users)
|
||||
becomeFriends(t, friends, a, b)
|
||||
ctxA := ctxUser(t, a.ID, "10.0.0.1")
|
||||
ctxB := ctxUser(t, b.ID, "10.0.0.2")
|
||||
post, err := svc.Publish(ctxA, a.ID, &PublishInput{Text: "hi"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.Like(ctxB, b.ID, post.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
unliked, err := svc.Unlike(ctxB, b.ID, post.ID)
|
||||
if err != nil || unliked.Liked || unliked.LikeCount != 0 {
|
||||
t.Fatalf("unlike %+v %v", unliked, err)
|
||||
}
|
||||
commented, err := svc.Comment(ctxB, b.ID, post.ID, &CommentInput{Text: "nice"})
|
||||
if err != nil || commented.CommentCount != 1 {
|
||||
t.Fatalf("comment %+v %v", commented, err)
|
||||
}
|
||||
cid := commented.Comments[0].ID
|
||||
if _, err := svc.DeleteComment(ctxA, a.ID, post.ID, cid); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := svc.Get(ctxA, a.ID, post.ID)
|
||||
if err != nil || got.CommentCount != 0 {
|
||||
t.Fatalf("deleted comment %+v %v", got, err)
|
||||
}
|
||||
days := entity.Visible30Days
|
||||
st, err := svc.PatchSettings(ctxA, a.ID, &PatchSettingsInput{VisibleDays: &days})
|
||||
if err != nil || st.VisibleDays != entity.Visible30Days {
|
||||
t.Fatalf("settings %+v %v", st, err)
|
||||
}
|
||||
if _, err := svc.PutHide(ctxB, b.ID, a.ID, &HideInput{HideHim: true}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.DeleteHide(ctxB, b.ID, a.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := svc.ReadNotices(ctxA, a.ID, &NoticeReadInput{Cursor: 0}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := svc.PurgeUser(ctxA, a.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := svc.Get(ctxA, a.ID, post.ID); !errors.Is(err, errs.ErrNotFound) {
|
||||
t.Fatalf("purged get %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMomentImageKeyRejectedIfChatPrefix(t *testing.T) {
|
||||
svc, _, users := testMoment(t)
|
||||
a, _ := twoAccounts(t, users)
|
||||
|
||||
7
internal/module/session/logic/policy_test.go
Normal file
7
internal/module/session/logic/policy_test.go
Normal file
@ -0,0 +1,7 @@
|
||||
package logic
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPolicyExists(_ *testing.T) {
|
||||
_ = Policy{}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user