stack_test.go

102 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
package platform

import (
	"strings"
	"testing"
)

func demoStack() *Stack {
	return &Stack{
		Volumes: []string{"data"},
		Services: []Service{
			{
				Name:        "app",
				Image:       "${APP_IMAGE}",
				Ports:       []Port{{Host: 80, Container: 5000}},
				Env:         map[string]string{"ENV": "production", "DB_PATH": "/data/app.db"},
				Volumes:     []Mount{{Source: "data", Target: "/data"}},
				DependsOn:   []string{"cache"},
				Healthcheck: &Healthcheck{Cmd: "curl -sf http://localhost:5000/health", Interval: "10s", Retries: 3},
			},
			{
				Name:          "cache",
				ContainerName: "cache",
				Image:         "redis:7",
				Command:       []string{"redis-server", "--save", ""},
			},
		},
	}
}

func TestComposeYAML(t *testing.T) {
	yaml := demoStack().ComposeYAML()
	for _, want := range []string{
		"services:",
		"  app:",
		"    image: ${APP_IMAGE}",
		"    restart: unless-stopped",
		`- "80:5000"`,
		"      DB_PATH: '/data/app.db'", // sorted before ENV, single-quoted
		"      ENV: 'production'",
		"- data:/data",
		"depends_on:",
		"- cache",
		"healthcheck:",
		"container_name: cache",
		"volumes:\n  data:\n",
	} {
		if !strings.Contains(yaml, want) {
			t.Errorf("compose YAML missing %q\n---\n%s", want, yaml)
		}
	}
}

func TestComposeDeterministic(t *testing.T) {
	// Env key ordering must be stable across renders (for hashing/diffs).
	if a, b := demoStack().ComposeYAML(), demoStack().ComposeYAML(); a != b {
		t.Fatal("ComposeYAML is not deterministic")
	}
}

func TestCloudInit(t *testing.T) {
	init := demoStack().CloudInit(CloudInitOptions{
		Env:       map[string]string{"APP_IMAGE": "registry.example/app:v1"},
		Registry:  &RegistryAuth{Host: "registry.example", Username: "u", Password: "p"},
		OpenPorts: []int{80},
	})
	for _, want := range []string{
		"#cloud-config",
		"ufw allow 80/tcp",
		"mkdir -p /opt/app",
		"docker login registry.example -u u --password-stdin",
		"base64 -d > /opt/app/docker-compose.yml",
		"echo 'APP_IMAGE=registry.example/app:v1' >> /opt/app/.env",
		"docker compose -f /opt/app/docker-compose.yml pull",
		"docker compose -f /opt/app/docker-compose.yml up -d",
	} {
		if !strings.Contains(init, want) {
			t.Errorf("cloud-init missing %q\n---\n%s", want, init)
		}
	}
}

func TestNewStackOptionsMatchStruct(t *testing.T) {
	opt := NewStack(
		WithVolume("data"),
		WithService("app",
			WithImage("${APP_IMAGE}"),
			WithPort(80, 5000),
			WithEnv("ENV", "production"),
			WithEnv("DB_PATH", "/data/app.db"),
			WithMount("data", "/data"),
			WithDependsOn("cache"),
			WithHealthcheck("curl -sf http://localhost:5000/health"),
		),
		WithService("cache", WithContainerName("cache"), WithImage("redis:7"), WithCommand("redis-server", "--save", "")),
	)
	opt.Services[0].Healthcheck.Interval = "10s"
	opt.Services[0].Healthcheck.Retries = 3
	if a, b := opt.ComposeYAML(), demoStack().ComposeYAML(); a != b {
		t.Fatalf("Options-built stack differs from struct-built:\n--- options ---\n%s\n--- struct ---\n%s", a, b)
	}
}