Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
468 lines
19 KiB
Go
468 lines
19 KiB
Go
package v1
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"github.com/gochat/gochat/internal/config"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/service"
|
|
)
|
|
|
|
// mockUploadService implements a mock for UploadService handler tests.
|
|
// We can't easily mock UploadService because it's a concrete type, not an interface.
|
|
// Instead, we test the handler layer by checking HTTP status codes and response shapes.
|
|
|
|
func setupUploadHandlerRouter(h *UploadHandler) *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
|
|
// Account upload route
|
|
api := r.Group("/api/v1/accounts/:account_id")
|
|
api.POST("/upload", h.Upload)
|
|
|
|
// Account direct upload route
|
|
api.POST("/direct_uploads", h.AccountDirectUpload)
|
|
api.POST("/conversations/:conversation_id/direct_uploads", h.ConversationDirectUpload)
|
|
api.PUT("/conversations/:conversation_id/direct_uploads/:upload_uuid", h.CompleteConversationDirectUpload)
|
|
|
|
// Widget direct upload route
|
|
widget := r.Group("/widget")
|
|
widget.POST("/direct_uploads", h.DirectUpload)
|
|
widget.PUT("/direct_uploads/:upload_uuid", h.CompleteWidgetDirectUpload)
|
|
|
|
chatwootWidget := r.Group("/api/v1/widget")
|
|
chatwootWidget.POST("/direct_uploads", h.DirectUpload)
|
|
chatwootWidget.PUT("/direct_uploads/:upload_uuid", h.CompleteWidgetDirectUpload)
|
|
|
|
return r
|
|
}
|
|
|
|
// makeMultipartUploadBody creates a multipart form body with a file field.
|
|
func makeMultipartUploadBody(filename string, content []byte) (body *bytes.Buffer, contentType string, err error) {
|
|
return makeMultipartUploadBodyWithField("file", filename, content)
|
|
}
|
|
|
|
func makeMultipartUploadBodyWithField(fieldName, filename string, content []byte) (body *bytes.Buffer, contentType string, err error) {
|
|
body = &bytes.Buffer{}
|
|
writer := multipart.NewWriter(body)
|
|
part, err := writer.CreateFormFile(fieldName, filename)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
part.Write(content)
|
|
writer.Close()
|
|
return body, writer.FormDataContentType(), nil
|
|
}
|
|
|
|
func TestUploadHandler_Upload_NoFile(t *testing.T) {
|
|
// Create handler with nil service — we only test validation before service call
|
|
h := &UploadHandler{svc: nil}
|
|
r := setupUploadHandlerRouter(h)
|
|
|
|
// Send request without file (no multipart body)
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/upload", nil)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
// Set account_id in context (simulating middleware)
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusUnprocessableEntity, w.Code)
|
|
}
|
|
|
|
func TestUploadHandler_Upload_ChatwootAttachmentFieldRawPayload(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(&model.DirectUpload{}))
|
|
uploadSvc := service.NewUploadService(repository.NewDirectUploadRepo(db), &config.Config{Storage: config.StorageConfig{LocalPath: tmpDir, MaxFileSize: 50 << 20}})
|
|
router := setupUploadHandlerRouter(NewUploadHandler(uploadSvc))
|
|
|
|
body, contentType, err := makeMultipartUploadBodyWithField("attachment", "macro.png", []byte("fake png"))
|
|
require.NoError(t, err)
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/upload", body)
|
|
req.Header.Set("Content-Type", contentType)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
var payload map[string]any
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
|
|
assert.NotContains(t, payload, "success")
|
|
assert.NotEmpty(t, payload["file_url"])
|
|
assert.NotEmpty(t, payload["blob_id"])
|
|
assert.Equal(t, payload["blob_id"], payload["blob_key"])
|
|
}
|
|
|
|
func TestUploadHandler_Upload_ChatwootExternalURLRawPayload(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(&model.DirectUpload{}))
|
|
uploadSvc := service.NewUploadService(repository.NewDirectUploadRepo(db), &config.Config{Storage: config.StorageConfig{LocalPath: tmpDir, MaxFileSize: 50 << 20}})
|
|
router := setupUploadHandlerRouter(NewUploadHandler(uploadSvc))
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "image/png")
|
|
_, _ = w.Write([]byte("external image"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
body := strings.NewReader(`{"external_url":"` + server.URL + `/image.png"}`)
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/upload", body)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
var payload map[string]any
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
|
|
assert.NotContains(t, payload, "success")
|
|
assert.NotEmpty(t, payload["file_url"])
|
|
assert.NotEmpty(t, payload["blob_id"])
|
|
}
|
|
|
|
func TestUploadHandler_DirectUpload_NoFile(t *testing.T) {
|
|
// Create handler with nil service — we only test validation before service call
|
|
h := &UploadHandler{svc: nil}
|
|
r := setupUploadHandlerRouter(h)
|
|
|
|
req, _ := http.NewRequest("POST", "/widget/direct_uploads", nil)
|
|
req.Header.Set("Content-Type", "multipart/form-data")
|
|
w := httptest.NewRecorder()
|
|
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestUploadHandler_WidgetActiveStorageDirectUploadFlow(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
tmpDir := t.TempDir()
|
|
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(
|
|
&model.Account{},
|
|
&model.Inbox{},
|
|
&model.Contact{},
|
|
&model.ContactInbox{},
|
|
&model.DirectUpload{},
|
|
))
|
|
|
|
account := &model.Account{Name: "Widget Upload Org", Status: "active"}
|
|
require.NoError(t, db.Create(account).Error)
|
|
channelConfig, err := json.Marshal(service.WebWidgetConfig{WebsiteToken: "upload_ws_token"})
|
|
require.NoError(t, err)
|
|
inbox := &model.Inbox{AccountID: account.ID, Name: "Upload Widget", ChannelType: "web_widget", Enabled: true, ChannelConfig: string(channelConfig)}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
contact := &model.Contact{AccountID: account.ID, Name: "Uploader"}
|
|
require.NoError(t, db.Create(contact).Error)
|
|
contactInbox := &model.ContactInbox{ContactID: contact.ID, InboxID: inbox.ID, PubsubToken: "upload_pubsub_token"}
|
|
require.NoError(t, db.Create(contactInbox).Error)
|
|
|
|
uploadSvc := service.NewUploadService(repository.NewDirectUploadRepo(db), &config.Config{
|
|
Storage: config.StorageConfig{LocalPath: tmpDir, MaxFileSize: 50 << 20},
|
|
}).WithWidgetAuth(repository.NewInboxRepo(db), repository.NewContactInboxRepo(db))
|
|
router := setupUploadHandlerRouter(NewUploadHandler(uploadSvc))
|
|
|
|
metadataBody, err := json.Marshal(map[string]any{
|
|
"blob": map[string]any{
|
|
"filename": "visitor.png",
|
|
"byte_size": 11,
|
|
"checksum": "checksum-token",
|
|
"content_type": "image/png",
|
|
"metadata": map[string]any{"identified": true},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
wCreate := httptest.NewRecorder()
|
|
reqCreate, _ := http.NewRequest("POST", "/api/v1/widget/direct_uploads?website_token=upload_ws_token", bytes.NewReader(metadataBody))
|
|
reqCreate.Header.Set("Content-Type", "application/json")
|
|
reqCreate.Header.Set("X-Auth-Token", "upload_pubsub_token")
|
|
router.ServeHTTP(wCreate, reqCreate)
|
|
require.Equal(t, http.StatusOK, wCreate.Code)
|
|
|
|
var createResp map[string]any
|
|
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
|
|
signedID, ok := createResp["signed_id"].(string)
|
|
require.True(t, ok)
|
|
require.NotEmpty(t, signedID)
|
|
assert.Equal(t, "visitor.png", createResp["filename"])
|
|
directUpload := createResp["direct_upload"].(map[string]any)
|
|
assert.Equal(t, "/api/v1/widget/direct_uploads/"+signedID, directUpload["url"])
|
|
assert.Equal(t, map[string]any{"Content-Type": "image/png"}, directUpload["headers"])
|
|
assert.Equal(t, "gochat_local", createResp["service_name"])
|
|
assert.Equal(t, float64(11), createResp["byte_size"])
|
|
assert.Equal(t, "checksum-token", createResp["checksum"])
|
|
assert.Equal(t, true, createResp["metadata"].(map[string]any)["identified"])
|
|
assert.NotEmpty(t, createResp["key"])
|
|
|
|
wPut := httptest.NewRecorder()
|
|
reqPut, _ := http.NewRequest("PUT", directUpload["url"].(string), bytes.NewReader([]byte("hello image")))
|
|
reqPut.Header.Set("Content-Type", "image/png")
|
|
router.ServeHTTP(wPut, reqPut)
|
|
require.Equal(t, http.StatusOK, wPut.Code)
|
|
var completeResp map[string]any
|
|
require.NoError(t, json.Unmarshal(wPut.Body.Bytes(), &completeResp))
|
|
require.Equal(t, true, completeResp["success"])
|
|
completeData := completeResp["data"].(map[string]any)
|
|
assert.Equal(t, signedID, completeData["upload_uuid"])
|
|
assert.Equal(t, "visitor.png", completeData["original_name"])
|
|
assert.Equal(t, "image", completeData["file_type"])
|
|
assert.Equal(t, "image/png", completeData["mime_type"])
|
|
assert.Equal(t, float64(11), completeData["file_size"])
|
|
|
|
var upload model.DirectUpload
|
|
require.NoError(t, db.Where("upload_uuid = ?", signedID).First(&upload).Error)
|
|
assert.Equal(t, account.ID, upload.AccountID)
|
|
storedBytes, err := os.ReadFile(filepath.Join(tmpDir, "widget_direct", signedID+".png"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []byte("hello image"), storedBytes)
|
|
}
|
|
|
|
func TestUploadHandler_ConversationActiveStorageDirectUploadFlow(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
tmpDir := t.TempDir()
|
|
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(
|
|
&model.Account{},
|
|
&model.Inbox{},
|
|
&model.Contact{},
|
|
&model.Conversation{},
|
|
&model.DirectUpload{},
|
|
))
|
|
|
|
account := &model.Account{Name: "Conversation Upload Org", Status: "active"}
|
|
require.NoError(t, db.Create(account).Error)
|
|
inbox := &model.Inbox{AccountID: account.ID, Name: "Conversation Upload Inbox", ChannelType: "web_widget", Enabled: true}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
contact := &model.Contact{AccountID: account.ID, Name: "Composer"}
|
|
require.NoError(t, db.Create(contact).Error)
|
|
displayID := uint(44)
|
|
conversation := &model.Conversation{
|
|
AccountID: account.ID,
|
|
InboxID: inbox.ID,
|
|
ContactID: contact.ID,
|
|
DisplayID: &displayID,
|
|
Status: "open",
|
|
ChannelType: "web_widget",
|
|
Channel: "web_widget",
|
|
}
|
|
require.NoError(t, db.Create(conversation).Error)
|
|
|
|
uploadSvc := service.NewUploadService(repository.NewDirectUploadRepo(db), &config.Config{
|
|
Storage: config.StorageConfig{LocalPath: tmpDir, MaxFileSize: 50 << 20},
|
|
}).WithConversationRepo(repository.NewConversationRepo(db))
|
|
router := setupUploadHandlerRouter(NewUploadHandler(uploadSvc))
|
|
|
|
metadataBody, err := json.Marshal(map[string]any{
|
|
"blob": map[string]any{
|
|
"filename": "agent-note.pdf",
|
|
"byte_size": 12,
|
|
"checksum": "pdf-checksum",
|
|
"content_type": "application/pdf",
|
|
"metadata": map[string]any{"identified": true},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
createPath := "/api/v1/accounts/" + strconv.FormatUint(uint64(account.ID), 10) + "/conversations/44/direct_uploads"
|
|
wCreate := httptest.NewRecorder()
|
|
reqCreate, _ := http.NewRequest("POST", createPath, bytes.NewReader(metadataBody))
|
|
reqCreate.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(wCreate, reqCreate)
|
|
require.Equal(t, http.StatusOK, wCreate.Code)
|
|
|
|
var createResp map[string]any
|
|
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
|
|
signedID, ok := createResp["signed_id"].(string)
|
|
require.True(t, ok)
|
|
require.NotEmpty(t, signedID)
|
|
assert.Equal(t, "agent-note.pdf", createResp["filename"])
|
|
directUpload := createResp["direct_upload"].(map[string]any)
|
|
assert.Equal(t, createPath+"/"+signedID, directUpload["url"])
|
|
assert.Equal(t, map[string]any{"Content-Type": "application/pdf"}, directUpload["headers"])
|
|
assert.Equal(t, "gochat_local", createResp["service_name"])
|
|
assert.Equal(t, float64(12), createResp["byte_size"])
|
|
assert.Equal(t, "pdf-checksum", createResp["checksum"])
|
|
assert.NotEmpty(t, createResp["key"])
|
|
|
|
wPut := httptest.NewRecorder()
|
|
reqPut, _ := http.NewRequest("PUT", directUpload["url"].(string), bytes.NewReader([]byte("hello report")))
|
|
reqPut.Header.Set("Content-Type", "application/pdf")
|
|
router.ServeHTTP(wPut, reqPut)
|
|
require.Equal(t, http.StatusOK, wPut.Code)
|
|
var completeResp map[string]any
|
|
require.NoError(t, json.Unmarshal(wPut.Body.Bytes(), &completeResp))
|
|
require.Equal(t, true, completeResp["success"])
|
|
completeData := completeResp["data"].(map[string]any)
|
|
assert.Equal(t, signedID, completeData["upload_uuid"])
|
|
assert.Equal(t, "agent-note.pdf", completeData["original_name"])
|
|
assert.Equal(t, "file", completeData["file_type"])
|
|
assert.Equal(t, "application/pdf", completeData["mime_type"])
|
|
assert.Equal(t, float64(12), completeData["file_size"])
|
|
|
|
var upload model.DirectUpload
|
|
require.NoError(t, db.Where("upload_uuid = ?", signedID).First(&upload).Error)
|
|
assert.Equal(t, account.ID, upload.AccountID)
|
|
assert.Equal(t, model.DirectUploadSourceAccount, upload.Source)
|
|
storedBytes, err := os.ReadFile(filepath.Join(tmpDir, "account", strconv.FormatUint(uint64(account.ID), 10), signedID+".pdf"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []byte("hello report"), storedBytes)
|
|
}
|
|
|
|
func TestUploadHandler_ConversationActiveStorageDirectUploadRejectsUnsupportedMIME(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(
|
|
&model.Account{},
|
|
&model.Inbox{},
|
|
&model.Contact{},
|
|
&model.Conversation{},
|
|
&model.DirectUpload{},
|
|
))
|
|
|
|
account := &model.Account{Name: "Conversation Upload Validation Org", Status: "active"}
|
|
require.NoError(t, db.Create(account).Error)
|
|
inbox := &model.Inbox{AccountID: account.ID, Name: "Conversation Upload Validation Inbox", ChannelType: "web_widget", Enabled: true}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
contact := &model.Contact{AccountID: account.ID, Name: "Composer Validation"}
|
|
require.NoError(t, db.Create(contact).Error)
|
|
displayID := uint(45)
|
|
conversation := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, DisplayID: &displayID, Status: "open", ChannelType: "web_widget", Channel: "web_widget"}
|
|
require.NoError(t, db.Create(conversation).Error)
|
|
|
|
uploadSvc := service.NewUploadService(repository.NewDirectUploadRepo(db), &config.Config{Storage: config.StorageConfig{LocalPath: t.TempDir(), MaxFileSize: 50 << 20}}).WithConversationRepo(repository.NewConversationRepo(db))
|
|
router := setupUploadHandlerRouter(NewUploadHandler(uploadSvc))
|
|
|
|
metadataBody, err := json.Marshal(map[string]any{
|
|
"blob": map[string]any{
|
|
"filename": "malware.exe",
|
|
"byte_size": 12,
|
|
"checksum": "bad-checksum",
|
|
"content_type": "application/x-msdownload",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
w := httptest.NewRecorder()
|
|
path := "/api/v1/accounts/" + strconv.FormatUint(uint64(account.ID), 10) + "/conversations/45/direct_uploads"
|
|
req, _ := http.NewRequest("POST", path, bytes.NewReader(metadataBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
|
|
require.Equal(t, http.StatusBadRequest, w.Code)
|
|
var payload map[string]any
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &payload))
|
|
assert.Equal(t, false, payload["success"])
|
|
assert.Equal(t, "VALIDATION_ERROR", payload["error"].(map[string]any)["code"])
|
|
assert.Contains(t, payload["error"].(map[string]any)["message"], "unsupported file type")
|
|
}
|
|
|
|
func TestUploadHandler_AccountDirectUpload_NoFile(t *testing.T) {
|
|
// Create handler with nil service — we only test validation before service call
|
|
h := &UploadHandler{svc: nil}
|
|
r := setupUploadHandlerRouter(h)
|
|
|
|
// Send request without file (no multipart body)
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/direct_uploads", nil)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestUploadHandler_AccountDirectUpload_NoAccountID(t *testing.T) {
|
|
// Create handler with nil service — we only test validation before service call
|
|
h := &UploadHandler{svc: nil}
|
|
r := setupUploadHandlerRouter(h)
|
|
|
|
// Use a non-numeric account_id so getAccountID returns 0
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/direct_uploads", nil)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
r.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func TestUploadHandler_AccountDirectUpload_WithFileButNilService(t *testing.T) {
|
|
// Create handler with nil service — request passes validation but service call panics.
|
|
// This test verifies that account_id extraction + file extraction work correctly
|
|
// before the service call. In real integration tests, we use a real service.
|
|
h := &UploadHandler{svc: nil}
|
|
r := setupUploadHandlerRouter(h)
|
|
|
|
body, contentType, err := makeMultipartUploadBody("test.png", []byte("fake png"))
|
|
assert.NoError(t, err)
|
|
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/direct_uploads", body)
|
|
req.Header.Set("Content-Type", contentType)
|
|
w := httptest.NewRecorder()
|
|
|
|
// This will panic because svc is nil, but it proves validation passes.
|
|
// We recover the panic and verify the request got past the validation checks.
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
// Expected: nil service causes panic after validation passes
|
|
t.Logf("Recovered expected panic from nil service: %v", r)
|
|
}
|
|
}()
|
|
r.ServeHTTP(w, req)
|
|
}
|
|
|
|
func TestUploadHandler_ResponseStructure(t *testing.T) {
|
|
// Verify UploadResponse DTO structure matches expected JSON keys
|
|
resp := service.UploadResponse{
|
|
UploadID: 1,
|
|
UploadUUID: "abc-123",
|
|
OriginalName: "test.png",
|
|
FileType: "image",
|
|
MimeType: "image/png",
|
|
FileSize: 1024,
|
|
FileURL: "/uploads/account/1/test.png",
|
|
ThumbURL: "/uploads/account/1/test.png",
|
|
Status: "pending",
|
|
}
|
|
|
|
data, err := json.Marshal(resp)
|
|
assert.NoError(t, err)
|
|
|
|
var parsed map[string]interface{}
|
|
assert.NoError(t, json.Unmarshal(data, &parsed))
|
|
|
|
assert.Equal(t, float64(1), parsed["upload_id"])
|
|
assert.Equal(t, "abc-123", parsed["upload_uuid"])
|
|
assert.Equal(t, "test.png", parsed["original_name"])
|
|
assert.Equal(t, "image", parsed["file_type"])
|
|
assert.Equal(t, "image/png", parsed["mime_type"])
|
|
assert.Equal(t, float64(1024), parsed["file_size"])
|
|
assert.Equal(t, "/uploads/account/1/test.png", parsed["file_url"])
|
|
assert.Equal(t, "/uploads/account/1/test.png", parsed["thumb_url"])
|
|
assert.Equal(t, "pending", parsed["status"])
|
|
}
|