178 lines
7.0 KiB
Go
178 lines
7.0 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
package tentacle
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func repository_files() map[string]string {
|
|
return map[string]string{
|
|
"repository.toml": "format-version = 1\nid = 'example.tentacles'\nname = 'Example tentacles'\ndescription = 'Test catalog'\nmaintainer = 'Maintainer'\n",
|
|
"catalog.toml": "format-version = 1\n[[tentacles]]\nid = 'echo'\nmanifest = 'echo/tentacle.toml'\n",
|
|
"echo/tentacle.toml": "format-version = 1\nid = 'echo'\nname = 'Echo'\nversion = '0.1'\ndescription = 'An example'\ncapabilities = ['presentation']\n",
|
|
}
|
|
}
|
|
func read_repository_fixture(files map[string]string) (Repository, error) {
|
|
return read_repository("https://example.org/team/tentacles", strings.Repeat("a", 40), func(filename string) ([]byte, error) {
|
|
data, found := files[filename]
|
|
if !found {
|
|
return nil, os.ErrNotExist
|
|
}
|
|
return []byte(data), nil
|
|
})
|
|
}
|
|
func TestRepositoryMetadataDiscoveryValidation(t *testing.T) {
|
|
repository, err := read_repository_fixture(repository_files())
|
|
if err != nil || repository.ID != "example.tentacles" || len(repository.Tentacles) != 1 || repository.Tentacles[0].Name != "Echo" {
|
|
t.Fatalf("discovery: %+v %v", repository, err)
|
|
}
|
|
for _, problem := range []string{"missing root", "version", "missing manifest", "traversal", "nested", "mismatch", "duplicate", "oversized", "invalid id"} {
|
|
t.Run(problem, func(t *testing.T) {
|
|
files := repository_files()
|
|
switch problem {
|
|
case "missing root":
|
|
delete(files, "repository.toml")
|
|
case "version":
|
|
files["repository.toml"] = strings.Replace(files["repository.toml"], "format-version = 1", "format-version = 2", 1)
|
|
case "missing manifest":
|
|
delete(files, "echo/tentacle.toml")
|
|
case "traversal":
|
|
files["catalog.toml"] = strings.Replace(files["catalog.toml"], "echo/tentacle.toml", "../tentacle.toml", 1)
|
|
case "nested":
|
|
files["catalog.toml"] = strings.Replace(files["catalog.toml"], "echo/tentacle.toml", "a/b/tentacle.toml", 1)
|
|
case "mismatch":
|
|
files["echo/tentacle.toml"] = strings.Replace(files["echo/tentacle.toml"], "id = 'echo'", "id = 'different'", 1)
|
|
case "duplicate":
|
|
files["catalog.toml"] += "[[tentacles]]\nid='echo'\nmanifest='echo/tentacle.toml'\n"
|
|
case "oversized":
|
|
files["repository.toml"] += "#" + strings.Repeat("x", MaxFrameBytes)
|
|
case "invalid id":
|
|
files["echo/tentacle.toml"] = strings.Replace(files["echo/tentacle.toml"], "id = 'echo'", "id = 'Echo!'", 1)
|
|
}
|
|
if _, err := read_repository_fixture(files); err == nil {
|
|
t.Fatal("accepted invalid metadata")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestRepositoryURLAndLabel(t *testing.T) {
|
|
origin, err := RepositoryURL(" https://EXAMPLE.org/team/tentacles.git/ ")
|
|
if err != nil || origin != "https://example.org/team/tentacles.git" || RepositoryLabel(origin) != "team/tentacles" {
|
|
t.Fatalf("URL %q %v", origin, err)
|
|
}
|
|
for _, origin := range []string{"file:///tmp/source", "http://example.org/team/repo", "ssh://example.org/team/repo", "https://Username@example.org/team/repo", "https://example.org/", "https://example.org/team/../repo", "https://example.org/team/repo?ref=main", "https://example.org/team/repo#ref", "https://example.org/team/%00repo"} {
|
|
if _, err := RepositoryURL(origin); err == nil {
|
|
t.Fatalf("accepted %q", origin)
|
|
}
|
|
}
|
|
}
|
|
func TestRepositorySnapshotPersistenceAndIsolation(t *testing.T) {
|
|
repository, err := read_repository_fixture(repository_files())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
registry := &Registry{Path: filepath.Join(t.TempDir(), "tentacles.toml")}
|
|
entries, err := registry.Load()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := registry.SaveState(entries, []Repository{repository}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reopened := &Registry{Path: registry.Path}
|
|
if _, err := reopened.Load(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cached := reopened.Repositories()
|
|
if len(cached) != 1 || cached[0].Tentacles[0].Name != "Echo" {
|
|
t.Fatal(cached)
|
|
}
|
|
cached[0].Tentacles[0].Name = "mutated"
|
|
if reopened.Repositories()[0].Tentacles[0].Name != "Echo" {
|
|
t.Fatal("shared snapshot storage")
|
|
}
|
|
if err := reopened.Save(nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := registry.Load(); err != nil || len(registry.Repositories()) != 1 {
|
|
t.Fatalf("entry save dropped catalog: %v", err)
|
|
}
|
|
}
|
|
func TestRepositoryGitIgnoresInheritedConfigAndReadsBlobs(t *testing.T) {
|
|
directory := t.TempDir()
|
|
// A poisoned inherited config must not affect even Git initialization.
|
|
t.Setenv("GIT_CONFIG_COUNT", "1")
|
|
t.Setenv("GIT_CONFIG_KEY_0", "core.bare")
|
|
t.Setenv("GIT_CONFIG_VALUE_0", "invalid")
|
|
ctx := context.Background()
|
|
if _, err := repository_git(ctx, directory, "init", "--bare", "--template="); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
source := t.TempDir()
|
|
for name, data := range repository_files() {
|
|
filename := filepath.Join(source, name)
|
|
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filename, []byte(data), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// Populate a local test snapshot without enabling file transport in the
|
|
// production fetch path or contacting a real repository.
|
|
run := func(args ...string) string {
|
|
command := exec.Command("git", args...)
|
|
command.Dir = source
|
|
command.Env = []string{"PATH=" + os.Getenv("PATH"), "HOME=" + source, "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_AUTHOR_NAME=Maintainer", "GIT_AUTHOR_EMAIL=maintainer@example.invalid", "GIT_COMMITTER_NAME=Maintainer", "GIT_COMMITTER_EMAIL=maintainer@example.invalid"}
|
|
output, err := command.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("git: %s %v", output, err)
|
|
}
|
|
return strings.TrimSpace(string(output))
|
|
}
|
|
run("init", "--template=")
|
|
run("add", ".")
|
|
run("commit", "-m", "Fixture")
|
|
setHead := func() {
|
|
t.Helper()
|
|
if err := os.WriteFile(filepath.Join(source, ".git", "FETCH_HEAD"), []byte(run("rev-parse", "HEAD")+"\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
setHead()
|
|
repository, err := read_git_repository(ctx, source, "https://example.org/team/tentacles")
|
|
if err != nil || len(repository.Tentacles) != 1 {
|
|
t.Fatalf("Git discovery: %+v %v", repository, err)
|
|
}
|
|
revision := run("rev-parse", "HEAD")
|
|
data, err := repository_git(ctx, source, "show", revision+":repository.toml")
|
|
if err != nil || !strings.Contains(string(data), "example.tentacles") {
|
|
t.Fatalf("read blob: %s %v", data, err)
|
|
}
|
|
// A regular-looking root symlink must be rejected rather than interpreted
|
|
// as metadata, including when it references an otherwise valid file.
|
|
if err := os.Remove(filepath.Join(source, "repository.toml")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink("echo/tentacle.toml", filepath.Join(source, "repository.toml")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
run("add", ".")
|
|
run("commit", "-m", "Symlink fixture")
|
|
setHead()
|
|
if _, err := read_git_repository(ctx, source, "https://example.org/team/tentacles"); err == nil {
|
|
t.Fatal("accepted symlink metadata")
|
|
}
|
|
cancelled, cancel := context.WithCancel(ctx)
|
|
cancel()
|
|
if _, err := repository_git(cancelled, source, "status"); err == nil {
|
|
t.Fatal("ignored cancellation")
|
|
}
|
|
}
|