feat(server): add file route integration tests with ownership
enforcement - test: cover authenticated register, login, upload, list, and download flow - test: verify file API routes reject test identity header injection - test: verify file API routes reject invalid, expired, and malformed tokens - test: verify cross-user ownership isolation for all file CRUD operations - refactor: rename test-only header constant to clarify it is not used in production
This commit is contained in:
@@ -22,6 +22,8 @@ import (
|
||||
"github.com/dhao2001/mygo/internal/storage"
|
||||
)
|
||||
|
||||
const testUserIDHeader = "X-Test-User-ID"
|
||||
|
||||
// inMemStore implements storage.StorageBackend with an in-memory map.
|
||||
type inMemStore struct {
|
||||
files map[string][]byte
|
||||
@@ -130,9 +132,9 @@ func setupFileRouterWithMaxUploadSizeAndStore(t *testing.T, maxUploadSize int64)
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
|
||||
// Middleware that injects user_id from header (simulates AuthRequired).
|
||||
// Test-only middleware that injects user_id to simulate a successful AuthRequired call.
|
||||
r.Use(func(c *gin.Context) {
|
||||
userID := c.GetHeader("X-User-ID")
|
||||
userID := c.GetHeader(testUserIDHeader)
|
||||
if userID != "" {
|
||||
c.Set("user_id", userID)
|
||||
}
|
||||
@@ -163,7 +165,7 @@ func TestFileHandler_Upload(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -191,7 +193,7 @@ func TestFileHandler_UploadRejectsOversizedFile(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -211,7 +213,7 @@ func TestFileHandler_UploadAllowsExactMaxUploadSize(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -231,7 +233,7 @@ func TestFileHandler_UploadUnlimitedWhenMaxUploadSizeIsZero(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -249,7 +251,7 @@ func TestFileHandler_UploadNoFile(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -269,7 +271,7 @@ func TestFileHandler_UploadUnexpectedFieldDoesNotEchoFieldName(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -289,7 +291,7 @@ func TestFileHandler_UploadMalformedMultipartDoesNotLeakParserError(t *testing.T
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", strings.NewReader("not a valid multipart body"))
|
||||
req.Header.Set("Content-Type", "multipart/form-data; boundary=mygo-boundary")
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -310,7 +312,7 @@ func TestFileHandler_CreateDir(t *testing.T) {
|
||||
body, _ := json.Marshal(gin.H{"name": "mydir"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -339,13 +341,13 @@ func TestFileHandler_List(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
// List files.
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/files", nil)
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -374,7 +376,7 @@ func TestFileHandler_Get(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -383,7 +385,7 @@ func TestFileHandler_Get(t *testing.T) {
|
||||
|
||||
// Get metadata.
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/files/"+uploaded.ID, nil)
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -404,7 +406,7 @@ func TestFileHandler_GetNotFound(t *testing.T) {
|
||||
r := setupFileRouter(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/files/nonexistent", nil)
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -426,7 +428,7 @@ func TestFileHandler_Download(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -435,7 +437,7 @@ func TestFileHandler_Download(t *testing.T) {
|
||||
|
||||
// Download.
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/files/"+uploaded.ID+"/content", nil)
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -462,7 +464,7 @@ func TestFileHandler_DownloadReadErrorAfterResponseStarted(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -474,7 +476,7 @@ func TestFileHandler_DownloadReadErrorAfterResponseStarted(t *testing.T) {
|
||||
store.failReads = true
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/files/"+uploaded.ID+"/content", nil)
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -498,7 +500,7 @@ func TestFileHandler_Update(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -509,7 +511,7 @@ func TestFileHandler_Update(t *testing.T) {
|
||||
updateBody, _ := json.Marshal(gin.H{"name": "newname.txt"})
|
||||
req = httptest.NewRequest(http.MethodPut, "/api/v1/files/"+uploaded.ID, bytes.NewReader(updateBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -538,7 +540,7 @@ func TestFileHandler_UpdateNoFields(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -548,7 +550,7 @@ func TestFileHandler_UpdateNoFields(t *testing.T) {
|
||||
updateBody, _ := json.Marshal(gin.H{})
|
||||
req = httptest.NewRequest(http.MethodPut, "/api/v1/files/"+uploaded.ID, bytes.NewReader(updateBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -569,7 +571,7 @@ func TestFileHandler_Delete(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -578,7 +580,7 @@ func TestFileHandler_Delete(t *testing.T) {
|
||||
|
||||
// Delete.
|
||||
req = httptest.NewRequest(http.MethodDelete, "/api/v1/files/"+uploaded.ID, nil)
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -588,7 +590,7 @@ func TestFileHandler_Delete(t *testing.T) {
|
||||
|
||||
// Verify gone.
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/files/"+uploaded.ID, nil)
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -609,7 +611,7 @@ func TestFileHandler_ForbiddenAccess(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
req.Header.Set(testUserIDHeader, "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
@@ -618,7 +620,7 @@ func TestFileHandler_ForbiddenAccess(t *testing.T) {
|
||||
|
||||
// Try to access as user2.
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/files/"+uploaded.ID, nil)
|
||||
req.Header.Set("X-User-ID", "user2")
|
||||
req.Header.Set(testUserIDHeader, "user2")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user