220 lines
6.7 KiB
Go
220 lines
6.7 KiB
Go
|
|
package server
|
||
|
|
|
||
|
|
import (
|
||
|
|
"archive/zip"
|
||
|
|
"bytes"
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"os"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/gin-gonic/gin"
|
||
|
|
|
||
|
|
"warpbox/lib/boxstore"
|
||
|
|
"warpbox/lib/config"
|
||
|
|
"warpbox/lib/models"
|
||
|
|
)
|
||
|
|
|
||
|
|
const oneTimeTestBoxID = "0123456789abcdef0123456789abcdef"
|
||
|
|
|
||
|
|
func TestOneTimeDownloadNotReadyDoesNotConsume(t *testing.T) {
|
||
|
|
app := setupOneTimeDownloadTest(t, false)
|
||
|
|
writeOneTimeManifest(t, models.FileStatusWork, false)
|
||
|
|
|
||
|
|
response := performOneTimeDownload(app)
|
||
|
|
if response.Code != http.StatusConflict {
|
||
|
|
t.Fatalf("expected not-ready download to return 409, got %d", response.Code)
|
||
|
|
}
|
||
|
|
|
||
|
|
manifest, err := boxstore.ReadManifest(oneTimeTestBoxID)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ReadManifest returned error: %v", err)
|
||
|
|
}
|
||
|
|
if manifest.Consumed {
|
||
|
|
t.Fatal("expected not-ready box to remain unconsumed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOneTimeDownloadReadyConsumesAndDeletes(t *testing.T) {
|
||
|
|
app := setupOneTimeDownloadTest(t, false)
|
||
|
|
writeOneTimeManifest(t, models.FileStatusReady, true)
|
||
|
|
|
||
|
|
response := performOneTimeDownload(app)
|
||
|
|
if response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("expected ready download to return 200, got %d", response.Code)
|
||
|
|
}
|
||
|
|
if _, err := zip.NewReader(bytes.NewReader(response.Body.Bytes()), int64(response.Body.Len())); err != nil {
|
||
|
|
t.Fatalf("expected valid zip body: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := os.Stat(boxstore.BoxPath(oneTimeTestBoxID)); !os.IsNotExist(err) {
|
||
|
|
t.Fatalf("expected consumed box to be deleted, stat err=%v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOneTimeDownloadWriterFailureConsumesByDefault(t *testing.T) {
|
||
|
|
app := setupOneTimeDownloadTest(t, false)
|
||
|
|
writeOneTimeManifest(t, models.FileStatusReady, false)
|
||
|
|
|
||
|
|
response := performOneTimeDownload(app)
|
||
|
|
if response.Code != http.StatusInternalServerError {
|
||
|
|
t.Fatalf("expected failed ZIP to return 500, got %d", response.Code)
|
||
|
|
}
|
||
|
|
if _, err := os.Stat(boxstore.BoxPath(oneTimeTestBoxID)); !os.IsNotExist(err) {
|
||
|
|
t.Fatalf("expected failed ZIP to delete box by default, stat err=%v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOneTimeDownloadWriterFailureCanRemainRetryable(t *testing.T) {
|
||
|
|
app := setupOneTimeDownloadTest(t, true)
|
||
|
|
writeOneTimeManifest(t, models.FileStatusReady, false)
|
||
|
|
|
||
|
|
response := performOneTimeDownload(app)
|
||
|
|
if response.Code != http.StatusInternalServerError {
|
||
|
|
t.Fatalf("expected failed ZIP to return 500, got %d", response.Code)
|
||
|
|
}
|
||
|
|
|
||
|
|
manifest, err := boxstore.ReadManifest(oneTimeTestBoxID)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ReadManifest returned error: %v", err)
|
||
|
|
}
|
||
|
|
if manifest.Consumed {
|
||
|
|
t.Fatal("expected failed retryable ZIP to remain unconsumed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOneTimeDownloadSecondAccessAfterConsumeIsGone(t *testing.T) {
|
||
|
|
app := setupOneTimeDownloadTest(t, false)
|
||
|
|
writeOneTimeManifest(t, models.FileStatusReady, true)
|
||
|
|
|
||
|
|
manifest, err := boxstore.ReadManifest(oneTimeTestBoxID)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ReadManifest returned error: %v", err)
|
||
|
|
}
|
||
|
|
manifest.Consumed = true
|
||
|
|
if err := boxstore.WriteManifest(oneTimeTestBoxID, manifest); err != nil {
|
||
|
|
t.Fatalf("WriteManifest returned error: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
response := performOneTimeDownload(app)
|
||
|
|
if response.Code != http.StatusGone {
|
||
|
|
t.Fatalf("expected consumed download to return 410, got %d", response.Code)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOneTimeStatusStripsThumbnailPath(t *testing.T) {
|
||
|
|
app := setupOneTimeDownloadTest(t, false)
|
||
|
|
app.config.APIEnabled = true
|
||
|
|
writeOneTimeManifest(t, models.FileStatusReady, true)
|
||
|
|
|
||
|
|
manifest, err := boxstore.ReadManifest(oneTimeTestBoxID)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ReadManifest returned error: %v", err)
|
||
|
|
}
|
||
|
|
thumbnailPath := "/box/" + oneTimeTestBoxID + "/thumbnails/0123456789abcdef"
|
||
|
|
manifest.Files[0].ThumbnailPath = &thumbnailPath
|
||
|
|
manifest.Files[0].ThumbnailStatus = models.ThumbnailStatusReady
|
||
|
|
if err := boxstore.WriteManifest(oneTimeTestBoxID, manifest); err != nil {
|
||
|
|
t.Fatalf("WriteManifest returned error: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
response := performOneTimeStatus(app)
|
||
|
|
if response.Code != http.StatusOK {
|
||
|
|
t.Fatalf("expected status to return 200, got %d", response.Code)
|
||
|
|
}
|
||
|
|
var payload struct {
|
||
|
|
Files []models.BoxFile `json:"files"`
|
||
|
|
}
|
||
|
|
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
|
||
|
|
t.Fatalf("json.Unmarshal returned error: %v", err)
|
||
|
|
}
|
||
|
|
if len(payload.Files) != 1 {
|
||
|
|
t.Fatalf("expected one file, got %#v", payload.Files)
|
||
|
|
}
|
||
|
|
if payload.Files[0].ThumbnailPath != nil {
|
||
|
|
t.Fatalf("expected one-time status to strip thumbnail path, got %q", *payload.Files[0].ThumbnailPath)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRuntimeConfigAppliesDBOneTimeExpiryOverride(t *testing.T) {
|
||
|
|
restoreExpiry := boxstore.OneTimeDownloadExpiry()
|
||
|
|
defer boxstore.SetOneTimeDownloadExpiry(restoreExpiry)
|
||
|
|
|
||
|
|
cfg, err := config.Load()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("Load returned error: %v", err)
|
||
|
|
}
|
||
|
|
if err := cfg.ApplyOverrides(map[string]string{config.SettingOneTimeDownloadExpirySecs: "42"}); err != nil {
|
||
|
|
t.Fatalf("ApplyOverrides returned error: %v", err)
|
||
|
|
}
|
||
|
|
applyBoxstoreRuntimeConfig(cfg)
|
||
|
|
|
||
|
|
if got := boxstore.OneTimeDownloadExpiry(); got != 42 {
|
||
|
|
t.Fatalf("expected runtime one-time expiry to be updated from config, got %d", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func setupOneTimeDownloadTest(t *testing.T, retryOnFailure bool) *App {
|
||
|
|
t.Helper()
|
||
|
|
gin.SetMode(gin.TestMode)
|
||
|
|
|
||
|
|
restoreUploadRoot := boxstore.UploadRoot()
|
||
|
|
t.Cleanup(func() { boxstore.SetUploadRoot(restoreUploadRoot) })
|
||
|
|
boxstore.SetUploadRoot(t.TempDir())
|
||
|
|
|
||
|
|
return &App{config: &config.Config{
|
||
|
|
ZipDownloadsEnabled: true,
|
||
|
|
OneTimeDownloadRetryOnFailure: retryOnFailure,
|
||
|
|
}}
|
||
|
|
}
|
||
|
|
|
||
|
|
func writeOneTimeManifest(t *testing.T, status string, createFile bool) {
|
||
|
|
t.Helper()
|
||
|
|
if err := os.MkdirAll(boxstore.BoxPath(oneTimeTestBoxID), 0755); err != nil {
|
||
|
|
t.Fatalf("MkdirAll returned error: %v", err)
|
||
|
|
}
|
||
|
|
if createFile {
|
||
|
|
path, ok := boxstore.SafeBoxFilePath(oneTimeTestBoxID, "file.txt")
|
||
|
|
if !ok {
|
||
|
|
t.Fatal("SafeBoxFilePath rejected test file")
|
||
|
|
}
|
||
|
|
if err := os.WriteFile(path, []byte("hello"), 0644); err != nil {
|
||
|
|
t.Fatalf("WriteFile returned error: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
manifest := models.BoxManifest{
|
||
|
|
Files: []models.BoxFile{{
|
||
|
|
ID: "0123456789abcdef",
|
||
|
|
Name: "file.txt",
|
||
|
|
Size: 5,
|
||
|
|
MimeType: "text/plain",
|
||
|
|
Status: status,
|
||
|
|
}},
|
||
|
|
CreatedAt: time.Now().UTC(),
|
||
|
|
OneTimeDownload: true,
|
||
|
|
}
|
||
|
|
if err := boxstore.WriteManifest(oneTimeTestBoxID, manifest); err != nil {
|
||
|
|
t.Fatalf("WriteManifest returned error: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func performOneTimeDownload(app *App) *httptest.ResponseRecorder {
|
||
|
|
router := gin.New()
|
||
|
|
router.GET("/box/:id/download", app.handleDownloadBox)
|
||
|
|
request := httptest.NewRequest(http.MethodGet, "/box/"+oneTimeTestBoxID+"/download", nil)
|
||
|
|
response := httptest.NewRecorder()
|
||
|
|
router.ServeHTTP(response, request)
|
||
|
|
return response
|
||
|
|
}
|
||
|
|
|
||
|
|
func performOneTimeStatus(app *App) *httptest.ResponseRecorder {
|
||
|
|
router := gin.New()
|
||
|
|
router.GET("/box/:id/status", app.handleBoxStatus)
|
||
|
|
request := httptest.NewRequest(http.MethodGet, "/box/"+oneTimeTestBoxID+"/status", nil)
|
||
|
|
response := httptest.NewRecorder()
|
||
|
|
router.ServeHTTP(response, request)
|
||
|
|
return response
|
||
|
|
}
|