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