1# This tests Discourse by:
2# 1. logging in as the admin user
3# 2. sending a private message to the admin user through the API
4# 3. replying to that message via email.
5
6import ./make-test-python.nix (
7 { pkgs, lib, package ? pkgs.discourse, ... }:
8 let
9 certs = import ./common/acme/server/snakeoil-certs.nix;
10 clientDomain = "client.fake.domain";
11 discourseDomain = certs.domain;
12 adminPassword = "eYAX85qmMJ5GZIHLaXGDAoszD7HSZp5d";
13 secretKeyBase = "381f4ac6d8f5e49d804dae72aa9c046431d2f34c656a705c41cd52fed9b4f6f76f51549f0b55db3b8b0dded7a00d6a381ebe9a4367d2d44f5e743af6628b4d42";
14 admin = {
15 email = "alice@${clientDomain}";
16 username = "alice";
17 fullName = "Alice Admin";
18 passwordFile = "${pkgs.writeText "admin-pass" adminPassword}";
19 };
20 in
21 {
22 name = "discourse";
23 meta = with pkgs.lib.maintainers; {
24 maintainers = [ talyz ];
25 };
26
27 nodes.discourse =
28 { nodes, ... }:
29 {
30 virtualisation.memorySize = 2048;
31 virtualisation.cores = 4;
32 virtualisation.useNixStoreImage = true;
33
34 imports = [ common/user-account.nix ];
35
36 security.pki.certificateFiles = [
37 certs.ca.cert
38 ];
39
40 networking.extraHosts = ''
41 127.0.0.1 ${discourseDomain}
42 ${nodes.client.config.networking.primaryIPAddress} ${clientDomain}
43 '';
44
45 services.postfix = {
46 enableSubmission = true;
47 enableSubmissions = true;
48 submissionsOptions = {
49 smtpd_sasl_auth_enable = "yes";
50 smtpd_client_restrictions = "permit";
51 };
52 };
53
54 environment.systemPackages = [ pkgs.jq ];
55
56 services.postgresql.package = pkgs.postgresql_13;
57
58 services.discourse = {
59 enable = true;
60 inherit admin package;
61 hostname = discourseDomain;
62 sslCertificate = "${certs.${discourseDomain}.cert}";
63 sslCertificateKey = "${certs.${discourseDomain}.key}";
64 secretKeyBaseFile = "${pkgs.writeText "secret-key-base" secretKeyBase}";
65 enableACME = false;
66 mail.outgoing.serverAddress = clientDomain;
67 mail.incoming.enable = true;
68 siteSettings = {
69 posting = {
70 min_post_length = 5;
71 min_first_post_length = 5;
72 min_personal_message_post_length = 5;
73 };
74 };
75 unicornTimeout = 900;
76 };
77
78 networking.firewall.allowedTCPPorts = [ 25 465 ];
79 };
80
81 nodes.client =
82 { nodes, ... }:
83 {
84 imports = [ common/user-account.nix ];
85
86 security.pki.certificateFiles = [
87 certs.ca.cert
88 ];
89
90 networking.extraHosts = ''
91 127.0.0.1 ${clientDomain}
92 ${nodes.discourse.config.networking.primaryIPAddress} ${discourseDomain}
93 '';
94
95 services.dovecot2 = {
96 enable = true;
97 protocols = [ "imap" ];
98 modules = [ pkgs.dovecot_pigeonhole ];
99 };
100
101 services.postfix = {
102 enable = true;
103 origin = clientDomain;
104 relayDomains = [ clientDomain ];
105 config = {
106 compatibility_level = "2";
107 smtpd_banner = "ESMTP server";
108 myhostname = clientDomain;
109 mydestination = clientDomain;
110 };
111 };
112
113 environment.systemPackages =
114 let
115 replyToEmail = pkgs.writeScriptBin "reply-to-email" ''
116 #!${pkgs.python3.interpreter}
117 import imaplib
118 import smtplib
119 import ssl
120 import email.header
121 from email import message_from_bytes
122 from email.message import EmailMessage
123
124 with imaplib.IMAP4('localhost') as imap:
125 imap.login('alice', 'foobar')
126 imap.select()
127 status, data = imap.search(None, 'ALL')
128 assert status == 'OK'
129
130 nums = data[0].split()
131 assert len(nums) == 1
132
133 status, msg_data = imap.fetch(nums[0], '(RFC822)')
134 assert status == 'OK'
135
136 msg = email.message_from_bytes(msg_data[0][1])
137 subject = str(email.header.make_header(email.header.decode_header(msg['Subject'])))
138 reply_to = email.header.decode_header(msg['Reply-To'])[0][0]
139 message_id = email.header.decode_header(msg['Message-ID'])[0][0]
140 date = email.header.decode_header(msg['Date'])[0][0]
141
142 ctx = ssl.create_default_context()
143 with smtplib.SMTP_SSL(host='${discourseDomain}', context=ctx) as smtp:
144 reply = EmailMessage()
145 reply['Subject'] = 'Re: ' + subject
146 reply['To'] = reply_to
147 reply['From'] = 'alice@${clientDomain}'
148 reply['In-Reply-To'] = message_id
149 reply['References'] = message_id
150 reply['Date'] = date
151 reply.set_content("Test reply.")
152
153 smtp.send_message(reply)
154 smtp.quit()
155 '';
156 in
157 [ replyToEmail ];
158
159 networking.firewall.allowedTCPPorts = [ 25 ];
160 };
161
162
163 testScript = { nodes }:
164 let
165 request = builtins.toJSON {
166 title = "Private message";
167 raw = "This is a test message.";
168 target_usernames = admin.username;
169 archetype = "private_message";
170 };
171 in ''
172 discourse.start()
173 client.start()
174
175 discourse.wait_for_unit("discourse.service")
176 discourse.wait_for_file("/run/discourse/sockets/unicorn.sock")
177 discourse.wait_until_succeeds("curl -sS -f https://${discourseDomain}")
178 discourse.succeed(
179 "curl -sS -f https://${discourseDomain}/session/csrf -c cookie -b cookie -H 'Accept: application/json' | jq -r '\"X-CSRF-Token: \" + .csrf' > csrf_token",
180 "curl -sS -f https://${discourseDomain}/session -c cookie -b cookie -H @csrf_token -H 'Accept: application/json' -d 'login=${nodes.discourse.config.services.discourse.admin.username}' -d \"password=${adminPassword}\" | jq -e '.user.username == \"${nodes.discourse.config.services.discourse.admin.username}\"'",
181 "curl -sS -f https://${discourseDomain}/login -v -H 'Accept: application/json' -c cookie -b cookie 2>&1 | grep ${nodes.discourse.config.services.discourse.admin.username}",
182 )
183
184 client.wait_for_unit("postfix.service")
185 client.wait_for_unit("dovecot2.service")
186
187 discourse.succeed(
188 "sudo -u discourse discourse-rake api_key:create_master[master] >api_key",
189 'curl -sS -f https://${discourseDomain}/posts -X POST -H "Content-Type: application/json" -H "Api-Key: $(<api_key)" -H "Api-Username: system" -d \'${request}\' ',
190 )
191
192 client.wait_until_succeeds("reply-to-email")
193
194 discourse.wait_until_succeeds(
195 'curl -sS -f https://${discourseDomain}/topics/private-messages/system -H "Accept: application/json" -H "Api-Key: $(<api_key)" -H "Api-Username: system" | jq -e \'if .topic_list.topics[0].id != null then .topic_list.topics[0].id else null end\' >topic_id'
196 )
197 discourse.succeed(
198 'curl -sS -f https://${discourseDomain}/t/$(<topic_id) -H "Accept: application/json" -H "Api-Key: $(<api_key)" -H "Api-Username: system" | jq -e \'if .post_stream.posts[1].cooked == "<p>Test reply.</p>" then true else null end\' '
199 )
200 '';
201 })