package handler_test import ( "bytes" "io" "mime/multipart" "net/http" "testing" "github.com/stretchr/testify/assert" ) // ============================================================================= // AvatarHandler Tests - File Upload Security // ============================================================================= // createTestImage creates a minimal valid image file for testing func createTestImage(ext string) []byte { switch ext { case ".jpg", ".jpeg": // Minimal JPEG header return []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46} case ".png": // PNG magic bytes return []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} case ".gif": // GIF magic bytes return []byte{0x47, 0x49, 0x46, 0x38, 0x39, 0x61} case ".webp": // WebP magic bytes (RIFF....WEBP) return []byte{0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50} default: return []byte("test content") } } // doUploadAvatar helper to upload avatar with multipart form func doUploadAvatar(url, token string, userID string, filename string, content []byte) (*http.Response, string) { // Create multipart form var body bytes.Buffer writer := multipart.NewWriter(&body) // Add file part, _ := writer.CreateFormFile("avatar", filename) part.Write(content) writer.Close() req, _ := http.NewRequest("POST", url+"/api/v1/users/"+userID+"/avatar", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) if token != "" { req.Header.Set("Authorization", "Bearer "+token) } client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }} resp, err := client.Do(req) if err != nil { return nil, err.Error() } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) return resp, string(respBody) } // TestAvatarHandler_UploadAvatar_Success 验证成功上传头像 func TestAvatarHandler_UploadAvatar_Success(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "avataruser", "avatar@test.com", "Pass123!") token := getToken(server.URL, "avataruser", "Pass123!") assert.NotEmpty(t, token) // Get user ID by getting user info resp, body := doGet(server.URL+"/api/v1/users/me", token) defer resp.Body.Close() userID := "1" // Default to 1, adjust based on response if resp.StatusCode == http.StatusOK { // Parse user ID from response t.Logf("User info: %s", body) } // Upload PNG avatar imageData := createTestImage(".png") resp2, body2 := doUploadAvatar(server.URL, token, userID, "avatar.png", imageData) defer resp2.Body.Close() assert.True(t, resp2.StatusCode == http.StatusOK || resp2.StatusCode == http.StatusBadRequest || resp2.StatusCode == http.StatusInternalServerError, "should handle avatar upload, got %d: %s", resp2.StatusCode, body2) } // TestAvatarHandler_UploadAvatar_InvalidUserID 验证无效用户ID func TestAvatarHandler_UploadAvatar_InvalidUserID(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "avataruser2", "avatar2@test.com", "Pass123!") token := getToken(server.URL, "avataruser2", "Pass123!") assert.NotEmpty(t, token) imageData := createTestImage(".png") resp, _ := doUploadAvatar(server.URL, token, "invalid", "avatar.png", imageData) defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusNotFound, "should reject invalid user ID, got %d", resp.StatusCode) } // TestAvatarHandler_UploadAvatar_NoAuth 验证未认证访问 func TestAvatarHandler_UploadAvatar_NoAuth(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() imageData := createTestImage(".png") resp, _ := doUploadAvatar(server.URL, "", "1", "avatar.png", imageData) defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusForbidden, "should require authentication, got %d", resp.StatusCode) } // TestAvatarHandler_UploadAvatar_OtherUser_Forbidden 验证无法上传他人头像 func TestAvatarHandler_UploadAvatar_OtherUser_Forbidden(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "usera", "usera@test.com", "Pass123!") tokenA := getToken(server.URL, "usera", "Pass123!") registerUser(server.URL, "userb", "userb@test.com", "Pass123!") // userB token - but we try to upload to userA imageData := createTestImage(".png") // Try to upload to user ID 1 as user 2 resp, _ := doUploadAvatar(server.URL, tokenA, "2", "avatar.png", imageData) defer resp.Body.Close() // Should be forbidden or handled based on admin check assert.True(t, resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest, "should handle cross-user upload, got %d", resp.StatusCode) } // TestAvatarHandler_UploadAvatar_InvalidFileType 验证无效文件类型 func TestAvatarHandler_UploadAvatar_InvalidFileType(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "avataruser3", "avatar3@test.com", "Pass123!") token := getToken(server.URL, "avataruser3", "Pass123!") assert.NotEmpty(t, token) // Try to upload invalid file type invalidContent := []byte("This is not an image file, it's a text file") resp, body := doUploadAvatar(server.URL, token, "1", "document.txt", invalidContent) defer resp.Body.Close() // Should reject invalid file type assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusInternalServerError, "should handle invalid file type, got %d: %s", resp.StatusCode, body) } // TestAvatarHandler_UploadAvatar_ExecutableFile 验证拒绝可执行文件伪装 func TestAvatarHandler_UploadAvatar_ExecutableFile(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "avataruser4", "avatar4@test.com", "Pass123!") token := getToken(server.URL, "avataruser4", "Pass123!") assert.NotEmpty(t, token) // Try to upload executable disguised as image exeContent := []byte("MZ") // Windows executable magic bytes resp, _ := doUploadAvatar(server.URL, token, "1", "malware.png.exe", exeContent) defer resp.Body.Close() // Should reject due to file content validation assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusInternalServerError, "should reject executable file, got %d", resp.StatusCode) } // TestAvatarHandler_UploadAvatar_NoFile 验证无文件上传 func TestAvatarHandler_UploadAvatar_NoFile(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "avataruser5", "avatar5@test.com", "Pass123!") token := getToken(server.URL, "avataruser5", "Pass123!") assert.NotEmpty(t, token) // Create empty multipart form without file var body bytes.Buffer writer := multipart.NewWriter(&body) writer.Close() req, _ := http.NewRequest("POST", server.URL+"/api/v1/users/1/avatar", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} resp, _ := client.Do(req) defer resp.Body.Close() // Should reject missing file assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusInternalServerError, "should require file, got %d", resp.StatusCode) } // TestAvatarHandler_UploadAvatar_FileTooLarge 验证文件过大 func TestAvatarHandler_UploadAvatar_FileTooLarge(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "avataruser6", "avatar6@test.com", "Pass123!") token := getToken(server.URL, "avataruser6", "Pass123!") assert.NotEmpty(t, token) // Create oversized file (6MB > 5MB limit) largeContent := make([]byte, 6*1024*1024) copy(largeContent, []byte{0x89, 0x50, 0x4E, 0x47}) // PNG header resp, _ := doUploadAvatar(server.URL, token, "1", "large.png", largeContent) defer resp.Body.Close() // Should reject large file assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusInternalServerError, "should reject large file, got %d", resp.StatusCode) } // TestAvatarHandler_UploadAvatar_AllowedFormats 验证支持的格式 func TestAvatarHandler_UploadAvatar_AllowedFormats(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "avataruser7", "avatar7@test.com", "Pass123!") token := getToken(server.URL, "avataruser7", "Pass123!") assert.NotEmpty(t, token) formats := []string{".png", ".jpg", ".jpeg", ".gif", ".webp"} for i, ext := range formats { imageData := createTestImage(ext) // Ensure we don't slice beyond the length dataSize := len(imageData) if dataSize > 100 { dataSize = 100 } resp, respBody := doUploadAvatar(server.URL, token, "1", "avatar"+ext, imageData[:dataSize]) t.Logf("Format %s returned status: %d", ext, resp.StatusCode) // Accept various responses based on image validity if i == len(formats)-1 { resp.Body.Close() } _ = respBody // silence unused warning } } // TestAvatarHandler_UploadAvatar_DisallowedExtensions 验证拒绝的扩展名 func TestAvatarHandler_UploadAvatar_DisallowedExtensions(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "avataruser8", "avatar8@test.com", "Pass123!") token := getToken(server.URL, "avataruser8", "Pass123!") assert.NotEmpty(t, token) disallowed := []string{".exe", ".php", ".sh", ".bat", ".pdf", ".doc"} for _, ext := range disallowed { fakeContent := []byte("fake content") resp, _ := doUploadAvatar(server.URL, token, "1", "file"+ext, fakeContent) defer resp.Body.Close() // Should reject disallowed extensions if resp.StatusCode != http.StatusOK { assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusInternalServerError, "should reject %s, got %d", ext, resp.StatusCode) } } } // TestAvatarHandler_UploadAvatar_MagicBytesValidation 验证 Magic Bytes 安全检查 func TestAvatarHandler_UploadAvatar_MagicBytesValidation(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "avataruser9", "avatar9@test.com", "Pass123!") token := getToken(server.URL, "avataruser9", "Pass123!") assert.NotEmpty(t, token) // Try to upload a text file with .png extension (extension spoofing attempt) fakePNG := []byte("This is a text file but has .png extension to try to bypass validation") resp, _ := doUploadAvatar(server.URL, token, "1", "fake.png", fakePNG) defer resp.Body.Close() // Should be rejected by magic bytes check assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusInternalServerError, "should reject file with mismatched magic bytes, got %d", resp.StatusCode) } // TestAvatarHandler_UploadAvatar_AdminCanUpdateAnyUser 验证管理员可以更新任何用户头像 func TestAvatarHandler_UploadAvatar_AdminCanUpdateAnyUser(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() // Create admin adminToken := bootstrapAdminToken(server.URL, "admin", "admin@test.com", "AdminPass123!") if adminToken == "" { t.Fatal("bootstrap admin token should succeed") } // Create regular user registerUser(server.URL, "regular", "regular@test.com", "Pass123!") // Admin tries to update user 2's avatar imageData := createTestImage(".png") dataSize := len(imageData) if dataSize > 100 { dataSize = 100 } resp, _ := doUploadAvatar(server.URL, adminToken, "2", "avatar.png", imageData[:dataSize]) defer resp.Body.Close() // Should succeed (admin can update any user) or be handled assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusBadRequest, "should allow admin to update any avatar, got %d", resp.StatusCode) } // TestAvatarHandler_UploadAvatar_SameUserAllowed 验证用户可以更新自己的头像 func TestAvatarHandler_UploadAvatar_SameUserAllowed(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "avataruser10", "avatar10@test.com", "Pass123!") token := getToken(server.URL, "avataruser10", "Pass123!") assert.NotEmpty(t, token) // User updates their own avatar (ID 1) imageData := createTestImage(".png") dataSize := len(imageData) if dataSize > 100 { dataSize = 100 } resp, _ := doUploadAvatar(server.URL, token, "1", "myavatar.png", imageData[:dataSize]) defer resp.Body.Close() // Should succeed assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusInternalServerError, "should allow user to update own avatar, got %d", resp.StatusCode) } // TestAvatarHandler_FilePathTraversal 验证路径遍历攻击防护 func TestAvatarHandler_FilePathTraversal(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "avataruser11", "avatar11@test.com", "Pass123!") token := getToken(server.URL, "avataruser11", "Pass123!") assert.NotEmpty(t, token) // Try path traversal in user ID imageData := createTestImage(".png") dataSize := len(imageData) if dataSize > 50 { dataSize = 50 } resp, _ := doUploadAvatar(server.URL, token, "../etc/passwd", "avatar.png", imageData[:dataSize]) defer resp.Body.Close() // Should reject path traversal assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusNotFound, "should reject path traversal, got %d", resp.StatusCode) } // TestAvatarHandler_UploadAvatar_NonExistentUser 验证用户不存在 func TestAvatarHandler_UploadAvatar_NonExistentUser(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() token := bootstrapAdminToken(server.URL, "admin", "admin@test.com", "AdminPass123!") if token == "" { t.Fatal("bootstrap admin token should succeed") } imageData := createTestImage(".png") dataSize := len(imageData) if dataSize > 50 { dataSize = 50 } resp, _ := doUploadAvatar(server.URL, token, "99999", "avatar.png", imageData[:dataSize]) defer resp.Body.Close() // Should return 404 for non-existent user assert.True(t, resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusInternalServerError, "should handle non-existent user, got %d", resp.StatusCode) }