Files
ThothII/tools/tht/internal/setup/files_test.go
T

292 lines
10 KiB
Go

package setup
import (
"bytes"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"github.com/aritmolab/thothii/tools/tht/internal/config"
)
func TestEnsureFilesCreatesDiscoverableConfigurationInProjectWithSpaces(t *testing.T) {
root := newProject(t, "checkout with spaces")
secrets := newExternalSecrets(t, root)
setNonInteractiveAnswers(t, secrets)
trackedExample := filepath.Join(root, "deploy", "env", "local.env.example")
before, err := os.ReadFile(trackedExample)
if err != nil {
t.Fatal(err)
}
result, err := EnsureFiles(Request{
ProjectRoot: root, InstallationID: "local-dev", Profile: "local", NonInteractive: true,
}, strings.NewReader(""), ioDiscard{})
if err != nil {
t.Fatal(err)
}
wantDirectory := filepath.Join(root, "deploy", "local-dev")
if result.DescriptorPath != filepath.Join(wantDirectory, "thothii-installation.yaml") {
t.Fatalf("descriptor = %q", result.DescriptorPath)
}
if result.EnvironmentPath != filepath.Join(wantDirectory, "operator.env") {
t.Fatalf("environment = %q", result.EnvironmentPath)
}
for _, path := range []string{result.DescriptorPath, result.EnvironmentPath} {
info, statErr := os.Stat(path)
if statErr != nil {
t.Fatalf("generated file %s: %v", path, statErr)
}
if info.Mode().Perm()&0o077 != 0 {
t.Errorf("generated file %s has permissions %o, want owner-only", path, info.Mode().Perm())
}
}
descriptor, err := os.ReadFile(result.DescriptorPath)
if err != nil {
t.Fatal(err)
}
environment, err := os.ReadFile(result.EnvironmentPath)
if err != nil {
t.Fatal(err)
}
for _, secretValue := range []string{"super-secret-value", "pi-secret-value", "private-key-value"} {
if bytes.Contains(descriptor, []byte(secretValue)) || bytes.Contains(environment, []byte(secretValue)) {
t.Fatalf("generated configuration contains a secret value %q", secretValue)
}
}
for _, path := range []string{secrets.secrets, secrets.piAuth, secrets.sshKey, secrets.knownHosts} {
contents, readErr := os.ReadFile(path)
if readErr != nil || len(contents) == 0 {
t.Fatalf("existing secret file %s was not preserved: %v", path, readErr)
}
}
if _, err := config.Resolve("", nil, root); err != nil {
t.Fatalf("generated descriptor was not discoverable: %v", err)
}
after, err := os.ReadFile(trackedExample)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(before, after) {
t.Fatal("tracked example was modified")
}
}
func TestEnsureFilesIsIdempotentForCompatibleFiles(t *testing.T) {
root := newProject(t, "linked worktree")
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
request := Request{ProjectRoot: root, InstallationID: "worktree", Profile: "local", NonInteractive: true}
first, err := EnsureFiles(request, strings.NewReader(""), ioDiscard{})
if err != nil {
t.Fatal(err)
}
before, err := os.ReadFile(first.EnvironmentPath)
if err != nil {
t.Fatal(err)
}
second, err := EnsureFiles(request, strings.NewReader(""), ioDiscard{})
if err != nil {
t.Fatal(err)
}
if len(second.Created) != 0 {
t.Fatalf("compatible rerun created %v, want no files", second.Created)
}
after, err := os.ReadFile(first.EnvironmentPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(before, after) {
t.Fatal("compatible environment was rewritten")
}
}
func TestEnsureFilesRefusesConflictingConfiguration(t *testing.T) {
root := newProject(t, "conflict")
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
directory := filepath.Join(root, "deploy", "existing")
if err := os.Mkdir(directory, 0o700); err != nil {
t.Fatal(err)
}
descriptor := filepath.Join(directory, "thothii-installation.yaml")
if err := os.WriteFile(descriptor, []byte("profile: server\n"), 0o600); err != nil {
t.Fatal(err)
}
_, err := EnsureFiles(Request{ProjectRoot: root, InstallationID: "existing", Profile: "local", NonInteractive: true}, strings.NewReader(""), ioDiscard{})
if err == nil || !strings.Contains(err.Error(), descriptor) || !strings.Contains(err.Error(), "different installation ID") {
t.Fatalf("EnsureFiles() error = %v, want exact file and corrective action", err)
}
if _, statErr := os.Stat(filepath.Join(directory, "operator.env")); !errors.Is(statErr, os.ErrNotExist) {
t.Fatalf("operator.env was created after conflict: %v", statErr)
}
}
func TestEnsureFilesRemovesOwnFilesWhenAtomicWriteIsInterrupted(t *testing.T) {
root := newProject(t, "interrupted")
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
previous := atomicWriteNewFile
t.Cleanup(func() { atomicWriteNewFile = previous })
calls := 0
atomicWriteNewFile = func(path string, contents []byte, mode os.FileMode) error {
calls++
if calls == 2 {
return errors.New("interrupted write")
}
return previous(path, contents, mode)
}
_, err := EnsureFiles(Request{ProjectRoot: root, InstallationID: "interrupted", Profile: "local", NonInteractive: true}, strings.NewReader(""), ioDiscard{})
if err == nil || !strings.Contains(err.Error(), "interrupted write") {
t.Fatalf("EnsureFiles() error = %v, want interrupted write", err)
}
directory := filepath.Join(root, "deploy", "interrupted")
for _, name := range []string{"thothii-installation.yaml", "operator.env"} {
if _, statErr := os.Stat(filepath.Join(directory, name)); !errors.Is(statErr, os.ErrNotExist) {
t.Fatalf("%s remains after interrupted write: %v", name, statErr)
}
}
}
func TestEnsureFilesCreatesSecretTemplatesOnlyAfterExplicitConfirmation(t *testing.T) {
root := newProject(t, "secret prompt")
var output bytes.Buffer
input := strings.Join([]string{
"demo", "local", "", "", "https://git.example.invalid/workspaces.git", "main", "https", "", "", "", "", "yes",
}, "\n") + "\n"
result, err := EnsureFiles(Request{ProjectRoot: root}, strings.NewReader(input), &output)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(output.String(), "Create blank secret-file templates") {
t.Fatalf("prompt = %q, want explicit secret-template confirmation", output.String())
}
for _, path := range []string{
filepath.Join(root, "deploy", "demo", "secrets", "thothii.secrets"),
filepath.Join(root, "deploy", "demo", "secrets", "pi-auth.json"),
filepath.Join(root, "deploy", "demo", "secrets", "workspace-git-credentials"),
filepath.Join(root, "deploy", "demo", "secrets", "workspace-git-ca.pem"),
} {
info, statErr := os.Stat(path)
if statErr != nil {
t.Fatalf("secret template %s: %v", path, statErr)
}
if info.Mode().Perm()&0o077 != 0 {
t.Errorf("secret template %s has permissions %o, want owner-only", path, info.Mode().Perm())
}
}
if _, err := os.Stat(result.DescriptorPath); err != nil {
t.Fatal(err)
}
}
func TestEnsureFilesRequiresExplicitNonInteractiveAnswers(t *testing.T) {
root := newProject(t, "noninteractive")
_, err := EnsureFiles(Request{ProjectRoot: root, InstallationID: "ci", Profile: "local", NonInteractive: true}, strings.NewReader(""), ioDiscard{})
if err == nil || !strings.Contains(err.Error(), "THT_SETUP_WORKSPACE_REMOTE") {
t.Fatalf("EnsureFiles() error = %v, want non-interactive environment guidance", err)
}
}
func TestEnsureFilesIncludesServerStorageLocations(t *testing.T) {
root := newProject(t, "server profile")
setNonInteractiveAnswers(t, newExternalSecrets(t, root))
result, err := EnsureFiles(Request{ProjectRoot: root, InstallationID: "server", Profile: "server", NonInteractive: true}, strings.NewReader(""), ioDiscard{})
if err != nil {
t.Fatal(err)
}
contents, err := os.ReadFile(result.EnvironmentPath)
if err != nil {
t.Fatal(err)
}
for _, name := range []string{"THT_DATA_ROOT", "THT_PI_STATE_ROOT", "THT_WORKSPACE_REGISTRY_ROOT", "THT_BACKUP_ROOT"} {
if !strings.Contains(string(contents), name+"=") {
t.Errorf("server configuration is missing %s: %s", name, contents)
}
}
}
func newProject(t *testing.T, name string) string {
t.Helper()
root := filepath.Join(t.TempDir(), name)
for _, path := range []string{
filepath.Join(root, "deploy", "env"),
filepath.Join(root, "deploy"),
filepath.Join(root, ".git"),
filepath.Join(root, "backend"),
filepath.Join(root, "frontend"),
filepath.Join(root, "harness"),
filepath.Join(root, "tools"),
} {
if err := os.MkdirAll(path, 0o700); err != nil {
t.Fatal(err)
}
}
for path, contents := range map[string]string{
filepath.Join(root, "compose.yaml"): "services: {}\n",
filepath.Join(root, "deploy", "compose.local.yaml"): "services: {}\n",
filepath.Join(root, "deploy", "compose.git-ssh.yaml"): "services: {}\n",
filepath.Join(root, "deploy", "compose.git-https.yaml"): "services: {}\n",
filepath.Join(root, "deploy", "env", "local.env.example"): "tracked example\n",
} {
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
}
canonical, err := filepath.EvalSymlinks(root)
if err != nil {
t.Fatal(err)
}
return canonical
}
type secretPaths struct {
secrets, piAuth, sshKey, knownHosts string
}
func newExternalSecrets(t *testing.T, root string) secretPaths {
t.Helper()
directory := filepath.Join(root, "external secrets")
if err := os.Mkdir(directory, 0o700); err != nil {
t.Fatal(err)
}
paths := secretPaths{
secrets: filepath.Join(directory, "thothii.secrets"), piAuth: filepath.Join(directory, "pi-auth.json"),
sshKey: filepath.Join(directory, "git-key"), knownHosts: filepath.Join(directory, "known-hosts"),
}
for path, contents := range map[string]string{
paths.secrets: "super-secret-value\n", paths.piAuth: "pi-secret-value\n", paths.sshKey: "private-key-value\n", paths.knownHosts: "git.example.invalid ssh-ed25519 AAAA\n",
} {
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
}
return paths
}
func setNonInteractiveAnswers(t *testing.T, paths secretPaths) {
t.Helper()
for name, value := range map[string]string{
"THT_SETUP_WORKSPACE_REMOTE": "git@git.example.invalid:team/workspaces.git",
"THT_SETUP_WORKSPACE_BRANCH": "main",
"THT_SETUP_WORKSPACE_ACCESS": "ssh",
"THT_SETUP_SECRETS_FILE": paths.secrets,
"THT_SETUP_PI_AUTH_FILE": paths.piAuth,
"THT_SETUP_GIT_SSH_KEY_FILE": paths.sshKey,
"THT_SETUP_GIT_KNOWN_HOSTS_FILE": paths.knownHosts,
"THT_SETUP_DWH_REST_URL": "https://dwh.example.invalid",
"THT_SETUP_LLM_URL": "https://llm.example.invalid",
} {
t.Setenv(name, value)
}
}
type ioDiscard struct{}
func (ioDiscard) Write(value []byte) (int, error) { return len(value), nil }