1{ system ? builtins.currentSystem
2, config ? { }
3, pkgs ? import ../.. { inherit system config; }
4}:
5
6with import ../lib/testing-python.nix { inherit system pkgs; };
7with pkgs.lib;
8
9let
10 ## gpg --faked-system-time='20230301T010000!' --quick-generate-key snakeoil ed25519 sign
11 signingPrivateKey = ''
12 -----BEGIN PGP PRIVATE KEY BLOCK-----
13
14 lFgEY/6jkBYJKwYBBAHaRw8BAQdADXiZRV8RJUyC9g0LH04wLMaJL9WTc+szbMi7
15 5fw4yP8AAQCl8EwGfzSLm/P6fCBfA3I9znFb3MEHGCCJhJ6VtKYyRw7ktAhzbmFr
16 ZW9pbIiUBBMWCgA8FiEE+wUM6VW/NLtAdSixTWQt6LZ4x50FAmP+o5ACGwMFCQPC
17 ZwAECwkIBwQVCgkIBRYCAwEAAh4FAheAAAoJEE1kLei2eMedFTgBAKQs1oGFZrCI
18 TZP42hmBTKxGAI1wg7VSdDEWTZxut/2JAQDGgo2sa4VHMfj0aqYGxrIwfP2B7JHO
19 GCqGCRf9O/hzBA==
20 =9Uy3
21 -----END PGP PRIVATE KEY BLOCK-----
22 '';
23 signingPrivateKeyId = "4D642DE8B678C79D";
24
25 actionsWorkflowYaml = ''
26 run-name: dummy workflow
27 on:
28 push:
29 jobs:
30 cat:
31 runs-on: native
32 steps:
33 - uses: http://localhost:3000/test/checkout@main
34 - run: cat testfile
35 '';
36 # https://github.com/actions/checkout/releases
37 checkoutActionSource = pkgs.fetchFromGitHub {
38 owner = "actions";
39 repo = "checkout";
40 rev = "v4.1.1";
41 hash = "sha256-h2/UIp8IjPo3eE4Gzx52Fb7pcgG/Ww7u31w5fdKVMos=";
42 };
43
44 supportedDbTypes = [ "mysql" "postgres" "sqlite3" ];
45 makeForgejoTest = type: nameValuePair type (makeTest {
46 name = "forgejo-${type}";
47 meta.maintainers = with maintainers; [ bendlas emilylange ];
48
49 nodes = {
50 server = { config, pkgs, ... }: {
51 virtualisation.memorySize = 2047;
52 services.forgejo = {
53 enable = true;
54 database = { inherit type; };
55 settings.service.DISABLE_REGISTRATION = true;
56 settings."repository.signing".SIGNING_KEY = signingPrivateKeyId;
57 settings.actions.ENABLED = true;
58 settings.repository = {
59 ENABLE_PUSH_CREATE_USER = true;
60 DEFAULT_PUSH_CREATE_PRIVATE = false;
61 };
62 };
63 environment.systemPackages = [ config.services.forgejo.package pkgs.gnupg pkgs.jq pkgs.file pkgs.htmlq ];
64 services.openssh.enable = true;
65
66 specialisation.runner = {
67 inheritParentConfig = true;
68 configuration.services.gitea-actions-runner = {
69 package = pkgs.forgejo-runner;
70 instances."test" = {
71 enable = true;
72 name = "ci";
73 url = "http://localhost:3000";
74 labels = [
75 # type ":host" does not depend on docker/podman/lxc
76 "native:host"
77 ];
78 tokenFile = "/var/lib/forgejo/runner_token";
79 };
80 };
81 };
82 specialisation.dump = {
83 inheritParentConfig = true;
84 configuration.services.forgejo.dump = {
85 enable = true;
86 type = "tar.zst";
87 file = "dump.tar.zst";
88 };
89 };
90 };
91 client = { ... }: {
92 programs.git = {
93 enable = true;
94 config = {
95 user.email = "test@localhost";
96 user.name = "test";
97 init.defaultBranch = "main";
98 };
99 };
100 programs.ssh.extraConfig = ''
101 Host *
102 StrictHostKeyChecking no
103 IdentityFile ~/.ssh/privk
104 '';
105 };
106 };
107
108 testScript = { nodes, ... }:
109 let
110 inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
111 serverSystem = nodes.server.system.build.toplevel;
112 dumpFile = with nodes.server.specialisation.dump.configuration.services.forgejo.dump; "${backupDir}/${file}";
113 remoteUri = "forgejo@server:test/repo";
114 remoteUriCheckoutAction = "forgejo@server:test/checkout";
115 in
116 ''
117 import json
118
119 start_all()
120
121 client.succeed("mkdir -p ~/.ssh")
122 client.succeed("(umask 0077; cat ${snakeOilPrivateKey} > ~/.ssh/privk)")
123
124 client.succeed("mkdir /tmp/repo")
125 client.succeed("git -C /tmp/repo init")
126 client.succeed("echo 'hello world' > /tmp/repo/testfile")
127 client.succeed("git -C /tmp/repo add .")
128 client.succeed("git -C /tmp/repo commit -m 'Initial import'")
129 client.succeed("git -C /tmp/repo remote add origin ${remoteUri}")
130
131 server.wait_for_unit("forgejo.service")
132 server.wait_for_open_port(3000)
133 server.wait_for_open_port(22)
134 server.succeed("curl --fail http://localhost:3000/")
135
136 server.succeed(
137 "su -l forgejo -c 'gpg --homedir /var/lib/forgejo/data/home/.gnupg "
138 + "--import ${toString (pkgs.writeText "forgejo.key" signingPrivateKey)}'"
139 )
140
141 assert "BEGIN PGP PUBLIC KEY BLOCK" in server.succeed("curl http://localhost:3000/api/v1/signing-key.gpg")
142
143 api_version = json.loads(server.succeed("curl http://localhost:3000/api/forgejo/v1/version")).get("version")
144 assert "development" != api_version and "${pkgs.forgejo.version}+gitea-" in api_version, (
145 "/api/forgejo/v1/version should not return 'development' "
146 + f"but should contain a forgejo+gitea compatibility version string. Got '{api_version}' instead."
147 )
148
149 server.succeed(
150 "curl --fail http://localhost:3000/user/sign_up | grep 'Registration is disabled. "
151 + "Please contact your site administrator.'"
152 )
153 server.succeed(
154 "su -l forgejo -c 'GITEA_WORK_DIR=/var/lib/forgejo gitea admin user create "
155 + "--username test --password totallysafe --email test@localhost --must-change-password=false'"
156 )
157
158 api_token = server.succeed(
159 "curl --fail -X POST http://test:totallysafe@localhost:3000/api/v1/users/test/tokens "
160 + "-H 'Accept: application/json' -H 'Content-Type: application/json' -d "
161 + "'{\"name\":\"token\",\"scopes\":[\"all\"]}' | jq '.sha1' | xargs echo -n"
162 )
163
164 server.succeed(
165 "curl --fail -X POST http://localhost:3000/api/v1/user/repos "
166 + "-H 'Accept: application/json' -H 'Content-Type: application/json' "
167 + f"-H 'Authorization: token {api_token}'"
168 + ' -d \'{"auto_init":false, "description":"string", "license":"mit", "name":"repo", "private":false}\'''
169 )
170
171 server.succeed(
172 "curl --fail -X POST http://localhost:3000/api/v1/user/keys "
173 + "-H 'Accept: application/json' -H 'Content-Type: application/json' "
174 + f"-H 'Authorization: token {api_token}'"
175 + ' -d \'{"key":"${snakeOilPublicKey}","read_only":true,"title":"SSH"}\'''
176 )
177
178 client.succeed("git -C /tmp/repo push origin main")
179
180 client.succeed("git clone ${remoteUri} /tmp/repo-clone")
181 print(client.succeed("ls -lash /tmp/repo-clone"))
182 assert "hello world" == client.succeed("cat /tmp/repo-clone/testfile").strip()
183
184 with subtest("Testing git protocol version=2 over ssh"):
185 git_protocol = client.succeed("GIT_TRACE2_EVENT=true git -C /tmp/repo-clone fetch |& grep negotiated-version")
186 version = json.loads(git_protocol).get("value")
187 assert version == "2", f"git did not negotiate protocol version 2, but version {version} instead."
188
189 server.wait_until_succeeds(
190 'test "$(curl http://localhost:3000/api/v1/repos/test/repo/commits '
191 + '-H "Accept: application/json" | jq length)" = "1"',
192 timeout=10
193 )
194
195 with subtest("Testing runner registration and action workflow"):
196 server.succeed(
197 "su -l forgejo -c 'GITEA_WORK_DIR=/var/lib/forgejo gitea actions generate-runner-token' | sed 's/^/TOKEN=/' | tee /var/lib/forgejo/runner_token"
198 )
199 server.succeed("${serverSystem}/specialisation/runner/bin/switch-to-configuration test")
200 server.wait_for_unit("gitea-runner-test.service")
201 server.succeed("journalctl -o cat -u gitea-runner-test.service | grep -q 'Runner registered successfully'")
202
203 # enable actions feature for this repository, defaults to disabled
204 server.succeed(
205 "curl --fail -X PATCH http://localhost:3000/api/v1/repos/test/repo "
206 + "-H 'Accept: application/json' -H 'Content-Type: application/json' "
207 + f"-H 'Authorization: token {api_token}'"
208 + ' -d \'{"has_actions":true}\'''
209 )
210
211 # mirror "actions/checkout" action
212 client.succeed("cp -R ${checkoutActionSource}/ /tmp/checkout")
213 client.succeed("git -C /tmp/checkout init")
214 client.succeed("git -C /tmp/checkout add .")
215 client.succeed("git -C /tmp/checkout commit -m 'Initial import'")
216 client.succeed("git -C /tmp/checkout remote add origin ${remoteUriCheckoutAction}")
217 client.succeed("git -C /tmp/checkout push origin main")
218
219 # push workflow to initial repo
220 client.succeed("mkdir -p /tmp/repo/.forgejo/workflows")
221 client.succeed("cp ${pkgs.writeText "dummy-workflow.yml" actionsWorkflowYaml} /tmp/repo/.forgejo/workflows/")
222 client.succeed("git -C /tmp/repo add .")
223 client.succeed("git -C /tmp/repo commit -m 'Add dummy workflow'")
224 client.succeed("git -C /tmp/repo push origin main")
225
226 def poll_workflow_action_status(_) -> bool:
227 output = server.succeed(
228 "curl --fail http://localhost:3000/test/repo/actions | "
229 + 'htmlq ".flex-item-leading span" --attribute "data-tooltip-content"'
230 ).strip()
231
232 # values taken from https://codeberg.org/forgejo/forgejo/src/commit/af47c583b4fb3190fa4c4c414500f9941cc02389/options/locale/locale_en-US.ini#L3649-L3661
233 if output in [ "Failure", "Canceled", "Skipped", "Blocked" ]:
234 raise Exception(f"Workflow status is '{output}', which we consider failed.")
235 server.log(f"Command returned '{output}', which we consider failed.")
236
237 elif output in [ "Unknown", "Waiting", "Running", "" ]:
238 server.log(f"Workflow status is '{output}'. Waiting some more...")
239 return False
240
241 elif output in [ "Success" ]:
242 return True
243
244 raise Exception(f"Workflow status is '{output}', which we don't know. Value mappings likely need updating.")
245
246 with server.nested("Waiting for the workflow run to be successful"):
247 retry(poll_workflow_action_status)
248
249 with subtest("Testing backup service"):
250 server.succeed("${serverSystem}/specialisation/dump/bin/switch-to-configuration test")
251 server.systemctl("start forgejo-dump")
252 assert "Zstandard compressed data" in server.succeed("file ${dumpFile}")
253 server.copy_from_vm("${dumpFile}")
254 '';
255 });
256in
257
258listToAttrs (map makeForgejoTest supportedDbTypes)