package server import ( "crypto/sha256" "database/sql" "encoding/hex" "os" "path/filepath" "testing" "time" "gitea.hsrv.site/mrleo1nid/backup-master/internal/catalog" ) func TestAgentConfigCatalogAndUpdate(t *testing.T) { dist := t.TempDir() bin := []byte("fake-backupctl-binary") if err := os.WriteFile(filepath.Join(dist, "backupctl-linux-amd64"), bin, 0o755); err != nil { t.Fatal(err) } s, err := New(Config{DataDir: t.TempDir(), DistDir: dist, Version: "v0.6.0"}) if err != nil { t.Fatal(err) } defer s.Close() tok := newToken() id, err := s.st.CreateClient("x", "x.example.com", hashToken(tok)) if err != nil { t.Fatal(err) } c, _ := s.st.GetClient(id) if !c.AutoUpdate { t.Fatal("автообновление должно быть включено по умолчанию") } c.ConfigYAML = "categories:\n - {name: scripts, disabled: true}\n" if err := s.st.UpdateClientSettings(c); err != nil { t.Fatal(err) } ts := newTestHTTP(t, s) var cfg AgentConfigResponse decode(t, call(t, "GET", ts+"/api/v1/agent/config?arch=amd64&version=v0.5.0", tok, nil), &cfg) cat, err := catalog.Parse([]byte(cfg.CatalogYAML)) if err != nil || cat.Validate() != nil { t.Fatalf("каталог не читается: %v", err) } if cat.Find("xui") == nil || !cat.Find("scripts").Disabled { t.Fatal("в каталоге нет встроенных категорий или переопределений клиента") } sum := sha256.Sum256(bin) if cfg.Update == nil || cfg.Update.Version != "v0.6.0" || cfg.Update.Path != "/dl/backupctl-linux-amd64" || cfg.Update.SHA256 != hex.EncodeToString(sum[:]) { t.Fatalf("update: %+v", cfg.Update) } if c, _ := s.st.GetClient(id); c.AgentVersion != "v0.5.0" { t.Fatalf("версия агента не записана: %q", c.AgentVersion) } // актуальная версия, неизвестная архитектура, выключенное автообновление — без обновления for _, q := range []string{"arch=amd64&version=v0.6.0", "arch=riscv64&version=v0.5.0"} { cfg = AgentConfigResponse{} decode(t, call(t, "GET", ts+"/api/v1/agent/config?"+q, tok, nil), &cfg) if cfg.Update != nil { t.Fatalf("%s: обновление не ожидалось", q) } } c, _ = s.st.GetClient(id) c.AutoUpdate = false _ = s.st.UpdateClientSettings(c) cfg = AgentConfigResponse{} decode(t, call(t, "GET", ts+"/api/v1/agent/config?arch=amd64&version=v0.5.0", tok, nil), &cfg) if cfg.Update != nil { t.Fatal("автообновление выключено, а обновление предложено") } } // Базы первых релизов без новых колонок мигрируются при открытии. func TestStoreMigration(t *testing.T) { path := filepath.Join(t.TempDir(), "old.db") db, err := sql.Open("sqlite", "file:"+path) if err != nil { t.Fatal(err) } _, err = db.Exec(`CREATE TABLE clients (id INTEGER PRIMARY KEY, name TEXT NOT NULL, domain TEXT NOT NULL UNIQUE, token_hash TEXT NOT NULL UNIQUE, config_yaml TEXT NOT NULL DEFAULT '', keep_daily INTEGER NOT NULL DEFAULT 7, keep_weekly INTEGER NOT NULL DEFAULT 4, keep_monthly INTEGER NOT NULL DEFAULT 6, heavy_keep INTEGER NOT NULL DEFAULT 1, heavy_every_days INTEGER NOT NULL DEFAULT 7, stale_hours INTEGER NOT NULL DEFAULT 26, created_at INTEGER NOT NULL, last_backup_at INTEGER NOT NULL DEFAULT 0, last_heavy_at INTEGER NOT NULL DEFAULT 0, last_seen_at INTEGER NOT NULL DEFAULT 0, last_error TEXT NOT NULL DEFAULT '', last_error_at INTEGER NOT NULL DEFAULT 0, stale_notified INTEGER NOT NULL DEFAULT 0, disabled INTEGER NOT NULL DEFAULT 0); INSERT INTO clients (name, domain, token_hash, created_at) VALUES ('a', 'a.example.com', 'h', 1);`) if err != nil { t.Fatal(err) } db.Close() st, err := OpenStore(path) if err != nil { t.Fatal(err) } defer st.Close() cs, err := st.ListClients() if err != nil || len(cs) != 1 || !cs[0].AutoUpdate { t.Fatalf("после миграции: %v %+v", err, cs) } // повторное открытие не падает на уже добавленных колонках st.Close() if st2, err := OpenStore(path); err != nil { t.Fatal(err) } else { st2.Close() } } func TestTakeoverRevokesOldAgent(t *testing.T) { s, ts := newTestServer(t) oldTok := newToken() id, err := s.st.CreateClient("x", "x.example.com", hashToken(oldTok)) if err != nil { t.Fatal(err) } if r := call(t, "GET", ts.URL+"/api/v1/agent/config", oldTok, nil); r.StatusCode != 200 { t.Fatalf("старый токен до takeover: %d", r.StatusCode) } // агентский токен takeover не делает if r := call(t, "POST", ts.URL+"/api/v1/restore/takeover", oldTok, nil); r.StatusCode != 401 { t.Fatalf("takeover агентским токеном: %d", r.StatusCode) } rt := newToken() if err := s.st.CreateRestoreToken(hashToken(rt), id, time.Hour); err != nil { t.Fatal(err) } var out struct{ Token string } decode(t, call(t, "POST", ts.URL+"/api/v1/restore/takeover", rt, nil), &out) if out.Token == "" || out.Token == oldTok { t.Fatalf("новый токен: %q", out.Token) } if r := call(t, "GET", ts.URL+"/api/v1/agent/config", oldTok, nil); r.StatusCode != 401 { t.Fatalf("старый сервер после takeover всё ещё принят: %d", r.StatusCode) } if r := call(t, "GET", ts.URL+"/api/v1/agent/config", out.Token, nil); r.StatusCode != 200 { t.Fatalf("новый токен: %d", r.StatusCode) } }