arche / internal/archesrv/settings_test.go

commit 154431fd
  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}