diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..f072163 --- /dev/null +++ b/main_test.go @@ -0,0 +1,296 @@ +package main + +import ( + "reflect" + "strings" + "testing" +) + +func TestParseEnvLine(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + line string + wantKey string + wantValue string + wantOK bool + }{ + {name: "simple", line: "FOO=bar", wantKey: "FOO", wantValue: "bar", wantOK: true}, + {name: "trims", line: " FOO = bar ", wantKey: "FOO", wantValue: "bar", wantOK: true}, + {name: "comment", line: "# comment", wantOK: false}, + {name: "empty", line: "", wantOK: false}, + {name: "no equals", line: "FOO", wantOK: false}, + {name: "invalid key digit", line: "1FOO=bar", wantOK: false}, + {name: "invalid key dash", line: "FOO-BAR=1", wantOK: false}, + {name: "underscore ok", line: "_FOO=bar", wantKey: "_FOO", wantValue: "bar", wantOK: true}, + {name: "digit ok after", line: "FOO1=2", wantKey: "FOO1", wantValue: "2", wantOK: true}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + key, value, ok := parseEnvLine(tt.line) + if ok != tt.wantOK { + t.Fatalf("ok=%v want %v", ok, tt.wantOK) + } + if key != tt.wantKey || value != tt.wantValue { + t.Fatalf("got key=%q value=%q want key=%q value=%q", key, value, tt.wantKey, tt.wantValue) + } + }) + } +} + +func TestIsEnvKey(t *testing.T) { + t.Parallel() + + tests := []struct { + key string + want bool + }{ + {key: "A", want: true}, + {key: "_A", want: true}, + {key: "A1", want: true}, + {key: "A_B2", want: true}, + {key: "", want: false}, + {key: "1A", want: false}, + {key: "A-B", want: false}, + {key: "A B", want: false}, + {key: "A.B", want: false}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.key, func(t *testing.T) { + if got := isEnvKey(tt.key); got != tt.want { + t.Fatalf("got %v want %v", got, tt.want) + } + }) + } +} + +func TestIsEnvLine(t *testing.T) { + t.Parallel() + + tests := []struct { + line string + want bool + }{ + {line: "", want: true}, + {line: " ", want: true}, + {line: "FOO=bar", want: true}, + {line: "FOO = bar", want: false}, + {line: "1FOO=bar", want: false}, + {line: "NO_EQUALS", want: false}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.line, func(t *testing.T) { + if got := isEnvLine(tt.line); got != tt.want { + t.Fatalf("got %v want %v", got, tt.want) + } + }) + } +} + +func TestApplyEnvAssignments(t *testing.T) { + t.Parallel() + + lines := []string{ + "# comment", + "FOO=old", + "BAR=keep", + "INVALID LINE", + "BAZ=gone", + "FOO=duplicate", + } + assignments := []envAssignment{ + {key: "FOO", value: "new"}, + {key: "BAZ", value: "zzz"}, + {key: "NEWKEY", value: "123"}, + } + want := []string{ + "# comment", + "FOO=new", + "BAR=keep", + "INVALID LINE", + "BAZ=zzz", + "NEWKEY=123", + } + + got := applyEnvAssignments(lines, assignments) + if !reflect.DeepEqual(got, want) { + t.Fatalf("got %#v want %#v", got, want) + } +} + +func TestRemoveEnvKeys(t *testing.T) { + t.Parallel() + + lines := []string{ + "KEEP=1", + "DROP=2", + "# comment", + "INVALID LINE", + "DROP=3", + "ALSO=4", + } + keys := []string{"DROP", "MISSING"} + want := []string{ + "KEEP=1", + "# comment", + "INVALID LINE", + "ALSO=4", + } + + got := removeEnvKeys(lines, keys) + if !reflect.DeepEqual(got, want) { + t.Fatalf("got %#v want %#v", got, want) + } +} + +func TestShellQuote(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want string + }{ + {name: "empty", input: "", want: "''"}, + {name: "simple", input: "abc", want: "'abc'"}, + {name: "spaces", input: "a b", want: "'a b'"}, + {name: "single quote", input: "a'b", want: "'a'\"'\"'b'"}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if got := shellQuote(tt.input); got != tt.want { + t.Fatalf("got %q want %q", got, tt.want) + } + }) + } +} + +func TestResolveVolumePath(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + template string + baseDir string + service string + wantResult string + }{ + {name: "empty uses base", template: "", baseDir: "/srv/pb", service: "svc", wantResult: "/srv/pb"}, + {name: "absolute path", template: "/data/{service}", baseDir: "/srv/pb", service: "svc", wantResult: "/data/svc"}, + {name: "relative path", template: "data/{service}", baseDir: "/srv/pb", service: "svc", wantResult: "/srv/pb/data/svc"}, + {name: "simple relative", template: "custom", baseDir: "/srv/pb", service: "svc", wantResult: "/srv/pb/custom"}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + if got := resolveVolumePath(tt.template, tt.baseDir, tt.service); got != tt.wantResult { + t.Fatalf("got %q want %q", got, tt.wantResult) + } + }) + } +} + +func TestTranslateMachineArch(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want string + wantErr bool + }{ + {name: "x86_64", input: " x86_64 ", want: "amd64"}, + {name: "amd64", input: "amd64", want: "amd64"}, + {name: "i686", input: "i686", want: "386"}, + {name: "armv7l", input: "armv7l", want: "armv7"}, + {name: "armv6l", input: "armv6l", want: "arm"}, + {name: "aarch64", input: "aarch64", want: "arm64"}, + {name: "arm64", input: "arm64", want: "arm64"}, + {name: "unknown", input: "mips", wantErr: true}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := translateMachineArch(tt.input) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Fatalf("got %q want %q", got, tt.want) + } + }) + } +} + +func TestSystemdScript(t *testing.T) { + t.Parallel() + + serviceDir := "/srv/pb" + envFile := "/etc/pb/env" + serviceName := "demo" + script := systemdScript(serviceDir, envFile, serviceName) + + required := []string{ + "/etc/systemd/system/pb@.service", + "StandardOutput = append:" + serviceDir + "/%i.log", + "StandardError = append:" + serviceDir + "/%i.log", + "WorkingDirectory = " + serviceDir, + "EnvironmentFile = " + envFile, + "ExecStart = " + serviceDir + "/pocketbase serve --http=127.0.0.1:${PORT} --hooksWatch=false --dir=${DATA_DIR} --hooksDir=" + serviceDir + "/pb_hooks --migrationsDir=" + serviceDir + "/pb_migrations --publicDir=" + serviceDir + "/pb_public", + "systemctl --no-block enable pb@" + serviceName, + } + + for _, snippet := range required { + if !strings.Contains(script, snippet) { + t.Fatalf("script missing %q", snippet) + } + } +} + +func TestSystemdOverrideScript(t *testing.T) { + t.Parallel() + + serviceName := "demo" + port := 8090 + volume := "/srv/pb/data" + script := systemdOverrideScript(serviceName, port, volume) + + required := []string{ + "/etc/systemd/system/pb@" + serviceName + ".service.d", + "Environment=PORT=8090", + "Environment=DATA_DIR=" + volume, + } + for _, snippet := range required { + if !strings.Contains(script, snippet) { + t.Fatalf("script missing %q", snippet) + } + } +} + +func TestSystemdRestartScript(t *testing.T) { + t.Parallel() + + serviceName := "demo" + script := systemdRestartScript(serviceName) + expected := "systemctl --no-block restart pb@" + serviceName + if !strings.Contains(script, expected) { + t.Fatalf("script missing %q", expected) + } +}