platform_test.go
package platform
import (
"errors"
"testing"
)
// --- Region tests ---
func TestRegionConstants(t *testing.T) {
expected := map[string]Region{
"nyc": NYC,
"sfo": SFO,
"tor": TOR,
"lon": LON,
"ams": AMS,
"fra": FRA,
"sgp": SGP,
"syd": SYD,
"blr": BLR,
}
for value, constant := range expected {
if string(constant) != value {
t.Errorf("expected region constant %q to have value %q, got %q", constant, value, string(constant))
}
}
}
func TestAllRegions(t *testing.T) {
regions := AllRegions()
expectedRegions := []Region{NYC, SFO, TOR, LON, AMS, FRA, SGP, SYD, BLR}
if len(regions) != len(expectedRegions) {
t.Fatalf("expected %d regions, got %d", len(expectedRegions), len(regions))
}
regionSet := make(map[Region]bool)
for _, r := range regions {
regionSet[r] = true
}
for _, expected := range expectedRegions {
if !regionSet[expected] {
t.Errorf("AllRegions() missing region %q", expected)
}
}
}
func TestAllRegionsOrder(t *testing.T) {
regions := AllRegions()
expected := []Region{NYC, SFO, TOR, LON, AMS, FRA, SGP, SYD, BLR}
for i, r := range regions {
if r != expected[i] {
t.Errorf("AllRegions()[%d] = %q, expected %q", i, r, expected[i])
}
}
}
func TestRegionIsString(t *testing.T) {
// Verify Region is a string type and can be compared/assigned as strings
var r Region = "custom"
if string(r) != "custom" {
t.Errorf("Region should be a string type")
}
}
// --- Size tests ---
func TestSizeConstants(t *testing.T) {
expected := map[string]Size{
"micro": Micro,
"small": Small,
"medium": Medium,
"large": Large,
"xlarge": XLarge,
}
for value, constant := range expected {
if string(constant) != value {
t.Errorf("expected size constant %q to have value %q, got %q", constant, value, string(constant))
}
}
}
func TestAllSizes(t *testing.T) {
sizes := AllSizes()
expectedSizes := []Size{Micro, Small, Medium, Large, XLarge}
if len(sizes) != len(expectedSizes) {
t.Fatalf("expected %d sizes, got %d", len(expectedSizes), len(sizes))
}
sizeSet := make(map[Size]bool)
for _, s := range sizes {
sizeSet[s] = true
}
for _, expected := range expectedSizes {
if !sizeSet[expected] {
t.Errorf("AllSizes() missing size %q", expected)
}
}
}
func TestAllSizesOrder(t *testing.T) {
sizes := AllSizes()
expected := []Size{Micro, Small, Medium, Large, XLarge}
for i, s := range sizes {
if s != expected[i] {
t.Errorf("AllSizes()[%d] = %q, expected %q", i, s, expected[i])
}
}
}
func TestSizeIsString(t *testing.T) {
var s Size = "custom"
if string(s) != "custom" {
t.Errorf("Size should be a string type")
}
}
// --- Error tests ---
func TestErrorsExist(t *testing.T) {
errs := map[string]error{
"ErrNotFound": ErrNotFound,
"ErrInvalidState": ErrInvalidState,
"ErrTimeout": ErrTimeout,
"ErrUnsupportedRegion": ErrUnsupportedRegion,
"ErrUnsupportedSize": ErrUnsupportedSize,
}
for name, err := range errs {
if err == nil {
t.Errorf("%s should not be nil", name)
}
}
}
func TestErrorMessages(t *testing.T) {
tests := []struct {
err error
message string
}{
{ErrNotFound, "resource not found"},
{ErrInvalidState, "invalid resource state"},
{ErrTimeout, "operation timed out"},
{ErrUnsupportedRegion, "unsupported region"},
{ErrUnsupportedSize, "unsupported size"},
}
for _, tt := range tests {
if tt.err.Error() != tt.message {
t.Errorf("expected error message %q, got %q", tt.message, tt.err.Error())
}
}
}
func TestErrorsAreDistinct(t *testing.T) {
allErrors := []error{ErrNotFound, ErrInvalidState, ErrTimeout, ErrUnsupportedRegion, ErrUnsupportedSize}
for i, e1 := range allErrors {
for j, e2 := range allErrors {
if i != j && errors.Is(e1, e2) {
t.Errorf("error %q should not match error %q", e1, e2)
}
}
}
}
func TestErrorWrapping(t *testing.T) {
// Verify errors can be wrapped and unwrapped correctly
wrapped := errors.Join(ErrNotFound, errors.New("additional context"))
if !errors.Is(wrapped, ErrNotFound) {
t.Error("wrapped ErrNotFound should still match via errors.Is")
}
}
// --- ServerOptions tests ---
func TestServerOptionsFields(t *testing.T) {
opts := ServerOptions{
Name: "my-server",
Size: Small,
Region: NYC,
Image: "ubuntu-22-04",
SSHKey: "aa:bb:cc:dd",
Tags: []string{"env=prod", "team=backend"},
}
if opts.Name != "my-server" {
t.Errorf("expected Name 'my-server', got %q", opts.Name)
}
if opts.Size != Small {
t.Errorf("expected Size Small, got %q", opts.Size)
}
if opts.Region != NYC {
t.Errorf("expected Region NYC, got %q", opts.Region)
}
if opts.Image != "ubuntu-22-04" {
t.Errorf("expected Image 'ubuntu-22-04', got %q", opts.Image)
}
if opts.SSHKey != "aa:bb:cc:dd" {
t.Errorf("expected SSHKey 'aa:bb:cc:dd', got %q", opts.SSHKey)
}
if len(opts.Tags) != 2 {
t.Errorf("expected 2 tags, got %d", len(opts.Tags))
}
}
func TestServerOptionsZeroValue(t *testing.T) {
var opts ServerOptions
if opts.Name != "" {
t.Errorf("zero value Name should be empty")
}
if opts.Size != "" {
t.Errorf("zero value Size should be empty")
}
if opts.Region != "" {
t.Errorf("zero value Region should be empty")
}
if opts.Image != "" {
t.Errorf("zero value Image should be empty")
}
if opts.SSHKey != "" {
t.Errorf("zero value SSHKey should be empty")
}
if opts.Tags != nil {
t.Errorf("zero value Tags should be nil")
}
}
// --- Server struct tests ---
func TestServerFields(t *testing.T) {
server := Server{
ID: "srv-123",
Name: "web-1",
IP: "10.0.0.1",
Size: "s-1vcpu-2gb",
Region: "nyc1",
Status: "active",
}
if server.ID != "srv-123" {
t.Errorf("expected ID 'srv-123', got %q", server.ID)
}
if server.Name != "web-1" {
t.Errorf("expected Name 'web-1', got %q", server.Name)
}
if server.IP != "10.0.0.1" {
t.Errorf("expected IP '10.0.0.1', got %q", server.IP)
}
if server.Status != "active" {
t.Errorf("expected Status 'active', got %q", server.Status)
}
}
// --- Volume struct tests ---
func TestVolumeFields(t *testing.T) {
volume := Volume{
ID: "vol-123",
Name: "data",
Size: 100,
Region: "nyc1",
ServerID: "srv-456",
}
if volume.ID != "vol-123" {
t.Errorf("expected ID 'vol-123', got %q", volume.ID)
}
if volume.Name != "data" {
t.Errorf("expected Name 'data', got %q", volume.Name)
}
if volume.Size != 100 {
t.Errorf("expected Size 100, got %d", volume.Size)
}
if volume.ServerID != "srv-456" {
t.Errorf("expected ServerID 'srv-456', got %q", volume.ServerID)
}
}
func TestVolumeDetachedState(t *testing.T) {
volume := Volume{
ID: "vol-123",
Name: "data",
}
if volume.ServerID != "" {
t.Errorf("detached volume should have empty ServerID")
}
}
// --- DNSZone and DNSRecord tests ---
func TestDNSZoneFields(t *testing.T) {
zone := DNSZone{
Name: "example.com",
TTL: 300,
}
if zone.Name != "example.com" {
t.Errorf("expected Name 'example.com', got %q", zone.Name)
}
if zone.TTL != 300 {
t.Errorf("expected TTL 300, got %d", zone.TTL)
}
}
func TestDNSRecordFields(t *testing.T) {
record := DNSRecord{
ID: "rec-123",
Type: "A",
Name: "@",
Value: "1.2.3.4",
TTL: 300,
Priority: 0,
}
if record.Type != "A" {
t.Errorf("expected Type 'A', got %q", record.Type)
}
if record.Name != "@" {
t.Errorf("expected Name '@', got %q", record.Name)
}
if record.Value != "1.2.3.4" {
t.Errorf("expected Value '1.2.3.4', got %q", record.Value)
}
}
func TestDNSRecordMX(t *testing.T) {
record := DNSRecord{
Type: "MX",
Name: "@",
Value: "mail.example.com",
TTL: 3600,
Priority: 10,
}
if record.Priority != 10 {
t.Errorf("expected Priority 10, got %d", record.Priority)
}
}
// --- Platform delegation via mock backend tests ---
// mockBackend is a minimal mock to test that Platform delegates correctly.
type mockBackend struct {
createServerCalled bool
getServerCalled bool
deleteServerCalled bool
createVolumeCalled bool
getVolumeCalled bool
attachVolumeCalled bool
detachVolumeCalled bool
createDNSCalled bool
getDNSCalled bool
addRecordCalled bool
deleteRecordCalled bool
}
func (m *mockBackend) CreateServer(opts ServerOptions) (*Server, error) {
m.createServerCalled = true
return &Server{ID: "mock-id", Name: opts.Name, Status: "active"}, nil
}
func (m *mockBackend) GetServer(name string) (*Server, error) {
m.getServerCalled = true
return &Server{ID: "mock-id", Name: name}, nil
}
func (m *mockBackend) DeleteServer(id string) error {
m.deleteServerCalled = true
return nil
}
func (m *mockBackend) CreateVolume(name string, sizeGB int, region Region) (*Volume, error) {
m.createVolumeCalled = true
return &Volume{ID: "vol-mock", Name: name, Size: sizeGB}, nil
}
func (m *mockBackend) GetVolume(name string) (*Volume, error) {
m.getVolumeCalled = true
return &Volume{ID: "vol-mock", Name: name}, nil
}
func (m *mockBackend) AttachVolume(volumeID, serverID string) error {
m.attachVolumeCalled = true
return nil
}
func (m *mockBackend) DetachVolume(volumeID string) error {
m.detachVolumeCalled = true
return nil
}
func (m *mockBackend) CreateDNSZone(domain string) (*DNSZone, error) {
m.createDNSCalled = true
return &DNSZone{Name: domain, TTL: 300}, nil
}
func (m *mockBackend) GetDNSZone(domain string) (*DNSZone, error) {
m.getDNSCalled = true
return &DNSZone{Name: domain}, nil
}
func (m *mockBackend) AddDNSRecord(zone string, record DNSRecord) error {
m.addRecordCalled = true
return nil
}
func (m *mockBackend) DeleteDNSRecord(zone, recordID string) error {
m.deleteRecordCalled = true
return nil
}
func TestPlatformDelegatesCreateServer(t *testing.T) {
mock := &mockBackend{}
p := &Platform{Backend: mock}
server, err := p.CreateServer(ServerOptions{Name: "test", Size: Small, Region: NYC})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !mock.createServerCalled {
t.Error("expected CreateServer to be delegated to backend")
}
if server.Name != "test" {
t.Errorf("expected server name 'test', got %q", server.Name)
}
}
func TestPlatformDelegatesGetServer(t *testing.T) {
mock := &mockBackend{}
p := &Platform{Backend: mock}
server, err := p.GetServer("web-1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !mock.getServerCalled {
t.Error("expected GetServer to be delegated to backend")
}
if server.Name != "web-1" {
t.Errorf("expected server name 'web-1', got %q", server.Name)
}
}
func TestPlatformDelegatesDeleteServer(t *testing.T) {
mock := &mockBackend{}
p := &Platform{Backend: mock}
err := p.DeleteServer("srv-123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !mock.deleteServerCalled {
t.Error("expected DeleteServer to be delegated to backend")
}
}
func TestPlatformDelegatesCreateVolume(t *testing.T) {
mock := &mockBackend{}
p := &Platform{Backend: mock}
vol, err := p.CreateVolume("data", 50, NYC)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !mock.createVolumeCalled {
t.Error("expected CreateVolume to be delegated to backend")
}
if vol.Name != "data" {
t.Errorf("expected volume name 'data', got %q", vol.Name)
}
if vol.Size != 50 {
t.Errorf("expected volume size 50, got %d", vol.Size)
}
}
func TestPlatformDelegatesGetVolume(t *testing.T) {
mock := &mockBackend{}
p := &Platform{Backend: mock}
vol, err := p.GetVolume("data")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !mock.getVolumeCalled {
t.Error("expected GetVolume to be delegated to backend")
}
if vol.Name != "data" {
t.Errorf("expected volume name 'data', got %q", vol.Name)
}
}
func TestPlatformDelegatesAttachVolume(t *testing.T) {
mock := &mockBackend{}
p := &Platform{Backend: mock}
err := p.AttachVolume("vol-1", "srv-1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !mock.attachVolumeCalled {
t.Error("expected AttachVolume to be delegated to backend")
}
}
func TestPlatformDelegatesDetachVolume(t *testing.T) {
mock := &mockBackend{}
p := &Platform{Backend: mock}
err := p.DetachVolume("vol-1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !mock.detachVolumeCalled {
t.Error("expected DetachVolume to be delegated to backend")
}
}
func TestPlatformDelegatesCreateDNSZone(t *testing.T) {
mock := &mockBackend{}
p := &Platform{Backend: mock}
zone, err := p.CreateDNSZone("example.com")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !mock.createDNSCalled {
t.Error("expected CreateDNSZone to be delegated to backend")
}
if zone.Name != "example.com" {
t.Errorf("expected zone name 'example.com', got %q", zone.Name)
}
}
func TestPlatformDelegatesGetDNSZone(t *testing.T) {
mock := &mockBackend{}
p := &Platform{Backend: mock}
zone, err := p.GetDNSZone("example.com")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !mock.getDNSCalled {
t.Error("expected GetDNSZone to be delegated to backend")
}
if zone.Name != "example.com" {
t.Errorf("expected zone name 'example.com', got %q", zone.Name)
}
}
func TestPlatformDelegatesAddDNSRecord(t *testing.T) {
mock := &mockBackend{}
p := &Platform{Backend: mock}
err := p.AddDNSRecord("example.com", DNSRecord{Type: "A", Name: "@", Value: "1.2.3.4"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !mock.addRecordCalled {
t.Error("expected AddDNSRecord to be delegated to backend")
}
}
func TestPlatformDelegatesDeleteDNSRecord(t *testing.T) {
mock := &mockBackend{}
p := &Platform{Backend: mock}
err := p.DeleteDNSRecord("example.com", "rec-123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !mock.deleteRecordCalled {
t.Error("expected DeleteDNSRecord to be delegated to backend")
}
}
// --- Service and related struct tests ---
func TestServiceFields(t *testing.T) {
svc := Service{
Name: "web",
Image: "nginx:latest",
Command: []string{"nginx", "-g", "daemon off;"},
Ports: []Port{{Host: 80, Container: 8080}},
Volumes: []Mount{{Source: "/data", Target: "/var/data"}},
Env: map[string]string{"ENV": "production"},
Network: "app-net",
Restart: "always",
}
if svc.Name != "web" {
t.Errorf("expected Name 'web', got %q", svc.Name)
}
if svc.Image != "nginx:latest" {
t.Errorf("expected Image 'nginx:latest', got %q", svc.Image)
}
if len(svc.Command) != 3 {
t.Errorf("expected 3 command args, got %d", len(svc.Command))
}
if len(svc.Ports) != 1 {
t.Errorf("expected 1 port, got %d", len(svc.Ports))
}
if svc.Ports[0].Host != 80 || svc.Ports[0].Container != 8080 {
t.Errorf("unexpected port mapping: %+v", svc.Ports[0])
}
}
func TestPortWithBind(t *testing.T) {
p := Port{Host: 443, Container: 8443, Bind: "127.0.0.1"}
if p.Bind != "127.0.0.1" {
t.Errorf("expected Bind '127.0.0.1', got %q", p.Bind)
}
}
func TestHealthcheckFields(t *testing.T) {
hc := Healthcheck{
Cmd: "curl -f http://localhost/health",
Interval: "30s",
Timeout: "10s",
Retries: 3,
StartPeriod: "5s",
}
if hc.Cmd != "curl -f http://localhost/health" {
t.Errorf("unexpected Cmd: %q", hc.Cmd)
}
if hc.Retries != 3 {
t.Errorf("expected Retries 3, got %d", hc.Retries)
}
}
func TestResourcesFields(t *testing.T) {
r := Resources{CPUs: "2", Memory: "1g"}
if r.CPUs != "2" {
t.Errorf("expected CPUs '2', got %q", r.CPUs)
}
if r.Memory != "1g" {
t.Errorf("expected Memory '1g', got %q", r.Memory)
}
}
func TestLogConfigFields(t *testing.T) {
lc := LogConfig{
Driver: "json-file",
Options: map[string]string{"max-size": "10m", "max-file": "3"},
}
if lc.Driver != "json-file" {
t.Errorf("expected Driver 'json-file', got %q", lc.Driver)
}
if len(lc.Options) != 2 {
t.Errorf("expected 2 log options, got %d", len(lc.Options))
}
}
// --- shellEscape tests ---
func TestShellEscapeSimpleStrings(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"hello", "hello"},
{"docker", "docker"},
{"my-server", "my-server"},
{"path/to/file", "path/to/file"},
{"key=value", "key=value"},
{"v1.0.0", "v1.0.0"},
{"host:port", "host:port"},
{"a_b", "a_b"},
{"item1,item2", "item1,item2"},
}
for _, tt := range tests {
result := shellEscape(tt.input)
if result != tt.expected {
t.Errorf("shellEscape(%q) = %q, expected %q", tt.input, result, tt.expected)
}
}
}
func TestShellEscapeSpecialCharacters(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"hello world", "'hello world'"},
{"it's", "'it'\"'\"'s'"},
{"$HOME", "'$HOME'"},
{"foo;bar", "'foo;bar'"},
{"a&b", "'a&b'"},
{"", "''"},
}
for _, tt := range tests {
result := shellEscape(tt.input)
if result != tt.expected {
t.Errorf("shellEscape(%q) = %q, expected %q", tt.input, result, tt.expected)
}
}
}
// --- SSHKeyProvider interface test ---
func TestSSHKeyProviderInterface(t *testing.T) {
// Verify the interface signature is correct by implementing it
var _ SSHKeyProvider = &testSSHKeyProvider{}
}
type testSSHKeyProvider struct{}
func (p *testSSHKeyProvider) GetSSHKeyFingerprint(publicKey string) (string, error) {
return "aa:bb:cc:dd", nil
}