519 lines
20 KiB
Go
519 lines
20 KiB
Go
package apt_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"proxyctl/internal/apt"
|
|
"proxyctl/internal/profile"
|
|
"proxyctl/internal/target"
|
|
"proxyctl/internal/target/targettest"
|
|
)
|
|
|
|
func TestAvailabilityRequiresDebianMarkerAPTToolAndConfigurationDirectory(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
marker, tool, dir, dumpWorks bool
|
|
want bool
|
|
}{
|
|
{"detected", true, true, true, true, true},
|
|
{"not Debian", false, true, true, true, false},
|
|
{"apt-config absent", true, false, true, true, false},
|
|
{"configuration absent", true, true, false, true, false},
|
|
{"apt-config inspection fails", true, true, true, false, false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := t.TempDir()
|
|
configDir := filepath.Join(root, "etc", "apt", "apt.conf.d")
|
|
if tc.dir {
|
|
mustMkdir(t, configDir)
|
|
}
|
|
if tc.marker {
|
|
mustMkdir(t, filepath.Dir(filepath.Join(root, "etc", "debian_version")))
|
|
mustWrite(t, filepath.Join(root, "etc", "debian_version"), "12\n")
|
|
}
|
|
runner := &fakeRunner{available: tc.tool, outputError: !tc.dumpWorks}
|
|
a := apt.New(apt.Configuration{DebianVersionFile: filepath.Join(root, "etc", "debian_version"), ConfigDirectory: configDir, TrustedRoot: root}, nil, runner)
|
|
got := a.Availability(context.Background())
|
|
if got.Available != tc.want || got.Evidence == "" {
|
|
t.Fatalf("availability = %#v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDiagnosticChecksCoverCommandSchemaAndAuthorityWithoutMutation(t *testing.T) {
|
|
a, _ := newAdapter(t, nil)
|
|
checks := a.DiagnosticChecks(context.Background(), compatibleProfile())
|
|
assertDiagnosticNames(t, checks, "required command", "configuration schema", "write authority")
|
|
if _, err := os.Stat(a.OverlayPath()); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("diagnostics created APT overlay: %v", err)
|
|
}
|
|
}
|
|
|
|
func assertDiagnosticNames(t *testing.T, checks []target.Check, names ...string) {
|
|
t.Helper()
|
|
for _, name := range names {
|
|
found := false
|
|
for _, check := range checks {
|
|
if check.Name == name {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("diagnostics missing %q: %#v", name, checks)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDescriptionExplainsEnvironmentPersistenceAndPrivilegeBoundary(t *testing.T) {
|
|
a, _ := newAdapter(t, nil)
|
|
got := a.Description()
|
|
combined := got.Summary + " " + got.Details
|
|
for _, phrase := range []string{"environment variables", "persistent", "privilege"} {
|
|
if !strings.Contains(strings.ToLower(combined), strings.ToLower(phrase)) {
|
|
t.Errorf("description does not mention %q: %#v", phrase, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAPTAdapterRunsReusableTargetContract(t *testing.T) {
|
|
targettest.Run(t, func(t *testing.T) targettest.Fixture {
|
|
a, runner := newAdapter(t, nil)
|
|
return targettest.Fixture{
|
|
Target: a, Active: compatibleProfile(), Secret: "secret",
|
|
MakeExternal: func(t *testing.T) { runner.external = `Acquire::http::Proxy "http://prior.example:3128";` },
|
|
MakeDrifted: func(t *testing.T) {
|
|
contents := readFile(t, a.OverlayPath())
|
|
mustWrite(t, a.OverlayPath(), strings.Replace(contents, "http://alice:secret@proxy.example:8080", "http://changed.example:8080", 1))
|
|
},
|
|
PermissionFailure: func(t *testing.T) target.OperationResult {
|
|
failed, _ := newFailingAdapter(t, &failingFS{permission: true})
|
|
return failed.SetDesired(context.Background(), true, compatibleProfile())
|
|
},
|
|
PartialFailure: func(t *testing.T) target.OperationResult {
|
|
failed, failedRunner := newAdapter(t, nil)
|
|
failedRunner.failDumpAfterWrite = failed.OverlayPath()
|
|
return failed.SetDesired(context.Background(), true, compatibleProfile())
|
|
},
|
|
RedactionFailure: func(t *testing.T) target.OperationResult {
|
|
failed, _ := newFailingAdapter(t, &failingFS{writeError: errors.New("write http://alice:secret@proxy.example:8080 failed")})
|
|
return failed.SetDesired(context.Background(), true, compatibleProfile())
|
|
},
|
|
RecoveryRetry: func(t *testing.T) (target.OperationResult, target.OperationResult, target.OperationResult) {
|
|
files := &failingFS{failOnce: true}
|
|
failed, runner := newFailingAdapter(t, files)
|
|
runner.external = `Acquire::http::Proxy "http://prior.example:3128";`
|
|
first := failed.SetDesired(context.Background(), true, compatibleProfile())
|
|
retry := failed.SetDesired(context.Background(), true, compatibleProfile())
|
|
disabled := failed.SetDesired(context.Background(), false, nil)
|
|
return first, retry, disabled
|
|
},
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAPTConfigurationKeysAndHostOverridesAreCaseInsensitive(t *testing.T) {
|
|
a, runner := newAdapter(t, nil)
|
|
runner.external = "aCqUiRe::HtTp::pRoXy \"http://prior.example:3128\";\n" +
|
|
"ACQUIRE::HTTP::PROXY::SPECIAL.EXAMPLE \"http://special.example:3128\";"
|
|
if got := a.Inspect(context.Background(), compatibleProfile()); got.State != target.External {
|
|
t.Fatalf("mixed-case external inspection = %#v", got)
|
|
}
|
|
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil || got.Inspection.State != target.Enabled {
|
|
t.Fatalf("enable = %#v", got)
|
|
}
|
|
managed := readFile(t, a.OverlayPath())
|
|
if !strings.Contains(managed, `Acquire::http::Proxy::special.example "http://alice:secret@proxy.example:8080";`) {
|
|
t.Fatalf("mixed-case host override was not masked case-insensitively:\n%s", managed)
|
|
}
|
|
|
|
mustWrite(t, a.OverlayPath(), strings.ReplaceAll(managed, "Acquire::http::Proxy", "ACQUIRE::HTTP::PROXY"))
|
|
if got := a.Inspect(context.Background(), compatibleProfile()); got.State != target.Enabled {
|
|
t.Fatalf("mixed-case managed inspection = %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestMissingProtocolWritesDIRECTAndDisableRestoresUnderlyingConfiguration(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, configuredKey, configuredURL, missingKey, priorURL string
|
|
}{
|
|
{"missing HTTPS", "Acquire::http::Proxy", "http://proxy.example:8080", "Acquire::https::Proxy", "https://prior.example:8443"},
|
|
{"missing HTTP", "Acquire::https::Proxy", "https://proxy.example:8443", "Acquire::http::Proxy", "http://prior.example:8080"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
a, runner := newAdapter(t, nil)
|
|
runner.external = tc.missingKey + ` "` + tc.priorURL + `";`
|
|
p := &profile.Profile{Name: "partial", Mode: profile.AdvancedMode}
|
|
if strings.Contains(tc.configuredKey, "http::") {
|
|
p.HTTPURL = tc.configuredURL
|
|
} else {
|
|
p.HTTPSURL = tc.configuredURL
|
|
}
|
|
|
|
if got := a.SetDesired(context.Background(), true, p); got.Err != nil || got.Inspection.State != target.Enabled {
|
|
t.Fatalf("enable = %#v", got)
|
|
}
|
|
managed := readFile(t, a.OverlayPath())
|
|
if !strings.Contains(managed, tc.configuredKey+` "`+tc.configuredURL+`";`) ||
|
|
!strings.Contains(managed, tc.missingKey+` "DIRECT";`) {
|
|
t.Fatalf("overlay does not explicitly mask missing protocol:\n%s", managed)
|
|
}
|
|
if got := a.SetDesired(context.Background(), false, nil); got.Err != nil || got.Inspection.State != target.External {
|
|
t.Fatalf("disable = %#v", got)
|
|
}
|
|
if !strings.Contains(runner.external, tc.priorURL) {
|
|
t.Fatalf("underlying configuration changed: %q", runner.external)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOwnedOverlayWinsAndDisableRevealsPriorEffectiveConfiguration(t *testing.T) {
|
|
a, runner := newAdapter(t, nil)
|
|
runner.external = "Acquire::http::Proxy \"http://prior.example:3128\";\n" +
|
|
"Acquire::http::Proxy::special.example \"http://special.example:3128\";"
|
|
if got := a.Inspect(context.Background(), compatibleProfile()); got.State != target.External || !strings.Contains(got.Evidence, "Acquire::http::Proxy") {
|
|
t.Fatalf("external inspection = %#v", got)
|
|
}
|
|
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil || got.Inspection.State != target.Enabled {
|
|
t.Fatalf("enable = %#v", got)
|
|
}
|
|
managed := readFile(t, a.OverlayPath())
|
|
for _, expected := range []string{
|
|
`Acquire::http::Proxy "http://alice:secret@proxy.example:8080";`,
|
|
`Acquire::https::Proxy "https://proxy.example:8443";`,
|
|
`Acquire::http::Proxy::localhost "DIRECT";`,
|
|
`Acquire::https::Proxy::10.0.0.1 "DIRECT";`,
|
|
`Acquire::http::Proxy::special.example "http://alice:secret@proxy.example:8080";`,
|
|
} {
|
|
if !strings.Contains(managed, expected) {
|
|
t.Errorf("overlay missing %q:\n%s", expected, managed)
|
|
}
|
|
}
|
|
if got := a.SetDesired(context.Background(), false, nil); got.Err != nil || got.Inspection.State != target.External {
|
|
t.Fatalf("disable = %#v", got)
|
|
}
|
|
if _, err := os.Stat(a.OverlayPath()); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("managed overlay remains: %v", err)
|
|
}
|
|
if !strings.Contains(runner.external, `Acquire::http::Proxy "http://prior.example:3128";`) || !strings.Contains(runner.external, `Acquire::http::Proxy::special.example "http://special.example:3128";`) {
|
|
t.Fatal("unrelated APT configuration changed")
|
|
}
|
|
if got := a.SetDesired(context.Background(), false, nil); got.Err != nil || got.Inspection.State != target.External {
|
|
t.Fatalf("idempotent disable = %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestOverlayNameCollisionNeverRewritesOrRemovesUnownedConfiguration(t *testing.T) {
|
|
a, _ := newAdapter(t, nil)
|
|
original := `Acquire::http::Proxy "http://user-owned.example:3128";` + "\n"
|
|
mustWrite(t, a.OverlayPath(), original)
|
|
if got := a.Inspect(context.Background(), compatibleProfile()); got.State != target.External {
|
|
t.Fatalf("unowned collision inspection = %#v", got)
|
|
}
|
|
got := a.SetDesired(context.Background(), true, compatibleProfile())
|
|
if got.Err == nil || !strings.Contains(got.Err.Error(), "not owned") || readFile(t, a.OverlayPath()) != original {
|
|
t.Fatalf("enable over collision = %#v, file %q", got, readFile(t, a.OverlayPath()))
|
|
}
|
|
if got := a.SetDesired(context.Background(), false, nil); got.Err != nil || got.Inspection.State != target.External || readFile(t, a.OverlayPath()) != original {
|
|
t.Fatalf("disable over collision = %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestDefaultOverlayNameSortsAfterCommonAPTFragments(t *testing.T) {
|
|
name := filepath.Base(apt.New(apt.Configuration{}, nil, nil).OverlayPath())
|
|
if filepath.Ext(name) != ".conf" {
|
|
t.Fatalf("default overlay name %q has no .conf extension; stricter APT versions ignore extensionless fragments", name)
|
|
}
|
|
for _, fragment := range []string{"proxy.conf", "99update-notifier", "01-vendor-ubuntu"} {
|
|
if strings.Compare(name, fragment) <= 0 {
|
|
t.Fatalf("default overlay name %q does not sort after common fragment %q; a later fragment shadows the overlay and enable can never become effective", name, fragment)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOwnedLegacyOverlayIsRemovedOnEnableAndDisable(t *testing.T) {
|
|
legacyContents := "// Generated by proxyctl. Do not edit.\nAcquire::http::Proxy \"http://stale.example:3128\";\n"
|
|
|
|
t.Run("enable", func(t *testing.T) {
|
|
a, _ := newAdapter(t, nil)
|
|
legacy := filepath.Join(filepath.Dir(a.OverlayPath()), "99proxyctl")
|
|
mustWrite(t, legacy, legacyContents)
|
|
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil || got.Inspection.State != target.Enabled {
|
|
t.Fatalf("enable = %#v", got)
|
|
}
|
|
if _, err := os.Stat(legacy); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("owned legacy overlay remains after enable: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("disable", func(t *testing.T) {
|
|
a, _ := newAdapter(t, nil)
|
|
legacy := filepath.Join(filepath.Dir(a.OverlayPath()), "99proxyctl")
|
|
mustWrite(t, legacy, legacyContents)
|
|
if got := a.SetDesired(context.Background(), false, nil); got.Err != nil {
|
|
t.Fatalf("disable = %#v", got)
|
|
}
|
|
if _, err := os.Stat(legacy); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("owned legacy overlay remains after disable: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("unowned legacy is untouched", func(t *testing.T) {
|
|
a, _ := newAdapter(t, nil)
|
|
legacy := filepath.Join(filepath.Dir(a.OverlayPath()), "99proxyctl")
|
|
original := "Acquire::http::Proxy \"http://user-owned.example:3128\";\n"
|
|
mustWrite(t, legacy, original)
|
|
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil || got.Inspection.State != target.Enabled {
|
|
t.Fatalf("enable = %#v", got)
|
|
}
|
|
if got := readFile(t, legacy); got != original {
|
|
t.Fatalf("unowned legacy overlay changed: %q", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDisablePermissionFailureReportsActualManagedState(t *testing.T) {
|
|
files := &failingFS{}
|
|
a, _ := newFailingAdapter(t, files)
|
|
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil {
|
|
t.Fatalf("enable = %#v", got)
|
|
}
|
|
files.removePermission = true
|
|
got := a.SetDesired(context.Background(), false, nil)
|
|
if got.Err == nil || !errors.Is(got.Err, os.ErrPermission) || got.Inspection.State != target.Drifted {
|
|
t.Fatalf("permission-denied disable = %#v", got)
|
|
}
|
|
if _, err := os.Stat(a.OverlayPath()); err != nil {
|
|
t.Fatalf("managed overlay should remain after denied removal: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEnableReturnsFailureWhenManagedValuesDoNotBecomeEffective(t *testing.T) {
|
|
a, runner := newAdapter(t, nil)
|
|
runner.override = `Acquire::http::Proxy "http://later.example:8080";`
|
|
got := a.SetDesired(context.Background(), true, compatibleProfile())
|
|
if got.Err == nil || got.Inspection.State != target.Drifted {
|
|
t.Fatalf("ineffective enable = %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestInspectionUsesEffectiveAPTConfiguration(t *testing.T) {
|
|
a, runner := newAdapter(t, nil)
|
|
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil {
|
|
t.Fatal(got.Err)
|
|
}
|
|
runner.override = `Acquire::http::Proxy "http://later.example:8080";`
|
|
if got := a.Inspect(context.Background(), compatibleProfile()); got.State != target.Drifted || !strings.Contains(got.Evidence, "effective") {
|
|
t.Fatalf("higher-precedence effective config = %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestIncompatibleProtocolOrBypassPreventsMutationAndNamesValue(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
mutate func(*profile.Profile)
|
|
value string
|
|
}{
|
|
{"socks5 local DNS", func(p *profile.Profile) { p.HTTPURL = "socks5://unsupported.example:1080" }, "socks5://unsupported.example:1080"},
|
|
{"incompatible ALL fallback", func(p *profile.Profile) { p.HTTPURL = ""; p.AllURL = "socks5://all.example:1080" }, "socks5://all.example:1080"},
|
|
{"domain suffix", func(p *profile.Profile) { p.Bypass = []string{".example.com"} }, ".example.com"},
|
|
{"CIDR", func(p *profile.Profile) { p.Bypass = []string{"10.0.0.0/8"} }, "10.0.0.0/8"},
|
|
{"IPv6", func(p *profile.Profile) { p.Bypass = []string{"2001:db8::1"} }, "2001:db8::1"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
a, _ := newAdapter(t, nil)
|
|
p := compatibleProfile()
|
|
tc.mutate(p)
|
|
got := a.SetDesired(context.Background(), true, p)
|
|
if got.Err == nil || !strings.Contains(got.Err.Error(), tc.value) || got.Inspection.State != target.Disabled {
|
|
t.Fatalf("result = %#v", got)
|
|
}
|
|
if _, err := os.Stat(a.OverlayPath()); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("incompatible Profile mutated overlay: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestManagedOverlayIsAtomicOwnerOnlyAndRejectsSymlinkVictims(t *testing.T) {
|
|
t.Run("owner-only atomic file", func(t *testing.T) {
|
|
a, _ := newAdapter(t, nil)
|
|
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err != nil {
|
|
t.Fatal(got.Err)
|
|
}
|
|
info, err := os.Stat(a.OverlayPath())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Mode().Perm() != 0o600 {
|
|
t.Fatalf("overlay mode = %o", info.Mode().Perm())
|
|
}
|
|
matches, _ := filepath.Glob(filepath.Join(filepath.Dir(a.OverlayPath()), ".proxyctl-*"))
|
|
if len(matches) != 0 {
|
|
t.Fatalf("atomic temporary files remain: %v", matches)
|
|
}
|
|
})
|
|
t.Run("final symlink", func(t *testing.T) {
|
|
a, _ := newAdapter(t, nil)
|
|
victim := filepath.Join(t.TempDir(), "victim")
|
|
mustWrite(t, victim, "unchanged\n")
|
|
if err := os.Symlink(victim, a.OverlayPath()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := a.SetDesired(context.Background(), true, compatibleProfile()); got.Err == nil {
|
|
t.Fatal("enable followed a symlinked overlay")
|
|
}
|
|
if got := readFile(t, victim); got != "unchanged\n" {
|
|
t.Fatalf("symlink victim changed: %q", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCompleteHTTPProfileIgnoresAdditionalSOCKSProxy(t *testing.T) {
|
|
a, _ := newAdapter(t, nil)
|
|
p := &profile.Profile{
|
|
Name: "dual", Mode: profile.AdvancedMode,
|
|
HTTPURL: "http://localhost:7890", HTTPSURL: "http://localhost:7890", AllURL: "socks5://localhost:7890",
|
|
}
|
|
got := a.SetDesired(context.Background(), true, p)
|
|
if got.Err != nil || got.Inspection.State != target.Enabled {
|
|
t.Fatalf("enable = %#v", got)
|
|
}
|
|
contents := readFile(t, a.OverlayPath())
|
|
if strings.Count(contents, `"http://localhost:7890"`) != 2 || strings.Contains(contents, "socks5") {
|
|
t.Fatalf("dual-protocol translation:\n%s", contents)
|
|
}
|
|
}
|
|
|
|
func TestAdvancedALLValueSuppliesMissingAPTProtocols(t *testing.T) {
|
|
a, _ := newAdapter(t, nil)
|
|
p := &profile.Profile{Name: "all", Mode: profile.AdvancedMode, AllURL: "socks5h://proxy.example:1080"}
|
|
if got := a.SetDesired(context.Background(), true, p); got.Err != nil || got.Inspection.State != target.Enabled {
|
|
t.Fatalf("enable = %#v", got)
|
|
}
|
|
contents := readFile(t, a.OverlayPath())
|
|
if strings.Count(contents, `"socks5h://proxy.example:1080"`) != 2 {
|
|
t.Fatalf("ALL translation did not supply both APT protocols:\n%s", contents)
|
|
}
|
|
}
|
|
|
|
func TestSingleModeUsesOneSupportedValueWithoutTreatingExpansionAsALL(t *testing.T) {
|
|
a, _ := newAdapter(t, nil)
|
|
p := &profile.Profile{Name: "single", Mode: profile.SingleMode, SingleURL: "socks5h://proxy.example:1080", Bypass: []string{"localhost"}}
|
|
if got := a.SetDesired(context.Background(), true, p); got.Err != nil || got.Inspection.State != target.Enabled {
|
|
t.Fatalf("enable = %#v", got)
|
|
}
|
|
contents := readFile(t, a.OverlayPath())
|
|
if !strings.Contains(contents, `Acquire::http::Proxy "socks5h://proxy.example:1080";`) || !strings.Contains(contents, `Acquire::https::Proxy "socks5h://proxy.example:1080";`) {
|
|
t.Fatalf("single translation:\n%s", contents)
|
|
}
|
|
}
|
|
|
|
func compatibleProfile() *profile.Profile {
|
|
return &profile.Profile{Name: "office", Mode: profile.AdvancedMode, HTTPURL: "http://alice:secret@proxy.example:8080", HTTPSURL: "https://proxy.example:8443", Bypass: []string{"localhost", "10.0.0.1"}}
|
|
}
|
|
|
|
func newAdapter(t *testing.T, files apt.FileSystem) (*apt.Adapter, *fakeRunner) {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
dir := filepath.Join(root, "etc", "apt", "apt.conf.d")
|
|
mustMkdir(t, dir)
|
|
mustWrite(t, filepath.Join(root, "etc", "debian_version"), "12\n")
|
|
runner := &fakeRunner{available: true}
|
|
if failure, ok := files.(*failingFS); ok {
|
|
failure.delegate = apt.NewOSFileSystem(root)
|
|
}
|
|
a := apt.New(apt.Configuration{DebianVersionFile: filepath.Join(root, "etc", "debian_version"), ConfigDirectory: dir, TrustedRoot: root}, files, runner)
|
|
runner.overlay = a.OverlayPath()
|
|
return a, runner
|
|
}
|
|
|
|
func newFailingAdapter(t *testing.T, files *failingFS) (*apt.Adapter, *fakeRunner) {
|
|
return newAdapter(t, files)
|
|
}
|
|
|
|
type fakeRunner struct {
|
|
available, outputError bool
|
|
overlay, external, override, failDumpAfterWrite string
|
|
}
|
|
|
|
func (r *fakeRunner) LookPath(string) (string, error) {
|
|
if !r.available {
|
|
return "", os.ErrNotExist
|
|
}
|
|
return "/usr/bin/apt-config", nil
|
|
}
|
|
func (r *fakeRunner) Output(context.Context, string, ...string) ([]byte, error) {
|
|
if r.outputError {
|
|
return nil, errors.New("apt-config dump failed")
|
|
}
|
|
if r.failDumpAfterWrite != "" {
|
|
if _, err := os.Stat(r.failDumpAfterWrite); err == nil {
|
|
return nil, errors.New("apt-config dump failed")
|
|
}
|
|
}
|
|
if r.override != "" {
|
|
return []byte(r.override + "\n"), nil
|
|
}
|
|
if r.overlay != "" {
|
|
if data, err := os.ReadFile(r.overlay); err == nil {
|
|
return []byte(r.external + "\n" + string(data)), nil
|
|
}
|
|
}
|
|
return []byte(r.external + "\n"), nil
|
|
}
|
|
|
|
type failingFS struct {
|
|
delegate apt.FileSystem
|
|
permission, failOnce, removePermission bool
|
|
writeError error
|
|
}
|
|
|
|
func (f *failingFS) ReadFile(path string) ([]byte, error) { return f.delegate.ReadFile(path) }
|
|
func (f *failingFS) AtomicWrite(path string, data []byte, mode os.FileMode) error {
|
|
if f.permission {
|
|
return os.ErrPermission
|
|
}
|
|
if f.writeError != nil {
|
|
return f.writeError
|
|
}
|
|
if f.failOnce {
|
|
f.failOnce = false
|
|
return os.ErrPermission
|
|
}
|
|
return f.delegate.AtomicWrite(path, data, mode)
|
|
}
|
|
func (f *failingFS) Remove(path string) error {
|
|
if f.removePermission {
|
|
return os.ErrPermission
|
|
}
|
|
return f.delegate.Remove(path)
|
|
}
|
|
func mustMkdir(t *testing.T, path string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(path, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
func mustWrite(t *testing.T, path, value string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(path, []byte(value), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
func readFile(t *testing.T, path string) string {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(data)
|
|
}
|