package relay import ( "bytes" "context" "encoding/base64" "os" "path/filepath" "reflect" "strings" "testing" "time" ) func TestParseMasters(t *testing.T) { in := "k1 " + b64(bytes.Repeat([]byte{1}, 32)) + " -\n" + "k0 " + b64(bytes.Repeat([]byte{2}, 32)) + " 2026-09-08T00:00:00Z\n" + "# comment\n\n" m, err := ParseMasters(strings.NewReader(in)) if err != nil { t.Fatal(err) } if got := m.Kids(); !reflect.DeepEqual(got, []string{"k0", "k1"}) { t.Fatalf("kids = %v", got) } before := time.Date(2026, 9, 7, 0, 0, 0, 0, time.UTC) after := time.Date(2026, 9, 9, 0, 0, 0, 0, time.UTC) if _, ok := m.Lookup("k0", before); !ok { t.Fatal("k0 should be live before retire_at") } if _, ok := m.Lookup("k0", after); ok { t.Fatal("k0 should be retired after retire_at") } if _, ok := m.Lookup("k1", after); !ok { t.Fatal("k1 never retires") } if _, ok := m.Lookup("kx", before); ok { t.Fatal("unknown kid") } } func TestParseMastersRejectsBadLines(t *testing.T) { 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"} { if _, err := ParseMasters(strings.NewReader(bad)); err == nil { t.Errorf("accepted %q", bad) } } } func b64(b []byte) string { return base64.StdEncoding.EncodeToString(b) } func TestWatchMastersReloadsOnChange(t *testing.T) { old := mastersPollInterval mastersPollInterval = 50 * time.Millisecond defer func() { mastersPollInterval = old }() path := filepath.Join(t.TempDir(), "relay-masters") write := func(s string) { if err := os.WriteFile(path, []byte(s), 0o600); err != nil { t.Fatal(err) } } write("k1 " + b64(bytes.Repeat([]byte{1}, 32)) + " -\n") ctx, cancel := context.WithCancel(context.Background()) defer cancel() got := make(chan []string, 4) errs := make(chan error, 4) go WatchMasters(ctx, path, func(m *Masters, err error) { if err != nil { errs <- err return } got <- m.Kids() }) select { case k := <-got: if !reflect.DeepEqual(k, []string{"k1"}) { t.Fatalf("initial kids = %v", k) } case err := <-errs: t.Fatal(err) case <-time.After(5 * time.Second): t.Fatal("no initial load") } // Make the mtime observably newer, then rewrite with a second kid. time.Sleep(20 * time.Millisecond) write("k1 " + b64(bytes.Repeat([]byte{1}, 32)) + " -\nk2 " + b64(bytes.Repeat([]byte{2}, 32)) + " -\n") future := time.Now().Add(2 * time.Second) if err := os.Chtimes(path, future, future); err != nil { t.Fatal(err) } select { case k := <-got: if !reflect.DeepEqual(k, []string{"k1", "k2"}) { t.Fatalf("reloaded kids = %v", k) } case err := <-errs: t.Fatal(err) case <-time.After(15 * time.Second): t.Fatal("no reload within three poll ticks") } } // Retiring is what `carlos relay status` turns into "when may this key leave // the box" (discussion #120). A master with no retirement date must be absent // rather than present with a zero time, or the CLI prints a retirement for // every key a box holds. func TestMastersRetiring(t *testing.T) { m, err := ParseMasters(strings.NewReader( "k0 " + base64.StdEncoding.EncodeToString(testMaster()) + " -\n" + "k1 " + base64.StdEncoding.EncodeToString(testMaster()) + " 2026-09-09T12:00:00Z\n")) if err != nil { t.Fatal(err) } r := m.Retiring() if len(r) != 1 { t.Fatalf("Retiring() = %v, want just k1", r) } if got, want := r["k1"].UTC().Format(time.RFC3339), "2026-09-09T12:00:00Z"; got != want { t.Errorf("k1 retire_at = %s, want %s", got, want) } if _, ok := r["k0"]; ok { t.Error("k0 has no retirement date and must not appear") } }