xref: /plugin/smtp/_test/IntegrationTest.php (revision 28d0809a4424b7a71eb621f24d1b3a19c5927170)
1<?php
2
3namespace dokuwiki\plugin\smtp\test;
4
5use DokuWikiTest;
6use dokuwiki\HTTP\DokuHTTPClient;
7use Mailer;
8
9/**
10 * Full integration test for the smtp plugin
11 *
12 * This sends a real mail through DokuWiki's Mailer (which the plugin intercepts and
13 * delivers over SMTP) to a running Mailpit server and then verifies through Mailpit's
14 * HTTP API that the message was delivered correctly.
15 *
16 * The Mailpit server is configured through environment variables:
17 *   MAILPIT_HOST       hostname of the Mailpit server (enables the test)
18 *   MAILPIT_SMTP_PORT  the SMTP port to deliver to         (default: 1025)
19 *   MAILPIT_HTTP_PORT  the HTTP API/web interface port     (default: 8025)
20 *
21 * When MAILPIT_HOST is not set the test is skipped, so it does not break the regular
22 * test suite which runs without the Mailpit service. When it is set the server must be
23 * reachable, otherwise the test fails.
24 *
25 * @group plugin_smtp
26 * @group plugins
27 */
28class IntegrationTest extends DokuWikiTest
29{
30    protected $pluginsEnabled = ['smtp'];
31
32    /** @var string hostname of the Mailpit server */
33    protected $host;
34
35    /** @var int SMTP port to deliver to */
36    protected $smtpPort;
37
38    /** @var string base URL of the Mailpit HTTP API, without trailing slash */
39    protected $apiBase;
40
41    /** @inheritdoc */
42    public function setUp(): void
43    {
44        parent::setUp();
45
46        // the test only runs when a Mailpit server is configured via MAILPIT_HOST,
47        // otherwise it is skipped (eg. regular CI/local runs without Mailpit)
48        $this->host = getenv('MAILPIT_HOST');
49        if ($this->host === false || $this->host === '') {
50            $this->markTestSkipped('MAILPIT_HOST is not set, skipping the Mailpit integration test');
51        }
52
53        $this->smtpPort = (int)(getenv('MAILPIT_SMTP_PORT') ?: 1025);
54        $httpPort = (int)(getenv('MAILPIT_HTTP_PORT') ?: 8025);
55        $this->apiBase = 'http://' . $this->host . ':' . $httpPort;
56
57        // a Mailpit server was requested, so it must actually be reachable
58        $sock = @fsockopen($this->host, $this->smtpPort, $errno, $errstr, 2);
59        if (!$sock) {
60            $this->fail("No Mailpit SMTP server reachable at $this->host:$this->smtpPort ($errstr)");
61        }
62        fclose($sock);
63
64        // point the plugin at our Mailpit server
65        global $conf;
66        $conf['plugin']['smtp']['smtp_host'] = $this->host;
67        $conf['plugin']['smtp']['smtp_port'] = $this->smtpPort;
68        $conf['plugin']['smtp']['smtp_ssl'] = '';
69        $conf['plugin']['smtp']['auth_user'] = '';
70        $conf['plugin']['smtp']['auth_pass'] = '';
71        $conf['plugin']['smtp']['localdomain'] = 'test.localhost';
72        $conf['plugin']['smtp']['debug'] = 0;
73
74        // give DokuWiki's Mailer a sane default sender
75        $conf['mailfrom'] = 'wiki@example.com';
76    }
77
78    /**
79     * A mail sent through DokuWiki's Mailer must be delivered to Mailpit over SMTP.
80     *
81     * To and Cc come from the message headers, while the Bcc recipient is delivered
82     * through the SMTP envelope even though the plugin strips the Bcc header from the
83     * message body.
84     */
85    public function testSendMail(): void
86    {
87        $subject = 'SMTP plugin integration test ' . uniqid('', true);
88        $bodyText = 'Hello from the DokuWiki smtp plugin integration test.';
89
90        $mailer = new Mailer();
91        $mailer->from('Wiki Admin <wiki@example.com>');
92        $mailer->to('Jane Doe <jane@example.com>');
93        $mailer->cc('carol@example.com');
94        $mailer->bcc('secret@example.com');
95        $mailer->subject($subject);
96        $mailer->setBody($bodyText);
97
98        $ok = $mailer->send();
99        $this->assertTrue($ok, 'Mailer::send() should report success when talking to Mailpit');
100
101        // the message should now be available through the Mailpit API
102        $message = $this->findMessageBySubject($subject);
103        $this->assertNotNull($message, 'The sent message should show up in Mailpit');
104
105        // sender and header recipients
106        $this->assertEquals('wiki@example.com', $message['From']['Address']);
107        $this->assertEquals(['jane@example.com'], $this->addresses($message['To']));
108        $this->assertEquals(['carol@example.com'], $this->addresses($message['Cc']));
109        $this->assertEquals(['secret@example.com'], $this->addresses($message['Bcc']));
110
111        // the body must have arrived intact
112        $full = $this->apiGet('/api/v1/message/' . rawurlencode($message['ID']));
113        $this->assertStringContainsString($bodyText, $full['Text']);
114    }
115
116    /**
117     * Reduce a Mailpit address list to a plain list of email addresses
118     *
119     * @param array $list list of {Name, Address} entries as returned by Mailpit
120     * @return string[]
121     */
122    protected function addresses(array $list): array
123    {
124        return array_map(static fn($addr) => $addr['Address'], $list);
125    }
126
127    /**
128     * Find a message in Mailpit whose subject contains the given needle
129     *
130     * DokuWiki prefixes the subject with the wiki title (eg. "[My Wiki] ..."), so we
131     * match on a unique substring rather than the full subject. Mailpit stores the
132     * message before acknowledging the SMTP DATA command, so it is available as soon
133     * as Mailer::send() returns - no polling needed.
134     *
135     * @param string $needle a unique substring of the subject
136     * @return array|null the message stub from the listing or null if not found
137     */
138    protected function findMessageBySubject($needle)
139    {
140        $list = $this->apiGet('/api/v1/messages');
141        foreach ($list['messages'] as $msg) {
142            if (strpos($msg['Subject'], $needle) !== false) return $msg;
143        }
144        return null;
145    }
146
147    /**
148     * Perform a GET request against the Mailpit API and return the decoded JSON
149     *
150     * @param string $path API path including the leading slash
151     * @return array
152     */
153    protected function apiGet($path)
154    {
155        $client = new DokuHTTPClient();
156        $body = $client->get($this->apiBase . $path);
157        $this->assertNotFalse($body, 'Mailpit API request to ' . $path . ' failed: ' . $client->error);
158        return json_decode($body, true);
159    }
160}
161