// Copyright 2023 schukai GmbH // SPDX-License-Identifier: AGPL-3.0 //go:build !bench && !race package jobqueue import ( "context" "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/stretchr/testify/assert" "net" "os" "testing" "time" ) const DOCKER_TEST_HOST_IP = "127.0.0.1" func startTestSMTPDockerImageAndContainer(t *testing.T, port string, ctx context.Context) error { t.Helper() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return err } imageName := "axllent/mailpit" reader, err := cli.ImagePull(ctx, imageName, types.ImagePullOptions{}) if err != nil { return err } // if debug image pull, comment out the following lines //_, _ = io.Copy(os.Stdout, reader) _ = reader hostConfig := &container.HostConfig{ PortBindings: nat.PortMap{ "1025/tcp": []nat.PortBinding{ { HostIP: DOCKER_TEST_HOST_IP, HostPort: port, }, }, // if you want to test the web interface, uncomment the following lines //"8025/tcp": []nat.PortBinding{ // { // HostIP: DOCKER_TEST_HOST_IP, // HostPort: "8025", // }, //}, }, } resp, err := cli.ContainerCreate(ctx, &container.Config{ Image: imageName, }, hostConfig, nil, nil, "") if err != nil { return err } if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { return err } go func() { <-ctx.Done() timeout := 0 stopOptions := container.StopOptions{ Timeout: &timeout, Signal: "SIGKILL", } newCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() if err := cli.ContainerStop(newCtx, resp.ID, stopOptions); err != nil { t.Errorf("ContainerStop returned error: %v", err) } if err := cli.ContainerRemove(newCtx, resp.ID, types.ContainerRemoveOptions{ Force: true, }); err != nil { t.Errorf("ContainerRemove returned error: %v", err) } }() statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) select { case err := <-errCh: if err != nil { // empty error means container exited normally (see container_wait.go) if err.Error() == "" { return nil } return err } case <-statusCh: } return nil } func TestMailRunner(t *testing.T) { if os.Getenv("CI_SERVER") != "" { t.Skip("Skipping test because CI_SERVER is set") // TODO: run this test in CI } ctb := context.Background() ctx, cancel := context.WithCancel(ctb) t.Cleanup(func() { cancel() time.Sleep(1 * time.Second) }) listener, err := net.Listen("tcp", DOCKER_TEST_HOST_IP+":0") if err != nil { t.Errorf("Unexpected error: %v", err) return } portAsInt := listener.Addr().(*net.TCPAddr).Port portAsString := fmt.Sprintf("%d", portAsInt) _ = listener.Close() done := make(chan bool) go func() { err = startTestSMTPDockerImageAndContainer(t, portAsString, ctx) if err != nil { t.Errorf("Unexpected error: %v", err) cancel() } done <- true }() waitCtx, waitCancel := context.WithTimeout(ctx, 60*time.Second) defer waitCancel() for { conn, err := net.DialTimeout("tcp", net.JoinHostPort(DOCKER_TEST_HOST_IP, portAsString), 1*time.Second) if err == nil { err = conn.Close() assert.Nil(t, err) break } select { case <-waitCtx.Done(): t.Error("Timeout waiting for container service") cancel() return default: time.Sleep(1 * time.Second) } } time.Sleep(1 * time.Second) mailRunnable := &MailRunnable{ To: "to@example.com", From: "from@example.com", Subject: "this is a test", Body: "this is the body", Server: DOCKER_TEST_HOST_IP, Port: portAsString, Username: "", Password: "", Headers: map[string]string{ "X-Test": "test", }, } xtx := context.Background() result, err := mailRunnable.Run(xtx) // Assertions assert.NoError(t, err) assert.Equal(t, ResultStatusSuccess, result.Status) assert.IsType(t, MailResult{}, result.Data) // check result.Data contains 4 files mailResult := result.Data.Sent assert.Equal(t, true, mailResult) cancel() select { case <-done: time.Sleep(1 * time.Second) case <-time.After(1 * time.Minute): t.Error("test hangs, timeout reached") } }