git » chasquid » main » tree

[main] / internal / dkim / canonicalize_test.go

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
}