package dkim
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestStringToCanonicalization(t *testing.T) {
cases := []struct {
in string
want canonicalization
err error
}{
{"simple", simpleCanonicalization, nil},
{"relaxed", relaxedCanonicalization, nil},
{"", "", errUnknownCanonicalization},
{" ", "", errUnknownCanonicalization},
{" simple", "", errUnknownCanonicalization},
{"simple ", "", errUnknownCanonicalization},
{"si mple ", "", errUnknownCanonicalization},
}
for _, c := range cases {
got, err := stringToCanonicalization(c.in)
if diff := cmp.Diff(c.want, got); diff != "" {
t.Errorf("stringToCanonicalization(%q) diff (-want +got): %s",
c.in, diff)
}
diff := cmp.Diff(c.err, err, cmpopts.EquateErrors())
if diff != "" {
t.Errorf("stringToCanonicalization(%q) err diff (-want +got): %s",
c.in, diff)
}
}
}
func TestSimpleBody(t *testing.T) {
cases := []struct {
in, want string
}{
// Bodies end with \r\n, including the empty one.
{"", "\r\n"},
{"a", "a\r\n"},
{"a\r\n", "a\r\n"},
// Repeated CRLF at the end of the body is replaced with a single CRLF.
{"Body \r\n\r\n\r\n", "Body \r\n"},
// Example from RFC.
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.5
{
" C \r\nD \t E\r\n\r\n\r\n",
" C \r\nD \t E\r\n",
},
}
for _, c := range cases {
got := simpleCanonicalization.body(c.in)
if diff := cmp.Diff(c.want, got); diff != "" {
t.Errorf("simpleCanonicalization.body(%q) diff (-want +got): %s",
c.in, diff)
}
}
}
func TestRelaxBody(t *testing.T) {
cases := []struct {
in, want string
}{
{"a\r\n", "a\r\n"},
// Repeated WSP before CRLF.
{"a \r\n", "a\r\n"},
{"a \r\n", "a\r\n"},
{"a \t \r\n", "a\r\n"},
{"a\t\t\t\r\n", "a\r\n"},
// Repeated WSP within a line.
{"a b\r\n", "a b\r\n"},
{"a\t\t\tb\r\n", "a b\r\n"},
{"a \t \t b\r\n", "a b\r\n"},
// Ignore empty lines at the end.
{"a\r\n\r\n", "a\r\n"},
{"a\r\n\r\n\r\n", "a\r\n"},
// Body must end with \r\n, unless it's empty.
{"", ""},
{"\r\n", "\r\n"},
{"a", "a\r\n"},
// Example from RFC.
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.5
{" C \r\nD \t E\r\n\r\n\r\n", " C\r\nD E\r\n"},
}
for _, c := range cases {
got := relaxedCanonicalization.body(c.in)
if diff := cmp.Diff(c.want, got); diff != "" {
t.Errorf("relaxedCanonicalization.body(%q) diff (-want +got): %s",
c.in, diff)
}
}
}
func mkHs(hs ...string) headers {
var headers headers
for i := 0; i < len(hs); i += 2 {
h := header{
Name: hs[i],
Value: hs[i+1],
Source: hs[i] + ":" + hs[i+1],
}
headers = append(headers, h)
}
return headers
}
func TestHeaders(t *testing.T) {
cases := []struct {
in string
wantS headers
wantR headers
}{
// Unfold headers.
{"A: B\r\n C\r\n", mkHs("A", " B\r\n C"), mkHs("a", "B C")},
{"A: B\r\n\tC\r\n", mkHs("A", " B\r\n\tC"), mkHs("a", "B C")},
{"A: B\r\n \t C\r\n", mkHs("A", " B\r\n \t C"), mkHs("a", "B C")},
// Reduce all sequences of WSP within a line to a single SP.
{"A: B C\r\n", mkHs("A", " B C"), mkHs("a", "B C")},
{"A: B\t\tC\r\n", mkHs("A", " B\t\tC"), mkHs("a", "B C")},
{"A: B \t \t C\r\n", mkHs("A", " B \t \t C"), mkHs("a", "B C")},
// Delete all WSP at the end of each unfolded header field.
{"A: B \r\n", mkHs("A", " B "), mkHs("a", "B")},
{"A: B \r\n", mkHs("A", " B "), mkHs("a", "B")},
{"A: B\t \r\n", mkHs("A", " B\t "), mkHs("a", "B")},
{"A: B\t\t\t\r\n", mkHs("A", " B\t\t\t"), mkHs("a", "B")},
{"A: B\r\n \t C \t\r\n",
mkHs("A", " B\r\n \t C \t"), mkHs("a", "B C")},
// Whitespace before and after the colon.
{"A : B\r\n", mkHs("A ", " B"), mkHs("a", "B")},
{"A : B\r\n", mkHs("A ", " B"), mkHs("a", "B")},
{"A\t:\tB\r\n", mkHs("A\t", "\tB"), mkHs("a", "B")},
{"A\t \t : \t \tB\r\n", mkHs("A\t \t ", " \t \tB"), mkHs("a", "B")},
// Example from RFC.
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.5
{"A: X\r\nB : Y\t\r\n\tZ \r\n",
mkHs("A", " X", "B ", " Y\t\r\n\tZ "),
mkHs("a", "X", "b", "Y Z")},
}
for i, c := range cases {
hs, _, err := parseMessage(c.in)
if err != nil {
t.Fatalf("parseMessage(%q) = %v, want nil", c.in, err)
}
gotS := simpleCanonicalization.headers(hs)
if diff := cmp.Diff(c.wantS, gotS); diff != "" {
t.Errorf("%d: simpleCanonicalization.headers(%q) diff (-want +got): %s",
i, c.in, diff)
}
gotR := relaxedCanonicalization.headers(hs)
if diff := cmp.Diff(c.wantR, gotR); diff != "" {
t.Errorf("%d: relaxedCanonicalization.headers(%q) diff (-want +got): %s",
i, c.in, diff)
}
// Test the single-header variant if possible.
if len(hs) == 1 {
gotS := simpleCanonicalization.header(hs[0])
if diff := cmp.Diff(c.wantS[0], gotS); diff != "" {
t.Errorf("%d: simpleCanonicalization.header(%q) diff (-want +got): %s",
i, c.in, diff)
}
gotR := relaxedCanonicalization.header(hs[0])
if diff := cmp.Diff(c.wantR[0], gotR); diff != "" {
t.Errorf("%d: relaxedCanonicalization.header(%q) diff (-want +got): %s",
i, c.in, diff)
}
}
}
}
func TestBadCanonicalization(t *testing.T) {
bad := canonicalization("bad")
if !panics(func() { bad.body("") }) {
t.Errorf("bad.body() did not panic")
}
if !panics(func() { bad.header(header{}) }) {
t.Errorf("bad.header() did not panic")
}
if !panics(func() { bad.headers(nil) }) {
t.Errorf("bad.headers() did not panic")
}
}
func panics(f func()) (panicked bool) {
defer func() {
r := recover()
panicked = r != nil
}()
f()
return
}