Files
ZFile/backend/api_ftp_test.go
2026-03-10 17:58:02 +03:00

257 lines
7.3 KiB
Go

package main
import (
"bytes"
"database/sql"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
ftpserver "goftp.io/server/v2"
)
func makeTestServer(t *testing.T, mutate func(*Config)) *Server {
t.Helper()
root := t.TempDir()
cfg := Config{
Addr: ":0",
DBPath: filepath.Join(root, "app.db"),
StorageRoot: filepath.Join(root, "users"),
AppDomain: "file.example.com",
AllowedHost: "file.example.com",
CORSOrigin: "https://file.example.com",
CookieSecure: false,
MaxBodyBytes: 8 * 1024 * 1024,
RateLimitPerMin: 1000,
AuthRateLimitPerMin: 1000,
JWTSecret: "test-jwt-secret-very-long-value-1234567890",
AccessTTL: 15 * time.Minute,
RefreshTTL: 24 * time.Hour,
ShareDefaultTTL: 24 * time.Hour,
AdminSessionTTL: 12 * time.Hour,
AdminLogin: "admin",
AdminPasswordHash: "sha256:dummy",
}
if mutate != nil {
mutate(&cfg)
}
db, err := openDB(cfg.DBPath)
if err != nil {
t.Fatalf("openDB failed: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
if err := migrate(db); err != nil {
t.Fatalf("migrate failed: %v", err)
}
storage, err := buildStorage(cfg)
if err != nil {
t.Fatalf("buildStorage failed: %v", err)
}
orm, err := newORMRepo(cfg.DBPath)
if err != nil {
t.Fatalf("newORMRepo failed: %v", err)
}
return &Server{
db: db,
orm: orm,
config: cfg,
storage: storage,
limiter: newRateLimiter(),
searchContent: newSearchContentCache(256),
}
}
func decodeJSONBody[T any](t *testing.T, res *http.Response, out *T) {
t.Helper()
defer res.Body.Close()
if err := json.NewDecoder(res.Body).Decode(out); err != nil {
t.Fatalf("decode json failed: %v", err)
}
}
func cookieByName(cookies []*http.Cookie, name string) *http.Cookie {
for _, c := range cookies {
if c.Name == name {
return c
}
}
return nil
}
func TestAPILoginRefreshAndMe(t *testing.T) {
t.Parallel()
s := makeTestServer(t, nil)
user, err := s.createUser("alice", "password123", "dracula", "auto")
if err != nil {
t.Fatalf("createUser failed: %v", err)
}
loginReq := httptest.NewRequest(http.MethodPost, "/api/auth/login", strings.NewReader(`{"username":"alice","password":"password123"}`))
loginReq.Header.Set("Content-Type", "application/json")
loginRec := httptest.NewRecorder()
s.handleLogin(loginRec, loginReq)
loginRes := loginRec.Result()
if loginRes.StatusCode != http.StatusOK {
t.Fatalf("login status = %d, want %d", loginRes.StatusCode, http.StatusOK)
}
if cookieByName(loginRes.Cookies(), "access_token") == nil {
t.Fatal("login did not set access_token cookie")
}
refreshCookie := cookieByName(loginRes.Cookies(), "refresh_token")
if refreshCookie == nil {
t.Fatal("login did not set refresh_token cookie")
}
var meResp User
decodeJSONBody(t, loginRes, &meResp)
if meResp.ID != user.ID || meResp.Username != user.Username {
t.Fatalf("login response user mismatch: got %+v want id=%d username=%q", meResp, user.ID, user.Username)
}
refreshReq := httptest.NewRequest(http.MethodPost, "/api/auth/refresh", nil)
refreshReq.AddCookie(refreshCookie)
refreshRec := httptest.NewRecorder()
s.handleRefresh(refreshRec, refreshReq)
refreshRes := refreshRec.Result()
if refreshRes.StatusCode != http.StatusOK {
t.Fatalf("refresh status = %d, want %d", refreshRes.StatusCode, http.StatusOK)
}
newAccess := cookieByName(refreshRes.Cookies(), "access_token")
if newAccess == nil {
t.Fatal("refresh did not rotate access_token cookie")
}
meReq := httptest.NewRequest(http.MethodGet, "/api/auth/me", nil)
meReq.AddCookie(newAccess)
meRec := httptest.NewRecorder()
s.authMiddleware(http.HandlerFunc(s.handleMe)).ServeHTTP(meRec, meReq)
if meRec.Code != http.StatusOK {
t.Fatalf("/api/auth/me status = %d, want %d", meRec.Code, http.StatusOK)
}
var meAfter User
if err := json.NewDecoder(meRec.Body).Decode(&meAfter); err != nil {
t.Fatalf("decode /api/auth/me failed: %v", err)
}
if meAfter.Username != "alice" {
t.Fatalf("/api/auth/me username = %q, want %q", meAfter.Username, "alice")
}
}
func TestAPIUserProtocolsFTPS(t *testing.T) {
t.Parallel()
s := makeTestServer(t, func(cfg *Config) {
cfg.FTPSEnabled = true
cfg.FTPSHost = "0.0.0.0"
cfg.FTPSPort = 2990
cfg.FTPSPublicIP = "198.51.100.10"
cfg.FTPSExplicit = true
cfg.FTPSForceTLS = true
})
if _, err := s.createUser("bob", "password123", "dracula", "auto"); err != nil {
t.Fatalf("createUser failed: %v", err)
}
loginReq := httptest.NewRequest(http.MethodPost, "/api/auth/login", strings.NewReader(`{"username":"bob","password":"password123"}`))
loginReq.Header.Set("Content-Type", "application/json")
loginRec := httptest.NewRecorder()
s.handleLogin(loginRec, loginReq)
if loginRec.Code != http.StatusOK {
t.Fatalf("login status = %d", loginRec.Code)
}
access := cookieByName(loginRec.Result().Cookies(), "access_token")
if access == nil {
t.Fatal("missing access token cookie")
}
req := httptest.NewRequest(http.MethodGet, "/api/user/protocols", nil)
req.AddCookie(access)
rec := httptest.NewRecorder()
s.authMiddleware(http.HandlerFunc(s.handleUserProtocols)).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("protocols status = %d, want %d", rec.Code, http.StatusOK)
}
var out userProtocolsResponse
if err := json.NewDecoder(rec.Body).Decode(&out); err != nil {
t.Fatalf("decode response failed: %v", err)
}
if out.FTPS == nil {
t.Fatal("expected FTPS profile in response")
}
if out.FTPS.Username != "bob" {
t.Fatalf("ftps username = %q, want %q", out.FTPS.Username, "bob")
}
if out.FTPS.Host != "198.51.100.10" || out.FTPS.Port != 2990 {
t.Fatalf("ftps endpoint mismatch: got %s:%d", out.FTPS.Host, out.FTPS.Port)
}
if !out.FTPS.ExplicitTLS || !out.FTPS.ForceTLS {
t.Fatal("expected explicit/forced TLS flags to be true")
}
}
func insertUserWithHash(t *testing.T, db *sql.DB, username, hash string) int64 {
t.Helper()
res, err := db.Exec(`INSERT INTO users(email, password_hash, theme, color_mode, archive_format) VALUES (?, ?, 'dracula', 'auto', 'zip')`, username, hash)
if err != nil {
t.Fatalf("insert user failed: %v", err)
}
id, err := res.LastInsertId()
if err != nil {
t.Fatalf("LastInsertId failed: %v", err)
}
return id
}
func testFTPContextWithUserID(userID int64) *ftpserver.Context {
return &ftpserver.Context{Sess: &ftpserver.Session{Data: map[string]interface{}{"filez_user_id": userID}}}
}
func TestFTPDriverPlainTransfer(t *testing.T) {
t.Parallel()
s := makeTestServer(t, nil)
hash, err := hashPasswordArgon2ID("password123")
if err != nil {
t.Fatalf("hash password failed: %v", err)
}
uid := insertUserWithHash(t, s.db, "dave", hash)
drv := &ftpUserDriver{db: s.db, root: s.config.StorageRoot}
ctx := testFTPContextWithUserID(uid)
plain := []byte("plain ftp payload")
if _, err := drv.PutFile(ctx, "/plain.txt", bytes.NewReader(plain), 0); err != nil {
t.Fatalf("PutFile failed: %v", err)
}
size, rc, err := drv.GetFile(ctx, "/plain.txt", 0)
if err != nil {
t.Fatalf("GetFile failed: %v", err)
}
defer rc.Close()
got, err := io.ReadAll(rc)
if err != nil {
t.Fatalf("ReadAll failed: %v", err)
}
if size != int64(len(plain)) || !bytes.Equal(got, plain) {
t.Fatalf("plain transfer mismatch: size=%d got=%q", size, string(got))
}
}