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)) } }