| new file mode 100644 |
| index 000000000..3b8fec5dc |
| --- /dev/null |
| +++ b/internal/caps/render_test.go |
| @@ -0,0 +1,209 @@ |
| +package caps |
| + |
| +import ( |
| + "bytes" |
| + "encoding/json" |
| + "os" |
| + "testing" |
| +) |
| + |
| +// keymailSet is the full declared set the worked example in spec §4a |
| +// renders from: command, runtime_dir, children, host_role and a single |
| +// privileged port, plus a `data` capability that (being self-service and |
| +// directive-free) must never show up in the rendered bytes. |
| +func keymailSet(t *testing.T) *Set { |
| + t.Helper() |
| + var s Set |
| + raw := `{ |
| + "v": 1, |
| + "command": "relay", |
| + "daemon": true, |
| + "children": true, |
| + "runtime_dir": "keymail", |
| + "ports": [{"port": 25, "name": "mx"}], |
| + "data": {"replicate": "platform", "dbs": ["registry.db"]}, |
| + "host_role": true |
| + }` |
| + if err := json.Unmarshal([]byte(raw), &s); err != nil { |
| + t.Fatalf("decode keymail set: %v", err) |
| + } |
| + return &s |
| +} |
| + |
| +func keymailGrants() Grants { |
| + return Grants{Ports: map[int]bool{25: true}, HostRole: true} |
| +} |
| + |
| +func consoleSet(t *testing.T) *Set { |
| + t.Helper() |
| + var s Set |
| + raw := `{"v": 1, "daemon": true, "host_role": true}` |
| + if err := json.Unmarshal([]byte(raw), &s); err != nil { |
| + t.Fatalf("decode console set: %v", err) |
| + } |
| + return &s |
| +} |
| + |
| +func consoleGrants() Grants { |
| + return Grants{HostRole: true} |
| +} |
| + |
| +func readGolden(t *testing.T, name string) []byte { |
| + t.Helper() |
| + b, err := os.ReadFile("testdata/" + name) |
| + if err != nil { |
| + t.Fatalf("read golden %s: %v", name, err) |
| + } |
| + return b |
| +} |
| + |
| +func TestRenderServiceMatchesKeymailGolden(t *testing.T) { |
| + got := RenderService("keymail", keymailSet(t), keymailGrants()) |
| + want := readGolden(t, "keymail.service.conf") |
| + if !bytes.Equal(got, want) { |
| + t.Fatalf("RenderService mismatch:\n got: %q\n want: %q", got, want) |
| + } |
| +} |
| + |
| +func TestRenderSocketMatchesKeymailGolden(t *testing.T) { |
| + got := RenderSocket(keymailSet(t), keymailGrants()) |
| + want := readGolden(t, "keymail.socket.conf") |
| + if !bytes.Equal(got, want) { |
| + t.Fatalf("RenderSocket mismatch:\n got: %q\n want: %q", got, want) |
| + } |
| +} |
| + |
| +func TestRenderServiceMatchesConsoleGolden(t *testing.T) { |
| + got := RenderService("console", consoleSet(t), consoleGrants()) |
| + want := readGolden(t, "console.service.conf") |
| + if !bytes.Equal(got, want) { |
| + t.Fatalf("RenderService mismatch:\n got: %q\n want: %q", got, want) |
| + } |
| +} |
| + |
| +// TestRenderIsDeterministic renders the same effective set from two |
| +// independently-decoded Sets whose "ports" arrays are given in different |
| +// orders, and asserts byte-identical output with ports in ascending order |
| +// regardless of declaration order. |
| +func TestRenderIsDeterministic(t *testing.T) { |
| + const a = `{"v":1,"command":"relay","ports":[{"port":587,"name":"submission"},{"port":25,"name":"mx"}]}` |
| + const b = `{"v":1,"command":"relay","ports":[{"port":25,"name":"mx"},{"port":587,"name":"submission"}]}` |
| + |
| + var sa, sb Set |
| + if err := json.Unmarshal([]byte(a), &sa); err != nil { |
| + t.Fatalf("decode a: %v", err) |
| + } |
| + if err := json.Unmarshal([]byte(b), &sb); err != nil { |
| + t.Fatalf("decode b: %v", err) |
| + } |
| + |
| + g := Grants{Ports: map[int]bool{25: true, 587: true}} |
| + |
| + gotA := RenderSocket(&sa, g) |
| + gotB := RenderSocket(&sb, g) |
| + if !bytes.Equal(gotA, gotB) { |
| + t.Fatalf("render not deterministic across declaration order:\n a: %q\n b: %q", gotA, gotB) |
| + } |
| + |
| + want := []byte(Marker + "\n[Socket]\nListenStream=25\nListenStream=587\n") |
| + if !bytes.Equal(gotA, want) { |
| + t.Fatalf("ports not sorted ascending: %q", gotA) |
| + } |
| + |
| + // Rendering again from the same Set must reproduce the identical bytes. |
| + again := RenderSocket(&sa, g) |
| + if !bytes.Equal(gotA, again) { |
| + t.Fatalf("render not repeatable: %q vs %q", gotA, again) |
| + } |
| + |
| + svcA := RenderService("keymail", &sa, g) |
| + svcAgain := RenderService("keymail", &sa, g) |
| + if !bytes.Equal(svcA, svcAgain) { |
| + t.Fatalf("service render not repeatable: %q vs %q", svcA, svcAgain) |
| + } |
| +} |
| + |
| +// TestUngrantedCapabilitiesMaterializeNothing: keymail's full set with no |
| +// grants at all still renders the self-service lines (command, runtime_dir, |
| +// children) but none of the operator-consented ones — no DynamicUser/User/ |
| +// Group, and a nil socket body. keymail with no port:25 grant serves HTTP |
| +// and does not bind the MX; it does not refuse to start. |
| +func TestUngrantedCapabilitiesMaterializeNothing(t *testing.T) { |
| + got := RenderService("keymail", keymailSet(t), Grants{}) |
| + if got == nil { |
| + t.Fatal("service body is nil, want the self-service lines") |
| + } |
| + for _, want := range []string{ |
| + "ExecStart=\n", |
| + "ExecStart=/opt/carlos/live/%i relay\n", |
| + "RuntimeDirectory=keymail\n", |
| + "KillMode=control-group\n", |
| + } { |
| + if !bytes.Contains(got, []byte(want)) { |
| + t.Errorf("service body missing self-service line %q:\n%s", want, got) |
| + } |
| + } |
| + for _, unwanted := range []string{"DynamicUser", "User=", "Group="} { |
| + if bytes.Contains(got, []byte(unwanted)) { |
| + t.Errorf("service body has ungranted host_role directive %q:\n%s", unwanted, got) |
| + } |
| + } |
| + |
| + if sock := RenderSocket(keymailSet(t), Grants{}); sock != nil { |
| + t.Fatalf("socket body = %q, want nil when port:25 is not granted", sock) |
| + } |
| +} |
| + |
| +// TestEmptyEffectiveSetRendersNil: daemon and data emit no directives, and |
| +// nothing else is declared, so both bodies are nil. |
| +func TestEmptyEffectiveSetRendersNil(t *testing.T) { |
| + s := &Set{V: 1, Daemon: true, Data: &Data{Replicate: DataReplicatePlatform, DBs: []string{"console.db"}}} |
| + if got := RenderService("console", s, Grants{}); got != nil { |
| + t.Fatalf("RenderService = %q, want nil", got) |
| + } |
| + if got := RenderSocket(s, Grants{}); got != nil { |
| + t.Fatalf("RenderSocket = %q, want nil", got) |
| + } |
| +} |
| + |
| +// TestNilSetRendersNil: a nil *Set (every manifest before this feature, and |
| +// every route that never declared capabilities) must render nil, not panic. |
| +func TestNilSetRendersNil(t *testing.T) { |
| + if got := RenderService("console", nil, Grants{HostRole: true}); got != nil { |
| + t.Fatalf("RenderService(nil) = %q, want nil", got) |
| + } |
| + if got := RenderSocket(nil, Grants{Ports: map[int]bool{25: true}}); got != nil { |
| + t.Fatalf("RenderSocket(nil) = %q, want nil", got) |
| + } |
| +} |
| + |
| +// TestRenderStartsWithTheMarker checks every non-nil body this file |
| +// produces opens with the marker line. |
| +func TestRenderStartsWithTheMarker(t *testing.T) { |
| + bodies := [][]byte{ |
| + RenderService("keymail", keymailSet(t), keymailGrants()), |
| + RenderSocket(keymailSet(t), keymailGrants()), |
| + RenderService("console", consoleSet(t), consoleGrants()), |
| + } |
| + for i, b := range bodies { |
| + if b == nil { |
| + t.Fatalf("body %d is nil, want non-nil to check its marker", i) |
| + } |
| + if !bytes.HasPrefix(b, []byte(Marker+"\n")) { |
| + t.Errorf("body %d does not start with the marker:\n%s", i, b) |
| + } |
| + } |
| +} |
| + |
| +// A command identical to the template's default ("serve") is still an |
| +// explicit declaration: the reset+rewrite pair is still correct and still |
| +// deterministic, even though the rewritten value matches what the template |
| +// already says. |
| +func TestCommandIdenticalToTemplateDefaultStillEmitsReset(t *testing.T) { |
| + s := &Set{V: 1, Command: "serve"} |
| + got := RenderService("console", s, Grants{}) |
| + want := []byte(Marker + "\n[Service]\nExecStart=\nExecStart=/opt/carlos/live/%i serve\n") |
| + if !bytes.Equal(got, want) { |
| + t.Fatalf("got %q, want %q", got, want) |
| + } |
| +} |