Все проверки выполнены успешно
CI / test (push) Successful in 3m13s
Docker / Build and publish worker image (push) Successful in 10m35s
418 строки
12 KiB
Go
418 строки
12 KiB
Go
package harness
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
func TestEnabled(t *testing.T) {
|
|
// os.Unsetenv would leak into later tests in the same process;
|
|
// t.Setenv restores the original value after this test.
|
|
t.Setenv("RSMON_TEST_DOCKER", "")
|
|
if Enabled() {
|
|
t.Fatal("Enabled() true without RSMON_TEST_DOCKER")
|
|
}
|
|
for _, v := range []string{"1", "true", "TRUE", "yes", "on"} {
|
|
t.Setenv("RSMON_TEST_DOCKER", v)
|
|
if !Enabled() {
|
|
t.Fatalf("Enabled() false for RSMON_TEST_DOCKER=%q", v)
|
|
}
|
|
}
|
|
for _, v := range []string{"0", "false", "no", "off", " "} {
|
|
t.Setenv("RSMON_TEST_DOCKER", v)
|
|
if Enabled() {
|
|
t.Fatalf("Enabled() true for RSMON_TEST_DOCKER=%q", v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFixtureDefaults(t *testing.T) {
|
|
fixtures := Fixtures()
|
|
if len(fixtures) != 3 {
|
|
t.Fatalf("Fixtures() = %d, want 3", len(fixtures))
|
|
}
|
|
byName := map[string]Fixture{}
|
|
for _, f := range fixtures {
|
|
byName[f.Name] = f
|
|
}
|
|
if f := byName["alpine"]; f.ImageRef() != "reg.rsxx.ru/library/alpine:3" || f.Distro != "alpine" {
|
|
t.Fatalf("alpine fixture wrong: %+v", f)
|
|
}
|
|
if f := byName["ubuntu"]; f.ImageRef() != "ubuntu:24.04" || f.Distro != "ubuntu" {
|
|
t.Fatalf("ubuntu fixture wrong: %+v", f)
|
|
}
|
|
if f := byName["arch"]; f.ImageRef() != "archlinux:latest" || f.Distro != "arch" {
|
|
t.Fatalf("arch fixture wrong: %+v", f)
|
|
}
|
|
if f := byName["alpine"]; f.UserOrDefault() != "root" {
|
|
t.Fatalf("default user = %q, want root", f.UserOrDefault())
|
|
}
|
|
}
|
|
|
|
func TestFixtureImageEnvOverride(t *testing.T) {
|
|
f := Fixture{Name: "ubuntu"}
|
|
t.Setenv("RSMON_TEST_IMAGE_UBUNTU", "reg.rsxx.ru/library/ubuntu:24.04")
|
|
if got := f.ImageRef(); got != "reg.rsxx.ru/library/ubuntu:24.04" {
|
|
t.Fatalf("env override not applied: %q", got)
|
|
}
|
|
t.Setenv("RSMON_TEST_IMAGE_UBUNTU", "")
|
|
if got := f.ImageRef(); got != "ubuntu:24.04" {
|
|
t.Fatalf("default image changed after env cleared: %q", got)
|
|
}
|
|
|
|
override := Fixture{Name: "arch", Image: "archlinux:2026.01.01"}
|
|
if got := override.ImageRef(); got != "archlinux:2026.01.01" {
|
|
t.Fatalf("fixture image override not applied: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestFixturesHaveDockerfiles(t *testing.T) {
|
|
for _, f := range Fixtures() {
|
|
path := f.DockerfilePath()
|
|
if _, err := os.Stat(path); err != nil {
|
|
t.Fatalf("fixture %s missing Dockerfile at %s: %v", f.Name, path, err)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(data), "FROM ") {
|
|
t.Fatalf("fixture %s Dockerfile has no FROM", f.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewValidatesName(t *testing.T) {
|
|
for _, bad := range []string{"", "with space", "UPPER", "semi;colon", "back`tick", strings.Repeat("a", 65)} {
|
|
if _, err := New(bad, Fixtures()[0]); err == nil {
|
|
t.Fatalf("New(%q) succeeded", bad)
|
|
}
|
|
}
|
|
h, err := New("fixture-alpine", Fixtures()[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if h.ContainerName() == "" || h.NetworkName() == "" {
|
|
t.Fatalf("empty container/network names: %+v", h)
|
|
}
|
|
if h.Addr() != "" {
|
|
t.Fatalf("Addr() = %q before Start, want empty", h.Addr())
|
|
}
|
|
}
|
|
|
|
func TestStartDisabled(t *testing.T) {
|
|
t.Setenv("RSMON_TEST_DOCKER", "")
|
|
h, err := New("unit-start", Fixtures()[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.Start(context.Background()); err == nil {
|
|
t.Fatal("Start succeeded with integration disabled")
|
|
} else if !strings.Contains(err.Error(), "RSMON_TEST_DOCKER") {
|
|
t.Fatalf("disabled Start error = %v, want opt-in hint", err)
|
|
}
|
|
}
|
|
|
|
func TestDialBeforeStart(t *testing.T) {
|
|
h, err := New("unit-dial", Fixtures()[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := h.Dial(); err == nil {
|
|
t.Fatal("Dial succeeded before Start")
|
|
}
|
|
}
|
|
|
|
func TestParsePublishedPort(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
in string
|
|
want int
|
|
}{
|
|
{in: "127.0.0.1:49153\n", want: 49153},
|
|
{in: "0.0.0.0:2222", want: 2222},
|
|
{in: "127.0.0.1:0\n", want: 0},
|
|
{in: "::1:32768\n", want: 32768},
|
|
{in: "127.0.0.1:notaport\n", want: 0},
|
|
{in: "", want: 0},
|
|
} {
|
|
got, err := parsePublishedPort(tc.in)
|
|
if tc.want == 0 {
|
|
if err == nil {
|
|
t.Fatalf("parsePublishedPort(%q) succeeded with %d", tc.in, got)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil || got != tc.want {
|
|
t.Fatalf("parsePublishedPort(%q) = %d, %v; want %d", tc.in, got, err, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestWaitForPort exercises the readiness probe against a real local
|
|
// listener so the polling loop is covered without Docker.
|
|
func TestWaitForPort(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ln.Close() //nolint:errcheck
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := waitForPort(ctx, ln.Addr().String(), 2*time.Second); err != nil {
|
|
t.Fatalf("waitForPort on open listener: %v", err)
|
|
}
|
|
|
|
// A port that never opens must time out.
|
|
closed, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
addr := closed.Addr().String()
|
|
closed.Close() //nolint:errcheck
|
|
|
|
short, shortCancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer shortCancel()
|
|
if err := waitForPort(short, addr, 1500*time.Millisecond); err == nil {
|
|
t.Fatal("waitForPort on closed port succeeded")
|
|
}
|
|
}
|
|
|
|
func TestWriteKnownHostsRoundTrip(t *testing.T) {
|
|
// Parsing a known_hosts entry requires a real key; use the bundled
|
|
// public key so the format is exercised.
|
|
raw, err := os.ReadFile(filepath.Join("testdata", "keys", "rsmon_test_ed25519.pub"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key, _, _, _, err := ssh.ParseAuthorizedKey(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path, err := writeKnownHosts(t.TempDir(), "127.0.0.1:49153", key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(data), "[127.0.0.1]:49153") {
|
|
t.Fatalf("known_hosts entry %q missing bracketed address", data)
|
|
}
|
|
}
|
|
|
|
func TestValidateName(t *testing.T) {
|
|
for _, ok := range []string{"alpine", "fixture-1", "a", strings.Repeat("x", 64)} {
|
|
if err := validateName(ok); err != nil {
|
|
t.Fatalf("validateName(%q): %v", ok, err)
|
|
}
|
|
}
|
|
for _, bad := range []string{"", "A", "a b", "a/b", strings.Repeat("x", 65)} {
|
|
if err := validateName(bad); err == nil {
|
|
t.Fatalf("validateName(%q) succeeded", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSetDockerBin(t *testing.T) {
|
|
SetDockerBin("docker")
|
|
SetDockerBin("")
|
|
if dockerBin != "docker" {
|
|
t.Fatalf("dockerBin = %q after reset", dockerBin)
|
|
}
|
|
SetDockerBin("/stub/docker")
|
|
if dockerBin != "/stub/docker" {
|
|
t.Fatalf("dockerBin = %q after set", dockerBin)
|
|
}
|
|
SetDockerBin("")
|
|
}
|
|
|
|
func TestDockerDNSOverride(t *testing.T) {
|
|
t.Setenv("RSMON_TEST_DOCKER_DNS", "")
|
|
if got := dockerDNS(); len(got) != 0 {
|
|
t.Fatalf("dockerDNS() with empty env = %v, want none", got)
|
|
}
|
|
t.Setenv("RSMON_TEST_DOCKER_DNS", "8.8.8.8, 1.1.1.1")
|
|
if got := dockerDNS(); len(got) != 4 || got[0] != "--dns" || got[1] != "8.8.8.8" || got[3] != "1.1.1.1" {
|
|
t.Fatalf("dockerDNS() = %v", got)
|
|
}
|
|
t.Setenv("RSMON_TEST_DOCKER_DNS", " ,,")
|
|
if got := dockerDNS(); len(got) != 0 {
|
|
t.Fatalf("dockerDNS() with blank entries = %v", got)
|
|
}
|
|
}
|
|
|
|
// writeStubDocker installs a fake docker binary that records its argv to
|
|
// logPath and returns the recorded path. The stub succeeds for build,
|
|
// network, run, and teardown calls; `port` fails so Start fails after
|
|
// the container and network exist. Setting STUB_DOCKER_FAIL_BUILD=1 makes
|
|
// the build step fail instead. Real Docker is never touched.
|
|
func writeStubDocker(t *testing.T, logPath string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
stub := filepath.Join(dir, "docker")
|
|
script := `#!/bin/sh
|
|
echo "$*" >> "$STUB_DOCKER_LOG"
|
|
case "$1" in
|
|
build)
|
|
if [ "${STUB_DOCKER_FAIL_BUILD:-0}" = "1" ]; then
|
|
echo "stub build failure" >&2
|
|
exit 1
|
|
fi
|
|
echo "sha256:stub-image-id"
|
|
;;
|
|
network) echo "stub-network-id" ;;
|
|
run) echo "stub-container-id" ;;
|
|
port) echo "stub port failure" >&2; exit 1 ;;
|
|
rm|-r|image|ps) exit 0 ;;
|
|
*) exit 0 ;;
|
|
esac
|
|
`
|
|
if err := os.WriteFile(stub, []byte(script), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("STUB_DOCKER_LOG", logPath)
|
|
SetDockerBin(stub)
|
|
t.Cleanup(func() { SetDockerBin("") })
|
|
return stub
|
|
}
|
|
|
|
func readDockerCalls(t *testing.T, logPath string) string {
|
|
t.Helper()
|
|
calls, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(calls)
|
|
}
|
|
|
|
// TestStartFailureCleansUpResources drives Start to failure after the
|
|
// container and network were created (the stub `docker port` fails) and
|
|
// asserts the failure path removed every resource: container, network,
|
|
// fixture image, and no temp known_hosts dir leaked.
|
|
func TestStartFailureCleansUpResources(t *testing.T) {
|
|
t.Setenv("RSMON_TEST_DOCKER", "1")
|
|
logPath := filepath.Join(t.TempDir(), "docker-calls.log")
|
|
writeStubDocker(t, logPath)
|
|
|
|
h, err := New("fail-cleanup", Fixtures()[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.Start(context.Background()); err == nil {
|
|
t.Fatal("Start succeeded despite stub docker port failure")
|
|
}
|
|
calls := readDockerCalls(t, logPath)
|
|
for _, want := range []string{"build", "network create", "run -d", "rm -f", "network rm", "image rm"} {
|
|
if !strings.Contains(calls, want) {
|
|
t.Fatalf("failed-start cleanup missing docker call %q; calls:\n%s", want, calls)
|
|
}
|
|
}
|
|
if h.KnownHostsPath() != "" {
|
|
t.Fatalf("known_hosts path set after failed start: %q", h.KnownHostsPath())
|
|
}
|
|
}
|
|
|
|
// TestStartBuildFailureIsSafe drives Start to fail at the very first
|
|
// step (build) and asserts teardown stays idempotent and harmless: no
|
|
// container or network was ever created, and the failure path leaves
|
|
// nothing behind.
|
|
func TestStartBuildFailureIsSafe(t *testing.T) {
|
|
t.Setenv("RSMON_TEST_DOCKER", "1")
|
|
t.Setenv("STUB_DOCKER_FAIL_BUILD", "1")
|
|
logPath := filepath.Join(t.TempDir(), "docker-calls.log")
|
|
writeStubDocker(t, logPath)
|
|
|
|
h, err := New("fail-build", Fixtures()[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.Start(context.Background()); err == nil {
|
|
t.Fatal("Start succeeded despite stub docker build failure")
|
|
}
|
|
if h.ContainerID() != "" {
|
|
t.Fatalf("container id set after build failure: %q", h.ContainerID())
|
|
}
|
|
if h.KnownHostsPath() != "" {
|
|
t.Fatalf("known_hosts path set after build failure: %q", h.KnownHostsPath())
|
|
}
|
|
if err := h.Stop(); err != nil {
|
|
t.Fatalf("Stop after failed build: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestStopRemovesKnownHostsDir verifies Stop deletes the temp known_hosts
|
|
// directory even when no container was ever started (the failed-start
|
|
// path registers the dir before the final write).
|
|
func TestStopRemovesKnownHostsDir(t *testing.T) {
|
|
logPath := filepath.Join(t.TempDir(), "docker-calls.log")
|
|
writeStubDocker(t, logPath)
|
|
|
|
h, err := New("stop-dir", Fixtures()[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dir := t.TempDir()
|
|
h.knownHosts = filepath.Join(dir, "known_hosts")
|
|
if err := os.WriteFile(h.knownHosts, []byte("placeholder"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.Stop(); err != nil {
|
|
t.Fatalf("Stop: %v", err)
|
|
}
|
|
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
|
t.Fatalf("known_hosts temp dir still exists after Stop: %v", err)
|
|
}
|
|
if h.KnownHostsPath() != "" {
|
|
t.Fatalf("KnownHostsPath = %q after Stop, want empty", h.KnownHostsPath())
|
|
}
|
|
}
|
|
|
|
// TestStopIdempotent verifies repeated Stop calls do not error.
|
|
func TestStopIdempotent(t *testing.T) {
|
|
logPath := filepath.Join(t.TempDir(), "docker-calls.log")
|
|
writeStubDocker(t, logPath)
|
|
|
|
h, err := New("stop-again", Fixtures()[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h.container = "rsmon-worker-test-none"
|
|
h.network = "rsmon-worker-test-none"
|
|
h.imageTag = "rsmon-worker-test/none:local"
|
|
h.knownHosts = filepath.Join(t.TempDir(), "known_hosts")
|
|
if err := h.Stop(); err != nil {
|
|
t.Fatalf("first Stop: %v", err)
|
|
}
|
|
if err := h.Stop(); err != nil {
|
|
t.Fatalf("second Stop: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestImageTagUnique verifies every harness instance gets its own image
|
|
// tag so teardown can never delete another instance's image.
|
|
func TestImageTagUnique(t *testing.T) {
|
|
a, err := New("img-a", Fixtures()[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := New("img-b", Fixtures()[0])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a.ImageTag() == b.ImageTag() {
|
|
t.Fatalf("image tags collide: %q", a.ImageTag())
|
|
}
|
|
if !strings.HasPrefix(a.ImageTag(), "rsmon-worker-test/alpine-") {
|
|
t.Fatalf("unexpected image tag: %q", a.ImageTag())
|
|
}
|
|
}
|