integration

package module
v0.0.0-...-355edc3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 28, 2023 License: BSD-2-Clause Imports: 20 Imported by: 2

README

go-milter integration tests

How it works

The integration test runner starts a receiving SMTP server and test milter servers. It then configures different MTAs to use the test milter servers and send all emails to the receiving SMTP server. When all this is set up and running, the test runner send the testcases as SMTP transactions to the MTA and checks if the right filter decision at the right time was made and whether the outgoing SMTP message is as expected.

Testcases

A testcase is a text file that has three parts: input steps, the expected milter decision (accept, reject etc.) and optional output data (mail from, header etc.) that gets compared with the actual output of the MTA.

Input steps

You can omit input steps. Necessary input steps get automatically added to the testcase.

HELO [hello-hostname]

Sends a HELO/EHLO to the SMTP server

STARTTLS

Start TLS encryption of connection

AUTH [user1@example.com|user2@example.com]

Authenticates SMTP connection. There are only two users hard-coded user1@example.com (password password1) and user2@example.com (password password2).

FROM <addr> args

Sends a MAIL FROM SMTP command.

TO <addr> args

Sends a RCPT TO SMTP command.

RESET

Sends a RSET SMTP command.

HEADER

Sends the DATA SMTP command and then the header. The header to send follows the HEADER line. The end of the header is marked with a single . in a line (like in SMTP connections)

BODY

Sends the body part of the DATA. The end of the body part is also marked with a single ..

DECISION [decision]@[step]

Every testcase needs to have a DECISION. Valid decisions are: ACCEPT, TEMPFAIL, REJECT, DISCARD-OR-QUARANTINE and CUSTOM. If you specify CUSTOM then the lines after the DECISION line get parsed as a SMTP response and the mitler should set this SMTP response.

The step can be HELO, FROM, TO, DATA, EOM and *. If the step is omitted * is assumed. * means that the decision can happen after any step.

Output

If you specified ACCEPT as decision you can add FROM, TO, HEADER and BODY lines (see syntax above) after the DECISION line. These values get compared with the actual result the MTA send to our receiving SMTP server.

How to add integration tests to your go-milter based mail filter

You need docker since the test are run inside a docker container.

Add a Makefile

GO_MILTER_INTEGRATION_DIR := $(shell cd integration && go list -f '{{.Dir}}' github.com/d--j/go-milter/integration)

integration:
	docker build -q -t go-milter-integration "$(GO_MILTER_INTEGRATION_DIR)/docker" && \
	docker run --rm -w /usr/src/root/integration -v $(PWD):/usr/src/root go-milter-integration \
	go run github.com/d--j/go-milter/integration/runner -filter '.*' ./tests

.PHONY: integration

Add an integration directory. Execute the following inside:

go mod init
go mod edit -require github.com/d--j/go-milter
go mod edit -require github.com/d--j/go-milter/integration
go mod edit -replace $(cd .. && go list '{{.Path}}')=..
mkdir tests

Tests consist of a test milter and testcases that get feed into an MTA that is configured to use the test milter.

A test milter can look something like this:

package main

import (
	"context"

	"github.com/d--j/go-milter/integration"
	"github.com/d--j/go-milter/mailfilter"
)

func main() {
	integration.RequiredTags("auth-plain", "auth-no", "tls-starttls", "tls-no")
	integration.Test(func(ctx context.Context, trx mailfilter.Trx) (mailfilter.Decision, error) {
		return mailfilter.CustomErrorResponse(501, "Test"), nil
	}, mailfilter.WithDecisionAt(mailfilter.DecisionAtMailFrom))
}

A testcase for this milter would be:

DECISION CUSTOM
501 Test

How to handle dynamic data

If your milter is time dependent or relies on external data you can use monkey pathing to make the output of your milter static. E.g. the following sets a constant time for time.Now and mocks the SPF checks of your milter to static values:

package patches

import (
	"net"
	"strings"
	"time"

	"blitiri.com.ar/go/spf"
	"github.com/agiledragon/gomonkey/v2"
)

var ConstantDate = time.Date(2023, time.January, 1, 12, 0, 0, 0, time.UTC)

func Apply() *gomonkey.Patches {
	return gomonkey.
		ApplyFuncReturn(time.Now, ConstantDate).
		ApplyFunc(spf.CheckHostWithSender, func(_ net.IP, helo, sender string, _ ...spf.Option) (spf.Result, error) {
			if strings.HasSuffix(sender, "@example.com") || helo == "example.com" {
				return spf.Pass, nil
			}
			if strings.HasSuffix(sender, "@example.net") || helo == "example.net" {
				return spf.Fail, nil
			}
			return spf.None, nil
		})
}

The Received line that the MTA add contains dynamic data (date, queue id). Your test milter will see this dynamic header, but before comparing the SMTP message with the testcase output data the test runner replaces the first Recieved header with the static header Received: placeholder.

Documentation

Overview

Package integration has integration tests and utilities for integration tests.

Index

Constants

View Source
const ExitSkip = 99

Variables

View Source
var Address = flag.String("address", "", "address")
View Source
var Network = flag.String("network", "", "network")
View Source
var Tags []string

Functions

func CompareOutputSendmail

func CompareOutputSendmail(expected, got *Output) bool

CompareOutputSendmail is a relaxed compare function that does only check that the header values are all there – the order and folding do not matter.

func DiffOutput

func DiffOutput(expected, got *Output) (string, bool)

func HasTag

func HasTag(tag string) bool

func RequiredTags

func RequiredTags(tags ...string)

func Skip

func Skip(reason string)

func Test

Types

type AddrArg

type AddrArg struct {
	Addr, Arg string
}

func ToAddrArg

func ToAddrArg(addr string, options *smtp.MailOptions) *AddrArg

func ToAddrArgRcpt

func ToAddrArgRcpt(addr string, options *smtp.RcptOptions) *AddrArg

type Decision

type Decision struct {
	Code    int
	Message *string
	Step    DecisionStep
}

func (Decision) Compare

func (d Decision) Compare(code uint16, message string, step DecisionStep) bool

func (Decision) String

func (d Decision) String() string

type DecisionStep

type DecisionStep int
const (
	StepAny DecisionStep = iota
	StepHelo
	StepFrom
	StepTo
	StepData
	StepEOM
)

func (DecisionStep) String

func (s DecisionStep) String() string

type InputStep

type InputStep struct {
	What      string
	Addr, Arg string
	Data      []byte
}

type Output

type Output struct {
	From         *AddrArg
	To           []*AddrArg
	Header, Body []byte
}

func (*Output) String

func (o *Output) String() string

type TestCase

type TestCase struct {
	InputSteps []*InputStep
	Decision   *Decision
	Output     *Output
}

func ParseTestCase

func ParseTestCase(filename string) (*TestCase, error)

func (*TestCase) ExpectsOutput

func (c *TestCase) ExpectsOutput() bool

Directories

Path Synopsis
mta
tests

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL