Introduce new environment variables to control the behavior of one-time download boxes: - `WARPBOX_ONE_TIME_DOWNLOAD_EXPIRY_SECONDS`: Sets the lifetime of a one-time box after uploads are complete. - `WARPBOX_ONE_TIME_DOWNLOAD_RETRY_ON_FAILURE`: Determines whether a box remains available if the ZIP creation or transfer fails. To support these settings, the ZIP delivery process was refactored to use a temporary file. This ensures that a one-time box is only marked as consumed after the file has been successfully transferred to the client, preventing data loss on network interruptions. Additionally, added a `DecorateFiles` helper in the box store to reduce code duplication.
207 lines
5.9 KiB
Go
207 lines
5.9 KiB
Go
package boxstore
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"warpbox/lib/models"
|
|
)
|
|
|
|
func TestStartRetentionWaitsForEveryFileToFinish(t *testing.T) {
|
|
manifest := models.BoxManifest{
|
|
RetentionSecs: 10,
|
|
Files: []models.BoxFile{
|
|
{ID: "one", Status: models.FileStatusReady},
|
|
{ID: "two", Status: models.FileStatusWork},
|
|
},
|
|
}
|
|
|
|
startRetentionIfTerminalUnlocked(&manifest)
|
|
|
|
if !manifest.ExpiresAt.IsZero() {
|
|
t.Fatalf("expected retention to stay unset while a file is still uploading, got %s", manifest.ExpiresAt)
|
|
}
|
|
}
|
|
|
|
func TestStartRetentionBeginsWhenEveryFileIsTerminal(t *testing.T) {
|
|
manifest := models.BoxManifest{
|
|
RetentionSecs: 10,
|
|
Files: []models.BoxFile{
|
|
{ID: "one", Status: models.FileStatusReady},
|
|
{ID: "two", Status: models.FileStatusFailed},
|
|
},
|
|
}
|
|
before := time.Now().UTC()
|
|
|
|
startRetentionIfTerminalUnlocked(&manifest)
|
|
|
|
if manifest.ExpiresAt.IsZero() {
|
|
t.Fatal("expected retention to start once every file is complete or failed")
|
|
}
|
|
if manifest.ExpiresAt.Before(before.Add(9 * time.Second)) {
|
|
t.Fatalf("expected retention to start from completion time, got %s", manifest.ExpiresAt)
|
|
}
|
|
}
|
|
|
|
func TestStartRetentionUsesConfiguredOneTimeDownloadExpiry(t *testing.T) {
|
|
restoreExpiry := OneTimeDownloadExpiry()
|
|
defer SetOneTimeDownloadExpiry(restoreExpiry)
|
|
SetOneTimeDownloadExpiry(30)
|
|
|
|
manifest := models.BoxManifest{
|
|
RetentionSecs: 10,
|
|
OneTimeDownload: true,
|
|
Files: []models.BoxFile{
|
|
{ID: "one", Status: models.FileStatusReady},
|
|
{ID: "two", Status: models.FileStatusReady},
|
|
},
|
|
}
|
|
before := time.Now().UTC()
|
|
|
|
startRetentionIfTerminalUnlocked(&manifest)
|
|
|
|
if manifest.ExpiresAt.IsZero() {
|
|
t.Fatal("expected one-time download expiry to start from configured expiry")
|
|
}
|
|
if manifest.ExpiresAt.Before(before.Add(29 * time.Second)) {
|
|
t.Fatalf("expected configured one-time expiry, got %s", manifest.ExpiresAt)
|
|
}
|
|
if manifest.ExpiresAt.After(before.Add(31 * time.Second)) {
|
|
t.Fatalf("expected configured one-time expiry near 30s, got %s", manifest.ExpiresAt)
|
|
}
|
|
}
|
|
|
|
func TestStartRetentionSkipsOneTimeDownloadWhenExpiryZero(t *testing.T) {
|
|
restoreExpiry := OneTimeDownloadExpiry()
|
|
defer SetOneTimeDownloadExpiry(restoreExpiry)
|
|
SetOneTimeDownloadExpiry(0)
|
|
|
|
manifest := models.BoxManifest{
|
|
RetentionSecs: 10,
|
|
OneTimeDownload: true,
|
|
Files: []models.BoxFile{
|
|
{ID: "one", Status: models.FileStatusReady},
|
|
},
|
|
}
|
|
|
|
startRetentionIfTerminalUnlocked(&manifest)
|
|
|
|
if !manifest.ExpiresAt.IsZero() {
|
|
t.Fatalf("expected zero one-time expiry to keep expiry unset, got %s", manifest.ExpiresAt)
|
|
}
|
|
}
|
|
|
|
func TestSafeBoxFilePathRejectsTraversal(t *testing.T) {
|
|
restoreUploadRoot := UploadRoot()
|
|
defer SetUploadRoot(restoreUploadRoot)
|
|
SetUploadRoot(t.TempDir())
|
|
|
|
boxID := "0123456789abcdef0123456789abcdef"
|
|
if _, ok := SafeBoxFilePath(boxID, "../outside.txt"); ok {
|
|
t.Fatal("expected traversal to be rejected")
|
|
}
|
|
if _, ok := SafeBoxFilePath("../bad", "file.txt"); ok {
|
|
t.Fatal("expected invalid box id to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestAddFileToZipRejectsUnsafeManifestName(t *testing.T) {
|
|
restoreUploadRoot := UploadRoot()
|
|
defer SetUploadRoot(restoreUploadRoot)
|
|
SetUploadRoot(t.TempDir())
|
|
|
|
var buffer bytes.Buffer
|
|
zipWriter := zip.NewWriter(&buffer)
|
|
if err := AddFileToZip(zipWriter, "0123456789abcdef0123456789abcdef", "../outside.txt"); err == nil {
|
|
t.Fatal("expected unsafe zip filename to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestListFilesSkipsSymlinks(t *testing.T) {
|
|
restoreUploadRoot := UploadRoot()
|
|
defer SetUploadRoot(restoreUploadRoot)
|
|
SetUploadRoot(t.TempDir())
|
|
|
|
boxID := "0123456789abcdef0123456789abcdef"
|
|
if err := os.MkdirAll(BoxPath(boxID), 0755); err != nil {
|
|
t.Fatalf("MkdirAll returned error: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(BoxPath(boxID), "safe.txt"), []byte("safe"), 0644); err != nil {
|
|
t.Fatalf("WriteFile returned error: %v", err)
|
|
}
|
|
if err := os.Symlink(filepath.Join(BoxPath(boxID), "safe.txt"), filepath.Join(BoxPath(boxID), "link.txt")); err != nil {
|
|
t.Skipf("symlink unavailable: %v", err)
|
|
}
|
|
|
|
files, err := ListFiles(boxID)
|
|
if err != nil {
|
|
t.Fatalf("ListFiles returned error: %v", err)
|
|
}
|
|
if len(files) != 1 || files[0].Name != "safe.txt" {
|
|
t.Fatalf("expected only regular file, got %#v", files)
|
|
}
|
|
}
|
|
|
|
func TestThumbnailTasksSkipOneTimeDownloadBoxes(t *testing.T) {
|
|
restoreUploadRoot := UploadRoot()
|
|
defer SetUploadRoot(restoreUploadRoot)
|
|
SetUploadRoot(t.TempDir())
|
|
|
|
boxID := "0123456789abcdef0123456789abcdef"
|
|
if err := os.MkdirAll(BoxPath(boxID), 0755); err != nil {
|
|
t.Fatalf("MkdirAll returned error: %v", err)
|
|
}
|
|
if err := WriteManifest(boxID, models.BoxManifest{
|
|
OneTimeDownload: true,
|
|
Files: []models.BoxFile{{
|
|
ID: "0123456789abcdef",
|
|
Name: "image.png",
|
|
MimeType: "image/png",
|
|
Status: models.FileStatusReady,
|
|
}},
|
|
}); err != nil {
|
|
t.Fatalf("WriteManifest returned error: %v", err)
|
|
}
|
|
|
|
if tasks := collectBoxThumbnailTasks(boxID, 10); len(tasks) != 0 {
|
|
t.Fatalf("expected no thumbnail tasks for one-time box, got %#v", tasks)
|
|
}
|
|
}
|
|
|
|
func TestBoxPasswordUsesBcryptAndVerifiesLegacy(t *testing.T) {
|
|
restoreUploadRoot := UploadRoot()
|
|
defer SetUploadRoot(restoreUploadRoot)
|
|
SetUploadRoot(t.TempDir())
|
|
|
|
boxID := "0123456789abcdef0123456789abcdef"
|
|
if err := os.MkdirAll(BoxPath(boxID), 0755); err != nil {
|
|
t.Fatalf("MkdirAll returned error: %v", err)
|
|
}
|
|
if _, err := CreateManifest(boxID, models.CreateBoxRequest{Password: "secret"}); err != nil {
|
|
t.Fatalf("CreateManifest returned error: %v", err)
|
|
}
|
|
manifest, err := ReadManifest(boxID)
|
|
if err != nil {
|
|
t.Fatalf("ReadManifest returned error: %v", err)
|
|
}
|
|
if manifest.PasswordHashAlg != "bcrypt" {
|
|
t.Fatalf("expected bcrypt password hash, got %q", manifest.PasswordHashAlg)
|
|
}
|
|
if !VerifyPassword(manifest, "secret") {
|
|
t.Fatal("expected bcrypt password to verify")
|
|
}
|
|
|
|
legacy := models.BoxManifest{
|
|
PasswordSalt: "salt",
|
|
PasswordHash: legacyPasswordHash("salt", "secret"),
|
|
AuthToken: "token",
|
|
}
|
|
if !VerifyPassword(legacy, "secret") {
|
|
t.Fatal("expected legacy password hash to verify")
|
|
}
|
|
}
|