mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
Cover loadOrCreateBootstrapToken failure path. Coverage: initializeBootstrapToken 92% -> 100%
577 lines
16 KiB
Go
577 lines
16 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
)
|
|
|
|
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)
|
|
}
|
|
if token == "" {
|
|
t.Error("generateBootstrapToken() returned empty string")
|
|
}
|
|
|
|
// Test token length (24 bytes = 48 hex characters)
|
|
if len(token) != 48 {
|
|
t.Errorf("generateBootstrapToken() length = %d, want 48", len(token))
|
|
}
|
|
|
|
// 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 TestBootstrapTokenValid(t *testing.T) {
|
|
t.Run("nil router returns false", func(t *testing.T) {
|
|
var r *Router = nil
|
|
if r.bootstrapTokenValid("anything") {
|
|
t.Error("expected false for nil router")
|
|
}
|
|
})
|
|
|
|
t.Run("empty hash returns false", func(t *testing.T) {
|
|
r := &Router{bootstrapTokenHash: ""}
|
|
if r.bootstrapTokenValid("anything") {
|
|
t.Error("expected false when bootstrapTokenHash is empty")
|
|
}
|
|
})
|
|
|
|
t.Run("empty token returns false", func(t *testing.T) {
|
|
// Generate a token and create a router with its hash
|
|
token, err := generateBootstrapToken()
|
|
if err != nil {
|
|
t.Fatalf("generateBootstrapToken() error: %v", err)
|
|
}
|
|
r := &Router{}
|
|
// Create a token and store its hash - use loadOrCreateBootstrapToken to get a hash indirectly
|
|
tmpDir := t.TempDir()
|
|
loadedToken, _, _, err := loadOrCreateBootstrapToken(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
|
|
}
|
|
// Since we can't directly access HashAPIToken, we need to use initializeBootstrapToken
|
|
// Instead, let's test with a known hash by using the auth package
|
|
_ = token
|
|
_ = loadedToken
|
|
// For this test, we just need any non-empty hash
|
|
r.bootstrapTokenHash = "somehash"
|
|
|
|
if r.bootstrapTokenValid("") {
|
|
t.Error("expected false for empty token")
|
|
}
|
|
if r.bootstrapTokenValid(" ") {
|
|
t.Error("expected false for whitespace-only token")
|
|
}
|
|
})
|
|
|
|
t.Run("valid token returns true", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
token, _, _, err := loadOrCreateBootstrapToken(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
|
|
}
|
|
|
|
// Create a router that simulates having loaded the token
|
|
// We need to use the auth package to hash the token
|
|
cfg := &config.Config{DataPath: tmpDir}
|
|
r := &Router{config: cfg}
|
|
r.initializeBootstrapToken()
|
|
|
|
if !r.bootstrapTokenValid(token) {
|
|
t.Error("expected true for valid token")
|
|
}
|
|
})
|
|
|
|
t.Run("invalid token returns false", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
_, _, _, err := loadOrCreateBootstrapToken(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{DataPath: tmpDir}
|
|
r := &Router{config: cfg}
|
|
r.initializeBootstrapToken()
|
|
|
|
if r.bootstrapTokenValid("wrongtoken") {
|
|
t.Error("expected false for wrong token")
|
|
}
|
|
})
|
|
|
|
t.Run("token with whitespace is trimmed", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
token, _, _, err := loadOrCreateBootstrapToken(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{DataPath: tmpDir}
|
|
r := &Router{config: cfg}
|
|
r.initializeBootstrapToken()
|
|
|
|
// Token with leading/trailing whitespace should still validate
|
|
if !r.bootstrapTokenValid(" " + token + " ") {
|
|
t.Error("expected true for token with surrounding whitespace")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestInitializeBootstrapToken_NilRouter(t *testing.T) {
|
|
var r *Router = nil
|
|
// Should not panic
|
|
r.initializeBootstrapToken()
|
|
}
|
|
|
|
func TestInitializeBootstrapToken_NilConfig(t *testing.T) {
|
|
r := &Router{config: nil}
|
|
// Should not panic
|
|
r.initializeBootstrapToken()
|
|
}
|
|
|
|
func TestInitializeBootstrapToken_LoadOrCreateFails(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 a file where the data path expects a directory
|
|
tmpDir := t.TempDir()
|
|
blockingFile := filepath.Join(tmpDir, "blocked")
|
|
if err := os.WriteFile(blockingFile, []byte("block"), 0o600); err != nil {
|
|
t.Fatalf("failed to create blocking file: %v", err)
|
|
}
|
|
|
|
// DataPath points to a file, so MkdirAll will fail inside loadOrCreateBootstrapToken
|
|
cfg := &config.Config{DataPath: blockingFile}
|
|
r := &Router{config: cfg}
|
|
|
|
// Should not panic, but should fail and leave bootstrapTokenHash empty
|
|
r.initializeBootstrapToken()
|
|
|
|
if r.bootstrapTokenHash != "" {
|
|
t.Errorf("expected empty bootstrapTokenHash when loadOrCreateBootstrapToken fails, got %q", r.bootstrapTokenHash)
|
|
}
|
|
if r.bootstrapTokenPath != "" {
|
|
t.Errorf("expected empty bootstrapTokenPath when loadOrCreateBootstrapToken fails, got %q", r.bootstrapTokenPath)
|
|
}
|
|
}
|
|
|
|
func TestInitializeBootstrapToken_OIDCEnabled(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create a bootstrap token file first
|
|
tokenPath := filepath.Join(tmpDir, bootstrapTokenFilename)
|
|
if err := os.WriteFile(tokenPath, []byte("testtoken\n"), 0o600); err != nil {
|
|
t.Fatalf("failed to create token file: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{
|
|
DataPath: tmpDir,
|
|
OIDC: &config.OIDCConfig{
|
|
Enabled: true,
|
|
},
|
|
}
|
|
r := &Router{
|
|
config: cfg,
|
|
bootstrapTokenPath: tokenPath, // Set path so clearBootstrapToken can delete the file
|
|
}
|
|
r.initializeBootstrapToken()
|
|
|
|
// Token should be cleared when OIDC is enabled
|
|
if r.bootstrapTokenHash != "" {
|
|
t.Error("expected empty bootstrapTokenHash when OIDC is enabled")
|
|
}
|
|
|
|
// Token file should be removed
|
|
if _, err := os.Stat(tokenPath); !os.IsNotExist(err) {
|
|
t.Error("expected token file to be deleted when OIDC is enabled")
|
|
}
|
|
}
|
|
|
|
func TestHandleValidateBootstrapToken_InvalidJSON(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
cfg := &config.Config{DataPath: tmpDir}
|
|
r := &Router{config: cfg}
|
|
r.initializeBootstrapToken()
|
|
|
|
// Test invalid JSON body triggers json.Decode error
|
|
req := httptest.NewRequest(http.MethodPost, "/api/security/validate-bootstrap-token", strings.NewReader("not valid json"))
|
|
rr := httptest.NewRecorder()
|
|
|
|
r.handleValidateBootstrapToken(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400 for invalid JSON, got %d", rr.Code)
|
|
}
|
|
if !strings.Contains(rr.Body.String(), "Invalid request payload") {
|
|
t.Errorf("expected 'Invalid request payload' error, got %q", rr.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestClearBootstrapToken(t *testing.T) {
|
|
t.Run("nil router does not panic", func(t *testing.T) {
|
|
var r *Router = nil
|
|
// Should not panic
|
|
r.clearBootstrapToken()
|
|
})
|
|
|
|
t.Run("clears token hash and path", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
_, _, tokenPath, err := loadOrCreateBootstrapToken(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("loadOrCreateBootstrapToken() error: %v", err)
|
|
}
|
|
|
|
r := &Router{
|
|
bootstrapTokenHash: "somehash",
|
|
bootstrapTokenPath: tokenPath,
|
|
}
|
|
|
|
r.clearBootstrapToken()
|
|
|
|
if r.bootstrapTokenHash != "" {
|
|
t.Errorf("expected empty bootstrapTokenHash, got %q", r.bootstrapTokenHash)
|
|
}
|
|
if r.bootstrapTokenPath != "" {
|
|
t.Errorf("expected empty bootstrapTokenPath, got %q", r.bootstrapTokenPath)
|
|
}
|
|
|
|
// Verify file was deleted
|
|
if _, err := os.Stat(tokenPath); !os.IsNotExist(err) {
|
|
t.Error("expected token file to be deleted")
|
|
}
|
|
})
|
|
|
|
t.Run("handles missing file gracefully", func(t *testing.T) {
|
|
r := &Router{
|
|
bootstrapTokenHash: "somehash",
|
|
bootstrapTokenPath: "/nonexistent/path/token",
|
|
}
|
|
|
|
// Should not panic, should clear the hash and path
|
|
r.clearBootstrapToken()
|
|
|
|
if r.bootstrapTokenHash != "" {
|
|
t.Errorf("expected empty bootstrapTokenHash, got %q", r.bootstrapTokenHash)
|
|
}
|
|
if r.bootstrapTokenPath != "" {
|
|
t.Errorf("expected empty bootstrapTokenPath, got %q", r.bootstrapTokenPath)
|
|
}
|
|
})
|
|
|
|
t.Run("handles empty path", func(t *testing.T) {
|
|
r := &Router{
|
|
bootstrapTokenHash: "somehash",
|
|
bootstrapTokenPath: "",
|
|
}
|
|
|
|
// Should not panic
|
|
r.clearBootstrapToken()
|
|
|
|
if r.bootstrapTokenHash != "" {
|
|
t.Errorf("expected empty bootstrapTokenHash, got %q", r.bootstrapTokenHash)
|
|
}
|
|
})
|
|
|
|
t.Run("remove failure logs warning but clears fields", func(t *testing.T) {
|
|
// Point to a directory with contents - os.Remove will fail with ENOTEMPTY
|
|
tmpDir := t.TempDir()
|
|
subFile := filepath.Join(tmpDir, "subfile")
|
|
if err := os.WriteFile(subFile, []byte("test"), 0o600); err != nil {
|
|
t.Fatalf("failed to create subfile: %v", err)
|
|
}
|
|
|
|
r := &Router{
|
|
bootstrapTokenHash: "somehash",
|
|
bootstrapTokenPath: tmpDir, // Point to directory, not file
|
|
}
|
|
|
|
// Should not panic and should clear fields even if remove fails
|
|
r.clearBootstrapToken()
|
|
|
|
if r.bootstrapTokenHash != "" {
|
|
t.Errorf("expected empty bootstrapTokenHash, got %q", r.bootstrapTokenHash)
|
|
}
|
|
if r.bootstrapTokenPath != "" {
|
|
t.Errorf("expected empty bootstrapTokenPath, got %q", r.bootstrapTokenPath)
|
|
}
|
|
})
|
|
}
|