package emitter import ( "strings" "testing" "git.cloudinit.dev/coreci/orca/internal/jobspec" ) func TestSystemdEmitter_RenderJob(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Job", Name: "backup", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/usr/bin/rsync -a /src /dst"}, } node := &Node{Hostname: "node-1", Runtime: []string{"process"}} files, err := SystemdEmitter{}.Render(spec, node) if err != nil { t.Fatalf("Render: %v", err) } if len(files) != 1 { t.Fatalf("got %d files, want 1", len(files)) } f := files[0] wantPath := "/etc/systemd/system/orca-v1-backup.service" if f.Path != wantPath { t.Errorf("Path = %q, want %q", f.Path, wantPath) } wantContent := "[Service]\nExecStart=/usr/bin/rsync -a /src /dst\n" if f.Content != wantContent { t.Errorf("Content = %q, want %q", f.Content, wantContent) } if f.Mode != "0644" { t.Errorf("Mode = %q, want 0644", f.Mode) } } func TestSystemdEmitter_RenderService(t *testing.T) { // The full service emitter (Traefik route + health checks) lands in // P02; here we only prove the systemd side renders for a Service // kind with a process runtime. spec := &jobspec.WorkloadSpec{ Kind: "Service", Name: "web", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/usr/local/bin/httpd -f"}, } node := &Node{Hostname: "node-1", Runtime: []string{"process"}} files, err := SystemdEmitter{}.Render(spec, node) if err != nil { t.Fatalf("Render: %v", err) } if len(files) != 1 { t.Fatalf("got %d files, want 1", len(files)) } if files[0].Path != "/etc/systemd/system/orca-v1-web.service" { t.Errorf("Path = %q, want /etc/systemd/system/orca-v1-web.service", files[0].Path) } if !strings.Contains(files[0].Content, "ExecStart=/usr/local/bin/httpd -f") { t.Errorf("Content = %q, want it to contain the ExecStart line", files[0].Content) } } func TestSystemdEmitter_EmptyCommandError(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Job", Name: "x", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: ""}, } _, err := SystemdEmitter{}.Render(spec, &Node{}) if err == nil { t.Fatal("expected error for empty command, got nil") } if !strings.Contains(err.Error(), "command is empty") { t.Errorf("error = %q, want 'command is empty'", err.Error()) } } func TestSystemdEmitter_WhitespaceCommandError(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Job", Name: "x", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: " "}, } _, err := SystemdEmitter{}.Render(spec, &Node{}) if err == nil { t.Fatal("expected error for whitespace-only command, got nil") } if !strings.Contains(err.Error(), "command is empty") { t.Errorf("error = %q, want 'command is empty'", err.Error()) } } func TestSystemdEmitter_NilSpec(t *testing.T) { v := SystemdEmitter{} if _, err := v.Render(nil, &Node{}); err == nil { t.Fatal("expected error for nil spec") } } func TestSystemdEmitter_EmptyName(t *testing.T) { spec := &jobspec.WorkloadSpec{ Kind: "Job", Name: " ", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/x"}, } _, err := SystemdEmitter{}.Render(spec, &Node{}) if err == nil { t.Fatal("expected error for empty name") } if !strings.Contains(err.Error(), "name is empty") { t.Errorf("error = %q, want 'name is empty'", err.Error()) } } func TestSystemdEmitter_NilRuntime(t *testing.T) { spec := &jobspec.WorkloadSpec{Kind: "Job", Name: "x"} _, err := SystemdEmitter{}.Render(spec, &Node{}) if err == nil { t.Fatal("expected error for nil runtime") } if !strings.Contains(err.Error(), "runtime block is nil") { t.Errorf("error = %q, want 'runtime block is nil'", err.Error()) } } func TestSystemdEmitter_UnitNamePrefix(t *testing.T) { // The orca-v1- prefix is load-bearing for the dual-write window // (REQ-090, I-C-006): the v0.8 daemon writes `orca-.service` // and the v0.9 SSH-push path writes `orca-v1-.service`, // so the two never collide. This test guards against accidental // removal of the prefix. spec := &jobspec.WorkloadSpec{ Kind: "Job", Name: "dual-write-safety", Runtime: &jobspec.RuntimeBlock{OneOf: "process", Command: "/bin/true"}, } files, err := SystemdEmitter{}.Render(spec, &Node{Hostname: "n"}) if err != nil { t.Fatalf("Render: %v", err) } if !strings.HasPrefix(files[0].Path, "/etc/systemd/system/orca-v1-") { t.Errorf("Path = %q, want it to start with /etc/systemd/system/orca-v1- (REQ-090)", files[0].Path) } if !strings.HasSuffix(files[0].Path, ".service") { t.Errorf("Path = %q, want it to end with .service", files[0].Path) } // Explicitly assert the full expected unit name to lock the contract. want := "/etc/systemd/system/orca-v1-dual-write-safety.service" if files[0].Path != want { t.Errorf("Path = %q, want %q", files[0].Path, want) } // Sanity: the prefix is exactly "orca-v1-", not "orca-v0" or "orca". if unitNamePrefix != "orca-v1-" { t.Errorf("unitNamePrefix = %q, want orca-v1-", unitNamePrefix) } }