This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
package gitx
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMatchRef(t *testing.T) {
|
||||
cases := []struct {
|
||||
pattern, ref string
|
||||
want bool
|
||||
}{
|
||||
{"refs/heads/*", "refs/heads/main", true},
|
||||
{"refs/heads/*", "refs/heads/feature/nested/deep", true}, // '*' spans '/'
|
||||
{"refs/heads/*", "refs/tags/v1", false},
|
||||
{"refs/heads/*", "refs/heads/", true},
|
||||
{"refs/heads/main", "refs/heads/main", true},
|
||||
{"refs/heads/main", "refs/heads/maint", false},
|
||||
{"refs/tags/v*", "refs/tags/v1.2.3", true},
|
||||
{"refs/tags/v*", "refs/tags/rc1", false},
|
||||
{"refs/heads/*-stable", "refs/heads/2.0-stable", true},
|
||||
{"refs/heads/*-stable", "refs/heads/2.0-beta", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := MatchRef(tc.pattern, tc.ref); got != tc.want {
|
||||
t.Errorf("MatchRef(%q, %q) = %v, want %v", tc.pattern, tc.ref, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRefsSkipsPeeledAndFilters(t *testing.T) {
|
||||
out := strings.Join([]string{
|
||||
"aaa\trefs/heads/main",
|
||||
"bbb\trefs/tags/v1",
|
||||
"ccc\trefs/tags/v1^{}", // peeled annotated tag
|
||||
"ddd\trefs/pull/7/head",
|
||||
"",
|
||||
}, "\n")
|
||||
|
||||
refs := parseRefs(out, "\t", true, []string{"refs/heads/*", "refs/tags/*"})
|
||||
|
||||
if len(refs) != 2 {
|
||||
t.Fatalf("got %d refs, want 2: %v", len(refs), refs)
|
||||
}
|
||||
if refs["refs/heads/main"] != "aaa" || refs["refs/tags/v1"] != "bbb" {
|
||||
t.Errorf("unexpected refs: %v", refs)
|
||||
}
|
||||
if _, ok := refs["refs/tags/v1^{}"]; ok {
|
||||
t.Error("peeled tag entry should be dropped")
|
||||
}
|
||||
if _, ok := refs["refs/pull/7/head"]; ok {
|
||||
t.Error("unmanaged ref should be filtered out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefsEqual(t *testing.T) {
|
||||
a := Refs{"refs/heads/main": "1", "refs/tags/v1": "2"}
|
||||
if !a.Equal(Refs{"refs/tags/v1": "2", "refs/heads/main": "1"}) {
|
||||
t.Error("same contents should compare equal regardless of order")
|
||||
}
|
||||
if a.Equal(Refs{"refs/heads/main": "1"}) {
|
||||
t.Error("different sizes should not compare equal")
|
||||
}
|
||||
if a.Equal(Refs{"refs/heads/main": "1", "refs/tags/v1": "9"}) {
|
||||
t.Error("different objects should not compare equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactURL(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"https://user:token@github.com/me/x.git", "https://user:redacted@github.com/me/x.git"},
|
||||
{"https://token@github.com/me/x.git", "https://token@github.com/me/x.git"},
|
||||
{"https://github.com/me/x.git", "https://github.com/me/x.git"},
|
||||
{"git@github.com:me/x.git", "git@github.com:me/x.git"},
|
||||
{"/srv/git/local.git", "/srv/git/local.git"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := RedactURL(tc.in); got != tc.want {
|
||||
t.Errorf("RedactURL(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLSecret(t *testing.T) {
|
||||
if got := URLSecret("https://x-access-token:ghp_abc@github.com/me/x.git"); got != "ghp_abc" {
|
||||
t.Errorf("URLSecret = %q, want ghp_abc", got)
|
||||
}
|
||||
if got := URLSecret("git@github.com:me/x.git"); got != "" {
|
||||
t.Errorf("URLSecret = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrubRemovesSecrets(t *testing.T) {
|
||||
got := scrub("fatal: auth failed for ghp_abc123", []string{"ghp_abc123"})
|
||||
if strings.Contains(got, "ghp_abc123") {
|
||||
t.Errorf("secret leaked: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShellQuote(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"ssh", "ssh"},
|
||||
{"/etc/keys/id_ed25519", "/etc/keys/id_ed25519"},
|
||||
{"/keys/my key", `'/keys/my key'`},
|
||||
{"it's", `'it'\''s'`},
|
||||
{"StrictHostKeyChecking=accept-new", "StrictHostKeyChecking=accept-new"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := shellQuote(tc.in); got != tc.want {
|
||||
t.Errorf("shellQuote(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A deploy key mounted from a secret store is often world-readable, which ssh
|
||||
// rejects outright. PrepareSSH must stage a 0600 copy instead of failing.
|
||||
func TestPrepareSSHStagesPermissiveKey(t *testing.T) {
|
||||
home, tmp := t.TempDir(), t.TempDir()
|
||||
key := filepath.Join(t.TempDir(), "deploy_key")
|
||||
if err := os.WriteFile(key, []byte("PRIVATE KEY"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd, err := PrepareSSH(SSH{KeyPath: key, StrictHostKey: "accept-new"}, home, tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(cmd, key) {
|
||||
t.Errorf("should use a staged copy, not the 0644 original: %s", cmd)
|
||||
}
|
||||
|
||||
staged := filepath.Join(tmp, "id_deploy_key")
|
||||
fi, err := os.Stat(staged)
|
||||
if err != nil {
|
||||
t.Fatalf("staged copy missing: %v", err)
|
||||
}
|
||||
if perm := fi.Mode().Perm(); perm != 0o600 {
|
||||
t.Errorf("staged key mode = %o, want 600", perm)
|
||||
}
|
||||
for _, want := range []string{"IdentitiesOnly=yes", "BatchMode=yes", "StrictHostKeyChecking=accept-new"} {
|
||||
if !strings.Contains(cmd, want) {
|
||||
t.Errorf("ssh command missing %q: %s", want, cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareSSHKeepsPrivateKeyPath(t *testing.T) {
|
||||
home, tmp := t.TempDir(), t.TempDir()
|
||||
key := filepath.Join(t.TempDir(), "deploy_key")
|
||||
if err := os.WriteFile(key, []byte("PRIVATE KEY"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd, err := PrepareSSH(SSH{KeyPath: key}, home, tmp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(cmd, key) {
|
||||
t.Errorf("an already-private key should be used in place: %s", cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareSSHCreatesKnownHosts(t *testing.T) {
|
||||
home, tmp := t.TempDir(), t.TempDir()
|
||||
|
||||
if _, err := PrepareSSH(SSH{}, home, tmp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(home, "known_hosts")); err != nil {
|
||||
t.Errorf("known_hosts not created in HOME: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironIsHermetic(t *testing.T) {
|
||||
t.Setenv("GIT_SSH_COMMAND", "ssh -i /attacker/key")
|
||||
t.Setenv("GIT_CONFIG_GLOBAL", "/attacker/gitconfig")
|
||||
t.Setenv("HTTPS_PROXY", "http://proxy.internal:3128")
|
||||
|
||||
env := environ(Options{Home: "/var/lib/syncbot/home", SSHCmd: "ssh -o BatchMode=yes"})
|
||||
|
||||
got := map[string]string{}
|
||||
for _, kv := range env {
|
||||
if k, v, ok := strings.Cut(kv, "="); ok {
|
||||
got[k] = v // later entries win, matching exec's behaviour
|
||||
}
|
||||
}
|
||||
|
||||
if got["GIT_SSH_COMMAND"] != "ssh -o BatchMode=yes" {
|
||||
t.Errorf("inherited GIT_SSH_COMMAND not overridden: %q", got["GIT_SSH_COMMAND"])
|
||||
}
|
||||
if got["GIT_CONFIG_GLOBAL"] != os.DevNull {
|
||||
t.Errorf("global git config not disabled: %q", got["GIT_CONFIG_GLOBAL"])
|
||||
}
|
||||
if got["HOME"] != "/var/lib/syncbot/home" {
|
||||
t.Errorf("HOME = %q", got["HOME"])
|
||||
}
|
||||
if got["GIT_TERMINAL_PROMPT"] != "0" {
|
||||
t.Error("git must never prompt for credentials")
|
||||
}
|
||||
if got["HTTPS_PROXY"] != "http://proxy.internal:3128" {
|
||||
t.Error("proxy settings should be inherited")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitedWriterCaps(t *testing.T) {
|
||||
var sb strings.Builder
|
||||
w := &limitedWriter{W: &sb, N: 10}
|
||||
|
||||
n, err := w.Write([]byte(strings.Repeat("x", 100)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 100 {
|
||||
t.Errorf("Write reported %d, want the full 100 so callers do not see a short write", n)
|
||||
}
|
||||
if sb.Len() != 10 {
|
||||
t.Errorf("captured %d bytes, want the 10-byte cap", sb.Len())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user