Traefik_Control/backend/internal/config/file/service_test.go
2026-09-04 00:02:26 -05:00

347 lines
8.6 KiB
Go

package file
import (
"database/sql"
"os"
"path/filepath"
"sync"
"testing"
_ "github.com/mattn/go-sqlite3"
"github.com/traefik/traefik-gui/backend/internal/database/repositories"
)
func newTestService(t *testing.T) (*Service, string, func()) {
t.Helper()
dir, err := os.MkdirTemp("", "traefik-gui-test-*")
if err != nil {
t.Fatalf("temp dir: %v", err)
}
db, err := sql.Open("sqlite3", filepath.Join(dir, "test.db")+"?_foreign_keys=on")
if err != nil {
t.Fatalf("open db: %v", err)
}
// create backups table
_, err = db.Exec(`CREATE TABLE backups (
id TEXT PRIMARY KEY,
filename TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by TEXT NOT NULL,
reason TEXT NOT NULL
)`)
if err != nil {
t.Fatalf("create backups: %v", err)
}
auditRepo := repositories.NewAuditRepository(db)
svc, err := NewService(filepath.Join(dir, "dynamic"), db, auditRepo)
if err != nil {
t.Fatalf("new service: %v", err)
}
cleanup := func() {
db.Close()
os.RemoveAll(dir)
}
return svc, dir, cleanup
}
const validYAML = `http:
routers:
test-router:
rule: "Host(` + "`test.example.com`" + `)"
service: test-service
entryPoints: ["web"]
services:
test-service:
loadBalancer:
servers:
- url: "http://127.0.0.1:8080"
`
const validYAML2 = `http:
routers:
test-router2:
rule: "Host(` + "`test2.example.com`" + `)"
service: test-service2
entryPoints: ["web"]
services:
test-service2:
loadBalancer:
servers:
- url: "http://127.0.0.1:8081"
`
func TestValidateContent_Valid(t *testing.T) {
errs := ValidateContent("app.yml", validYAML)
if len(errs) != 0 {
t.Fatalf("expected no errors, got %v", errs)
}
}
func TestValidateContent_InvalidYAML(t *testing.T) {
invalid := "http:\n routers: [\ninvalid yaml"
errs := ValidateContent("app.yml", invalid)
if len(errs) == 0 {
t.Fatal("expected validation errors for invalid yaml")
}
}
func TestValidateContent_Empty(t *testing.T) {
errs := ValidateContent("app.yml", " ")
if len(errs) == 0 {
t.Fatal("expected error for empty content")
}
}
func TestValidateContent_NoTopLevel(t *testing.T) {
errs := ValidateContent("app.yml", "foo: bar\nbaz: qux\n")
if len(errs) == 0 {
t.Fatal("expected error for missing http/tcp/udp/tls")
}
}
func TestValidateContent_DangerousFilename(t *testing.T) {
errs := ValidateContent("../evil.yml", validYAML)
if len(errs) == 0 {
t.Fatal("expected error for path traversal filename")
}
errs = ValidateContent("app.txt", validYAML)
if len(errs) == 0 {
t.Fatal("expected error for wrong extension")
}
}
func TestPreview_Diff(t *testing.T) {
svc, _, cleanup := newTestService(t)
defer cleanup()
// Preview new file (no existing)
res := svc.Preview("app.yml", validYAML)
if !res.Valid {
t.Fatalf("preview should be valid, got errs %v", res.Errors)
}
if res.Diff == "" {
t.Fatal("expected diff for new file")
}
// Preview with same content -> no diff? Actually diff will be content vs empty, so diff present
// Second: after apply, preview same content should have empty diff
_, err := svc.Apply("app.yml", validYAML, "test-user", true)
if err != nil {
t.Fatalf("apply: %v", err)
}
res2 := svc.Preview("app.yml", validYAML)
if !res2.Valid {
t.Fatalf("preview2 valid %v", res2.Errors)
}
// Same content should give empty diff
if res2.Diff != "" {
t.Fatalf("expected empty diff for same content, got %q", res2.Diff)
}
// Different content should give diff
res3 := svc.Preview("app.yml", validYAML2)
if res3.Diff == "" {
t.Fatal("expected diff for changed content")
}
}
func TestAtomicWrite(t *testing.T) {
svc, _, cleanup := newTestService(t)
defer cleanup()
// Apply valid
res, err := svc.Apply("app.yml", validYAML, "user1", true)
if err != nil {
t.Fatalf("apply err %v", err)
}
if !res.Valid {
t.Fatalf("not valid %v", res.Errors)
}
// Read back
content, err := svc.ReadFile("app.yml")
if err != nil {
t.Fatalf("read %v", err)
}
if content != validYAML {
t.Fatalf("content mismatch")
}
// Ensure no temp files left
files, _ := svc.ListFiles()
for _, f := range files {
if len(f) > 4 && f[len(f)-4:] == ".tmp" {
t.Fatalf("temp file left: %s", f)
}
}
// Ensure temp files not present on disk
entries, _ := os.ReadDir(svc.ConfigDir())
for _, e := range entries {
if len(e.Name()) > 4 && contains(e.Name(), ".tmp.") {
t.Fatalf("temp file on disk: %s", e.Name())
}
}
}
func contains(s, sub string) bool {
return len(s) >= len(sub) && (func() bool {
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
})()
}
func TestRejectInvalid(t *testing.T) {
svc, _, cleanup := newTestService(t)
defer cleanup()
// Try apply invalid YAML - should not write
res, err := svc.Apply("bad.yml", "http:\n bad: [\n", "user1", true)
if err != nil {
t.Fatalf("apply should return preview error, not err %v", err)
}
if res.Valid {
t.Fatal("invalid yaml should be rejected")
}
// Ensure file not created
if _, err := svc.ReadFile("bad.yml"); err == nil {
t.Fatal("invalid file should not be created")
}
}
func TestRejectEmpty(t *testing.T) {
svc, _, cleanup := newTestService(t)
defer cleanup()
res, _ := svc.Apply("empty.yml", " ", "user1", true)
if res.Valid {
t.Fatal("empty should be rejected")
}
}
func TestRejectNoConfirm(t *testing.T) {
svc, _, cleanup := newTestService(t)
defer cleanup()
_, err := svc.Apply("app.yml", validYAML, "user1", false)
if err == nil {
t.Fatal("expected error for missing confirm")
}
}
func TestRollback(t *testing.T) {
svc, _, cleanup := newTestService(t)
defer cleanup()
// Apply v1
_, err := svc.Apply("app.yml", validYAML, "user1", true)
if err != nil {
t.Fatalf("apply v1 %v", err)
}
// Apply v2
_, err = svc.Apply("app.yml", validYAML2, "user1", true)
if err != nil {
t.Fatalf("apply v2 %v", err)
}
// Verify v2 present
content, _ := svc.ReadFile("app.yml")
if content != validYAML2 {
t.Fatalf("expected v2")
}
// Rollback to previous (v1)
_, err = svc.Rollback("app.yml", "", "user1")
if err != nil {
t.Fatalf("rollback %v", err)
}
content, _ = svc.ReadFile("app.yml")
if content != validYAML {
t.Fatalf("expected rollback to v1, got %q", content)
}
// Check history has entries
hist, err := svc.History("app.yml")
if err != nil {
t.Fatalf("history %v", err)
}
if len(hist) < 2 {
t.Fatalf("expected at least 2 history entries, got %d", len(hist))
}
}
func TestRollbackSpecificBackup(t *testing.T) {
svc, _, cleanup := newTestService(t)
defer cleanup()
svc.Apply("app.yml", validYAML, "user1", true)
svc.Apply("app.yml", validYAML2, "user1", true)
hist, _ := svc.History("app.yml")
if len(hist) < 2 {
t.Fatalf("need 2 backups")
}
// hist[0] is most recent (before v2), hist[1] is before v1 (empty)
// Rollback to specific backup: choose oldest that has content validYAML
// The most recent backup content is validYAML (before v2)
_, err := svc.Rollback("app.yml", hist[0].ID, "user1")
if err != nil {
t.Fatalf("rollback specific %v", err)
}
content, _ := svc.ReadFile("app.yml")
if content != validYAML {
t.Fatalf("expected v1 after specific rollback")
}
}
func TestConcurrentUpdates(t *testing.T) {
svc, _, cleanup := newTestService(t)
defer cleanup()
// Start with valid
svc.Apply("concurrent.yml", validYAML, "user1", true)
var wg sync.WaitGroup
errs := make([]error, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
content := validYAML
if idx%2 == 0 {
content = validYAML2
}
_, err := svc.Apply("concurrent.yml", content, "user1", true)
errs[idx] = err
}(i)
}
wg.Wait()
for i, e := range errs {
if e != nil {
t.Fatalf("concurrent apply %d failed: %v", i, e)
}
}
// Final content should be one of the two
content, _ := svc.ReadFile("concurrent.yml")
if content != validYAML && content != validYAML2 {
t.Fatalf("unexpected final content")
}
// History should have 11 entries (initial + 10)
hist, _ := svc.History("concurrent.yml")
if len(hist) != 11 {
t.Fatalf("expected 11 history, got %d", len(hist))
}
}
func TestFileOwnershipAndValidation(t *testing.T) {
svc, _, cleanup := newTestService(t)
defer cleanup()
// Try to write outside directory via traversal — should fail validation
res, _ := svc.Apply("../evil.yml", validYAML, "user1", true)
if res.Valid {
t.Fatal("expected invalid for traversal")
}
// Try wrong extension
res, _ = svc.Apply("evil.txt", validYAML, "user1", true)
if res.Valid {
t.Fatal("expected invalid for wrong extension")
}
// Try empty filename
res2 := svc.Preview("", validYAML)
if res2.Valid {
t.Fatal("expected invalid for empty filename")
}
}