1{ pkgs, lib, ... }:
2{
3 name = "castopod";
4 meta = with lib.maintainers; {
5 maintainers = [ alexoundos ];
6 };
7
8 nodes.castopod =
9 { nodes, ... }:
10 {
11 # otherwise 500 MiB file upload fails!
12 virtualisation.diskSize = 512 + 3 * 512;
13
14 networking.firewall.allowedTCPPorts = [ 80 ];
15 networking.extraHosts = lib.strings.concatStringsSep "\n" (
16 lib.attrsets.mapAttrsToList (
17 name: _: "127.0.0.1 ${name}"
18 ) nodes.castopod.services.nginx.virtualHosts
19 );
20
21 services.castopod = {
22 enable = true;
23 database.createLocally = true;
24 localDomain = "castopod.example.com";
25 maxUploadSize = "512M";
26 };
27 };
28
29 nodes.client =
30 {
31 nodes,
32 pkgs,
33 lib,
34 ...
35 }:
36 let
37 domain = nodes.castopod.services.castopod.localDomain;
38
39 getIP = node: (builtins.head node.networking.interfaces.eth1.ipv4.addresses).address;
40
41 targetPodcastSize = 500 * 1024 * 1024;
42 lameMp3Bitrate = 348300;
43 lameMp3FileAdjust = -800;
44 targetPodcastDuration = toString ((targetPodcastSize + lameMp3FileAdjust) / (lameMp3Bitrate / 8));
45 bannerWidth = 3000;
46 banner = pkgs.runCommand "gen-castopod-cover.jpg" { } ''
47 ${pkgs.imagemagick}/bin/magick `
48 `-background green -bordercolor white -gravity northwest xc:black `
49 `-duplicate 99 `
50 `-seed 1 -resize "%[fx:rand()*72+24]" `
51 `-seed 0 -rotate "%[fx:rand()*360]" -border 6x6 -splice 16x36 `
52 `-seed 0 -rotate "%[fx:floor(rand()*4)*90]" -resize "150x50!" `
53 `+append -crop 10x1@ +repage -roll "+%[fx:(t%2)*72]+0" -append `
54 `-resize ${toString bannerWidth} -quality 1 $out
55 '';
56
57 coverWidth = toString 3000;
58 cover = pkgs.runCommand "gen-castopod-banner.jpg" { } ''
59 ${pkgs.imagemagick}/bin/magick `
60 `-background white -bordercolor white -gravity northwest xc:black `
61 `-duplicate 99 `
62 `-seed 1 -resize "%[fx:rand()*72+24]" `
63 `-seed 0 -rotate "%[fx:rand()*360]" -border 6x6 -splice 36x36 `
64 `-seed 0 -rotate "%[fx:floor(rand()*4)*90]" -resize "144x144!" `
65 `+append -crop 10x1@ +repage -roll "+%[fx:(t%2)*72]+0" -append `
66 `-resize ${coverWidth} -quality 1 $out
67 '';
68 in
69 {
70 networking.extraHosts = lib.strings.concatStringsSep "\n" (
71 lib.attrsets.mapAttrsToList (
72 name: _: "${getIP nodes.castopod} ${name}"
73 ) nodes.castopod.services.nginx.virtualHosts
74 );
75
76 environment.systemPackages =
77 let
78 username = "admin";
79 email = "admin@${domain}";
80 password = "Abcd1234";
81 podcastTitle = "Some Title";
82 episodeTitle = "Episode Title";
83 browser-test =
84 pkgs.writers.writePython3Bin "browser-test"
85 {
86 libraries = [ pkgs.python3Packages.selenium ];
87 flakeIgnore = [
88 "E124"
89 "E501"
90 ];
91 }
92 ''
93 from selenium.webdriver.common.by import By
94 from selenium.webdriver import Firefox
95 from selenium.webdriver.firefox.options import Options
96 from selenium.webdriver.firefox.service import Service
97 from selenium.webdriver.support.ui import WebDriverWait
98 from selenium.webdriver.support import expected_conditions as EC
99 from subprocess import STDOUT
100 import logging
101
102 selenium_logger = logging.getLogger("selenium")
103 selenium_logger.setLevel(logging.DEBUG)
104 selenium_logger.addHandler(logging.StreamHandler())
105
106 options = Options()
107 options.add_argument('--headless')
108 service = Service(log_output=STDOUT)
109 driver = Firefox(options=options, service=service)
110 driver = Firefox(options=options)
111 driver.implicitly_wait(30)
112 driver.set_page_load_timeout(60)
113
114 # install ##########################################################
115
116 driver.get('http://${domain}/cp-install')
117
118 wait = WebDriverWait(driver, 20)
119
120 wait.until(EC.title_contains("installer"))
121
122 driver.find_element(By.CSS_SELECTOR, '#username').send_keys(
123 '${username}'
124 )
125 driver.find_element(By.CSS_SELECTOR, '#email').send_keys(
126 '${email}'
127 )
128 driver.find_element(By.CSS_SELECTOR, '#password').send_keys(
129 '${password}'
130 )
131 driver.find_element(By.XPATH,
132 "//button[contains(., 'Finish install')]"
133 ).click()
134
135 wait.until(EC.title_contains("Auth"))
136
137 driver.find_element(By.CSS_SELECTOR, '#email').send_keys(
138 '${email}'
139 )
140 driver.find_element(By.CSS_SELECTOR, '#password').send_keys(
141 '${password}'
142 )
143 driver.find_element(By.XPATH,
144 "//button[contains(., 'Login')]"
145 ).click()
146
147 wait.until(EC.title_contains("Admin dashboard"))
148
149 # create podcast ###################################################
150
151 driver.get('http://${domain}/admin/podcasts/new')
152
153 wait.until(EC.title_contains("Create podcast"))
154
155 driver.find_element(By.CSS_SELECTOR, '#cover').send_keys(
156 '${cover}'
157 )
158 driver.find_element(By.CSS_SELECTOR, '#banner').send_keys(
159 '${banner}'
160 )
161 driver.find_element(By.CSS_SELECTOR, '#title').send_keys(
162 '${podcastTitle}'
163 )
164 driver.find_element(By.CSS_SELECTOR, '#handle').send_keys(
165 'some_handle'
166 )
167 driver.find_element(By.CSS_SELECTOR, '#description').send_keys(
168 'Some description'
169 )
170 driver.find_element(By.CSS_SELECTOR, '#owner_name').send_keys(
171 'Owner Name'
172 )
173 driver.find_element(By.CSS_SELECTOR, '#owner_email').send_keys(
174 'owner@email.xyz'
175 )
176 driver.find_element(By.XPATH,
177 "//button[contains(., 'Create podcast')]"
178 ).click()
179
180 wait.until(EC.title_contains("${podcastTitle}"))
181
182 driver.find_element(By.XPATH,
183 "//span[contains(., 'Add an episode')]"
184 ).click()
185
186 wait.until(EC.title_contains("Add an episode"))
187
188 # upload podcast ###################################################
189
190 driver.find_element(By.CSS_SELECTOR, '#audio_file').send_keys(
191 '/tmp/podcast.mp3'
192 )
193 driver.find_element(By.CSS_SELECTOR, '#cover').send_keys(
194 '${cover}'
195 )
196 driver.find_element(By.CSS_SELECTOR, '#description').send_keys(
197 'Episode description'
198 )
199 driver.find_element(By.CSS_SELECTOR, '#title').send_keys(
200 '${episodeTitle}'
201 )
202 driver.find_element(By.XPATH,
203 "//button[contains(., 'Create episode')]"
204 ).click()
205
206 wait.until(EC.title_contains("${episodeTitle}"))
207
208 driver.close()
209 driver.quit()
210 '';
211 in
212 [
213 pkgs.firefox-unwrapped
214 pkgs.geckodriver
215 browser-test
216 (pkgs.writeShellApplication {
217 name = "build-mp3";
218 runtimeInputs = with pkgs; [
219 sox
220 lame
221 ];
222 text = ''
223 out=/tmp/podcast.mp3
224 sox -n -r 48000 -t wav - synth ${targetPodcastDuration} sine 440 `
225 `| lame --noreplaygain --cbr -q 9 -b 320 - $out
226 FILESIZE="$(stat -c%s $out)"
227 [ "$FILESIZE" -gt 0 ]
228 [ "$FILESIZE" -le "${toString targetPodcastSize}" ]
229 '';
230 })
231 ];
232 };
233
234 testScript = ''
235 start_all()
236 castopod.wait_for_unit("castopod-setup.service")
237 castopod.wait_for_file("/run/phpfpm/castopod.sock")
238 castopod.wait_for_unit("nginx.service")
239 castopod.wait_for_open_port(80)
240 castopod.wait_until_succeeds("curl -sS -f http://castopod.example.com")
241
242 client.succeed("build-mp3")
243
244 with subtest("Create superadmin, log in, create and upload a podcast"):
245 client.succeed(\
246 "PYTHONUNBUFFERED=1 systemd-cat -t browser-test browser-test")
247 '';
248}