carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3// printRestartNotice's three states (discussion #16). The assertions are on
4// STRUCTURE — which hosts are named, which remedy is offered, whether
5// anything is said at all — never on the exact sentences, which are reviewed
6// copy and change without changing what the notice means.
7
8import (
9 "bytes"
10 "strings"
11 "testing"
12
13 "github.com/carlosframework/platform/internal/appconfig"
14)
15
16func TestPrintRestartNoticeSaysNothingWhenNoClaimWasMade(t *testing.T) {
17 var b bytes.Buffer
18 printRestartNotice(&b, "relay", nil)
19 if b.Len() != 0 {
20 t.Fatalf("printed %q for a nil answer, want silence — an older console said nothing, which is not a claim that nothing needs restarting", b.String())
21 }
22}
23
24func TestPrintRestartNoticeSaysNothingWhenNothingPending(t *testing.T) {
25 var b bytes.Buffer
26 printRestartNotice(&b, "relay", &restartAnswer{known: true})
27 if b.Len() != 0 {
28 t.Fatalf("printed %q when the answer is a genuine none", b.String())
29 }
30}
31
32// The one that matters: a console that LOOKED and could not tell must say
33// so, because staying quiet here is the failure being fixed.
34func TestPrintRestartNoticeSpeaksUpWhenItCouldNotTell(t *testing.T) {
35 var b bytes.Buffer
36 printRestartNotice(&b, "relay", &restartAnswer{known: false})
37 if b.Len() == 0 {
38 t.Fatal("printed nothing for known:false — 'could not check' must not render as 'all clear'")
39 }
40}
41
42func TestPrintRestartNoticeNamesEachHost(t *testing.T) {
43 var b bytes.Buffer
44 printRestartNotice(&b, "relay", &restartAnswer{known: true, pending: appconfig.RestartPending{
45 Hosts: []appconfig.RestartHost{
46 {Host: "one.example.com"},
47 {Host: "two.example.com"},
48 },
49 }})
50 got := b.String()
51 for _, want := range []string{"one.example.com", "two.example.com"} {
52 if !strings.Contains(got, want) {
53 t.Fatalf("output %q does not name %q", got, want)
54 }
55 }
56 // One line per host, plus the single trailing scope caveat.
57 if n := strings.Count(got, "\n"); n != 3 {
58 t.Fatalf("output has %d lines, want one per host plus one caveat:\n%s", n, got)
59 }
60 if n := strings.Count(got, restartCopyScope); n != 1 {
61 t.Fatalf("scope caveat appears %d times, want exactly once:\n%s", n, got)
62 }
63}
64
65// The remedy split. A route whose unit carlos knows gets pointed at a
66// command; a route without one must NOT be, because `carlos restart` is a
67// documented no-op for that shape and sending someone to run it would be a
68// second silent failure answering the first.
69func TestPrintRestartNoticeOffersTheRightRemedy(t *testing.T) {
70 var withUnit bytes.Buffer
71 printRestartNotice(&withUnit, "relay", &restartAnswer{known: true, pending: appconfig.RestartPending{
72 Hosts: []appconfig.RestartHost{{Host: "console.example.com", Unit: "console.service"}},
73 }})
74 if !strings.Contains(withUnit.String(), "console.service") {
75 t.Fatalf("output %q does not name the unit that can be cycled", withUnit.String())
76 }
77
78 var plain bytes.Buffer
79 printRestartNotice(&plain, "relay", &restartAnswer{known: true, pending: appconfig.RestartPending{
80 Hosts: []appconfig.RestartHost{{Host: "connect.example.com"}},
81 }})
82 if strings.Contains(plain.String(), "carlos restart") {
83 t.Fatalf("output %q offers `carlos restart` for a route it cannot cycle", plain.String())
84 }
85}
86
87func TestPrintRestartNoticeNamesUnresolvedHosts(t *testing.T) {
88 var b bytes.Buffer
89 printRestartNotice(&b, "relay", &restartAnswer{known: true, pending: appconfig.RestartPending{
90 Unresolved: []string{"ghost.example.com"},
91 }})
92 if !strings.Contains(b.String(), "ghost.example.com") {
93 t.Fatalf("output %q does not name the host it could not resolve", b.String())
94 }
95}
96
97// checkNoEmptyValues: the report's aside, on the CLI side of the seal. This
98// is the only gate in direct mode, and it fires before the network
99// round-trip that fetches the sealing key.
100func TestCheckNoEmptyValues(t *testing.T) {
101 if err := checkNoEmptyValues(map[string]string{"A": "1", "B": "2"}); err != nil {
102 t.Fatalf("refused a pair of real values: %v", err)
103 }
104 err := checkNoEmptyValues(map[string]string{"CLIENT_ID": "", "CLIENT_SECRET": "", "FINE": "v"})
105 if err == nil {
106 t.Fatal("accepted empty values")
107 }
108 // Every offending key, not just the first — a mistyped `op read` field
109 // usually spoils several at once, and Go's map order would otherwise
110 // name a different one each run.
111 for _, want := range []string{"CLIENT_ID", "CLIENT_SECRET"} {
112 if !strings.Contains(err.Error(), want) {
113 t.Fatalf("error %q does not name %q", err, want)
114 }
115 }
116 if strings.Contains(err.Error(), "FINE") {
117 t.Fatalf("error %q names a key that was fine", err)
118 }
119 // Sorted, so the message is the same on every run.
120 if i, j := strings.Index(err.Error(), "CLIENT_ID"), strings.Index(err.Error(), "CLIENT_SECRET"); i > j {
121 t.Fatalf("keys are not sorted in %q", err)
122 }
123}
124
125// restartFromAPI's nil-preservation is the wire half of the same three
126// states: a console that omits the field must not be turned into one that
127// claims there is nothing to do.
128func TestRestartFromAPIPreservesNil(t *testing.T) {
129 if got := restartFromAPI(nil); got != nil {
130 t.Fatalf("restartFromAPI(nil) = %+v, want nil", got)
131 }
132 got := restartFromAPI(&apiConfigRestart{
133 Known: true,
134 Hosts: []apiConfigRestartHost{{Host: "h.example.com", Unit: "u.service"}},
135 })
136 if got == nil || !got.known {
137 t.Fatalf("restartFromAPI dropped a present answer: %+v", got)
138 }
139 if len(got.pending.Hosts) != 1 || got.pending.Hosts[0].Unit != "u.service" {
140 t.Fatalf("hosts = %+v, want the unit carried through", got.pending.Hosts)
141 }
142}
143