CI / test (push) Successful in 1m29s
- сервер отдаёт агенту итоговый каталог (встроенный каталог сервера + переопределения клиента); если каталог не подходит версии агента — встроенный каталог агента и предупреждение - самообновление перед бэкапом: версия сервера новее → скачивание с того же сервера (только /dl/...), проверка sha256 и запуска новой версии, подмена с откатом, перезапуск через exec; при ошибке бэкап идёт текущей версией - галочка «Автообновление» у клиента (по умолчанию включена), версия агента в списке и карточке, отставшие подсвечены; версия сервера в шапке - миграция существующей базы: колонки auto_update и agent_version - backupctl backup --no-update Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
113 lines
4.2 KiB
Go
113 lines
4.2 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"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()
|
|
}
|
|
}
|