arche / internal/archesrv/server_test.go

commit 154431fd
  1package archesrv
  2
  3import (
  4	"io"
  5	"log/slog"
  6	"net/http"
  7	"net/http/httptest"
  8	"net/url"
  9	"os"
 10	"path/filepath"
 11	"testing"
 12	"time"
 13
 14	"arche/internal/repo"
 15	"arche/internal/store"
 16	"arche/internal/syncpkg"
 17	"arche/internal/wc"
 18
 19	_ "github.com/mattn/go-sqlite3"
 20)
 21
 22func newTestServer(t *testing.T) (*forgeServer, *httptest.Server) {
 23	t.Helper()
 24	dir := t.TempDir()
 25	db, err := openDB(filepath.Join(dir, "server.db"))
 26	if err != nil {
 27		t.Fatalf("openDB: %v", err)
 28	}
 29	t.Cleanup(func() { db.Close() })
 30
 31	cfg := DefaultConfig()
 32	cfg.Storage.DataDir = dir
 33
 34	s := &forgeServer{
 35		db:  db,
 36		cfg: cfg,
 37		log: slog.New(slog.NewTextHandler(io.Discard, nil)),
 38	}
 39	ts := httptest.NewServer(s.routes())
 40	t.Cleanup(ts.Close)
 41	return s, ts
 42}
 43
 44func makeLocalRepoWithCommit(t *testing.T) *repo.Repo {
 45	t.Helper()
 46	dir := t.TempDir()
 47	r, err := repo.Init(dir)
 48	if err != nil {
 49		t.Fatalf("repo.Init local: %v", err)
 50	}
 51	t.Cleanup(func() { r.Close() })
 52
 53	if err := os.WriteFile(filepath.Join(dir, "hello.txt"), []byte("hello arche\n"), 0o644); err != nil {
 54		t.Fatalf("WriteFile: %v", err)
 55	}
 56
 57	w := wc.New(r)
 58	_, commitID, err := w.Snap("initial commit")
 59	if err != nil {
 60		t.Fatalf("Snap: %v", err)
 61	}
 62
 63	tx, err := r.Store.Begin()
 64	if err != nil {
 65		t.Fatalf("Store.Begin: %v", err)
 66	}
 67	if err := r.Store.SetBookmark(tx, store.Bookmark{Name: "main", CommitID: commitID}); err != nil {
 68		r.Store.Rollback(tx)
 69		t.Fatalf("SetBookmark: %v", err)
 70	}
 71	if err := r.Store.Commit(tx); err != nil {
 72		t.Fatalf("Store.Commit: %v", err)
 73	}
 74	return r
 75}
 76
 77func TestForgeServer_SetupPage(t *testing.T) {
 78	_, ts := newTestServer(t)
 79	resp, err := http.Get(ts.URL + "/setup")
 80	if err != nil {
 81		t.Fatalf("GET /setup: %v", err)
 82	}
 83	defer resp.Body.Close()
 84	if resp.StatusCode != http.StatusOK {
 85		t.Errorf("GET /setup: want 200, got %d", resp.StatusCode)
 86	}
 87}
 88
 89func TestForgeServer_SyncPushPull(t *testing.T) {
 90	s, ts := newTestServer(t)
 91
 92	admin, err := s.db.CreateUser("admin", "adminpass", true)
 93	if err != nil {
 94		t.Fatalf("CreateUser: %v", err)
 95	}
 96	token, err := s.db.CreateAPIToken(admin.ID, "test")
 97	if err != nil {
 98		t.Fatalf("CreateAPIToken: %v", err)
 99	}
100
101	if _, err := s.db.CreateRepo("testrepo", "test repo", "private"); err != nil {
102		t.Fatalf("CreateRepo: %v", err)
103	}
104	if _, err := repo.Init(filepath.Join(s.dataDir(), "testrepo")); err != nil {
105		t.Fatalf("repo.Init server side: %v", err)
106	}
107
108	localRepo := makeLocalRepoWithCommit(t)
109
110	remoteURL := ts.URL + "/testrepo"
111
112	pushClient := syncpkg.NewClient(localRepo, remoteURL, token)
113	if err := pushClient.Push(); err != nil {
114		t.Fatalf("Push: %v", err)
115	}
116
117	pullDir := t.TempDir()
118	pullRepo, err := repo.Init(pullDir)
119	if err != nil {
120		t.Fatalf("repo.Init pull: %v", err)
121	}
122	defer pullRepo.Close()
123
124	pullClient := syncpkg.NewClient(pullRepo, remoteURL, token)
125	if err := pullClient.Pull(); err != nil {
126		t.Fatalf("Pull: %v", err)
127	}
128
129	localBMs, err := localRepo.Store.ListBookmarks()
130	if err != nil {
131		t.Fatalf("ListBookmarks local: %v", err)
132	}
133	pullBMs, err := pullRepo.Store.ListBookmarks()
134	if err != nil {
135		t.Fatalf("ListBookmarks pulled: %v", err)
136	}
137	if len(pullBMs) == 0 {
138		t.Fatal("pulled repo has no bookmarks after Pull()")
139	}
140	for _, lb := range localBMs {
141		found := false
142		for _, pb := range pullBMs {
143			if lb.Name == pb.Name && lb.CommitID == pb.CommitID {
144				found = true
145				break
146			}
147		}
148		if !found {
149			t.Errorf("bookmark %q (commitID %x) missing from pulled repo", lb.Name, lb.CommitID[:8])
150		}
151	}
152
153	time.Sleep(50 * time.Millisecond)
154}
155
156func TestForgeServer_SyncUnauthorized(t *testing.T) {
157	s, ts := newTestServer(t)
158
159	admin, _ := s.db.CreateUser("admin", "adminpass", true)
160	_, _ = s.db.CreateAPIToken(admin.ID, "test")
161	_, _ = s.db.CreateRepo("privaterepo", "", "private")
162	_, _ = repo.Init(filepath.Join(s.dataDir(), "privaterepo"))
163
164	resp, err := http.Get(ts.URL + "/privaterepo/arche/v1/info")
165	if err != nil {
166		t.Fatalf("GET info: %v", err)
167	}
168	defer resp.Body.Close()
169	if resp.StatusCode != http.StatusUnauthorized {
170		t.Errorf("expected 401 for unauthenticated read, got %d", resp.StatusCode)
171	}
172}
173
174func TestForgeServer_SyncWrongToken(t *testing.T) {
175	s, ts := newTestServer(t)
176
177	admin, _ := s.db.CreateUser("admin", "adminpass", true)
178	_, _ = s.db.CreateAPIToken(admin.ID, "test")
179	_, _ = s.db.CreateRepo("privaterepo", "", "private")
180	_, _ = repo.Init(filepath.Join(s.dataDir(), "privaterepo"))
181
182	req, _ := http.NewRequest(http.MethodGet, ts.URL+"/privaterepo/arche/v1/info", nil)
183	req.Header.Set("Authorization", "Bearer wrongtoken")
184	resp, err := http.DefaultClient.Do(req)
185	if err != nil {
186		t.Fatalf("GET info: %v", err)
187	}
188	defer resp.Body.Close()
189	if resp.StatusCode != http.StatusUnauthorized {
190		t.Errorf("expected 401 for wrong token, got %d", resp.StatusCode)
191	}
192}
193
194func TestForgeServer_AdminCRUD(t *testing.T) {
195	s, ts := newTestServer(t)
196
197	_, err := s.db.CreateUser("admin", "adminpass", true)
198	if err != nil {
199		t.Fatalf("CreateUser: %v", err)
200	}
201
202	jar := newCookieJar()
203	client := &http.Client{
204		Jar: jar,
205		CheckRedirect: func(req *http.Request, via []*http.Request) error {
206			return http.ErrUseLastResponse
207		},
208	}
209
210	loginResp, err := client.PostForm(ts.URL+"/login", map[string][]string{
211		"username": {"admin"},
212		"password": {"adminpass"},
213	})
214	if err != nil {
215		t.Fatalf("POST /login: %v", err)
216	}
217	loginResp.Body.Close()
218	if loginResp.StatusCode != http.StatusFound && loginResp.StatusCode != http.StatusSeeOther {
219		t.Fatalf("POST /login: want redirect (302/303), got %d", loginResp.StatusCode)
220	}
221
222	repoResp, err := client.PostForm(ts.URL+"/admin/repos", map[string][]string{
223		"name":        {"myrepo"},
224		"description": {"a test repo"},
225		"visibility":  {"private"},
226	})
227	if err != nil {
228		t.Fatalf("POST /admin/repos: %v", err)
229	}
230	repoResp.Body.Close()
231	if repoResp.StatusCode >= 400 {
232		t.Errorf("POST /admin/repos: got %d", repoResp.StatusCode)
233	}
234
235	rec, err := s.db.GetRepo("myrepo")
236	if err != nil || rec == nil {
237		t.Fatalf("GetRepo: repo not found after create: %v", err)
238	}
239	if rec.Description != "a test repo" {
240		t.Errorf("Description: want %q, got %q", "a test repo", rec.Description)
241	}
242
243	userResp, err := client.PostForm(ts.URL+"/admin/users", map[string][]string{
244		"username": {"bob"},
245		"password": {"bobpass"},
246	})
247	if err != nil {
248		t.Fatalf("POST /admin/users: %v", err)
249	}
250	userResp.Body.Close()
251	if userResp.StatusCode >= 400 {
252		t.Errorf("POST /admin/users: got %d", userResp.StatusCode)
253	}
254
255	users, err := s.db.ListUsers()
256	if err != nil {
257		t.Fatalf("ListUsers: %v", err)
258	}
259	found := false
260	for _, u := range users {
261		if u.Username == "bob" {
262			found = true
263		}
264	}
265	if !found {
266		t.Error("user 'bob' not found after admin create")
267	}
268}
269
270type simpleCookieJar struct {
271	cookies []*http.Cookie
272}
273
274func newCookieJar() *simpleCookieJar { return &simpleCookieJar{} }
275
276func (j *simpleCookieJar) SetCookies(_ *url.URL, cookies []*http.Cookie) {
277	j.cookies = append(j.cookies, cookies...)
278}
279
280func (j *simpleCookieJar) Cookies(_ *url.URL) []*http.Cookie {
281	return j.cookies
282}