Files
CyberStrikeAI/internal/update/source_test.go
T
Sycun 94a18263b9 feat: make the update source one repository address, the official repo by default
The update source used to be three things - a remote name or an address, plus an
optional branch - and unset it followed whatever this directory's remotes and
upstream happened to say. That asked people to understand git topology before
they could update, and the person it was built for could not tell what "follow
the remote this tree tracks" meant. It is now one field: config.yaml update.repo,
an address, empty meaning the official repository
(https://github.com/AIPentest/CyberStrikeAI.git). Point it at a mirror (GitHub is
hard to reach in some places) or at your own fork and that is the whole
configuration.

The branch nothing is any more: an update follows the source repository's own
default branch, resolved from the source itself (ls-remote --symref HEAD), so a
fork whose default is not "main" still works and nobody has to name a branch.
Remote names, the mine/origin/upstream ranking, @{upstream} tracking and the
adopt flow's branch argument are all gone with it.

The console page loses two input fields: the source section is one address, a
hint that empty means the official repository, and a save button; clearing the
address is the way back to the default. The install facts drop the remote name,
the check section shows the source's default branch once the network answer
arrives, and the adopt entry no longer requires saving an address first - the
official default makes connecting a tarball install a zero-configuration action.

The CLI now reads the same update.repo field the page saves (LoadUpdateRepo reads
just that field, without validating the rest of the file - a broken unrelated
section must not block the path that fixes the tree), so the keyboard and the
button cannot update from different repositories. -check-update prints the source
it is about to use.

Tests: the source suite now pins the default (official), the single-address save
(including the empty-string reset), and that the followed branch comes from the
source's own default branch rather than a hardcoded "main"; the console suite
posts the one field and renders the default hint; docs, config.example.yaml and
upgrade.sh describe the one-field story.
2026-10-08 13:46:14 +08:00

165 lines
5.5 KiB
Go

