| 1 | package 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 | |
| 8 | import ( |
| 9 | "bytes" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/appconfig" |
| 14 | ) |
| 15 | |
| 16 | func 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 | |
| 24 | func 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. |
| 34 | func 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 | |
| 42 | func 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. |
| 69 | func 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 | |
| 87 | func 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. |
| 100 | func 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. |
| 128 | func 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 | |