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:
2026-07-15 23:48:20 +08:00
parent 52dd56ff06
commit bb86950632
4 changed files with 533 additions and 221 deletions
+30 -28
View File
@@ -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)