mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 06:17:41 +00:00
* fix(backup): harden tenant restore preview and lookup handling * test(pg): fix hook test migration path * test(ci): stabilize race coverage tests * fix(hooks): scope GetByID and allow loopback tests * fix(backup): harden tenant restore replace/new contracts + race-safe SSRF flag - Replace mode no longer deletes the tenants row (FK safe vs excluded diagnostic tables: traces, activity_logs, usage_snapshots, spans, embedding_cache, pairing_requests, paired_devices, channel_pending_messages, cron_run_logs). Metadata is preserved in place. - shouldRestoreTable now excludes tenants for both new and replace modes. - CLI: add validateTenantRestoreFlags guardrail. mode=new requires --new-tenant-slug and rejects --tenant/--tenant-id; upsert/replace warn on stray --new-tenant-slug; invalid --mode values rejected. TAB in help text fixed; flag descriptions clarified. - HTTP: resolveRestoreTarget rejects tenant_id for mode=new regardless of tenant_slug (matches CLI contract). New i18n key MsgRestoreNewModeRejectsTenantID (en/vi/zh). - security/ssrf: allowLoopbackForTest switched to atomic.Bool so concurrent reads from outbound dialers are race-safe. - Polish: vi backup.json key order matches en/zh; TenantRestoreOptions.Mode doc comment documents upsert/replace/new semantics including clone behavior for new. - Tests: unit coverage for validator (12 cases), HTTP guardrails (3 cases), shouldRestoreTable replace branch. Integration test tests/integration/tenant_restore_replace_test.go regression-guards the FK fix using activity_logs seed + DeleteTenantDataForTest helper. --------- Co-authored-by: Viet Tran <viettranx@gmail.com>
98 lines
2.8 KiB
Go
98 lines
2.8 KiB
Go
package backup
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func TestLoadTenantRestoreRow(t *testing.T) {
|
|
row, err := loadTenantRestoreRow([]byte(`{"id":"0193a5b0-7000-7000-8000-000000000001","name":"Master","slug":"master","status":"active","settings":"{\"theme\":\"dark\"}"}`))
|
|
if err != nil {
|
|
t.Fatalf("loadTenantRestoreRow() error = %v", err)
|
|
}
|
|
if row.Name != "Master" {
|
|
t.Fatalf("row.Name = %q, want %q", row.Name, "Master")
|
|
}
|
|
if row.Slug != "master" {
|
|
t.Fatalf("row.Slug = %q, want %q", row.Slug, "master")
|
|
}
|
|
if row.Status != "active" {
|
|
t.Fatalf("row.Status = %q, want %q", row.Status, "active")
|
|
}
|
|
if row.Settings != `{"theme":"dark"}` {
|
|
t.Fatalf("row.Settings = %q, want %q", row.Settings, `{"theme":"dark"}`)
|
|
}
|
|
}
|
|
|
|
func TestShouldRestoreTable(t *testing.T) {
|
|
if shouldRestoreTable("new", TableDef{Name: "tenants"}) {
|
|
t.Fatal("shouldRestoreTable(new, tenants) = true, want false")
|
|
}
|
|
if shouldRestoreTable("replace", TableDef{Name: "tenants"}) {
|
|
t.Fatal("shouldRestoreTable(replace, tenants) = true, want false")
|
|
}
|
|
if !shouldRestoreTable("upsert", TableDef{Name: "tenants"}) {
|
|
t.Fatal("shouldRestoreTable(upsert, tenants) = false, want true")
|
|
}
|
|
if !shouldRestoreTable("new", TableDef{Name: "agents"}) {
|
|
t.Fatal("shouldRestoreTable(new, agents) = false, want true")
|
|
}
|
|
if !shouldRestoreTable("replace", TableDef{Name: "agents"}) {
|
|
t.Fatal("shouldRestoreTable(replace, agents) = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestEnsureTenantSlugAvailable(t *testing.T) {
|
|
existingID := uuid.MustParse("0193a5b0-7000-7000-8000-000000000002")
|
|
|
|
tests := []struct {
|
|
name string
|
|
lookup func(context.Context, string) (uuid.UUID, error)
|
|
wantErrContains string
|
|
}{
|
|
{
|
|
name: "available",
|
|
lookup: func(context.Context, string) (uuid.UUID, error) {
|
|
return uuid.Nil, sql.ErrNoRows
|
|
},
|
|
},
|
|
{
|
|
name: "duplicate",
|
|
lookup: func(context.Context, string) (uuid.UUID, error) {
|
|
return existingID, nil
|
|
},
|
|
wantErrContains: "already exists",
|
|
},
|
|
{
|
|
name: "lookup failure",
|
|
lookup: func(context.Context, string) (uuid.UUID, error) {
|
|
return uuid.Nil, errors.New("boom")
|
|
},
|
|
wantErrContains: "check tenant slug",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := ensureTenantSlugAvailable(context.Background(), "new-slug", tt.lookup)
|
|
if tt.wantErrContains == "" {
|
|
if err != nil {
|
|
t.Fatalf("ensureTenantSlugAvailable() error = %v, want nil", err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil {
|
|
t.Fatal("ensureTenantSlugAvailable() error = nil, want non-nil")
|
|
}
|
|
if !strings.Contains(err.Error(), tt.wantErrContains) {
|
|
t.Fatalf("ensureTenantSlugAvailable() error = %q, want substring %q", err.Error(), tt.wantErrContains)
|
|
}
|
|
})
|
|
}
|
|
}
|