package notices import ( "os" "path/filepath" "reflect" "strings" "testing" ) func TestParseRejectsUnknownField(t *testing.T) { _, err := parse([]byte("global:\n - type: info\n subject: hi\n")) if err == nil { t.Fatal("unknown field accepted") } if !strings.Contains(err.Error(), "subject") { t.Fatalf("error does not name the field: %v", err) } } func TestParseRejectsBadType(t *testing.T) { _, err := parse([]byte("global:\n - type: critcal\n title: hi\n")) if err == nil || !strings.Contains(err.Error(), "critcal") { t.Fatalf("bad type: err = %v", err) } } func TestParseEmpty(t *testing.T) { for _, in := range [][]byte{nil, []byte(""), []byte("{}\n")} { doc, err := parse(in) if err != nil { t.Fatalf("parse(%q): %v", in, err) } if len(doc.Global) != 0 || len(doc.Repos) != 0 { t.Fatalf("parse(%q) = %+v, want empty", in, doc) } if doc.Repos == nil { t.Fatalf("parse(%q): Repos is nil", in) } } } func TestTypeAliasesAndTrimming(t *testing.T) { doc, err := parse([]byte("global:\n - type: informational\n title: ' spaced '\n")) if err != nil { t.Fatal(err) } n := doc.Global[0] if n.Type != TypeInfo || n.Title != "spaced" { t.Fatalf("got %+v", n) } } func TestLinkSchemes(t *testing.T) { cases := []struct { link string ok bool }{ {"/status", true}, {"https://example.org/a", true}, {"mailto:ops@example.org", true}, {"javascript:alert(1)", false}, {"//evil.example", false}, {"ftp://example.org", false}, } for _, tc := range cases { doc := Document{Global: []Notice{{Type: TypeInfo, Title: "x", Link: tc.link}}} err := validate(&doc) if tc.ok && err != nil { t.Errorf("link %q rejected: %v", tc.link, err) } if !tc.ok && err == nil { t.Errorf("link %q accepted", tc.link) } } } func TestSetRoundTripAndRevision(t *testing.T) { path := filepath.Join(t.TempDir(), "notices.yaml") st, err := Open(path) if err != nil { t.Fatal(err) } t.Cleanup(st.Close) if st.Revision() == "" { t.Fatal("empty store has no revision") } doc := Document{ Global: []Notice{{Type: TypeCritical, Title: "Outage", Body: "line\nline"}}, Repos: map[string][]Notice{"demo": {{Type: "informational", Title: "Repo"}}}, } if err := st.Set(doc); err != nil { t.Fatal(err) } // informational must have been canonicalised in the published snapshot. if got := st.Snapshot().Doc.Repos["demo"][0].Type; got != TypeInfo { t.Fatalf("published type = %q, want %q", got, TypeInfo) } rev := st.Revision() fi, err := os.Stat(path) if err != nil { t.Fatal(err) } if perm := fi.Mode().Perm(); perm != 0o600 { t.Errorf("file mode = %o, want 600", perm) } again, err := Open(path) if err != nil { t.Fatal(err) } t.Cleanup(again.Close) if !reflect.DeepEqual(st.Snapshot().Doc, again.Snapshot().Doc) { t.Fatalf("round trip mismatch:\n%+v\n%+v", st.Snapshot().Doc, again.Snapshot().Doc) } // A no-op save is byte-identical and keeps the revision. if err := st.Set(doc); err != nil { t.Fatal(err) } if st.Revision() != rev { t.Fatalf("no-op save changed revision %q -> %q", rev, st.Revision()) } // A content change moves it. doc.Global[0].Body = "different" if err := st.Set(doc); err != nil { t.Fatal(err) } if st.Revision() == rev { t.Fatal("content change kept the same revision") } } func TestOpenMissingFileAndBadDir(t *testing.T) { dir := t.TempDir() st, err := Open(filepath.Join(dir, "notices.yaml")) if err != nil { t.Fatal(err) } t.Cleanup(st.Close) if len(st.Snapshot().Doc.Global) != 0 { t.Fatal("missing file is not empty") } if _, err := Open(filepath.Join(dir, "nope", "notices.yaml")); err == nil { t.Fatal("missing directory accepted") } }