carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package relay
2
3import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "os"
8 "path/filepath"
9 "reflect"
10 "strings"
11 "testing"
12 "time"
13)
14
15func TestParseMasters(t *testing.T) {
16 in := "k1 " + b64(bytes.Repeat([]byte{1}, 32)) + " -\n" +
17 "k0 " + b64(bytes.Repeat([]byte{2}, 32)) + " 2026-09-08T00:00:00Z\n" +
18 "# comment\n\n"
19 m, err := ParseMasters(strings.NewReader(in))
20 if err != nil {
21 t.Fatal(err)
22 }
23 if got := m.Kids(); !reflect.DeepEqual(got, []string{"k0", "k1"}) {
24 t.Fatalf("kids = %v", got)
25 }
26 before := time.Date(2026, 9, 7, 0, 0, 0, 0, time.UTC)
27 after := time.Date(2026, 9, 9, 0, 0, 0, 0, time.UTC)
28 if _, ok := m.Lookup("k0", before); !ok {
29 t.Fatal("k0 should be live before retire_at")
30 }
31 if _, ok := m.Lookup("k0", after); ok {
32 t.Fatal("k0 should be retired after retire_at")
33 }
34 if _, ok := m.Lookup("k1", after); !ok {
35 t.Fatal("k1 never retires")
36 }
37 if _, ok := m.Lookup("kx", before); ok {
38 t.Fatal("unknown kid")
39 }
40}
41
42func TestParseMastersRejectsBadLines(t *testing.T) {
43 for _, bad := range []string{"k1 notbase64 -", "k1 " + b64([]byte{1}) + " -", "K1 " + b64(bytes.Repeat([]byte{1}, 32)) + " -", "k1 " + b64(bytes.Repeat([]byte{1}, 32)) + " yesterday"} {
44 if _, err := ParseMasters(strings.NewReader(bad)); err == nil {
45 t.Errorf("accepted %q", bad)
46 }
47 }
48}
49
50func b64(b []byte) string { return base64.StdEncoding.EncodeToString(b) }
51
52func TestWatchMastersReloadsOnChange(t *testing.T) {
53 old := mastersPollInterval
54 mastersPollInterval = 50 * time.Millisecond
55 defer func() { mastersPollInterval = old }()
56
57 path := filepath.Join(t.TempDir(), "relay-masters")
58 write := func(s string) {
59 if err := os.WriteFile(path, []byte(s), 0o600); err != nil {
60 t.Fatal(err)
61 }
62 }
63 write("k1 " + b64(bytes.Repeat([]byte{1}, 32)) + " -\n")
64 ctx, cancel := context.WithCancel(context.Background())
65 defer cancel()
66 got := make(chan []string, 4)
67 errs := make(chan error, 4)
68 go WatchMasters(ctx, path, func(m *Masters, err error) {
69 if err != nil {
70 errs <- err
71 return
72 }
73 got <- m.Kids()
74 })
75 select {
76 case k := <-got:
77 if !reflect.DeepEqual(k, []string{"k1"}) {
78 t.Fatalf("initial kids = %v", k)
79 }
80 case err := <-errs:
81 t.Fatal(err)
82 case <-time.After(5 * time.Second):
83 t.Fatal("no initial load")
84 }
85 // Make the mtime observably newer, then rewrite with a second kid.
86 time.Sleep(20 * time.Millisecond)
87 write("k1 " + b64(bytes.Repeat([]byte{1}, 32)) + " -\nk2 " + b64(bytes.Repeat([]byte{2}, 32)) + " -\n")
88 future := time.Now().Add(2 * time.Second)
89 if err := os.Chtimes(path, future, future); err != nil {
90 t.Fatal(err)
91 }
92 select {
93 case k := <-got:
94 if !reflect.DeepEqual(k, []string{"k1", "k2"}) {
95 t.Fatalf("reloaded kids = %v", k)
96 }
97 case err := <-errs:
98 t.Fatal(err)
99 case <-time.After(15 * time.Second):
100 t.Fatal("no reload within three poll ticks")
101 }
102}
103
104// Retiring is what `carlos relay status` turns into "when may this key leave
105// the box" (discussion #120). A master with no retirement date must be absent
106// rather than present with a zero time, or the CLI prints a retirement for
107// every key a box holds.
108func TestMastersRetiring(t *testing.T) {
109 m, err := ParseMasters(strings.NewReader(
110 "k0 " + base64.StdEncoding.EncodeToString(testMaster()) + " -\n" +
111 "k1 " + base64.StdEncoding.EncodeToString(testMaster()) + " 2026-09-09T12:00:00Z\n"))
112 if err != nil {
113 t.Fatal(err)
114 }
115 r := m.Retiring()
116 if len(r) != 1 {
117 t.Fatalf("Retiring() = %v, want just k1", r)
118 }
119 if got, want := r["k1"].UTC().Format(time.RFC3339), "2026-09-09T12:00:00Z"; got != want {
120 t.Errorf("k1 retire_at = %s, want %s", got, want)
121 }
122 if _, ok := r["k0"]; ok {
123 t.Error("k0 has no retirement date and must not appear")
124 }
125}
126