mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-19 14:07:36 +02:00
config.yaml gains an optional update section (remote / remote_url / branch). The resolver keeps its default - whatever remote this tree already tracks - and the explicit choice wins when set, which is also what lets a tarball installation that never saw git get on the update rails at all. - RemoteURL is fetched straight into a ref namespace of its own, so a URL source cannot be shadowed by a same-named remote's stale refs. ValidRemoteURL allowlists https/http/ssh/git/file and local absolute paths, and rejects git's ext:: transport (which executes commands) before anything reaches argv. - Adopt connects a plain directory to the source. Preview is side-effect free (the fetch happens in a throwaway bare repository) and lists every local file whose content the target would replace, plus the operator content that is put aside. The confirmed run inits, adds origin, stashes operator content, keeps every replaced file under .update-backup/<ts>/overwritten/ so "the update ate my edit" has an address, lands the target content, sets upstream, rebuilds the binary. A failure removes only the .git it created itself; once the content has landed the tree stays adopted even if the build fails. - Console/API: the status envelope carries the configured source; POST /system/update/source writes it through the same config path every other setting uses (validated, audited); POST /system/update/adopt is preview-then-job, with the job's kind recorded. OpenAPI updated; RBAC rides the existing update:apply prefix and the route inventory stays green. Tests: URL-mode check/apply against real repositories (including the address beating a remote name), URL validation table, and the full adoption story - preview touches nothing, operator content survives, the replaced edit is in the backup, and the adopted tree updates and rolls back like any other installation.
135 lines
4.4 KiB
Go
135 lines
4.4 KiB
Go
package update
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The explicit update source (config.yaml update.remote_url) is what lets an installation
|
|
// follow a repository that is not one of its own remotes - the official one, a fork, or a
|
|
// second-development repository somebody was told about. These tests keep that path honest:
|
|
// the address must win over remote names, must not invent a remote, and must not let git's
|
|
// ext:: transport (which executes commands) 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 TestCheckAndApplyFromAnExplicitURL(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, RemoteURL: tr.upstream, BinaryName: "none"}
|
|
|
|
snap, err := Status(context.Background(), urlOpts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !snap.Installed || snap.Remote != "" || snap.RemoteURL != tr.upstream {
|
|
t.Fatalf("source must resolve to the configured address: installed=%v remote=%q url=%q",
|
|
snap.Installed, snap.Remote, snap.RemoteURL)
|
|
}
|
|
// With no upstream to name a branch, the current branch is the default.
|
|
if snap.Branch != "main" {
|
|
t.Fatalf("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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
// A URL 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 URL source must not create a remote, got %q", remotes)
|
|
}
|
|
}
|
|
|
|
func TestRemoteURLWinsOverARemoteName(t *testing.T) {
|
|
requireGit(t)
|
|
tr := newTree(t)
|
|
// A second repository with its own newer commit: the address must beat the origin remote.
|
|
other := filepath.Join(filepath.Dir(tr.upstream), "other")
|
|
mustGit(t, filepath.Dir(tr.upstream), "clone", "-q", tr.upstream, other)
|
|
writeFile(t, filepath.Join(other, "internal_service.go"), "package service\n\nconst Version = \"9\"\n")
|
|
mustGit(t, other, "add", ".")
|
|
mustGit(t, other, "-c", "user.email=t@example.com", "-c", "user.name=T", "commit", "-q", "-m", "other moves")
|
|
|
|
snap, err := Check(context.Background(), Options{
|
|
Root: tr.install, Remote: "origin", RemoteURL: other, BinaryName: "none",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if snap.CheckError != "" {
|
|
t.Fatalf("check failed: %s", snap.CheckError)
|
|
}
|
|
if snap.RemoteURL != other || snap.Behind != 1 || len(snap.Incoming) != 1 || snap.Incoming[0].Subject != "other moves" {
|
|
t.Fatalf("the address must win over the remote name: %+v", snap)
|
|
}
|
|
}
|
|
|
|
func TestInvalidRemoteURLIsAReadableStateNotAnError(t *testing.T) {
|
|
requireGit(t)
|
|
tr := newTree(t)
|
|
snap, err := Status(context.Background(), Options{Root: tr.install, RemoteURL: "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)
|
|
}
|
|
}
|