This repository has been archived on 2026-03-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
greywall/internal/templates/templates_test.go
2026-01-05 17:23:14 -08:00

562 lines
14 KiB
Go

package templates
import (
"os"
"path/filepath"
"testing"
"github.com/Use-Tusk/fence/internal/config"
)
func TestList(t *testing.T) {
templates := List()
if len(templates) == 0 {
t.Fatal("expected at least one template")
}
// Check that code template exists
found := false
for _, tmpl := range templates {
if tmpl.Name == "code" {
found = true
if tmpl.Description == "" {
t.Error("code template should have a description")
}
break
}
}
if !found {
t.Error("code template not found")
}
}
func TestLoad(t *testing.T) {
tests := []struct {
name string
wantErr bool
}{
{"code", false},
{"disable-telemetry", false},
{"git-readonly", false},
{"local-dev-server", false},
{"nonexistent", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := Load(tt.name)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if cfg == nil {
t.Error("expected config, got nil")
}
}
})
}
}
func TestLoadWithJsonExtension(t *testing.T) {
// Should work with or without .json extension
cfg1, err := Load("disable-telemetry")
if err != nil {
t.Fatalf("failed to load disable-telemetry: %v", err)
}
cfg2, err := Load("disable-telemetry.json")
if err != nil {
t.Fatalf("failed to load disable-telemetry.json: %v", err)
}
// Both should return valid configs
if cfg1 == nil || cfg2 == nil {
t.Error("expected both configs to be non-nil")
}
}
func TestExists(t *testing.T) {
if !Exists("code") {
t.Error("code template should exist")
}
if Exists("nonexistent") {
t.Error("nonexistent should not exist")
}
}
func TestCodeTemplate(t *testing.T) {
cfg, err := Load("code")
if err != nil {
t.Fatalf("failed to load code template: %v", err)
}
// Verify key settings
if !cfg.AllowPty {
t.Error("code template should have AllowPty=true")
}
if len(cfg.Network.AllowedDomains) == 0 {
t.Error("code template should have allowed domains")
}
// Check that *.anthropic.com is in allowed domains
found := false
for _, domain := range cfg.Network.AllowedDomains {
if domain == "*.anthropic.com" {
found = true
break
}
}
if !found {
t.Error("*.anthropic.com should be in allowed domains")
}
// Check that cloud metadata domains are denied
if len(cfg.Network.DeniedDomains) == 0 {
t.Error("code template should have denied domains")
}
// Check command deny list
if len(cfg.Command.Deny) == 0 {
t.Error("code template should have denied commands")
}
}
func TestCodeRelaxedTemplate(t *testing.T) {
cfg, err := Load("code-relaxed")
if err != nil {
t.Fatalf("failed to load code-relaxed template: %v", err)
}
// Should inherit AllowPty from code template
if !cfg.AllowPty {
t.Error("code-relaxed should inherit AllowPty=true from code")
}
// Should have wildcard in allowed domains
hasWildcard := false
for _, domain := range cfg.Network.AllowedDomains {
if domain == "*" {
hasWildcard = true
break
}
}
if !hasWildcard {
t.Error("code-relaxed should have '*' in allowed domains")
}
// Should inherit denied domains from code
if len(cfg.Network.DeniedDomains) == 0 {
t.Error("code-relaxed should inherit denied domains from code")
}
// Should inherit filesystem config from code
if len(cfg.Filesystem.AllowWrite) == 0 {
t.Error("code-relaxed should inherit allowWrite from code")
}
if len(cfg.Filesystem.DenyRead) == 0 {
t.Error("code-relaxed should inherit denyRead from code")
}
if len(cfg.Filesystem.DenyWrite) == 0 {
t.Error("code-relaxed should inherit denyWrite from code")
}
// Should inherit command config from code
if len(cfg.Command.Deny) == 0 {
t.Error("code-relaxed should inherit command deny list from code")
}
// Extends should be cleared after resolution
if cfg.Extends != "" {
t.Error("extends should be cleared after loading")
}
}
func TestResolveExtends(t *testing.T) {
t.Run("nil config", func(t *testing.T) {
result, err := ResolveExtends(nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != nil {
t.Error("expected nil result for nil input")
}
})
t.Run("no extends", func(t *testing.T) {
cfg := &config.Config{
AllowPty: true,
Network: config.NetworkConfig{
AllowedDomains: []string{"example.com"},
},
}
result, err := ResolveExtends(cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != cfg {
t.Error("expected same config when no extends")
}
})
t.Run("extends code template", func(t *testing.T) {
cfg := &config.Config{
Extends: "code",
Network: config.NetworkConfig{
AllowedDomains: []string{"private-registry.company.com"},
},
}
result, err := ResolveExtends(cfg)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Should have merged config
if result.Extends != "" {
t.Error("extends should be cleared after resolution")
}
// Should have AllowPty from base template
if !result.AllowPty {
t.Error("should inherit AllowPty from code template")
}
// Should have domains from both
hasPrivateRegistry := false
hasAnthropic := false
for _, domain := range result.Network.AllowedDomains {
if domain == "private-registry.company.com" {
hasPrivateRegistry = true
}
if domain == "*.anthropic.com" {
hasAnthropic = true
}
}
if !hasPrivateRegistry {
t.Error("should have private-registry.company.com from override")
}
if !hasAnthropic {
t.Error("should have *.anthropic.com from base template")
}
})
t.Run("extends nonexistent template", func(t *testing.T) {
cfg := &config.Config{
Extends: "nonexistent-template",
}
_, err := ResolveExtends(cfg)
if err == nil {
t.Error("expected error for nonexistent template")
}
})
}
func TestExtendsChainDepth(t *testing.T) {
// This tests that the maxExtendsDepth limit is respected.
// We can't easily create a deep chain with embedded templates,
// but we can test that the code template (which has no extends)
// loads correctly.
cfg, err := Load("code")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if cfg == nil {
t.Error("expected non-nil config")
}
}
func TestIsPath(t *testing.T) {
tests := []struct {
input string
want bool
}{
// Template names (not paths)
{"code", false},
{"npm-install", false},
{"my-template", false},
// Absolute paths
{"/path/to/config.json", true},
{"/etc/fence/base.json", true},
// Relative paths
{"./base.json", true},
{"../shared/base.json", true},
{"configs/base.json", true},
// Windows-style paths
{"C:\\path\\to\\config.json", true},
{".\\base.json", true},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := isPath(tt.input)
if got != tt.want {
t.Errorf("isPath(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
func TestExtendsFilePath(t *testing.T) {
// Create temp directory for test files
tmpDir := t.TempDir()
t.Run("extends absolute path", func(t *testing.T) {
// Create base config file
baseContent := `{
"network": {
"allowedDomains": ["base.example.com"]
},
"filesystem": {
"allowWrite": ["/tmp"]
}
}`
basePath := filepath.Join(tmpDir, "base.json")
if err := os.WriteFile(basePath, []byte(baseContent), 0o600); err != nil {
t.Fatalf("failed to write base config: %v", err)
}
// Config that extends the base via absolute path
cfg := &config.Config{
Extends: basePath,
Network: config.NetworkConfig{
AllowedDomains: []string{"override.example.com"},
},
}
result, err := ResolveExtendsWithBaseDir(cfg, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Should have merged domains
if len(result.Network.AllowedDomains) != 2 {
t.Errorf("expected 2 domains, got %d: %v", len(result.Network.AllowedDomains), result.Network.AllowedDomains)
}
// Should have filesystem from base
if len(result.Filesystem.AllowWrite) != 1 || result.Filesystem.AllowWrite[0] != "/tmp" {
t.Errorf("expected AllowWrite [/tmp], got %v", result.Filesystem.AllowWrite)
}
})
t.Run("extends relative path", func(t *testing.T) {
// Create base config in subdir
subDir := filepath.Join(tmpDir, "configs")
if err := os.MkdirAll(subDir, 0o750); err != nil {
t.Fatalf("failed to create subdir: %v", err)
}
baseContent := `{
"allowPty": true,
"network": {
"allowedDomains": ["relative-base.example.com"]
}
}`
basePath := filepath.Join(subDir, "base.json")
if err := os.WriteFile(basePath, []byte(baseContent), 0o600); err != nil {
t.Fatalf("failed to write base config: %v", err)
}
// Config that extends via relative path
cfg := &config.Config{
Extends: "./configs/base.json",
Network: config.NetworkConfig{
AllowedDomains: []string{"child.example.com"},
},
}
result, err := ResolveExtendsWithBaseDir(cfg, tmpDir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Should inherit AllowPty
if !result.AllowPty {
t.Error("should inherit AllowPty from base")
}
// Should have merged domains
if len(result.Network.AllowedDomains) != 2 {
t.Errorf("expected 2 domains, got %d", len(result.Network.AllowedDomains))
}
})
t.Run("extends nonexistent file", func(t *testing.T) {
cfg := &config.Config{
Extends: "/nonexistent/path/config.json",
}
_, err := ResolveExtendsWithBaseDir(cfg, "")
if err == nil {
t.Error("expected error for nonexistent file")
}
})
t.Run("extends invalid JSON file", func(t *testing.T) {
invalidPath := filepath.Join(tmpDir, "invalid.json")
if err := os.WriteFile(invalidPath, []byte("{invalid json}"), 0o600); err != nil {
t.Fatalf("failed to write invalid config: %v", err)
}
cfg := &config.Config{
Extends: invalidPath,
}
_, err := ResolveExtendsWithBaseDir(cfg, "")
if err == nil {
t.Error("expected error for invalid JSON")
}
})
t.Run("extends file with invalid config", func(t *testing.T) {
// Create config with invalid domain pattern
invalidContent := `{
"network": {
"allowedDomains": ["*.com"]
}
}`
invalidPath := filepath.Join(tmpDir, "invalid-domain.json")
if err := os.WriteFile(invalidPath, []byte(invalidContent), 0o600); err != nil {
t.Fatalf("failed to write config: %v", err)
}
cfg := &config.Config{
Extends: invalidPath,
}
_, err := ResolveExtendsWithBaseDir(cfg, "")
if err == nil {
t.Error("expected error for invalid config")
}
})
t.Run("circular extends via files", func(t *testing.T) {
// Create two files that extend each other
fileA := filepath.Join(tmpDir, "a.json")
fileB := filepath.Join(tmpDir, "b.json")
contentA := `{"extends": "` + fileB + `"}`
contentB := `{"extends": "` + fileA + `"}`
if err := os.WriteFile(fileA, []byte(contentA), 0o600); err != nil {
t.Fatalf("failed to write a.json: %v", err)
}
if err := os.WriteFile(fileB, []byte(contentB), 0o600); err != nil {
t.Fatalf("failed to write b.json: %v", err)
}
cfg := &config.Config{
Extends: fileA,
}
_, err := ResolveExtendsWithBaseDir(cfg, "")
if err == nil {
t.Error("expected error for circular extends")
}
})
t.Run("nested extends chain", func(t *testing.T) {
// Create a chain: child -> middle -> base
baseContent := `{
"network": {
"allowedDomains": ["base.com"]
}
}`
basePath := filepath.Join(tmpDir, "chain-base.json")
if err := os.WriteFile(basePath, []byte(baseContent), 0o600); err != nil {
t.Fatalf("failed to write base: %v", err)
}
middleContent := `{
"extends": "` + basePath + `",
"network": {
"allowedDomains": ["middle.com"]
}
}`
middlePath := filepath.Join(tmpDir, "chain-middle.json")
if err := os.WriteFile(middlePath, []byte(middleContent), 0o600); err != nil {
t.Fatalf("failed to write middle: %v", err)
}
cfg := &config.Config{
Extends: middlePath,
Network: config.NetworkConfig{
AllowedDomains: []string{"child.com"},
},
}
result, err := ResolveExtendsWithBaseDir(cfg, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Should have all three domains
if len(result.Network.AllowedDomains) != 3 {
t.Errorf("expected 3 domains, got %d: %v", len(result.Network.AllowedDomains), result.Network.AllowedDomains)
}
})
t.Run("file extends template", func(t *testing.T) {
// Create a file that extends a built-in template
fileContent := `{
"extends": "code",
"network": {
"allowedDomains": ["extra.example.com"]
}
}`
filePath := filepath.Join(tmpDir, "extends-template.json")
if err := os.WriteFile(filePath, []byte(fileContent), 0o600); err != nil {
t.Fatalf("failed to write config: %v", err)
}
// Config that extends this file
cfg := &config.Config{
Extends: filePath,
Network: config.NetworkConfig{
AllowedDomains: []string{"top.example.com"},
},
}
result, err := ResolveExtendsWithBaseDir(cfg, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Should have AllowPty from code template
if !result.AllowPty {
t.Error("should inherit AllowPty from code template")
}
// Should have domains from all levels
hasAnthropic := false
hasExtra := false
hasTop := false
for _, domain := range result.Network.AllowedDomains {
switch domain {
case "*.anthropic.com":
hasAnthropic = true
case "extra.example.com":
hasExtra = true
case "top.example.com":
hasTop = true
}
}
if !hasAnthropic {
t.Error("should have *.anthropic.com from code template")
}
if !hasExtra {
t.Error("should have extra.example.com from middle file")
}
if !hasTop {
t.Error("should have top.example.com from top config")
}
})
}