Files
BackupX/server/internal/service/node_service_test.go
2026-05-13 14:24:45 +08:00

208 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"path/filepath"
"testing"
"time"
"backupx/server/internal/model"
"backupx/server/internal/repository"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
gormlogger "gorm.io/gorm/logger"
)
func openNodeServiceDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "ns.db")),
&gorm.Config{Logger: gormlogger.Default.LogMode(gormlogger.Silent)})
if err != nil {
t.Fatalf("open: %v", err)
}
if err := db.AutoMigrate(&model.Node{}); err != nil {
t.Fatalf("migrate: %v", err)
}
if err := db.AutoMigrate(&model.AgentCommand{}); err != nil {
t.Fatalf("migrate agent commands: %v", err)
}
return db
}
func TestBatchCreateNodes(t *testing.T) {
db := openNodeServiceDB(t)
svc := NewNodeService(repository.NewNodeRepository(db), "test")
ctx := context.Background()
items, err := svc.BatchCreate(ctx, []string{"a", "b", "c"})
if err != nil {
t.Fatalf("batch: %v", err)
}
if len(items) != 3 {
t.Fatalf("expected 3, got %d", len(items))
}
for _, it := range items {
if it.ID == 0 || it.Name == "" {
t.Errorf("invalid item %+v", it)
}
}
}
func TestBatchCreateRejectsDuplicatesAgainstDB(t *testing.T) {
db := openNodeServiceDB(t)
svc := NewNodeService(repository.NewNodeRepository(db), "test")
ctx := context.Background()
if _, err := svc.Create(ctx, NodeCreateInput{Name: "a"}); err != nil {
t.Fatalf("create: %v", err)
}
_, err := svc.BatchCreate(ctx, []string{"a", "b"})
if err == nil {
t.Fatalf("expected error on duplicate with existing")
}
}
func TestBatchCreateRejectsIntraBatchDuplicates(t *testing.T) {
db := openNodeServiceDB(t)
svc := NewNodeService(repository.NewNodeRepository(db), "test")
_, err := svc.BatchCreate(context.Background(), []string{"x", "x"})
if err == nil {
t.Fatalf("expected error on intra-batch duplicate")
}
}
func TestBatchCreateLimitEnforced(t *testing.T) {
db := openNodeServiceDB(t)
svc := NewNodeService(repository.NewNodeRepository(db), "test")
names := make([]string, 51)
for i := range names {
names[i] = "n" + string(rune('A'+i))
}
_, err := svc.BatchCreate(context.Background(), names)
if err == nil {
t.Fatalf("expected error on >50 batch")
}
}
func TestBatchCreateSkipsEmptyLines(t *testing.T) {
db := openNodeServiceDB(t)
svc := NewNodeService(repository.NewNodeRepository(db), "test")
items, err := svc.BatchCreate(context.Background(), []string{"a", " ", "", "b"})
if err != nil {
t.Fatalf("batch: %v", err)
}
if len(items) != 2 {
t.Fatalf("expected 2 (a,b), got %d", len(items))
}
}
func TestRotateToken(t *testing.T) {
db := openNodeServiceDB(t)
repo := repository.NewNodeRepository(db)
svc := NewNodeService(repo, "test")
ctx := context.Background()
_, err := svc.Create(ctx, NodeCreateInput{Name: "rot"})
if err != nil {
t.Fatalf("create: %v", err)
}
var node model.Node
db.First(&node, "name = ?", "rot")
oldTok := node.Token
newTok, err := svc.RotateToken(ctx, node.ID)
if err != nil {
t.Fatalf("rotate: %v", err)
}
if newTok == oldTok || len(newTok) != 64 {
t.Fatalf("invalid new token: %s", newTok)
}
// 旧 token 仍可查24h 内)
found, _ := repo.FindByToken(ctx, oldTok)
if found == nil || found.ID != node.ID {
t.Fatalf("old token should still work via prev_token fallback")
}
found2, _ := repo.FindByToken(ctx, newTok)
if found2 == nil || found2.ID != node.ID {
t.Fatalf("new token should work")
}
db.First(&node, node.ID)
if node.PrevTokenExpires == nil {
t.Fatalf("prev_token_expires not set")
}
diff := node.PrevTokenExpires.Sub(time.Now().UTC())
if diff < 23*time.Hour || diff > 25*time.Hour {
t.Fatalf("prev_token_expires out of range: %v", diff)
}
}
func TestRotateTokenRejectsLocal(t *testing.T) {
db := openNodeServiceDB(t)
repo := repository.NewNodeRepository(db)
svc := NewNodeService(repo, "test")
ctx := context.Background()
if err := svc.EnsureLocalNode(ctx); err != nil {
t.Fatalf("ensure local: %v", err)
}
local, _ := repo.FindLocal(ctx)
if _, err := svc.RotateToken(ctx, local.ID); err == nil {
t.Fatalf("expected error rotating local node")
}
}
func TestRotateTokenNotFound(t *testing.T) {
db := openNodeServiceDB(t)
svc := NewNodeService(repository.NewNodeRepository(db), "test")
if _, err := svc.RotateToken(context.Background(), 9999); err == nil {
t.Fatalf("expected not found error")
}
}
func TestNodeServiceListIncludesQueueHealthSummary(t *testing.T) {
db := openNodeServiceDB(t)
nodeRepo := repository.NewNodeRepository(db)
cmdRepo := repository.NewAgentCommandRepository(db)
svc := NewNodeService(nodeRepo, "test")
svc.SetAgentCommandRepository(cmdRepo)
ctx := context.Background()
node := &model.Node{
Name: "edge-a",
Token: "edge-token",
Status: model.NodeStatusOnline,
IsLocal: false,
LastSeen: time.Now().UTC(),
}
if err := nodeRepo.Create(ctx, node); err != nil {
t.Fatalf("Create node returned error: %v", err)
}
old := time.Now().UTC().Add(-time.Minute)
if err := cmdRepo.Create(ctx, &model.AgentCommand{NodeID: node.ID, Type: model.AgentCommandTypeRunTask, Status: model.AgentCommandStatusPending, CreatedAt: old}); err != nil {
t.Fatalf("Create pending command returned error: %v", err)
}
completedAt := time.Now().UTC()
if err := cmdRepo.Create(ctx, &model.AgentCommand{NodeID: node.ID, Type: model.AgentCommandTypeRunTask, Status: model.AgentCommandStatusTimeout, ErrorMessage: "agent timeout", CompletedAt: &completedAt}); err != nil {
t.Fatalf("Create timeout command returned error: %v", err)
}
items, err := svc.List(ctx)
if err != nil {
t.Fatalf("List returned error: %v", err)
}
if len(items) != 1 {
t.Fatalf("expected one node, got %#v", items)
}
got := items[0]
if got.Queue.Pending != 1 || got.Queue.Depth != 1 || got.Queue.Timeouts != 1 {
t.Fatalf("unexpected queue summary: %#v", got.Queue)
}
if got.Health != "degraded" || got.LastError != "agent timeout" {
t.Fatalf("expected terminal command errors to degrade healthy node, got %#v", got)
}
if got.Queue.OldestActiveAt == nil || got.Queue.OldestActiveAgeS <= 0 {
t.Fatalf("expected oldest active metadata, got %#v", got.Queue)
}
}