package policy import ( "bytes" "fmt" "io" "log" "os" "path" "slices" "strings" "testing" "postern/internal/db" "github.com/emersion/go-message/mail" "go.starlark.net/starlark" ) func TestExampleModule(t *testing.T) { posternDB, err := db.OpenDB(&db.Config{ DBPath: path.Join(t.TempDir(), "meta.db"), }) if err != nil { t.Fatal(err) } defer func(posternDB *db.DB) { err := posternDB.Close() if err != nil { log.Fatal(err) } }(posternDB) policy := ` def init(): return None def example(user, msg, source_folder, dest_folder): filenames = [] pdf_data = b"" bin_data = "" for a in msg.attachments: filenames.append(a.filename) if a.filename == "sample.pdf": pdf_data = a.data elif a.filename == "data.bin": bin_data = a.data return { "subject": msg.subject, "from": msg.header_from.address, "to": msg.header_to.address, "text_body": msg.text_body, "html_body": msg.html_body, "attachment_count": len(msg.attachments), "filenames": filenames, "pdf_data": pdf_data, "bin_data": bin_data, } ` policyFile := path.Join(t.TempDir(), "policy.star") err = os.WriteFile(policyFile, []byte(policy), 0644) if err != nil { t.Fatal(err) } e, err := NewEngine(policyFile, posternDB) if err != nil { t.Fatal(err) } hookVal, ok := e.globals["example"] if !ok { t.Fatal("example not found in engine globals") } msgBody, err := testMessage.assemble() if err != nil { t.Fatal(err) } m := Message{ Size: 0, OpenStream: func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(msgBody)), nil }, } k, v := starlark.String("key"), starlark.MakeInt(1) result, err := starlark.Call(e.thread, hookVal, starlark.Tuple{k, &m, k, v}, nil) if err != nil { t.Fatal(err) } // Assert the returned dict contains the expected values dict, ok := result.(*starlark.Dict) if !ok { t.Fatalf("expected dict result, got %s", result.Type()) } // Helper to get bytes value from dict dictBytes := func(dict *starlark.Dict, key string) []byte { v, _, err := dict.Get(starlark.String(key)) if err != nil || v == starlark.None { return nil } if b, ok := v.(starlark.Bytes); ok { return []byte(b) } return nil } // Helper to get list value from dict dictList := func(dict *starlark.Dict, key string) []string { v, _, err := dict.Get(starlark.String(key)) if err != nil || v == starlark.None { return nil } if l, ok := v.(*starlark.List); ok { r := make([]string, l.Len()) for i := 0; i < l.Len(); i++ { item := l.Index(i) if s, ok := item.(starlark.String); ok { r[i] = string(s) } } return r } return nil } // Validate each field if got := dictString(dict, "subject"); got != testMessage.subject { t.Errorf("subject = %q, want %q", got, testMessage.subject) } if got := dictString(dict, "from"); got != testMessage.fromAddress { t.Errorf("from = %q, want %q", got, testMessage.fromAddress) } if got := dictString(dict, "to"); got != testMessage.toAddress { t.Errorf("to = %q, want %q", got, testMessage.toAddress) } expectedTextBody := strings.ReplaceAll(testMessage.textBody, "\n", "\r\n") if got := dictString(dict, "text_body"); got != expectedTextBody { t.Errorf("text_body = %q, want %q", got, expectedTextBody) } if got := dictString(dict, "html_body"); got != testMessage.htmlBody { t.Errorf("html_body = %q, want %q", got, testMessage.htmlBody) } if got := dictInt(dict, "attachment_count"); got != len(testMessage.attachments) { t.Errorf("attachment_count = %d, want %d", got, len(testMessage.attachments)) } filenames := dictList(dict, "filenames") if len(filenames) != len(testMessage.attachments) { t.Errorf("filenames length = %d, want %d", len(filenames), len(testMessage.attachments)) } for _, f := range testMessage.attachments { if !slices.Contains(filenames, f.filename) { t.Errorf("filenames = %q, want %q", filenames, f.filename) } } if got := dictBytes(dict, "pdf_data"); string(got) != string(testMessage.attachments[0].data) { t.Errorf("pdf_data = %v, want %v", got, testMessage.attachments[0].data) } if got := dictBytes(dict, "bin_data"); string(got) != string(testMessage.attachments[1].data) { t.Errorf("bin_data = %v, want %v", got, testMessage.attachments[1].data) } } type attachmentFixture struct { filename string contentType string data []byte } type messageFixture struct { subject string fromHeader string fromAddress string toAddress string textBody string htmlBody string attachments []attachmentFixture } var testMessage = messageFixture{ subject: "Test Subject", fromHeader: `"noreply@example.com" `, fromAddress: "noreply@example.com", toAddress: "postern@example.com", textBody: "Hello,\n\nThis is the plain text body of the email.", htmlBody: "

Hello

This is the HTML body of the email.

", attachments: []attachmentFixture{ {filename: "sample.pdf", contentType: "application/pdf", data: []byte("%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\n%%EOF")}, {filename: "data.bin", contentType: "application/octet-stream", data: []byte{0x7F, 0x45, 0x4C, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00}}, }, } func (f messageFixture) assemble() ([]byte, error) { headers := map[string][]string{ "Delivered-To": {"postern@example.com"}, "Received": {"by mx1.p25.dev with ESMTP id b9d16c6997d311f18ac19200070e8571; Fri, 14 Aug 2026 11:31:44 +0000"}, "Date": {"Fri, 14 Aug 2026 11:31:43 +0000 (UTC)"}, "From": {f.fromHeader}, "To": {f.toAddress}, "Message-ID": {"<8555705.115511.1786707553174@example.com>"}, "Subject": {f.subject}, } var buf bytes.Buffer // Create top-level writer (defaults to multipart/mixed when inline/attachment parts are added) mw, err := mail.CreateWriter(&buf, mail.HeaderFromMap(headers)) if err != nil { return nil, fmt.Errorf("failed to create mail writer: %w", err) } inlineWriter, err := mw.CreateInline() if err != nil { return nil, fmt.Errorf("failed to create inline writer: %w", err) } // Plain Text Part var textHeader mail.InlineHeader textHeader.SetContentType("text/plain", map[string]string{"charset": "utf-8"}) textWriter, err := inlineWriter.CreatePart(textHeader) if err != nil { return nil, fmt.Errorf("failed to create text part: %w", err) } if _, err := textWriter.Write([]byte(f.textBody)); err != nil { return nil, err } if err := textWriter.Close(); err != nil { return nil, err } // HTML Part var htmlHeader mail.InlineHeader htmlHeader.SetContentType("text/html", map[string]string{"charset": "utf-8"}) htmlWriter, err := inlineWriter.CreatePart(htmlHeader) if err != nil { return nil, fmt.Errorf("failed to create HTML part: %w", err) } if _, err := htmlWriter.Write([]byte(f.htmlBody)); err != nil { return nil, err } if err := htmlWriter.Close(); err != nil { return nil, err } // Close alternative container if err := inlineWriter.Close(); err != nil { return nil, err } // Attachments for _, att := range f.attachments { var header mail.AttachmentHeader header.SetContentType(att.contentType, nil) header.SetFilename(att.filename) writer, err := mw.CreateAttachment(header) if err != nil { return nil, fmt.Errorf("failed to create attachment %s: %w", att.filename, err) } if _, err := writer.Write(att.data); err != nil { return nil, err } if err := writer.Close(); err != nil { return nil, err } } // Finalize email body formatting and boundaries if err := mw.Close(); err != nil { return nil, err } return buf.Bytes(), nil }