package server import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "gitea.hsrv.site/mrleo1nid/backup-master/internal/manifest" ) func newTestServer(t *testing.T) (*Server, *httptest.Server) { t.Helper() s, err := New(Config{DataDir: t.TempDir(), PublicURL: "https://backup.test"}) if err != nil { t.Fatal(err) } ts := httptest.NewServer(s.Handler()) t.Cleanup(func() { ts.Close(); s.Close() }) return s, ts } func call(t *testing.T, method, url, token string, body io.Reader) *http.Response { t.Helper() req, err := http.NewRequest(method, url, body) if err != nil { t.Fatal(err) } if token != "" { req.Header.Set("Authorization", "Bearer "+token) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } t.Cleanup(func() { resp.Body.Close() }) return resp } func decode(t *testing.T, resp *http.Response, v any) { t.Helper() if err := json.NewDecoder(resp.Body).Decode(v); err != nil { t.Fatal(err) } } func sum(b []byte) string { h := sha256.Sum256(b) return hex.EncodeToString(h[:]) } func TestUploadAndRestoreFlow(t *testing.T) { s, ts := newTestServer(t) agentTok := newToken() clientID, err := s.st.CreateClient("nl1", "nl1.example.com", hashToken(agentTok)) if err != nil { t.Fatal(err) } // без токена — 401 if r := call(t, "GET", ts.URL+"/api/v1/agent/config", "", nil); r.StatusCode != 401 { t.Fatalf("без токена: %d", r.StatusCode) } var cfg AgentConfigResponse r := call(t, "GET", ts.URL+"/api/v1/agent/config", agentTok, nil) decode(t, r, &cfg) if cfg.Domain != "nl1.example.com" || !cfg.HeavyDue { t.Fatalf("config: %+v", cfg) } m := manifest.Manifest{FormatVersion: 1, Hostname: "nl1", Parts: []string{"main", "stoat"}} raw, _ := json.Marshal(m) r = call(t, "POST", ts.URL+"/api/v1/backups", agentTok, bytes.NewReader(raw)) if r.StatusCode != 201 { t.Fatalf("create: %d", r.StatusCode) } var created struct{ ID int64 } decode(t, r, &created) mainData := []byte("main-archive-data") heavyData := bytes.Repeat([]byte("minio"), 1000) for name, data := range map[string][]byte{"main": mainData, "stoat": heavyData} { r = call(t, "PUT", fmt.Sprintf("%s/api/v1/backups/%d/parts/%s", ts.URL, created.ID, name), agentTok, bytes.NewReader(data)) if r.StatusCode != 200 { t.Fatalf("upload %s: %d", name, r.StatusCode) } } // часть вне манифеста r = call(t, "PUT", fmt.Sprintf("%s/api/v1/backups/%d/parts/other", ts.URL, created.ID), agentTok, strings.NewReader("x")) if r.StatusCode != 400 { t.Fatalf("чужая часть: %d", r.StatusCode) } done := CompleteRequest{Parts: []PartInfo{ {Name: "main", Size: int64(len(mainData)), SHA256: sum(mainData)}, {Name: "stoat", Size: int64(len(heavyData)), SHA256: sum(heavyData)}, }} raw, _ = json.Marshal(done) r = call(t, "POST", fmt.Sprintf("%s/api/v1/backups/%d/complete", ts.URL, created.ID), agentTok, bytes.NewReader(raw)) if r.StatusCode != 200 { body, _ := io.ReadAll(r.Body) t.Fatalf("complete: %d %s", r.StatusCode, body) } c, _ := s.st.GetClient(clientID) if c.LastBackupAt == 0 || c.LastHeavyAt == 0 { t.Fatalf("клиент не отмечен: %+v", c) } r = call(t, "GET", ts.URL+"/api/v1/agent/config", agentTok, nil) decode(t, r, &cfg) if cfg.HeavyDue { t.Fatal("heavy только что загружен") } // upload-токен не даёт читать if r := call(t, "GET", ts.URL+"/api/v1/restore/manifest", agentTok, nil); r.StatusCode != 401 { t.Fatalf("агентский токен читает бэкапы: %d", r.StatusCode) } restoreTok := newToken() if err := s.st.CreateRestoreToken(hashToken(restoreTok), clientID, time.Hour); err != nil { t.Fatal(err) } var rm RestoreManifestResponse r = call(t, "GET", ts.URL+"/api/v1/restore/manifest", restoreTok, nil) decode(t, r, &rm) if rm.BackupID != created.ID || len(rm.Parts) != 2 { t.Fatalf("restore manifest: %+v", rm) } var got manifest.Manifest if err := json.Unmarshal(rm.Manifest, &got); err != nil || got.Domain != "nl1.example.com" { t.Fatalf("manifest domain: %q %v", got.Domain, err) } // скачивание с Range (докачка) req, _ := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/restore/%d/parts/main", ts.URL, created.ID), nil) req.Header.Set("Authorization", "Bearer "+restoreTok) req.Header.Set("Range", "bytes=5-") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } body, _ := io.ReadAll(resp.Body) resp.Body.Close() if resp.StatusCode != 206 || string(body) != string(mainData[5:]) { t.Fatalf("range: %d %q", resp.StatusCode, body) } } func TestChecksumMismatchFails(t *testing.T) { s, ts := newTestServer(t) tok := newToken() if _, err := s.st.CreateClient("x", "x.example.com", hashToken(tok)); err != nil { t.Fatal(err) } raw, _ := json.Marshal(manifest.Manifest{Parts: []string{"main"}}) r := call(t, "POST", ts.URL+"/api/v1/backups", tok, bytes.NewReader(raw)) var created struct{ ID int64 } decode(t, r, &created) call(t, "PUT", fmt.Sprintf("%s/api/v1/backups/%d/parts/main", ts.URL, created.ID), tok, strings.NewReader("data")) raw, _ = json.Marshal(CompleteRequest{Parts: []PartInfo{{Name: "main", Size: 4, SHA256: "bad"}}}) r = call(t, "POST", fmt.Sprintf("%s/api/v1/backups/%d/complete", ts.URL, created.ID), tok, bytes.NewReader(raw)) if r.StatusCode != 400 { t.Fatalf("complete с неверной суммой: %d", r.StatusCode) } b, _ := s.st.GetBackup(created.ID) if b.Status != StatusFailed { t.Fatalf("статус %s", b.Status) } } func TestWebLoginAndPages(t *testing.T) { s, ts := newTestServer(t) if err := s.st.SetAdmin("admin", "secret-pass"); err != nil { t.Fatal(err) } client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }} resp, err := client.Get(ts.URL + "/") if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != 303 { t.Fatalf("без сессии: %d", resp.StatusCode) } resp, err = client.PostForm(ts.URL+"/login", map[string][]string{"user": {"admin"}, "password": {"secret-pass"}}) if err != nil { t.Fatal(err) } resp.Body.Close() var session *http.Cookie for _, c := range resp.Cookies() { if c.Name == sessionCookie { session = c } } if session == nil { t.Fatal("нет cookie сессии") } for _, p := range []string{"/", "/clients/new", "/settings"} { req, _ := http.NewRequest("GET", ts.URL+p, nil) req.AddCookie(session) resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != 200 { t.Fatalf("%s: %d", p, resp.StatusCode) } } // POST без CSRF отклоняется req, _ := http.NewRequest("POST", ts.URL+"/clients", strings.NewReader("domain=a.example.com")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(session) resp, err = client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != 403 { t.Fatalf("без CSRF: %d", resp.StatusCode) } }