Files
goclaw/cmd/tenant_restore_test.go
badgerbeesandViet Tran f5917b0e46 fix(backup): harden tenant restore preview and lookup handling (#920)
* 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>
2026-04-16 15:49:50 +07:00

43 lines
1.4 KiB
Go

package cmd
import (
"strings"
"testing"
)
func TestValidateTenantRestoreFlags(t *testing.T) {
tests := []struct {
name, mode, tenant, tenantID, newSlug, wantErr string
}{
{"new ok", "new", "", "", "fresh-slug", ""},
{"new missing slug", "new", "", "", "", "required for mode=new"},
{"new with tenant slug", "new", "acme", "", "fresh-slug", "does not accept"},
{"new with tenant-id", "new", "", "uuid-x", "fresh-slug", "does not accept"},
{"upsert ok tenant", "upsert", "acme", "", "", ""},
{"upsert ok tenant-id", "upsert", "", "uuid-x", "", ""},
{"upsert ok both", "upsert", "acme", "uuid-x", "", ""},
{"upsert missing", "upsert", "", "", "", "required for mode=upsert"},
{"upsert ignores new slug (no error)", "upsert", "acme", "", "ignored", ""},
{"replace ok", "replace", "acme", "", "", ""},
{"replace missing", "replace", "", "", "", "required for mode=replace"},
{"invalid mode", "bogus", "acme", "", "", "invalid --mode"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := validateTenantRestoreFlags(tc.mode, tc.tenant, tc.tenantID, tc.newSlug)
if tc.wantErr == "" {
if err != nil {
t.Fatalf("want nil, got %v", err)
}
return
}
if err == nil {
t.Fatalf("want err containing %q, got nil", tc.wantErr)
}
if !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("want err containing %q, got %q", tc.wantErr, err.Error())
}
})
}
}