package config import ( "os" "path/filepath" "strconv" "strings" "testing" ) func TestLoadSelectsLocalComposeFilesForAnInstallationInPathsWithSpaces(t *testing.T) { t.Parallel() installationPath, projectDirectory, envFile, override := writeInstallation(t, "local") installation, err := Load(installationPath) if err != nil { t.Fatalf("Load() error = %v", err) } if installation.ProjectDirectory != projectDirectory { t.Errorf("ProjectDirectory = %q, want %q", installation.ProjectDirectory, projectDirectory) } if installation.EnvFile != envFile { t.Errorf("EnvFile = %q, want %q", installation.EnvFile, envFile) } if !strings.Contains(installationPath, "installation folder with spaces") { t.Fatalf("test setup must exercise a path with spaces: %q", installationPath) } want := []string{ filepath.Join(projectDirectory, "compose.yaml"), filepath.Join(projectDirectory, "deploy", "compose.local.yaml"), override, } assertStringsEqual(t, installation.ComposeFiles(), want) } func TestLoadSelectsServerComposeFiles(t *testing.T) { t.Parallel() installationPath, projectDirectory, _, override := writeInstallation(t, "server") installation, err := Load(installationPath) if err != nil { t.Fatalf("Load() error = %v", err) } want := []string{ filepath.Join(projectDirectory, "compose.yaml"), filepath.Join(projectDirectory, "deploy", "compose.server.yaml"), override, } assertStringsEqual(t, installation.ComposeFiles(), want) } func TestLoadRequiresAndReturnsTypedWorkspaceRepositoryForGitInstallations(t *testing.T) { installationPath, projectDirectory, envFile, _ := writeInstallation(t, "local") gitOverride := filepath.Join(projectDirectory, "deploy", "compose.git-ssh.yaml") if err := os.WriteFile(gitOverride, []byte("services: {}\n"), 0o600); err != nil { t.Fatal(err) } secretRoot := filepath.Dir(envFile) privateKey := filepath.Join(secretRoot, "git-key") knownHosts := filepath.Join(secretRoot, "known-hosts") for _, file := range []string{privateKey, knownHosts} { if err := os.WriteFile(file, []byte("fixture\n"), 0o600); err != nil { t.Fatal(err) } } remote := "git@gitea.example.org:clinical/workspaces.git" environment := strings.Join([]string{ "THT_AUTH_CONFIG_ROOT=" + strconv.Quote(filepath.Join(filepath.Dir(envFile), "auth")), "THT_WORKSPACE_GIT_REMOTE=" + remote, "THT_WORKSPACE_GIT_BRANCH=main", "THT_WORKSPACE_GIT_SSH_KEY_FILE=" + privateKey, "THT_WORKSPACE_GIT_KNOWN_HOSTS_FILE=" + knownHosts, }, "\n") + "\n" if err := os.WriteFile(envFile, []byte(environment), 0o600); err != nil { t.Fatal(err) } contents := "profile: local\nprojectDirectory: " + projectDirectory + "\nenvFile: " + envFile + "\nauthentication:\n configDirectory: " + filepath.Join(filepath.Dir(envFile), "auth") + "\nworkspaceRepository:\n remote: " + remote + "\n branch: main\n access: ssh\noverrides:\n - " + gitOverride + "\n" if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil { t.Fatal(err) } installation, err := Load(installationPath) if err != nil { t.Fatal(err) } if installation.WorkspaceRepository.Remote != remote || installation.WorkspaceRepository.Branch != "main" || installation.WorkspaceRepository.Access != "ssh" { t.Fatalf("WorkspaceRepository = %#v", installation.WorkspaceRepository) } } func TestLoadRejectsGitOverrideWithoutTypedWorkspaceRepository(t *testing.T) { installationPath, projectDirectory, envFile, _ := writeInstallation(t, "local") gitOverride := filepath.Join(projectDirectory, "deploy", "compose.git-https.yaml") if err := os.WriteFile(gitOverride, []byte("services: {}\n"), 0o600); err != nil { t.Fatal(err) } contents := "profile: local\nprojectDirectory: " + projectDirectory + "\nenvFile: " + envFile + "\noverrides:\n - " + gitOverride + "\n" contents = strings.Replace(contents, "\noverrides:", "\nauthentication:\n configDirectory: "+filepath.Join(filepath.Dir(envFile), "auth")+"\noverrides:", 1) if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil { t.Fatal(err) } _, err := Load(installationPath) if err == nil || !strings.Contains(err.Error(), "workspaceRepository") { t.Fatalf("Load() error = %v, want workspaceRepository error", err) } } func TestComposeArgsAutomaticallyIncludeTheInstallationCurrentImageOverride(t *testing.T) { t.Parallel() installationPath, _, _, _ := writeInstallation(t, "local") seed, err := Load(installationPath) if err != nil { t.Fatal(err) } currentImage := seed.CurrentImageOverridePath() if err := os.MkdirAll(filepath.Dir(currentImage), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(currentImage, []byte("services:\n core:\n image: candidate\n"), 0o600); err != nil { t.Fatal(err) } installation, err := Load(installationPath) if err != nil { t.Fatal(err) } args := installation.ComposeArgs("up", "--detach") want := []string{"-f", currentImage, "up", "--detach"} if !containsSequence(args, want) { t.Fatalf("ComposeArgs() = %#v, want durable override immediately before command", args) } } func TestComposeArgsWithFinalOverridePreservesCurrentImagePrecedence(t *testing.T) { installationPath, _, _, _ := writeInstallation(t, "server") seed, err := Load(installationPath) if err != nil { t.Fatal(err) } if err := os.MkdirAll(seed.ControlDirectory(), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(seed.CurrentImageOverridePath(), []byte("services: {}\n"), 0o600); err != nil { t.Fatal(err) } final := filepath.Join(seed.ControlDirectory(), "migration.yaml") if err := os.WriteFile(final, []byte("services: {}\n"), 0o600); err != nil { t.Fatal(err) } installation, err := Load(installationPath) if err != nil { t.Fatal(err) } args, err := installation.ComposeArgsWithFinalOverride(final, "--profile", "session-migrate", "config") if err != nil { t.Fatal(err) } want := []string{"-f", installation.CurrentImageOverridePath(), "-f", final, "--profile", "session-migrate", "config"} if !containsSequence(args, want) { t.Fatalf("ComposeArgsWithFinalOverride() = %#v, want %#v", args, want) } } func TestPreservationPathsReturnsCanonicalBindRootsBackupsAndSecretFiles(t *testing.T) { installationPath, _, envFile, _ := writeInstallation(t, "server") root := filepath.Dir(envFile) var wanted []string var lines []string for _, item := range []struct{ key, name string }{ {"THT_DATA_ROOT", "data"}, {"THT_PI_STATE_ROOT", "pi-state"}, {"THT_WORKSPACE_REGISTRY_ROOT", "workspace-registry"}, {"THT_BACKUP_ROOT", "backups"}, } { path := filepath.Join(root, item.name) if err := os.Mkdir(path, 0o700); err != nil { t.Fatal(err) } wanted = append(wanted, path) lines = append(lines, item.key+"="+path) } secret := filepath.Join(root, "secret") if err := os.WriteFile(secret, []byte("secret"), 0o600); err != nil { t.Fatal(err) } wanted = append(wanted, secret) lines = append(lines, "APP_TOKEN_FILE="+secret) lines = append(lines, "THT_AUTH_CONFIG_ROOT="+strconv.Quote(filepath.Join(root, "auth"))) if err := os.WriteFile(envFile, []byte(strings.Join(lines, "\n")+"\n"), 0o600); err != nil { t.Fatal(err) } installation, err := Load(installationPath) if err != nil { t.Fatal(err) } got, err := installation.PreservationPaths() if err != nil { t.Fatal(err) } assertStringsEqual(t, got, wanted) } func TestInstallationControlPathsAreIsolatedForDescriptorsSharingOneCheckout(t *testing.T) { projectDirectory := t.TempDir() first := Installation{Path: filepath.Join(t.TempDir(), installationFileName), ProjectDirectory: projectDirectory} second := Installation{Path: filepath.Join(t.TempDir(), installationFileName), ProjectDirectory: projectDirectory} if first.CurrentImageOverridePath() == second.CurrentImageOverridePath() { t.Fatalf("shared-checkout installations reused %q", first.CurrentImageOverridePath()) } for _, installation := range []Installation{first, second} { if filepath.Dir(filepath.Dir(installation.CurrentImageOverridePath())) != filepath.Join(projectDirectory, ".tht") { t.Fatalf("current-image path %q is not installation-specific under .tht", installation.CurrentImageOverridePath()) } if filepath.Dir(installation.UpdateStatePath()) != filepath.Dir(installation.CurrentImageOverridePath()) { t.Fatalf("state %q and selector %q do not share one installation control directory", installation.UpdateStatePath(), installation.CurrentImageOverridePath()) } if got, want := installation.RestartStatePath(), filepath.Join(installation.ControlDirectory(), "restart-state.json"); got != want { t.Fatalf("RestartStatePath() = %q, want %q", got, want) } } } func TestLoadRejectsRelativeInstallationPaths(t *testing.T) { t.Parallel() _, err := Load("thothii-installation.yaml") if err == nil || !strings.Contains(err.Error(), "absolute") { t.Fatalf("Load() error = %v, want an absolute-path error", err) } } func TestLoadRequiresCanonicalAuthenticationDirectoryMatchingEnvironment(t *testing.T) { installationPath, _, envFile, _ := writeInstallation(t, "local") authDirectory := filepath.Join(filepath.Dir(installationPath), "auth") contents, err := os.ReadFile(installationPath) if err != nil { t.Fatal(err) } if err := os.WriteFile(envFile, []byte("THT_AUTH_CONFIG_ROOT="+strconv.Quote(authDirectory)+"\n"), 0o600); err != nil { t.Fatal(err) } installation, err := Load(installationPath) if err != nil { t.Fatal(err) } if got := installation.AuthenticationDirectory(); got != authDirectory { t.Fatalf("AuthenticationDirectory() = %q, want %q", got, authDirectory) } for _, invalid := range []string{"relative/auth", authDirectory + "/../auth"} { bad := strings.Replace(string(contents), authDirectory, invalid, 1) if err := os.WriteFile(installationPath, []byte(bad), 0o600); err != nil { t.Fatal(err) } if _, err := Load(installationPath); err == nil { t.Fatalf("Load accepted unsafe auth directory %q", invalid) } } } func TestParseAuthenticationDirectoryEnvironment(t *testing.T) { values, err := parseComposeDotenv([]byte("THT_AUTH_CONFIG_ROOT=\"/tmp/auth\"\n")) if err != nil || values["THT_AUTH_CONFIG_ROOT"] != "/tmp/auth" { t.Fatalf("values=%#v err=%v", values, err) } } func writeInstallation(t *testing.T, profile string) (string, string, string, string) { t.Helper() temporaryRoot, err := filepath.EvalSymlinks(os.TempDir()) if err != nil { t.Fatal(err) } physicalRoot, err := os.MkdirTemp(temporaryRoot, "tht-config-") if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.RemoveAll(physicalRoot) }) root := filepath.Join(physicalRoot, "installation folder with spaces") projectDirectory := filepath.Join(root, "project directory with spaces") if err := os.MkdirAll(filepath.Join(projectDirectory, "deploy"), 0o755); err != nil { t.Fatal(err) } for _, name := range []string{"compose.yaml", filepath.Join("deploy", "compose.local.yaml"), filepath.Join("deploy", "compose.server.yaml")} { if err := os.WriteFile(filepath.Join(projectDirectory, name), []byte("services: {}\n"), 0o600); err != nil { t.Fatal(err) } } envFile := filepath.Join(root, "environment file.env") authDirectory := filepath.Join(root, "auth") if err := os.WriteFile(envFile, []byte("SAFE_VALUE=1\nTHT_AUTH_CONFIG_ROOT="+strconv.Quote(authDirectory)+"\n"), 0o600); err != nil { t.Fatal(err) } override := filepath.Join(root, "extra override.yaml") if err := os.WriteFile(override, []byte("services: {}\n"), 0o600); err != nil { t.Fatal(err) } installationPath := filepath.Join(root, "thothii-installation.yaml") contents := "profile: " + profile + "\nprojectDirectory: " + projectDirectory + "\nenvFile: " + envFile + "\nauthentication:\n configDirectory: " + authDirectory + "\noverrides:\n - " + override + "\n" if err := os.WriteFile(installationPath, []byte(contents), 0o600); err != nil { t.Fatal(err) } return installationPath, projectDirectory, envFile, override } func assertStringsEqual(t *testing.T, got, want []string) { t.Helper() if len(got) != len(want) { t.Fatalf("length = %d, want %d: got %#v", len(got), len(want), got) } for i := range want { if got[i] != want[i] { t.Errorf("value[%d] = %q, want %q", i, got[i], want[i]) } } } func containsSequence(values, wanted []string) bool { for start := range values { if len(values)-start < len(wanted) { continue } matched := true for offset := range wanted { if values[start+offset] != wanted[offset] { matched = false break } } if matched { return true } } return false }