Files
orca/internal/emitter/drift_path_test.go
T
Jon Chery 03f3585f16 feat(P10b): drift detection (R-018/R-019/R-020, REQ-103..113)
internal/drift/drift.go: Detector (Watch via iter.Seq2, Aggregate,
Remediate with cooldown-on-success, Acknowledge), Config with tiered
cadence (critical 5s + Path units, standard 30s, default 60s).
internal/cli/drift.go: orca drift {show,watch,acknowledge,remediate,
config}. internal/emitter/drift_path.go: systemd Path+service unit
emitter (User=orca, ProtectSystem=strict). scripts/orca-drift-notify.sh
(sha256 event JSON), orca-remediate.sh (cooldown-on-success, transient
retry). Pre-flight gate (R-020, --force + per-ns scoping). orca
system user (REQ-111), NFS detection (D-233), orca job restart for
EnvironmentFile drift (D-235).

---ci---
project: orca
phase: 10b
milestone: v0.11
status: execute
---/ci---
2026-08-07 07:17:41 +00:00

159 lines
4.2 KiB
Go

package emitter
import (
"strings"
"testing"
)
func TestDriftPathUnit_RenderUnits(t *testing.T) {
specs := []DriftPathSpec{
{Pattern: "/etc/traefik/dynamic/orca.yml", SystemdPathUnit: true},
{Pattern: "/etc/nftables.d/orca.nft", SystemdPathUnit: true},
}
files, err := (DriftPathUnit{}).RenderDriftUnits(specs)
if err != nil {
t.Fatalf("RenderDriftUnits: %v", err)
}
if len(files) != 4 {
t.Fatalf("got %d files, want 4 (2 .path + 2 .service)", len(files))
}
var pathUnits, svcUnits int
for _, f := range files {
if strings.HasSuffix(f.Path, ".path") {
pathUnits++
}
if strings.HasSuffix(f.Path, ".service") {
svcUnits++
}
}
if pathUnits != 2 || svcUnits != 2 {
t.Errorf("path=%d service=%d, want 2 and 2", pathUnits, svcUnits)
}
}
func TestDriftPathUnit_PathUnitContent(t *testing.T) {
files, err := (DriftPathUnit{}).RenderDriftUnits([]DriftPathSpec{
{Pattern: "/etc/traefik/dynamic/orca.yml", SystemdPathUnit: true},
})
if err != nil {
t.Fatalf("Render: %v", err)
}
var pathFile *File
for i := range files {
if strings.HasSuffix(files[i].Path, ".path") {
pathFile = &files[i]
}
}
if pathFile == nil {
t.Fatal("no .path unit emitted")
}
c := pathFile.Content
for _, want := range []string{
"PathChanged=/etc/traefik/dynamic/orca.yml",
"RateLimitIntervalSec=1s",
"RateLimitBurst=5",
"WantedBy=multi-user.target",
} {
if !strings.Contains(c, want) {
t.Errorf("path unit missing %q:\n%s", want, c)
}
}
}
func TestDriftPathUnit_ServiceUnitSecurity(t *testing.T) {
files, err := (DriftPathUnit{}).RenderDriftUnits([]DriftPathSpec{
{Pattern: "/etc/nftables.d/orca.nft", SystemdPathUnit: true},
})
if err != nil {
t.Fatalf("Render: %v", err)
}
var svcFile *File
for i := range files {
if strings.HasSuffix(files[i].Path, ".service") {
svcFile = &files[i]
}
}
if svcFile == nil {
t.Fatal("no .service unit emitted")
}
c := svcFile.Content
for _, want := range []string{
"Type=oneshot",
"ExecStart=/usr/local/bin/orca-drift-notify.sh %f",
"User=orca",
"Group=orca",
"NoNewPrivileges=yes",
"ProtectSystem=strict",
"ReadWritePaths=/etc/orca/state/drift-events",
"ProtectHome=yes",
} {
if !strings.Contains(c, want) {
t.Errorf("service unit missing %q:\n%s", want, c)
}
}
}
func TestDriftPathUnit_UnitNameSlug(t *testing.T) {
cases := []struct {
pattern string
want string
}{
{"/etc/traefik/dynamic/orca.yml", "orca-drift-etc-traefik-dynamic-orca-yml"},
{"/etc/systemd/system/orca-alloc-*.service", "orca-drift-etc-systemd-system-orca-alloc-service"},
{"/etc/orca/actual/*/etc/sudoers.d/orca-*", "orca-drift-etc-orca-actual-etc-sudoers-d-orca"},
}
for _, c := range cases {
got := driftUnitName(c.pattern)
if got != c.want {
t.Errorf("driftUnitName(%q) = %q, want %q", c.pattern, got, c.want)
}
}
}
func TestDriftPathUnit_EmptyPatternErrors(t *testing.T) {
_, err := (DriftPathUnit{}).RenderDriftUnits([]DriftPathSpec{{Pattern: ""}})
if err == nil {
t.Fatal("expected error for empty pattern")
}
}
func TestDriftPathUnit_NoSpecsNoFiles(t *testing.T) {
files, err := (DriftPathUnit{}).RenderDriftUnits(nil)
if err != nil {
t.Fatalf("RenderDriftUnits nil: %v", err)
}
if files != nil {
t.Errorf("got %d files, want nil", len(files))
}
}
func TestDriftPathUnit_DedupesSameSlug(t *testing.T) {
// Two patterns that produce the same slug (after non-alnum collapse)
// should dedupe to a single .path + .service pair.
files, err := (DriftPathUnit{}).RenderDriftUnits([]DriftPathSpec{
{Pattern: "/etc/x/a.yml", SystemdPathUnit: true},
{Pattern: "/etc/x/a.yml", SystemdPathUnit: true},
})
if err != nil {
t.Fatalf("Render: %v", err)
}
if len(files) != 2 {
t.Errorf("got %d files, want 2 (deduped identical patterns)", len(files))
}
}
func TestDriftPathUnit_ServiceAfterPath(t *testing.T) {
files, _ := (DriftPathUnit{}).RenderDriftUnits([]DriftPathSpec{
{Pattern: "/etc/traefik/dynamic/orca.yml", SystemdPathUnit: true},
})
for _, f := range files {
if strings.HasSuffix(f.Path, ".service") {
if !strings.Contains(f.Content, "After=orca-drift-etc-traefik-dynamic-orca-yml.path") {
t.Errorf("service missing After=<name>.path:\n%s", f.Content)
}
return
}
}
t.Fatal("no service file found")
}