1package archesrv
2
3import (
4 "crypto/ed25519"
5 "crypto/rand"
6 "fmt"
7 "io"
8 "net/http"
9 "strings"
10 "testing"
11
12 "golang.org/x/crypto/ssh"
13)
14
15func TestForgeServer_Settings_TokenCRUD(t *testing.T) {
16 s, ts := newTestServer(t)
17 _, client := loginAsAdmin(t, s, ts)
18
19 resp, err := client.PostForm(ts.URL+"/settings/tokens", map[string][]string{
20 "label": {"ci"},
21 })
22 if err != nil {
23 t.Fatalf("POST /settings/tokens: %v", err)
24 }
25 resp.Body.Close()
26 if resp.StatusCode >= 400 {
27 t.Errorf("create token: got %d", resp.StatusCode)
28 }
29
30 admin, _, _ := s.db.GetUserByName("admin")
31 tokens, err := s.db.ListAPITokens(admin.ID)
32 if err != nil {
33 t.Fatalf("ListAPITokens: %v", err)
34 }
35 if len(tokens) == 0 {
36 t.Fatal("expected at least one token")
37 }
38
39 req, _ := http.NewRequest(http.MethodDelete,
40 fmt.Sprintf("%s/settings/tokens/%d", ts.URL, tokens[0].ID), nil)
41 resp2, err := client.Do(req)
42 if err != nil {
43 t.Fatalf("DELETE token: %v", err)
44 }
45 resp2.Body.Close()
46 if resp2.StatusCode >= 400 {
47 t.Errorf("delete token: got %d", resp2.StatusCode)
48 }
49
50 tokens2, _ := s.db.ListAPITokens(admin.ID)
51 if len(tokens2) != 0 {
52 t.Error("token should be gone after delete")
53 }
54}
55
56func TestForgeServer_Settings_TokenPage(t *testing.T) {
57 s, ts := newTestServer(t)
58 _, client := loginAsAdmin(t, s, ts)
59
60 resp, err := client.Get(ts.URL + "/settings/token")
61 if err != nil {
62 t.Fatalf("GET /settings/token: %v", err)
63 }
64 defer resp.Body.Close()
65 if resp.StatusCode != http.StatusOK {
66 t.Errorf("token settings page: want 200, got %d", resp.StatusCode)
67 }
68}
69
70func TestForgeServer_Settings_SSHKey_CRUD(t *testing.T) {
71 s, ts := newTestServer(t)
72 _, client := loginAsAdmin(t, s, ts)
73
74 pubKeyStr := genTestSSHPubKey(t)
75
76 resp, err := client.PostForm(ts.URL+"/settings/keys", map[string][]string{
77 "label": {"laptop"},
78 "public_key": {pubKeyStr},
79 })
80 if err != nil {
81 t.Fatalf("POST /settings/keys: %v", err)
82 }
83 resp.Body.Close()
84 if resp.StatusCode >= 400 {
85 t.Errorf("add SSH key: got %d", resp.StatusCode)
86 }
87
88 admin, _, _ := s.db.GetUserByName("admin")
89 keys, err := s.db.ListSSHKeys(admin.ID)
90 if err != nil {
91 t.Fatalf("ListSSHKeys: %v", err)
92 }
93 if len(keys) == 0 {
94 t.Fatal("expected at least one key")
95 }
96 if keys[0].Label != "laptop" {
97 t.Errorf("key label: want laptop, got %q", keys[0].Label)
98 }
99
100 req, _ := http.NewRequest(http.MethodDelete,
101 fmt.Sprintf("%s/settings/keys/%d", ts.URL, keys[0].ID), nil)
102 resp2, err := client.Do(req)
103 if err != nil {
104 t.Fatalf("DELETE key: %v", err)
105 }
106 resp2.Body.Close()
107 if resp2.StatusCode >= 400 {
108 t.Errorf("delete SSH key: got %d", resp2.StatusCode)
109 }
110
111 keys2, _ := s.db.ListSSHKeys(admin.ID)
112 if len(keys2) != 0 {
113 t.Error("SSH key should be gone after deletion")
114 }
115}
116
117func TestForgeServer_Settings_SSHKey_InvalidKeyRejected(t *testing.T) {
118 s, ts := newTestServer(t)
119 _, client := loginAsAdmin(t, s, ts)
120
121 resp, err := client.PostForm(ts.URL+"/settings/keys", map[string][]string{
122 "label": {"bad"},
123 "public_key": {"not-a-real-ssh-key"},
124 })
125 if err != nil {
126 t.Fatalf("POST: %v", err)
127 }
128 body, _ := io.ReadAll(resp.Body)
129 resp.Body.Close()
130
131 admin, _, _ := s.db.GetUserByName("admin")
132 keys, _ := s.db.ListSSHKeys(admin.ID)
133 if len(keys) != 0 {
134 t.Error("invalid key should not be stored")
135 }
136 if resp.StatusCode >= 500 {
137 t.Errorf("want 200 (error form) or 4xx, got %d", resp.StatusCode)
138 }
139 _ = body
140}
141
142func TestForgeServer_Settings_KeysPage(t *testing.T) {
143 s, ts := newTestServer(t)
144 _, client := loginAsAdmin(t, s, ts)
145
146 resp, err := client.Get(ts.URL + "/settings/keys")
147 if err != nil {
148 t.Fatalf("GET /settings/keys: %v", err)
149 }
150 defer resp.Body.Close()
151 if resp.StatusCode != http.StatusOK {
152 t.Errorf("keys page: want 200, got %d", resp.StatusCode)
153 }
154}
155
156func TestForgeServer_Settings_RepoSettingsPage(t *testing.T) {
157 s, ts := newTestServer(t)
158 _, client := loginAsAdmin(t, s, ts)
159 setupRepoWithDisk(t, s, "myrepo", "private")
160
161 resp, err := client.Get(ts.URL + "/myrepo/settings")
162 if err != nil {
163 t.Fatalf("GET /myrepo/settings: %v", err)
164 }
165 defer resp.Body.Close()
166 if resp.StatusCode != http.StatusOK {
167 t.Errorf("repo settings page: want 200, got %d", resp.StatusCode)
168 }
169}
170
171func TestForgeServer_Settings_UpdateRepoDescription(t *testing.T) {
172 s, ts := newTestServer(t)
173 _, client := loginAsAdmin(t, s, ts)
174 setupRepoWithDisk(t, s, "myrepo", "private")
175
176 resp, err := client.PostForm(ts.URL+"/myrepo/settings", map[string][]string{
177 "description": {"my updated description"},
178 "visibility": {"private"},
179 })
180 if err != nil {
181 t.Fatalf("POST settings: %v", err)
182 }
183 resp.Body.Close()
184 if resp.StatusCode >= 400 {
185 t.Errorf("update settings: got %d", resp.StatusCode)
186 }
187
188 rec, _ := s.db.GetRepo("myrepo")
189 if rec.Description != "my updated description" {
190 t.Errorf("description: want 'my updated description', got %q", rec.Description)
191 }
192}
193
194func TestForgeServer_Settings_UpdateRepoVisibility(t *testing.T) {
195 s, ts := newTestServer(t)
196 _, client := loginAsAdmin(t, s, ts)
197 setupRepoWithDisk(t, s, "myrepo", "private")
198
199 r0, _ := http.Get(ts.URL + "/myrepo/issues")
200 r0.Body.Close()
201 if r0.StatusCode != http.StatusUnauthorized {
202 t.Fatalf("expected 401 for private repo, got %d", r0.StatusCode)
203 }
204
205 resp, err := client.PostForm(ts.URL+"/myrepo/settings", map[string][]string{
206 "description": {""},
207 "visibility": {"public"},
208 })
209 if err != nil {
210 t.Fatalf("POST settings: %v", err)
211 }
212 resp.Body.Close()
213
214 r2, _ := http.Get(ts.URL + "/myrepo/issues")
215 r2.Body.Close()
216 if r2.StatusCode != http.StatusOK {
217 t.Errorf("after making public, anon should get 200, got %d", r2.StatusCode)
218 }
219}
220
221func TestForgeServer_Settings_DeleteRepo(t *testing.T) {
222 s, ts := newTestServer(t)
223 _, client := loginAsAdmin(t, s, ts)
224 setupRepoWithDisk(t, s, "myrepo", "private")
225
226 resp, err := client.PostForm(ts.URL+"/myrepo/settings/delete", nil)
227 if err != nil {
228 t.Fatalf("POST /myrepo/settings/delete: %v", err)
229 }
230 resp.Body.Close()
231 if resp.StatusCode >= 400 {
232 t.Errorf("repo delete: got %d", resp.StatusCode)
233 }
234
235 rec, _ := s.db.GetRepo("myrepo")
236 if rec != nil {
237 t.Error("repo should not exist after deletion")
238 }
239}
240
241func TestForgeServer_Settings_NonAdminCannotDeleteRepo(t *testing.T) {
242 s, ts := newTestServer(t)
243 s.db.CreateUser("admin", "adminpass", true) //nolint:errcheck
244 setupRepoWithDisk(t, s, "myrepo", "private")
245
246 alice, _ := s.db.CreateUser("alice", "pass", false)
247 rec, _ := s.db.GetRepo("myrepo")
248 s.db.SetPermission(rec.ID, alice.ID, "write") //nolint:errcheck
249
250 aliceClient := loginAs(t, ts, "alice", "pass")
251 resp, err := aliceClient.PostForm(ts.URL+"/myrepo/settings/delete", nil)
252 if err != nil {
253 t.Fatalf("POST: %v", err)
254 }
255 resp.Body.Close()
256 if resp.StatusCode < 400 {
257 t.Errorf("non-admin should not delete repo, got %d", resp.StatusCode)
258 }
259
260 if _, err := s.db.GetRepo("myrepo"); err != nil {
261 }
262 rec2, _ := s.db.GetRepo("myrepo")
263 if rec2 == nil {
264 t.Error("repo should still exist — non-admin should not have deleted it")
265 }
266}
267
268func genTestSSHPubKey(t *testing.T) string {
269 t.Helper()
270 pub, _, err := ed25519.GenerateKey(rand.Reader)
271 if err != nil {
272 t.Fatalf("generate ed25519 key: %v", err)
273 }
274 sshPub, err := ssh.NewPublicKey(pub)
275 if err != nil {
276 t.Fatalf("ssh.NewPublicKey: %v", err)
277 }
278 return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPub)))
279}