578 lines
18 KiB
Go
578 lines
18 KiB
Go
//go:build integration || error_handling
|
|
// +build integration error_handling
|
|
|
|
package error_handling
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap/zaptest"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
apperrors "veza-backend-api/internal/errors"
|
|
"veza-backend-api/internal/middleware"
|
|
"veza-backend-api/internal/models"
|
|
"veza-backend-api/internal/response"
|
|
)
|
|
|
|
// setupErrorHandlingTestRouter crée un router de test pour les tests de gestion d'erreurs
|
|
func setupErrorHandlingTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, func()) {
|
|
gin.SetMode(gin.TestMode)
|
|
logger := zaptest.NewLogger(t)
|
|
|
|
// Setup in-memory SQLite database
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
db.Exec("PRAGMA foreign_keys = ON")
|
|
|
|
// Auto-migrate models
|
|
err = db.AutoMigrate(
|
|
&models.User{},
|
|
&models.Track{},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Create router with error handler
|
|
router := gin.New()
|
|
router.Use(middleware.ErrorHandler(logger, nil, false))
|
|
|
|
// Test endpoints that return different error types
|
|
router.GET("/test/validation-error", func(c *gin.Context) {
|
|
c.Error(apperrors.NewValidationError("validation failed", apperrors.ErrorDetail{
|
|
Field: "email",
|
|
Message: "invalid email format",
|
|
}))
|
|
c.Abort()
|
|
})
|
|
|
|
router.GET("/test/not-found", func(c *gin.Context) {
|
|
c.Error(apperrors.NewNotFoundError("resource"))
|
|
c.Abort()
|
|
})
|
|
|
|
router.GET("/test/unauthorized", func(c *gin.Context) {
|
|
c.Error(apperrors.NewUnauthorizedError("unauthorized"))
|
|
c.Abort()
|
|
})
|
|
|
|
router.GET("/test/forbidden", func(c *gin.Context) {
|
|
c.Error(apperrors.NewForbiddenError("forbidden"))
|
|
c.Abort()
|
|
})
|
|
|
|
router.GET("/test/internal-error", func(c *gin.Context) {
|
|
c.Error(apperrors.New(apperrors.ErrCodeInternal, "internal server error"))
|
|
c.Abort()
|
|
})
|
|
|
|
router.GET("/test/database-error", func(c *gin.Context) {
|
|
c.Error(apperrors.New(apperrors.ErrCodeDatabase, "database error"))
|
|
c.Abort()
|
|
})
|
|
|
|
router.GET("/test/conflict", func(c *gin.Context) {
|
|
c.Error(apperrors.New(apperrors.ErrCodeConflict, "resource conflict"))
|
|
c.Abort()
|
|
})
|
|
|
|
router.GET("/test/rate-limit", func(c *gin.Context) {
|
|
c.Error(apperrors.New(apperrors.ErrCodeRateLimitExceeded, "rate limit exceeded"))
|
|
c.Abort()
|
|
})
|
|
|
|
router.GET("/test/quota-exceeded", func(c *gin.Context) {
|
|
c.Error(apperrors.New(apperrors.ErrCodeQuotaExceeded, "quota exceeded"))
|
|
c.Abort()
|
|
})
|
|
|
|
// Endpoint that recovers from error
|
|
router.GET("/test/recovery", func(c *gin.Context) {
|
|
// Simulate error
|
|
if c.Query("error") == "true" {
|
|
c.Error(apperrors.New(apperrors.ErrCodeInternal, "simulated error"))
|
|
c.Abort()
|
|
return
|
|
}
|
|
// Success path
|
|
response.Success(c, gin.H{"message": "recovered"}, "success")
|
|
})
|
|
|
|
// Endpoint with error details
|
|
router.POST("/test/validation-details", func(c *gin.Context) {
|
|
var req struct {
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
details := []apperrors.ErrorDetail{
|
|
{Field: "email", Message: "email is required"},
|
|
{Field: "password", Message: "password is required"},
|
|
}
|
|
c.Error(apperrors.NewValidationError("validation failed", details...))
|
|
c.Abort()
|
|
return
|
|
}
|
|
response.Success(c, gin.H{"email": req.Email}, "valid")
|
|
})
|
|
|
|
cleanup := func() {
|
|
// Cleanup handled by in-memory DB
|
|
}
|
|
|
|
return router, db, cleanup
|
|
}
|
|
|
|
// TestErrorResponse_Format teste que les réponses d'erreur sont au format standardisé
|
|
func TestErrorResponse_Format(t *testing.T) {
|
|
router, _, cleanup := setupErrorHandlingTestRouter(t)
|
|
defer cleanup()
|
|
|
|
tests := []struct {
|
|
name string
|
|
endpoint string
|
|
expectedStatus int
|
|
expectedCode apperrors.ErrorCode
|
|
validateError func(t *testing.T, body []byte)
|
|
}{
|
|
{
|
|
name: "Validation Error",
|
|
endpoint: "/test/validation-error",
|
|
expectedStatus: http.StatusBadRequest,
|
|
expectedCode: apperrors.ErrCodeValidation,
|
|
validateError: func(t *testing.T, body []byte) {
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(body, &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok, "Error should be a map")
|
|
assert.Equal(t, float64(apperrors.ErrCodeValidation), errorData["code"])
|
|
assert.Contains(t, errorData, "message")
|
|
assert.Contains(t, errorData, "details")
|
|
},
|
|
},
|
|
{
|
|
name: "Not Found Error",
|
|
endpoint: "/test/not-found",
|
|
expectedStatus: http.StatusNotFound,
|
|
expectedCode: apperrors.ErrCodeNotFound,
|
|
validateError: func(t *testing.T, body []byte) {
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(body, &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, float64(apperrors.ErrCodeNotFound), errorData["code"])
|
|
},
|
|
},
|
|
{
|
|
name: "Unauthorized Error",
|
|
endpoint: "/test/unauthorized",
|
|
expectedStatus: http.StatusUnauthorized,
|
|
expectedCode: apperrors.ErrCodeUnauthorized,
|
|
validateError: func(t *testing.T, body []byte) {
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(body, &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, float64(apperrors.ErrCodeUnauthorized), errorData["code"])
|
|
},
|
|
},
|
|
{
|
|
name: "Forbidden Error",
|
|
endpoint: "/test/forbidden",
|
|
expectedStatus: http.StatusForbidden,
|
|
expectedCode: apperrors.ErrCodeForbidden,
|
|
validateError: func(t *testing.T, body []byte) {
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(body, &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, float64(apperrors.ErrCodeForbidden), errorData["code"])
|
|
},
|
|
},
|
|
{
|
|
name: "Internal Server Error",
|
|
endpoint: "/test/internal-error",
|
|
expectedStatus: http.StatusInternalServerError,
|
|
expectedCode: apperrors.ErrCodeInternal,
|
|
validateError: func(t *testing.T, body []byte) {
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(body, &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, float64(apperrors.ErrCodeInternal), errorData["code"])
|
|
},
|
|
},
|
|
{
|
|
name: "Database Error",
|
|
endpoint: "/test/database-error",
|
|
expectedStatus: http.StatusInternalServerError,
|
|
expectedCode: apperrors.ErrCodeDatabase,
|
|
validateError: func(t *testing.T, body []byte) {
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(body, &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, float64(apperrors.ErrCodeDatabase), errorData["code"])
|
|
},
|
|
},
|
|
{
|
|
name: "Conflict Error",
|
|
endpoint: "/test/conflict",
|
|
expectedStatus: http.StatusConflict,
|
|
expectedCode: apperrors.ErrCodeConflict,
|
|
validateError: func(t *testing.T, body []byte) {
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(body, &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, float64(apperrors.ErrCodeConflict), errorData["code"])
|
|
},
|
|
},
|
|
{
|
|
name: "Rate Limit Error",
|
|
endpoint: "/test/rate-limit",
|
|
expectedStatus: http.StatusTooManyRequests,
|
|
expectedCode: apperrors.ErrCodeRateLimitExceeded,
|
|
validateError: func(t *testing.T, body []byte) {
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(body, &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, float64(apperrors.ErrCodeRateLimitExceeded), errorData["code"])
|
|
},
|
|
},
|
|
{
|
|
name: "Quota Exceeded Error",
|
|
endpoint: "/test/quota-exceeded",
|
|
expectedStatus: http.StatusForbidden,
|
|
expectedCode: apperrors.ErrCodeQuotaExceeded,
|
|
validateError: func(t *testing.T, body []byte) {
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(body, &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, float64(apperrors.ErrCodeQuotaExceeded), errorData["code"])
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, tt.endpoint, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tt.expectedStatus, w.Code,
|
|
fmt.Sprintf("Expected status %d, got %d", tt.expectedStatus, w.Code))
|
|
|
|
if tt.validateError != nil {
|
|
tt.validateError(t, w.Body.Bytes())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestErrorResponse_Details teste que les détails d'erreur sont présents quand nécessaire
|
|
func TestErrorResponse_Details(t *testing.T) {
|
|
router, _, cleanup := setupErrorHandlingTestRouter(t)
|
|
defer cleanup()
|
|
|
|
// Test validation error with details
|
|
req := httptest.NewRequest(http.MethodGet, "/test/validation-error", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
|
|
details, ok := errorData["details"].([]interface{})
|
|
require.True(t, ok, "Details should be present")
|
|
assert.NotEmpty(t, details, "Details should not be empty")
|
|
|
|
// Verify detail structure
|
|
detail, ok := details[0].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Contains(t, detail, "field")
|
|
assert.Contains(t, detail, "message")
|
|
}
|
|
|
|
// TestErrorRecovery teste la récupération après erreur
|
|
func TestErrorRecovery(t *testing.T) {
|
|
router, _, cleanup := setupErrorHandlingTestRouter(t)
|
|
defer cleanup()
|
|
|
|
// Test error case
|
|
req := httptest.NewRequest(http.MethodGet, "/test/recovery?error=true", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
|
|
|
var errorResp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &errorResp)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, errorResp, "error")
|
|
|
|
// Test recovery (success case)
|
|
req2 := httptest.NewRequest(http.MethodGet, "/test/recovery", nil)
|
|
w2 := httptest.NewRecorder()
|
|
router.ServeHTTP(w2, req2)
|
|
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
|
|
var successResp map[string]interface{}
|
|
err = json.Unmarshal(w2.Body.Bytes(), &successResp)
|
|
require.NoError(t, err)
|
|
assert.True(t, successResp["success"].(bool))
|
|
assert.Contains(t, successResp, "data")
|
|
}
|
|
|
|
// TestErrorRecovery_ValidationDetails teste la récupération avec détails de validation
|
|
func TestErrorRecovery_ValidationDetails(t *testing.T) {
|
|
router, _, cleanup := setupErrorHandlingTestRouter(t)
|
|
defer cleanup()
|
|
|
|
// Test with invalid input (should return validation error)
|
|
invalidPayload := map[string]interface{}{}
|
|
body, _ := json.Marshal(invalidPayload)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/test/validation-details", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
var errorResp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &errorResp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := errorResp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
|
|
details, ok := errorData["details"].([]interface{})
|
|
require.True(t, ok)
|
|
assert.Len(t, details, 2, "Should have 2 validation errors")
|
|
|
|
// Test with valid input (should succeed)
|
|
validPayload := map[string]interface{}{
|
|
"email": "test@example.com",
|
|
"password": "password123",
|
|
}
|
|
body2, _ := json.Marshal(validPayload)
|
|
|
|
req2 := httptest.NewRequest(http.MethodPost, "/test/validation-details", bytes.NewBuffer(body2))
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
w2 := httptest.NewRecorder()
|
|
router.ServeHTTP(w2, req2)
|
|
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
|
|
var successResp map[string]interface{}
|
|
err = json.Unmarshal(w2.Body.Bytes(), &successResp)
|
|
require.NoError(t, err)
|
|
assert.True(t, successResp["success"].(bool))
|
|
}
|
|
|
|
// TestErrorResponse_Timestamp teste que le timestamp est présent dans les erreurs
|
|
func TestErrorResponse_Timestamp(t *testing.T) {
|
|
router, _, cleanup := setupErrorHandlingTestRouter(t)
|
|
defer cleanup()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/test/validation-error", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
|
|
// Note: The error handler might not include timestamp in the current format
|
|
// This test verifies the error structure is valid
|
|
assert.Contains(t, errorData, "code")
|
|
assert.Contains(t, errorData, "message")
|
|
}
|
|
|
|
// TestErrorResponse_Context teste que le contexte est présent dans les erreurs
|
|
func TestErrorResponse_Context(t *testing.T) {
|
|
router, _, cleanup := setupErrorHandlingTestRouter(t)
|
|
defer cleanup()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/test/validation-error", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
|
|
// Context might be present if set by middleware
|
|
// This test verifies the error structure is valid
|
|
assert.Contains(t, errorData, "code")
|
|
assert.Contains(t, errorData, "message")
|
|
}
|
|
|
|
// TestErrorResponse_Consistency teste que toutes les erreurs suivent le même format
|
|
func TestErrorResponse_Consistency(t *testing.T) {
|
|
router, _, cleanup := setupErrorHandlingTestRouter(t)
|
|
defer cleanup()
|
|
|
|
endpoints := []string{
|
|
"/test/validation-error",
|
|
"/test/not-found",
|
|
"/test/unauthorized",
|
|
"/test/forbidden",
|
|
"/test/internal-error",
|
|
}
|
|
|
|
for _, endpoint := range endpoints {
|
|
t.Run(fmt.Sprintf("Consistency check: %s", endpoint), func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, endpoint, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
|
|
// All errors should have the same structure
|
|
assert.Contains(t, resp, "error")
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok, "Error should be a map")
|
|
|
|
// Required fields
|
|
assert.Contains(t, errorData, "code")
|
|
assert.Contains(t, errorData, "message")
|
|
|
|
// Code should be a number
|
|
code, ok := errorData["code"].(float64)
|
|
assert.True(t, ok, "Code should be a number")
|
|
assert.Greater(t, code, float64(0), "Code should be positive")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestErrorRecovery_RetryLogic teste la logique de retry après erreur
|
|
func TestErrorRecovery_RetryLogic(t *testing.T) {
|
|
router, _, cleanup := setupErrorHandlingTestRouter(t)
|
|
defer cleanup()
|
|
|
|
// Simulate retry after error
|
|
// First request: error
|
|
req1 := httptest.NewRequest(http.MethodGet, "/test/recovery?error=true", nil)
|
|
w1 := httptest.NewRecorder()
|
|
router.ServeHTTP(w1, req1)
|
|
assert.Equal(t, http.StatusInternalServerError, w1.Code)
|
|
|
|
// Second request: success (recovery)
|
|
req2 := httptest.NewRequest(http.MethodGet, "/test/recovery", nil)
|
|
w2 := httptest.NewRecorder()
|
|
router.ServeHTTP(w2, req2)
|
|
assert.Equal(t, http.StatusOK, w2.Code)
|
|
|
|
var successResp map[string]interface{}
|
|
err := json.Unmarshal(w2.Body.Bytes(), &successResp)
|
|
require.NoError(t, err)
|
|
assert.True(t, successResp["success"].(bool))
|
|
}
|
|
|
|
// TestErrorHandling_AppErrorWrapper teste que les AppError sont correctement wrappées
|
|
func TestErrorHandling_AppErrorWrapper(t *testing.T) {
|
|
router, _, cleanup := setupErrorHandlingTestRouter(t)
|
|
defer cleanup()
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/test/validation-error", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
|
|
// Verify AppError structure
|
|
assert.Equal(t, float64(apperrors.ErrCodeValidation), errorData["code"])
|
|
assert.NotEmpty(t, errorData["message"])
|
|
}
|
|
|
|
// TestErrorHandling_HTTPStatusMapping teste que les codes d'erreur sont correctement mappés vers les codes HTTP
|
|
func TestErrorHandling_HTTPStatusMapping(t *testing.T) {
|
|
router, _, cleanup := setupErrorHandlingTestRouter(t)
|
|
defer cleanup()
|
|
|
|
tests := []struct {
|
|
endpoint string
|
|
expectedStatus int
|
|
expectedCode apperrors.ErrorCode
|
|
}{
|
|
{"/test/validation-error", http.StatusBadRequest, apperrors.ErrCodeValidation},
|
|
{"/test/not-found", http.StatusNotFound, apperrors.ErrCodeNotFound},
|
|
{"/test/unauthorized", http.StatusUnauthorized, apperrors.ErrCodeUnauthorized},
|
|
{"/test/forbidden", http.StatusForbidden, apperrors.ErrCodeForbidden},
|
|
{"/test/internal-error", http.StatusInternalServerError, apperrors.ErrCodeInternal},
|
|
{"/test/database-error", http.StatusInternalServerError, apperrors.ErrCodeDatabase},
|
|
{"/test/conflict", http.StatusConflict, apperrors.ErrCodeConflict},
|
|
{"/test/rate-limit", http.StatusTooManyRequests, apperrors.ErrCodeRateLimitExceeded},
|
|
{"/test/quota-exceeded", http.StatusForbidden, apperrors.ErrCodeQuotaExceeded},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.endpoint, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, tt.endpoint, nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tt.expectedStatus, w.Code,
|
|
fmt.Sprintf("Expected HTTP status %d for error code %d", tt.expectedStatus, tt.expectedCode))
|
|
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
|
|
errorData, ok := resp["error"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, float64(tt.expectedCode), errorData["code"])
|
|
})
|
|
}
|
|
}
|