package release import ( "bytes" "crypto/ed25519" "crypto/rand" "encoding/json" "os" "path/filepath" "strings" "testing" ) // Пакет обновления: сборка → распаковка → подпись → план в порядке «ядро, потом модули»; // модуль, требующий более новое ядро, чем будет после установки, помечается заблокированным. func TestBundleRoundTripAndPlanOrder(t *testing.T) { pub, priv, _ := ed25519.GenerateKey(rand.Reader) rel := t.TempDir() write := func(name, body string) Artifact { p := filepath.Join(rel, name) if err := os.WriteFile(p, []byte(body), 0o644); err != nil { t.Fatal(err) } sha, size, _ := HashFile(p) return Artifact{Name: name, File: name, Version: "2.0.0", SHA256: sha, Size: size, Exec: true} } core := write("bj-server", "ядро 2.0.0") lk := write("bj-lk", "кабинет 2.0.0") lk.MinCore = "2.0.0" npr := write("bj-npr", "нпр 2.0.0") npr.MinCore = "2.5.0" // собран под ядро новее этого релиза — ставить нельзя m := &Manifest{Schema: CurrentSchema, Version: "2.0.0", Channel: "stable", Artifacts: []Artifact{lk, npr, core}} sm, err := Sign(m, priv, "k1") if err != nil { t.Fatal(err) } raw, _ := json.Marshal(sm) if err := os.WriteFile(filepath.Join(rel, BundleManifestName), raw, 0o644); err != nil { t.Fatal(err) } var buf bytes.Buffer if err := WriteBundle(&buf, rel, func(a Artifact) bool { return a.Name != "bj-npr" }); err != nil { t.Fatal(err) } zipPath := filepath.Join(t.TempDir(), "bundle.zip") if err := os.WriteFile(zipPath, buf.Bytes(), 0o644); err != nil { t.Fatal(err) } staging := filepath.Join(t.TempDir(), "staging") names, err := ExtractBundle(zipPath, staging) if err != nil { t.Fatal(err) } if strings.Join(names, ",") != "manifest.json,bj-lk,bj-server" { t.Fatalf("распаковано: %v", names) } if _, _, err := ReadStagedManifest(staging, pub); err != nil { t.Fatalf("подпись: %v", err) } other, _, _ := ed25519.GenerateKey(rand.Reader) if _, _, err := ReadStagedManifest(staging, other); err == nil { t.Fatal("манифест принят с чужим ключом") } // План на сервере с ядром 1.9.0 и кабинетом 1.9.0 root := t.TempDir() for _, c := range []string{"bj-server", "bj-lk", "bj-npr"} { comp, _ := ComponentByName(c) _ = os.MkdirAll(filepath.Dir(comp.PathAt(root)), 0o755) _ = os.WriteFile(comp.PathAt(root), []byte("старый "+c), 0o755) } plan := Plan(root, m, map[string]Installed{}, "1.9.0") var order []string byName := map[string]ComponentState{} for _, c := range plan { byName[c.Name] = c if c.Update { order = append(order, c.Name) } } if strings.Join(order, ",") != "bj-server,bj-lk" { t.Fatalf("порядок установки: %v (ядро должно быть первым, npr заблокирован)", order) } if b := byName["bj-npr"]; b.Update || !strings.Contains(b.Blocked, "2.5.0") { t.Fatalf("модуль с требованием к ядру не заблокирован: %+v", b) } // Без ядра в пакете кабинет 2.0.0 на ядро 1.9.0 не встаёт m2 := &Manifest{Schema: CurrentSchema, Version: "2.0.0", Artifacts: []Artifact{lk}} if b := Plan(root, m2, map[string]Installed{}, "1.9.0"); !strings.Contains(findState(b, "bj-lk").Blocked, "сначала обновите ядро") { t.Fatalf("кабинет без нового ядра должен быть заблокирован: %+v", findState(b, "bj-lk")) } // Не zip — понятная ошибка bad := filepath.Join(t.TempDir(), "bad.zip") _ = os.WriteFile(bad, []byte("не архив"), 0o644) if _, err := ExtractBundle(bad, filepath.Join(t.TempDir(), "s")); err == nil || !strings.Contains(err.Error(), "zip") { t.Fatalf("не-zip принят: %v", err) } } func findState(list []ComponentState, name string) ComponentState { for _, c := range list { if c.Name == name { return c } } return ComponentState{} }