1# This test runs the radicle-node and radicle-httpd services on a seed host,
2# and verifies that an alice peer can host a repository on the seed,
3# and that a bob peer can send alice a patch via the seed.
4
5{ pkgs, ... }:
6
7let
8 # The Node ID depends on nodes.seed.services.radicle.privateKeyFile
9 seed-nid = "z6Mkg52RcwDrPKRzzHaYgBkHH3Gi5p4694fvPstVE9HTyMB6";
10 seed-ssh-keys = import ./ssh-keys.nix pkgs;
11 seed-tls-certs = import common/acme/server/snakeoil-certs.nix;
12
13 commonHostConfig =
14 {
15 nodes,
16 config,
17 pkgs,
18 ...
19 }:
20 {
21 environment.systemPackages = [
22 config.services.radicle.package
23 pkgs.curl
24 pkgs.gitMinimal
25 pkgs.jq
26 ];
27 environment.etc."gitconfig".text = ''
28 [init]
29 defaultBranch = main
30 [user]
31 email = root@${config.networking.hostName}
32 name = ${config.networking.hostName}
33 '';
34 networking = {
35 extraHosts = ''
36 ${nodes.seed.networking.primaryIPAddress} ${nodes.seed.services.radicle.httpd.nginx.serverName}
37 '';
38 };
39 security.pki.certificateFiles = [
40 seed-tls-certs.ca.cert
41 ];
42 };
43
44 radicleConfig =
45 { nodes, ... }:
46 alias:
47 pkgs.writeText "config.json" (
48 builtins.toJSON {
49 preferredSeeds = [
50 "${seed-nid}@seed:${toString nodes.seed.services.radicle.node.listenPort}"
51 ];
52 node = {
53 inherit alias;
54 relay = "never";
55 seedingPolicy = {
56 default = "block";
57 };
58 };
59 }
60 );
61in
62
63{
64 name = "radicle";
65
66 meta = with pkgs.lib.maintainers; {
67 maintainers = [
68 julm
69 lorenzleutgeb
70 ];
71 };
72
73 nodes = {
74 seed =
75 { pkgs, config, ... }:
76 {
77 imports = [ commonHostConfig ];
78
79 services.radicle = {
80 enable = true;
81 privateKeyFile = seed-ssh-keys.snakeOilEd25519PrivateKey;
82 publicKey = seed-ssh-keys.snakeOilEd25519PublicKey;
83 node = {
84 openFirewall = true;
85 };
86 httpd = {
87 enable = true;
88 nginx = {
89 serverName = seed-tls-certs.domain;
90 addSSL = true;
91 sslCertificate = seed-tls-certs.${seed-tls-certs.domain}.cert;
92 sslCertificateKey = seed-tls-certs.${seed-tls-certs.domain}.key;
93 };
94 };
95 settings = {
96 preferredSeeds = [ ];
97 node = {
98 relay = "always";
99 seedingPolicy = {
100 default = "allow";
101 scope = "all";
102 };
103 };
104 };
105 };
106
107 services.nginx = {
108 enable = true;
109 };
110
111 networking.firewall.allowedTCPPorts = [ 443 ];
112 };
113
114 alice = {
115 imports = [ commonHostConfig ];
116 };
117
118 bob = {
119 imports = [ commonHostConfig ];
120 };
121 };
122
123 testScript =
124 { nodes, ... }@args:
125 ''
126 start_all()
127
128 with subtest("seed can run radicle-node"):
129 # The threshold and/or hardening may have to be changed with new features/checks
130 print(seed.succeed("systemd-analyze security radicle-node.service --threshold=10 --no-pager"))
131 seed.wait_for_unit("radicle-node.service")
132 seed.wait_for_open_port(${toString nodes.seed.services.radicle.node.listenPort})
133
134 with subtest("seed can run radicle-httpd"):
135 # The threshold and/or hardening may have to be changed with new features/checks
136 print(seed.succeed("systemd-analyze security radicle-httpd.service --threshold=10 --no-pager"))
137 seed.wait_for_unit("radicle-httpd.service")
138 seed.wait_for_open_port(${toString nodes.seed.services.radicle.httpd.listenPort})
139 seed.wait_for_open_port(443)
140 assert alice.succeed("curl -sS 'https://${nodes.seed.services.radicle.httpd.nginx.serverName}/api/v1' | jq -r .nid") == "${seed-nid}\n"
141 assert bob.succeed("curl -sS 'https://${nodes.seed.services.radicle.httpd.nginx.serverName}/api/v1' | jq -r .nid") == "${seed-nid}\n"
142
143 with subtest("alice can create a Node ID"):
144 alice.succeed("rad auth --alias alice --stdin </dev/null")
145 alice.copy_from_host("${radicleConfig args "alice"}", "/root/.radicle/config.json")
146 with subtest("alice can run a node"):
147 alice.succeed("rad node start")
148 with subtest("alice can create a Git repository"):
149 alice.succeed(
150 "mkdir /tmp/repo",
151 "git -C /tmp/repo init",
152 "echo hello world > /tmp/repo/testfile",
153 "git -C /tmp/repo add .",
154 "git -C /tmp/repo commit -m init"
155 )
156 with subtest("alice can create a Repository ID"):
157 alice.succeed(
158 "cd /tmp/repo && rad init --name repo --description descr --default-branch main --public"
159 )
160 alice_repo_rid=alice.succeed("cd /tmp/repo && rad inspect --rid").rstrip("\n")
161 with subtest("alice can send a repository to the seed"):
162 alice.succeed(f"rad sync --seed ${seed-nid} {alice_repo_rid}")
163
164 with subtest(f"seed can receive the repository {alice_repo_rid}"):
165 seed.wait_until_succeeds("test 1 = \"$(rad-system stats | jq .local.repos)\"")
166
167 with subtest("bob can create a Node ID"):
168 bob.succeed("rad auth --alias bob --stdin </dev/null")
169 bob.copy_from_host("${radicleConfig args "bob"}", "/root/.radicle/config.json")
170 bob.succeed("rad node start")
171 with subtest("bob can clone alice's repository from the seed"):
172 bob.succeed(f"rad clone {alice_repo_rid} /tmp/repo")
173 assert bob.succeed("cat /tmp/repo/testfile") == "hello world\n"
174
175 with subtest("bob can clone alice's repository from the seed through the HTTP gateway"):
176 bob.succeed(f"git clone https://${nodes.seed.services.radicle.httpd.nginx.serverName}/{alice_repo_rid[4:]}.git /tmp/repo-http")
177 assert bob.succeed("cat /tmp/repo-http/testfile") == "hello world\n"
178
179 with subtest("alice can push the main branch to the rad remote"):
180 alice.succeed(
181 "echo hello bob > /tmp/repo/testfile",
182 "git -C /tmp/repo add .",
183 "git -C /tmp/repo commit -m 'hello to bob'",
184 "git -C /tmp/repo push rad main"
185 )
186 with subtest("bob can sync bob's repository from the seed"):
187 bob.succeed(
188 "cd /tmp/repo && rad sync --seed ${seed-nid}",
189 "cd /tmp/repo && git pull"
190 )
191 assert bob.succeed("cat /tmp/repo/testfile") == "hello bob\n"
192
193 with subtest("bob can push a patch"):
194 bob.succeed(
195 "echo hello alice > /tmp/repo/testfile",
196 "git -C /tmp/repo checkout -b for-alice",
197 "git -C /tmp/repo add .",
198 "git -C /tmp/repo commit -m 'hello to alice'",
199 "git -C /tmp/repo push -o patch.message='hello for alice' rad HEAD:refs/patches"
200 )
201
202 bob_repo_patch1_pid=bob.succeed("cd /tmp/repo && git branch --remotes | sed -ne 's:^ *rad/patches/::'p").rstrip("\n")
203 with subtest("alice can receive the patch"):
204 alice.wait_until_succeeds("test 1 = \"$(rad stats | jq .local.patches)\"")
205 alice.succeed(
206 f"cd /tmp/repo && rad patch show {bob_repo_patch1_pid} | grep -E '{bob_repo_patch1_pid[:7]} @ .+ by bob'",
207 f"cd /tmp/repo && rad patch checkout {bob_repo_patch1_pid}"
208 )
209 assert alice.succeed("cat /tmp/repo/testfile") == "hello alice\n"
210 with subtest("alice can comment the patch"):
211 alice.succeed(
212 f"cd /tmp/repo && rad patch comment {bob_repo_patch1_pid} -m thank-you"
213 )
214 with subtest("alice can merge the patch"):
215 alice.succeed(
216 "git -C /tmp/repo checkout main",
217 f"git -C /tmp/repo merge patch/{bob_repo_patch1_pid[:7]}",
218 "git -C /tmp/repo push rad main",
219 "cd /tmp/repo && rad patch list | grep -qxF 'Nothing to show.'"
220 )
221 '';
222}