mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-07 15:13:51 +08:00
- 新增 WebDAV 与 S3 独立配置、凭据和远端状态管理 - 使用 Argon2id 与 AES-256-GCM 加密连接及配置备份 - 支持分类备份、自动同步、远端预览和一次性确认恢复 - 连接与已保存查询按 ID 合并恢复,并保留本地独有数据 - 补充前后端回归测试、六语言文案和恢复失败回滚
138 lines
5.4 KiB
Go
138 lines
5.4 KiB
Go
package cloudbackup
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestWebDAVRemoteUsesPathSegmentsAndBasicAuth(t *testing.T) {
|
|
var gotPath, gotUser, gotPassword string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.EscapedPath()
|
|
gotUser, gotPassword, _ = r.BasicAuth()
|
|
body, _ := io.ReadAll(r.Body)
|
|
if r.Method == http.MethodPut && string(body) != "payload" {
|
|
t.Fatalf("unexpected PUT body: %q", body)
|
|
}
|
|
w.Header().Set("ETag", `"etag"`)
|
|
w.WriteHeader(http.StatusCreated)
|
|
}))
|
|
defer server.Close()
|
|
|
|
remote, err := NewRemote(RemoteConfig{Provider: ProviderWebDAV, Endpoint: server.URL, ObjectKey: "gonavi/backup.gonavi"}, Credentials{Username: "user", Password: "pass"}, server.Client())
|
|
if err != nil {
|
|
t.Fatalf("NewRemote returned error: %v", err)
|
|
}
|
|
metadata, err := remote.Put(context.Background(), []byte("payload"))
|
|
if err != nil {
|
|
t.Fatalf("Put returned error: %v", err)
|
|
}
|
|
if metadata.ETag != "etag" || gotPath != "/gonavi/backup.gonavi" || gotUser != "user" || gotPassword != "pass" {
|
|
t.Fatalf("unexpected request path/auth/metadata: path=%q user=%q password=%q metadata=%#v", gotPath, gotUser, gotPassword, metadata)
|
|
}
|
|
}
|
|
|
|
func TestWebDAVRemoteCreatesMissingCollectionsBeforeRetryingPut(t *testing.T) {
|
|
collectionExists := false
|
|
var requests []string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests = append(requests, r.Method+" "+r.URL.EscapedPath())
|
|
switch {
|
|
case r.Method == http.MethodPut && !collectionExists:
|
|
w.WriteHeader(http.StatusConflict)
|
|
case r.Method == "MKCOL" && r.URL.EscapedPath() == "/dav/gonavi":
|
|
collectionExists = true
|
|
w.WriteHeader(http.StatusCreated)
|
|
case r.Method == http.MethodPut && collectionExists:
|
|
body, _ := io.ReadAll(r.Body)
|
|
if string(body) != "payload" {
|
|
t.Fatalf("unexpected PUT body: %q", body)
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
remote, err := NewRemote(RemoteConfig{Provider: ProviderWebDAV, Endpoint: server.URL + "/dav", ObjectKey: "gonavi/backup.gonavi"}, Credentials{Username: "user", Password: "pass"}, server.Client())
|
|
if err != nil {
|
|
t.Fatalf("NewRemote returned error: %v", err)
|
|
}
|
|
if _, err := remote.Put(context.Background(), []byte("payload")); err != nil {
|
|
t.Fatalf("Put returned error: %v", err)
|
|
}
|
|
want := []string{"PUT /dav/gonavi/backup.gonavi", "MKCOL /dav/gonavi", "PUT /dav/gonavi/backup.gonavi"}
|
|
if !slices.Equal(requests, want) {
|
|
t.Fatalf("unexpected WebDAV request sequence: got %v want %v", requests, want)
|
|
}
|
|
}
|
|
|
|
func TestNewRemoteRejectsInsecureNonLoopbackEndpoint(t *testing.T) {
|
|
_, err := NewRemote(RemoteConfig{Provider: ProviderWebDAV, Endpoint: "http://backup.example.test", ObjectKey: "backup"}, Credentials{Username: "user", Password: "pass"}, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "HTTPS") {
|
|
t.Fatalf("expected HTTPS validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNewRemoteUsesProviderSpecificRequiredFieldErrors(t *testing.T) {
|
|
webdav, err := NewRemote(RemoteConfig{Provider: ProviderWebDAV, Endpoint: "https://dav.example.test"}, Credentials{Username: "user", Password: "pass"}, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "WebDAV backup file path") {
|
|
t.Fatalf("expected WebDAV path validation error, got remote=%v err=%v", webdav, err)
|
|
}
|
|
s3, err := NewRemote(RemoteConfig{Provider: ProviderS3, Endpoint: "https://s3.example.test", Bucket: "bucket", Region: "us-east-1"}, Credentials{AccessKey: "access", SecretKey: "secret"}, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "S3 object key") {
|
|
t.Fatalf("expected S3 object key validation error, got remote=%v err=%v", s3, err)
|
|
}
|
|
}
|
|
|
|
func TestRemoteEscapesSpecialObjectKeyCharactersOnce(t *testing.T) {
|
|
var gotPath string
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.EscapedPath()
|
|
w.WriteHeader(http.StatusCreated)
|
|
}))
|
|
defer server.Close()
|
|
|
|
remote, err := NewRemote(RemoteConfig{Provider: ProviderWebDAV, Endpoint: server.URL + "/dav", ObjectKey: "folder/a b%2Fc"}, Credentials{Username: "user", Password: "pass"}, server.Client())
|
|
if err != nil {
|
|
t.Fatalf("NewRemote returned error: %v", err)
|
|
}
|
|
if _, err := remote.Put(context.Background(), []byte("payload")); err != nil {
|
|
t.Fatalf("Put returned error: %v", err)
|
|
}
|
|
if gotPath != "/dav/folder/a%20b%252Fc" {
|
|
t.Fatalf("unexpected escaped path: %q", gotPath)
|
|
}
|
|
}
|
|
|
|
func TestS3RemoteKeepsCanonicalEscapedPathInSyncWithRequestURL(t *testing.T) {
|
|
remote, err := NewRemote(RemoteConfig{
|
|
Provider: ProviderS3, Endpoint: "https://s3.example.test/prefix", Bucket: "bucket name", Region: "us-east-1", ObjectKey: "folder/a b%2Fc",
|
|
}, Credentials{AccessKey: "access", SecretKey: "secret"}, nil)
|
|
if err != nil {
|
|
t.Fatalf("NewRemote returned error: %v", err)
|
|
}
|
|
s3, ok := remote.(*s3Remote)
|
|
if !ok {
|
|
t.Fatalf("expected *s3Remote, got %T", remote)
|
|
}
|
|
requestURL, canonicalPath, err := s3.objectURL()
|
|
if err != nil {
|
|
t.Fatalf("objectURL returned error: %v", err)
|
|
}
|
|
parsed, err := url.Parse(requestURL)
|
|
if err != nil {
|
|
t.Fatalf("request URL is invalid: %v", err)
|
|
}
|
|
if parsed.EscapedPath() != canonicalPath || canonicalPath != "/prefix/bucket%20name/folder/a%20b%252Fc" {
|
|
t.Fatalf("request path and canonical path diverged: url=%q canonical=%q", parsed.EscapedPath(), canonicalPath)
|
|
}
|
|
}
|