package update
import (
"context"
"path/filepath"
"strings"
"testing"
)
// The update source is one address (config.yaml update.repo): the official repository by
// default, changeable in one field to a mirror, a fork or a second-development
// repository. The branch that is followed is the source's own default branch, so nothing
// about branches needs configuring. These tests keep that path honest: the configured
// address must be what git is pointed at, the default must be the official repository,
// the followed branch must come from the source itself, and git's ext:: transport (which
// executes commands) must never get through.
func TestValidRemoteURL(t *testing.T) {
yes := []string{
"https://github.com/Sycun/CyberStrikeAI.git",
"http://example.com/x.git",
"ssh://git@example.com/x.git",
"git://example.com/x.git",
"file:///srv/repos/x.git",
"/Volumes/code/CyberStrikeAI/开发版-CyberStrikeAI",
}
no := []string{
"",
"-u./evil",
"ext::sh -c whoami",
"ext::touch /tmp/x",
"a\nb",
"javascript:alert(1)",
"ftp://example.com/x",
" https://github.com/x/y.git",
strings.Repeat("a", 600),
}
for _, s := range yes {
if !ValidRemoteURL(s) {
t.Errorf("%q must be accepted as a fetch source", s)
}
}
for _, s := range no {
if ValidRemoteURL(s) {
t.Errorf("%q must be rejected before it reaches git", s)
}
}
}
func TestDefaultRepoIsTheOfficialRepository(t *testing.T) {
requireGit(t)
tr := newTree(t)
snap, err := Status(context.Background(), Options{Root: tr.install})
if err != nil {
t.Fatal(err)
}
if snap.Repo != DefaultRepoURL || snap.RepoConfigured {
t.Fatalf("repo = %q configured=%v, want the official default %q without configuration",
snap.Repo, snap.RepoConfigured, DefaultRepoURL)
}
if DefaultRepoURL != "https://github.com/AIPentest/CyberStrikeAI.git" {
t.Errorf("DefaultRepoURL = %q, want the official repository", DefaultRepoURL)
}
}
func TestCheckAndApplyFromTheConfiguredRepo(t *testing.T) {
requireGit(t)
tr := newTree(t)
// No remote at all: the source is purely the configured address.
mustGit(t, tr.install, "remote", "remove", "origin")
urlOpts := Options{Root: tr.install, Repo: tr.upstream, BinaryName: "none"}
snap, err := Status(context.Background(), urlOpts)
if err != nil {
t.Fatal(err)
}
if !snap.Installed || snap.Repo != tr.upstream || !snap.RepoConfigured {
t.Fatalf("source must resolve to the configured address: installed=%v repo=%q configured=%v",
snap.Installed, snap.Repo, snap.RepoConfigured)
}
if snap.Branch != "main" {
t.Fatalf("local branch = %q, want main", snap.Branch)
}
tr.upstreamCommit(t, "second: bump service", map[string]string{
"internal_service.go": "package service\n\nconst Version = \"2\"\n",
})
snap, err = Check(context.Background(), urlOpts)
if err != nil {
t.Fatal(err)
}
if snap.CheckError != "" {
t.Fatalf("check failed: %s", snap.CheckError)
}
if !snap.UpdateAvailable || snap.Behind != 1 {
t.Fatalf("behind=%d available=%v, want 1/true", snap.Behind, snap.UpdateAvailable)
}
if snap.TargetBranch != "main" {
t.Fatalf("targetBranch = %q, want the source's own default branch", snap.TargetBranch)
}
res, err := Apply(context.Background(), urlOpts, nil)
if err != nil {
t.Fatalf("apply failed: %v\nresult: %+v", err, res)
}
if res.Commits != 1 {
t.Fatalf("res = %+v, want one commit moved", res)
}
if got := readFile(t, filepath.Join(tr.install, "internal_service.go")); !strings.Contains(got, "Version = \"2\"") {
t.Fatalf("code was not updated: %q", got)
}
// The configured source parks its ref in a namespace of its own; it must not fabricate a
// remote the operator never configured.
if remotes := strings.TrimSpace(mustGit(t, tr.install, "remote")); remotes != "" {
t.Fatalf("a configured repo must not create a remote, got %q", remotes)
}
}
func TestSourceFollowsTheRepositoriesDefaultBranch(t *testing.T) {
requireGit(t)
tr := newTree(t)
// The source publishes a default branch that is not "main": the update follows what
// the source itself names, so nothing about branches needs configuring.
mustGit(t, tr.upstream, "checkout", "-q", "-b", "stable")
tr.upstreamCommit(t, "stable: bump service", map[string]string{
"internal_service.go": "package service\n\nconst Version = \"2\"\n",
})
snap, err := Check(context.Background(), Options{Root: tr.install, Repo: tr.upstream, BinaryName: "none"})
if err != nil {
t.Fatal(err)
}
if snap.CheckError != "" {
t.Fatalf("check failed: %s", snap.CheckError)
}
if snap.TargetBranch != "stable" {
t.Fatalf("targetBranch = %q, want the source's default branch stable", snap.TargetBranch)
}
if !snap.UpdateAvailable || snap.Behind != 1 {
t.Fatalf("behind=%d available=%v, want 1/true against the source's default branch", snap.Behind, snap.UpdateAvailable)
}
res, err := Apply(context.Background(), Options{Root: tr.install, Repo: tr.upstream, BinaryName: "none"}, nil)
if err != nil {
t.Fatalf("apply failed: %v\nresult: %+v", err, res)
}
if got := readFile(t, filepath.Join(tr.install, "internal_service.go")); !strings.Contains(got, "Version = \"2\"") {
t.Fatalf("code was not updated from the source's default branch: %q", got)
}
}
func TestInvalidRemoteURLIsAReadableStateNotAnError(t *testing.T) {
requireGit(t)
tr := newTree(t)
snap, err := Status(context.Background(), Options{Root: tr.install, Repo: "ext::sh -c true"})
if err != nil {
t.Fatalf("a bad configured source is a state the page can show, not a fault: %v", err)
}
if !strings.Contains(snap.CheckError, "不合法") {
t.Fatalf("checkError = %q, want the illegality named", snap.CheckError)
}
}