package bimi import ( "fmt" "os" "path/filepath" "strings" "testing" ) // A logo that satisfies every rule, used as the starting point each case // then breaks in exactly one way. const goodLogo = ` LambdaMail ` func rulesOf(vs []Violation) []string { out := make([]string, 0, len(vs)) for _, v := range vs { out = append(out, v.Rule) } return out } func contains(rules []string, want string) bool { for _, r := range rules { if r == want { return true } } return false } func TestValidate_AcceptsAConformingLogo(t *testing.T) { if vs := Validate([]byte(goodLogo)); len(vs) != 0 { t.Errorf("a conforming logo was rejected: %v", vs) } } func TestValidate_RequiresTheProfileReceiversLookFor(t *testing.T) { cases := map[string]struct{ from, to, rule string }{ // Without the profile the document is a plain SVG, and a receiver // ignores it without saying so. "missing baseProfile": {`baseProfile="tiny-ps" `, "", "baseProfile"}, "wrong baseProfile": {`baseProfile="tiny-ps"`, `baseProfile="full"`, "baseProfile"}, "missing version": {`version="1.2" `, "", "version"}, "wrong version": {`version="1.2"`, `version="1.1"`, "version"}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { logo := strings.Replace(goodLogo, tc.from, tc.to, 1) if !contains(rulesOf(Validate([]byte(logo))), tc.rule) { t.Errorf("no %s violation reported", tc.rule) } }) } } // Every client crops or letterboxes a non-square mark differently, so the // specification requires the viewBox itself be square. func TestValidate_RequiresASquareViewBox(t *testing.T) { for _, viewBox := range []string{"0 0 512 256", "0 0 100 200", "0 0 512", "", "0 0 0 0", "0 0 a b"} { t.Run(viewBox, func(t *testing.T) { logo := strings.Replace(goodLogo, `viewBox="0 0 512 512"`, fmt.Sprintf(`viewBox=%q`, viewBox), 1) if !contains(rulesOf(Validate([]byte(logo))), "viewBox") { t.Errorf("viewBox %q was accepted", viewBox) } }) } // Commas are a legal separator and must not be read as a malformed box. commas := strings.Replace(goodLogo, `viewBox="0 0 512 512"`, `viewBox="0,0,512,512"`, 1) if contains(rulesOf(Validate([]byte(commas))), "viewBox") { t.Error("a comma-separated square viewBox was rejected") } } func TestValidate_RequiresExactlyOneTitle(t *testing.T) { none := strings.Replace(goodLogo, "LambdaMail", "", 1) if !contains(rulesOf(Validate([]byte(none))), "title") { t.Error("a logo with no title was accepted") } two := strings.Replace(goodLogo, "LambdaMail", "AB", 1) if !contains(rulesOf(Validate([]byte(two))), "title") { t.Error("a logo with two titles was accepted") } empty := strings.Replace(goodLogo, "LambdaMail", " ", 1) if !contains(rulesOf(Validate([]byte(empty))), "title") { t.Error("an empty title was accepted") } } // The logo is fetched and rendered by other people's mail clients. Anything // active or anything that reaches out over the network has no business in it. func TestValidate_RefusesAnythingActiveOrExternal(t *testing.T) { cases := map[string]struct{ insert, rule string }{ "script element": {``, "forbiddenElement"}, "link": {`x`, "forbiddenElement"}, "raster image": {``, "forbiddenElement"}, "foreign object": {`x`, "forbiddenElement"}, "animation": {``, "forbiddenElement"}, "event handler": {``, "script"}, "external href": {``, "externalReference"}, "data uri": {``, "externalReference"}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { logo := strings.Replace(goodLogo, "", tc.insert+"", 1) if !contains(rulesOf(Validate([]byte(logo))), tc.rule) { t.Errorf("%s was accepted: %v", name, Validate([]byte(logo))) } }) } } func TestValidate_RefusesWhatCannotBeReadAtAll(t *testing.T) { if !contains(rulesOf(Validate([]byte(""))), "empty") { t.Error("an empty file was accepted") } if !contains(rulesOf(Validate([]byte(""))), "xml") { t.Error("malformed XML was accepted") } // A PNG renamed to .svg is the likeliest wrong upload of all. if len(Validate([]byte{0x89, 'P', 'N', 'G'})) == 0 { t.Error("a PNG was accepted as a logo") } } func TestValidate_RefusesALogoTooBigToBeFetched(t *testing.T) { padding := strings.Repeat(" ", MaxLogoBytes) logo := strings.Replace(goodLogo, "", "", 1) if !contains(rulesOf(Validate([]byte(logo))), "size") { t.Error("an oversized logo was accepted") } } // One rule at a time, against a receiver that shows nothing either way, is a // miserable way to learn these - so every violation is reported at once. func TestValidate_ReportsEveryViolationAtOnce(t *testing.T) { broken := `` rules := rulesOf(Validate([]byte(broken))) for _, want := range []string{"baseProfile", "version", "viewBox", "title", "forbiddenElement"} { if !contains(rules, want) { t.Errorf("missing %s; got %v", want, rules) } } } // The webmail's logo studio (services/webmail/src/lib/bimiLogo.ts) produces the // files in testdata/studio, and its tests keep them current. This is the other // half of the contract: every one of them has to pass the validator that guards // the upload. A studio that starts producing something the server refuses // fails here, instead of in front of an operator who did everything right. func TestWhatTheStudioProducesIsAccepted(t *testing.T) { files, err := filepath.Glob("testdata/studio/*.svg") if err != nil || len(files) == 0 { t.Fatalf("no studio output to check (%v)", err) } for _, file := range files { t.Run(filepath.Base(file), func(t *testing.T) { svg, err := os.ReadFile(file) if err != nil { t.Fatal(err) } if violations := Validate(svg); len(violations) != 0 { t.Errorf("the validator refused what the studio made: %v", violations) } }) } }