337 lines
12 KiB
Go
337 lines
12 KiB
Go
|
|
package handler_test
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"io"
|
||
|
|
"mime/multipart"
|
||
|
|
"net/http"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/stretchr/testify/assert"
|
||
|
|
)
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// ExportHandler Tests - Data Export/Import
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
// TestExportHandler_ExportUsers_Success 验证导出用户数据
|
||
|
|
func TestExportHandler_ExportUsers_Success(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")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/exports/users", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusInternalServerError,
|
||
|
|
"should export users, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_ExportUsers_WithFormat 验证指定格式导出
|
||
|
|
func TestExportHandler_ExportUsers_WithFormat(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")
|
||
|
|
}
|
||
|
|
|
||
|
|
// CSV format
|
||
|
|
resp1, _ := doGet(server.URL+"/api/v1/exports/users?format=csv", token)
|
||
|
|
defer resp1.Body.Close()
|
||
|
|
assert.True(t, resp1.StatusCode == http.StatusOK || resp1.StatusCode == http.StatusForbidden,
|
||
|
|
"should export CSV, got %d", resp1.StatusCode)
|
||
|
|
|
||
|
|
// Excel format
|
||
|
|
resp2, _ := doGet(server.URL+"/api/v1/exports/users?format=excel", token)
|
||
|
|
defer resp2.Body.Close()
|
||
|
|
assert.True(t, resp2.StatusCode == http.StatusOK || resp2.StatusCode == http.StatusForbidden || resp2.StatusCode == http.StatusBadRequest,
|
||
|
|
"should export Excel, got %d", resp2.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_ExportUsers_WithFields 验证指定字段导出
|
||
|
|
func TestExportHandler_ExportUsers_WithFields(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")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/exports/users?fields=id,username,email&format=csv", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden,
|
||
|
|
"should export with fields, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_ExportUsers_WithFilter 验证带过滤条件导出
|
||
|
|
func TestExportHandler_ExportUsers_WithFilter(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")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/exports/users?keyword=admin&status=1&format=csv", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusBadRequest,
|
||
|
|
"should export with filter, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_ExportUsers_NonAdmin 验证非管理员导出
|
||
|
|
func TestExportHandler_ExportUsers_NonAdmin(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
registerUser(server.URL, "regular", "regular@test.com", "Pass123!")
|
||
|
|
token := getToken(server.URL, "regular", "Pass123!")
|
||
|
|
assert.NotEmpty(t, token)
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/exports/users", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK,
|
||
|
|
"should handle non-admin export, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_ExportUsers_Unauthorized 验证未认证导出
|
||
|
|
func TestExportHandler_ExportUsers_Unauthorized(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/exports/users", "")
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden,
|
||
|
|
"should require auth, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_ImportUsers_Success 验证导入用户数据
|
||
|
|
func TestExportHandler_ImportUsers_Success(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")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create multipart form with CSV data
|
||
|
|
var body bytes.Buffer
|
||
|
|
writer := multipart.NewWriter(&body)
|
||
|
|
part, _ := writer.CreateFormFile("file", "users.csv")
|
||
|
|
csvData := "username,email,password\nuser1,user1@test.com,Pass123!\nuser2,user2@test.com,Pass123!"
|
||
|
|
part.Write([]byte(csvData))
|
||
|
|
writer.Close()
|
||
|
|
|
||
|
|
req, _ := http.NewRequest("POST", server.URL+"/api/v1/exports/users?format=csv", &body)
|
||
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
||
|
|
|
||
|
|
client := &http.Client{}
|
||
|
|
resp, err := client.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("request failed: %v", err)
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
respBody, _ := io.ReadAll(resp.Body)
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusInternalServerError,
|
||
|
|
"should import users, got %d: %s", resp.StatusCode, string(respBody))
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_ImportUsers_NoFile 验证无文件导入
|
||
|
|
func TestExportHandler_ImportUsers_NoFile(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")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create empty multipart form
|
||
|
|
var body bytes.Buffer
|
||
|
|
writer := multipart.NewWriter(&body)
|
||
|
|
writer.Close()
|
||
|
|
|
||
|
|
req, _ := http.NewRequest("POST", server.URL+"/api/v1/exports/users", &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()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusOK,
|
||
|
|
"should require file, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_ImportUsers_InvalidFormat 验证无效格式导入
|
||
|
|
func TestExportHandler_ImportUsers_InvalidFormat(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")
|
||
|
|
}
|
||
|
|
|
||
|
|
var body bytes.Buffer
|
||
|
|
writer := multipart.NewWriter(&body)
|
||
|
|
part, _ := writer.CreateFormFile("file", "users.txt")
|
||
|
|
part.Write([]byte("invalid content"))
|
||
|
|
writer.Close()
|
||
|
|
|
||
|
|
req, _ := http.NewRequest("POST", server.URL+"/api/v1/exports/users?format=invalid", &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()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden,
|
||
|
|
"should handle invalid format, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_ImportUsers_NonAdmin 验证非管理员导入
|
||
|
|
func TestExportHandler_ImportUsers_NonAdmin(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
registerUser(server.URL, "regular", "regular@test.com", "Pass123!")
|
||
|
|
token := getToken(server.URL, "regular", "Pass123!")
|
||
|
|
assert.NotEmpty(t, token)
|
||
|
|
|
||
|
|
var body bytes.Buffer
|
||
|
|
writer := multipart.NewWriter(&body)
|
||
|
|
part, _ := writer.CreateFormFile("file", "users.csv")
|
||
|
|
part.Write([]byte("username,email\nuser1,user1@test.com"))
|
||
|
|
writer.Close()
|
||
|
|
|
||
|
|
req, _ := http.NewRequest("POST", server.URL+"/api/v1/exports/users", &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()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK,
|
||
|
|
"should handle non-admin import, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_GetImportTemplate_Success 验证获取导入模板
|
||
|
|
func TestExportHandler_GetImportTemplate_Success(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")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/exports/template", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusInternalServerError,
|
||
|
|
"should get template, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_GetImportTemplate_CSV 验证 CSV 模板
|
||
|
|
func TestExportHandler_GetImportTemplate_CSV(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")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/exports/template?format=csv", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden,
|
||
|
|
"should get CSV template, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_GetImportTemplate_Excel 验证 Excel 模板
|
||
|
|
func TestExportHandler_GetImportTemplate_Excel(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")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/exports/template?format=excel", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusBadRequest,
|
||
|
|
"should get Excel template, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_GetImportTemplate_Unauthorized 验证未认证获取模板
|
||
|
|
func TestExportHandler_GetImportTemplate_Unauthorized(t *testing.T) {
|
||
|
|
server, cleanup := setupHandlerTestServer(t)
|
||
|
|
defer cleanup()
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/exports/template", "")
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden,
|
||
|
|
"should require auth, got %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_ExportResponse_ContentType 验证导出响应内容类型
|
||
|
|
func TestExportHandler_ExportResponse_ContentType(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")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/exports/users?format=csv", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
if resp.StatusCode == http.StatusOK {
|
||
|
|
contentType := resp.Header.Get("Content-Type")
|
||
|
|
// Content-Type may or may not be set depending on implementation
|
||
|
|
t.Logf("Content-Type: %s", contentType)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestExportHandler_ExportResponse_ContentDisposition 验证导出响应文件名
|
||
|
|
func TestExportHandler_ExportResponse_ContentDisposition(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")
|
||
|
|
}
|
||
|
|
|
||
|
|
resp, _ := doGet(server.URL+"/api/v1/exports/users?format=csv", token)
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
if resp.StatusCode == http.StatusOK {
|
||
|
|
disposition := resp.Header.Get("Content-Disposition")
|
||
|
|
// Disposition may or may not be set depending on implementation
|
||
|
|
t.Logf("Content-Disposition: %s", disposition)
|
||
|
|
}
|
||
|
|
}
|