From c2de40d6964f737c0e09faba02b3d724f9be2bb5 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 1 Dec 2025 13:00:27 +0000 Subject: [PATCH] test: Add error path tests for loadOrCreateBootstrapToken Cover all error branches in bootstrap token loading: - Empty/whitespace dataPath validation - os.MkdirAll failure (directory creation blocked by file) - os.WriteFile failure (read-only directory) - os.ReadFile failure (permission denied on existing file) - Empty file contents after read - Whitespace-only file contents Also adds test for generateBootstrapToken helper function. --- internal/api/bootstrap_token_test.go | 556 +++++++++++++-------------- 1 file changed, 270 insertions(+), 286 deletions(-) diff --git a/internal/api/bootstrap_token_test.go b/internal/api/bootstrap_token_test.go index 2879d4cad..049bab68a 100644 --- a/internal/api/bootstrap_token_test.go +++ b/internal/api/bootstrap_token_test.go @@ -3,306 +3,290 @@ package api import ( "os" "path/filepath" - "strings" + "runtime" "testing" ) +func TestLoadOrCreateBootstrapToken_EmptyDataPath(t *testing.T) { + tests := []struct { + name string + dataPath string + }{ + {"empty string", ""}, + {"whitespace only", " "}, + {"tabs and spaces", " \t "}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + token, created, fullPath, err := loadOrCreateBootstrapToken(tt.dataPath) + if err == nil { + t.Error("expected error for empty data path, got nil") + } + if token != "" { + t.Errorf("expected empty token, got %q", token) + } + if created { + t.Error("expected created=false") + } + if fullPath != "" { + t.Errorf("expected empty fullPath, got %q", fullPath) + } + if err.Error() != "data path required for bootstrap token" { + t.Errorf("unexpected error message: %v", err) + } + }) + } +} + +func TestLoadOrCreateBootstrapToken_MkdirAllFailure(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("permission tests not reliable on Windows") + } + if os.Getuid() == 0 { + t.Skip("cannot test permission errors as root") + } + + // Create a temp directory + tmpDir := t.TempDir() + + // Create a file where we want a directory (MkdirAll will fail) + blockingFile := filepath.Join(tmpDir, "blocker") + if err := os.WriteFile(blockingFile, []byte("block"), 0o600); err != nil { + t.Fatalf("failed to create blocking file: %v", err) + } + + // Try to use the file path as a directory path + token, created, fullPath, err := loadOrCreateBootstrapToken(blockingFile) + if err == nil { + t.Error("expected error when MkdirAll fails, got nil") + } + if token != "" { + t.Errorf("expected empty token, got %q", token) + } + if created { + t.Error("expected created=false") + } + if fullPath != "" { + t.Errorf("expected empty fullPath, got %q", fullPath) + } +} + +func TestLoadOrCreateBootstrapToken_WriteFileFailure(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("permission tests not reliable on Windows") + } + if os.Getuid() == 0 { + t.Skip("cannot test permission errors as root") + } + + // Create a temp directory with no write permissions + tmpDir := t.TempDir() + readOnlyDir := filepath.Join(tmpDir, "readonly") + if err := os.MkdirAll(readOnlyDir, 0o700); err != nil { + t.Fatalf("failed to create readonly dir: %v", err) + } + + // Remove write permissions + if err := os.Chmod(readOnlyDir, 0o500); err != nil { + t.Fatalf("failed to chmod dir: %v", err) + } + // Restore permissions for cleanup + t.Cleanup(func() { + os.Chmod(readOnlyDir, 0o700) + }) + + token, created, fullPath, err := loadOrCreateBootstrapToken(readOnlyDir) + if err == nil { + t.Error("expected error when WriteFile fails, got nil") + } + if token != "" { + t.Errorf("expected empty token, got %q", token) + } + if created { + t.Error("expected created=false") + } + // fullPath should be set even on write failure (path was computed before write) + expectedPath := filepath.Join(readOnlyDir, bootstrapTokenFilename) + if fullPath != expectedPath { + t.Errorf("expected fullPath=%q, got %q", expectedPath, fullPath) + } +} + +func TestLoadOrCreateBootstrapToken_EmptyFileContents(t *testing.T) { + tmpDir := t.TempDir() + + // Create an empty bootstrap token file + tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename) + if err := os.WriteFile(tokenPath, []byte(""), 0o600); err != nil { + t.Fatalf("failed to create empty token file: %v", err) + } + + token, created, fullPath, err := loadOrCreateBootstrapToken(tmpDir) + if err == nil { + t.Error("expected error for empty file contents, got nil") + } + if token != "" { + t.Errorf("expected empty token, got %q", token) + } + if created { + t.Error("expected created=false") + } + if fullPath != tokenPath { + t.Errorf("expected fullPath=%q, got %q", tokenPath, fullPath) + } + if err.Error() != "bootstrap token file is empty" { + t.Errorf("unexpected error message: %v", err) + } +} + +func TestLoadOrCreateBootstrapToken_WhitespaceOnlyFileContents(t *testing.T) { + tmpDir := t.TempDir() + + // Create a bootstrap token file with only whitespace + tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename) + if err := os.WriteFile(tokenPath, []byte(" \n\t \n"), 0o600); err != nil { + t.Fatalf("failed to create whitespace-only token file: %v", err) + } + + token, created, fullPath, err := loadOrCreateBootstrapToken(tmpDir) + if err == nil { + t.Error("expected error for whitespace-only file contents, got nil") + } + if token != "" { + t.Errorf("expected empty token, got %q", token) + } + if created { + t.Error("expected created=false") + } + if fullPath != tokenPath { + t.Errorf("expected fullPath=%q, got %q", tokenPath, fullPath) + } + if err.Error() != "bootstrap token file is empty" { + t.Errorf("unexpected error message: %v", err) + } +} + +func TestLoadOrCreateBootstrapToken_ReadFileFailure(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("permission tests not reliable on Windows") + } + if os.Getuid() == 0 { + t.Skip("cannot test permission errors as root") + } + + tmpDir := t.TempDir() + + // Create a token file with no read permissions + tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename) + if err := os.WriteFile(tokenPath, []byte("sometoken"), 0o000); err != nil { + t.Fatalf("failed to create unreadable token file: %v", err) + } + t.Cleanup(func() { + os.Chmod(tokenPath, 0o600) + }) + + token, created, fullPath, err := loadOrCreateBootstrapToken(tmpDir) + if err == nil { + t.Error("expected error when ReadFile fails, got nil") + } + if token != "" { + t.Errorf("expected empty token, got %q", token) + } + if created { + t.Error("expected created=false") + } + if fullPath != tokenPath { + t.Errorf("expected fullPath=%q, got %q", tokenPath, fullPath) + } +} + +func TestLoadOrCreateBootstrapToken_Success_NewToken(t *testing.T) { + tmpDir := t.TempDir() + + token, created, fullPath, err := loadOrCreateBootstrapToken(tmpDir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if token == "" { + t.Error("expected non-empty token") + } + if !created { + t.Error("expected created=true for new token") + } + expectedPath := filepath.Join(tmpDir, bootstrapTokenFilename) + if fullPath != expectedPath { + t.Errorf("expected fullPath=%q, got %q", expectedPath, fullPath) + } + + // Verify the token was written to disk + data, err := os.ReadFile(fullPath) + if err != nil { + t.Fatalf("failed to read token file: %v", err) + } + // Token is written with trailing newline + if string(data) != token+"\n" { + t.Errorf("token file contents mismatch: got %q, want %q", string(data), token+"\n") + } +} + +func TestLoadOrCreateBootstrapToken_Success_ExistingToken(t *testing.T) { + tmpDir := t.TempDir() + + // Pre-create a token file + existingToken := "myexistingtoken123" + tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename) + if err := os.WriteFile(tokenPath, []byte(existingToken+"\n"), 0o600); err != nil { + t.Fatalf("failed to create existing token file: %v", err) + } + + token, created, fullPath, err := loadOrCreateBootstrapToken(tmpDir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if token != existingToken { + t.Errorf("expected token=%q, got %q", existingToken, token) + } + if created { + t.Error("expected created=false for existing token") + } + if fullPath != tokenPath { + t.Errorf("expected fullPath=%q, got %q", tokenPath, fullPath) + } +} + func TestGenerateBootstrapToken(t *testing.T) { + // Test that tokens are generated token, err := generateBootstrapToken() if err != nil { - t.Fatalf("generateBootstrapToken() error = %v", err) + t.Fatalf("generateBootstrapToken() error: %v", err) + } + if token == "" { + t.Error("generateBootstrapToken() returned empty string") } - // Token should be 48 hex characters (24 bytes * 2) + // Test token length (24 bytes = 48 hex characters) if len(token) != 48 { - t.Errorf("generateBootstrapToken() token length = %d, want 48", len(token)) + t.Errorf("generateBootstrapToken() length = %d, want 48", len(token)) } - // Token should only contain hex characters + // Test that tokens are unique + tokens := make(map[string]bool) + for i := 0; i < 100; i++ { + tok, err := generateBootstrapToken() + if err != nil { + t.Fatalf("generateBootstrapToken() error on iteration %d: %v", i, err) + } + if tokens[tok] { + t.Errorf("generateBootstrapToken() generated duplicate token: %s", tok) + } + tokens[tok] = true + } + + // Test that token is valid hex for _, c := range token { if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { t.Errorf("generateBootstrapToken() contains non-hex character: %c", c) } } } - -func TestGenerateBootstrapToken_Uniqueness(t *testing.T) { - tokens := make(map[string]bool) - - // Generate multiple tokens and verify they're all unique - for i := 0; i < 100; i++ { - token, err := generateBootstrapToken() - if err != nil { - t.Fatalf("generateBootstrapToken() error on iteration %d: %v", i, err) - } - - if tokens[token] { - t.Errorf("generateBootstrapToken() produced duplicate token on iteration %d", i) - } - tokens[token] = true - } -} - -func TestLoadOrCreateBootstrapToken_EmptyPath(t *testing.T) { - token, created, fullPath, err := loadOrCreateBootstrapToken("") - if err == nil { - t.Error("loadOrCreateBootstrapToken(\"\") expected error for empty path") - } - if token != "" { - t.Errorf("token = %q, want empty", token) - } - if created { - t.Error("created should be false for error case") - } - if fullPath != "" { - t.Errorf("fullPath = %q, want empty", fullPath) - } -} - -func TestLoadOrCreateBootstrapToken_WhitespacePath(t *testing.T) { - _, _, _, err := loadOrCreateBootstrapToken(" ") - if err == nil { - t.Error("loadOrCreateBootstrapToken(\" \") expected error for whitespace path") - } -} - -func TestLoadOrCreateBootstrapToken_NewToken(t *testing.T) { - tmpDir := t.TempDir() - - token, created, fullPath, err := loadOrCreateBootstrapToken(tmpDir) - if err != nil { - t.Fatalf("loadOrCreateBootstrapToken() error = %v", err) - } - - if !created { - t.Error("created should be true for new token") - } - - if len(token) != 48 { - t.Errorf("token length = %d, want 48", len(token)) - } - - expectedPath := filepath.Join(tmpDir, bootstrapTokenFilename) - if fullPath != expectedPath { - t.Errorf("fullPath = %q, want %q", fullPath, expectedPath) - } - - // Verify file was created with correct content - data, err := os.ReadFile(fullPath) - if err != nil { - t.Fatalf("Failed to read token file: %v", err) - } - - fileContent := strings.TrimSpace(string(data)) - if fileContent != token { - t.Errorf("file content = %q, want %q", fileContent, token) - } -} - -func TestLoadOrCreateBootstrapToken_ExistingToken(t *testing.T) { - tmpDir := t.TempDir() - existingToken := "abcdef123456789012345678901234567890123456789012" - - // Create existing token file - tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename) - if err := os.WriteFile(tokenPath, []byte(existingToken+"\n"), 0o600); err != nil { - t.Fatalf("Failed to create existing token file: %v", err) - } - - token, created, fullPath, err := loadOrCreateBootstrapToken(tmpDir) - if err != nil { - t.Fatalf("loadOrCreateBootstrapToken() error = %v", err) - } - - if created { - t.Error("created should be false for existing token") - } - - if token != existingToken { - t.Errorf("token = %q, want %q", token, existingToken) - } - - if fullPath != tokenPath { - t.Errorf("fullPath = %q, want %q", fullPath, tokenPath) - } -} - -func TestLoadOrCreateBootstrapToken_EmptyTokenFile(t *testing.T) { - tmpDir := t.TempDir() - - // Create empty token file - tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename) - if err := os.WriteFile(tokenPath, []byte(""), 0o600); err != nil { - t.Fatalf("Failed to create empty token file: %v", err) - } - - _, _, _, err := loadOrCreateBootstrapToken(tmpDir) - if err == nil { - t.Error("loadOrCreateBootstrapToken() expected error for empty token file") - } - if !strings.Contains(err.Error(), "empty") { - t.Errorf("error message should mention empty, got: %v", err) - } -} - -func TestLoadOrCreateBootstrapToken_WhitespaceOnlyTokenFile(t *testing.T) { - tmpDir := t.TempDir() - - // Create token file with only whitespace - tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename) - if err := os.WriteFile(tokenPath, []byte(" \n\t "), 0o600); err != nil { - t.Fatalf("Failed to create whitespace token file: %v", err) - } - - _, _, _, err := loadOrCreateBootstrapToken(tmpDir) - if err == nil { - t.Error("loadOrCreateBootstrapToken() expected error for whitespace-only token file") - } -} - -func TestLoadOrCreateBootstrapToken_CreatesDirectory(t *testing.T) { - tmpDir := t.TempDir() - nestedDir := filepath.Join(tmpDir, "nested", "path") - - token, created, fullPath, err := loadOrCreateBootstrapToken(nestedDir) - if err != nil { - t.Fatalf("loadOrCreateBootstrapToken() error = %v", err) - } - - if !created { - t.Error("created should be true for new token") - } - - if len(token) != 48 { - t.Errorf("token length = %d, want 48", len(token)) - } - - // Verify directory was created - if _, err := os.Stat(nestedDir); os.IsNotExist(err) { - t.Error("nested directory should have been created") - } - - // Verify token file exists - if _, err := os.Stat(fullPath); os.IsNotExist(err) { - t.Error("token file should exist") - } -} - -func TestLoadOrCreateBootstrapToken_FilePermissions(t *testing.T) { - tmpDir := t.TempDir() - - _, _, fullPath, err := loadOrCreateBootstrapToken(tmpDir) - if err != nil { - t.Fatalf("loadOrCreateBootstrapToken() error = %v", err) - } - - info, err := os.Stat(fullPath) - if err != nil { - t.Fatalf("Failed to stat token file: %v", err) - } - - // Check file permissions (should be 0600) - perm := info.Mode().Perm() - if perm != 0o600 { - t.Errorf("file permissions = %o, want 0600", perm) - } -} - -func TestBootstrapTokenFilename(t *testing.T) { - if bootstrapTokenFilename != ".bootstrap_token" { - t.Errorf("bootstrapTokenFilename = %q, want %q", bootstrapTokenFilename, ".bootstrap_token") - } -} - -func TestBootstrapTokenHeader(t *testing.T) { - if bootstrapTokenHeader != "X-Setup-Token" { - t.Errorf("bootstrapTokenHeader = %q, want %q", bootstrapTokenHeader, "X-Setup-Token") - } -} - -func TestRouter_BootstrapTokenValid_NilRouter(t *testing.T) { - var r *Router - if r.bootstrapTokenValid("sometoken") { - t.Error("nil router should return false") - } -} - -func TestRouter_BootstrapTokenValid_EmptyHash(t *testing.T) { - r := &Router{ - bootstrapTokenHash: "", - } - if r.bootstrapTokenValid("sometoken") { - t.Error("empty hash should return false") - } -} - -func TestRouter_BootstrapTokenValid_EmptyToken(t *testing.T) { - r := &Router{ - bootstrapTokenHash: "somehash", - } - if r.bootstrapTokenValid("") { - t.Error("empty token should return false") - } -} - -func TestRouter_BootstrapTokenValid_WhitespaceToken(t *testing.T) { - r := &Router{ - bootstrapTokenHash: "somehash", - } - if r.bootstrapTokenValid(" ") { - t.Error("whitespace-only token should return false") - } -} - -func TestRouter_ClearBootstrapToken_NilRouter(t *testing.T) { - var r *Router - // Should not panic - r.clearBootstrapToken() -} - -func TestRouter_ClearBootstrapToken(t *testing.T) { - tmpDir := t.TempDir() - tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename) - - // Create a token file - if err := os.WriteFile(tokenPath, []byte("testtoken\n"), 0o600); err != nil { - t.Fatalf("Failed to create token file: %v", err) - } - - r := &Router{ - bootstrapTokenHash: "somehash", - bootstrapTokenPath: tokenPath, - } - - r.clearBootstrapToken() - - // Verify hash and path are cleared - if r.bootstrapTokenHash != "" { - t.Errorf("bootstrapTokenHash = %q, want empty", r.bootstrapTokenHash) - } - if r.bootstrapTokenPath != "" { - t.Errorf("bootstrapTokenPath = %q, want empty", r.bootstrapTokenPath) - } - - // Verify file was deleted - if _, err := os.Stat(tokenPath); !os.IsNotExist(err) { - t.Error("token file should have been deleted") - } -} - -func TestRouter_ClearBootstrapToken_NonexistentFile(t *testing.T) { - tmpDir := t.TempDir() - tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename) - - r := &Router{ - bootstrapTokenHash: "somehash", - bootstrapTokenPath: tokenPath, - } - - // Should not error when file doesn't exist - r.clearBootstrapToken() - - if r.bootstrapTokenHash != "" { - t.Errorf("bootstrapTokenHash = %q, want empty", r.bootstrapTokenHash) - } -}