342 lines
13 KiB
Swift
342 lines
13 KiB
Swift
//
|
|
// APITestView.swift
|
|
// AppClientBaseSwift
|
|
//
|
|
// Debug view for testing API responses
|
|
// View debug để test API responses
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
// MARK: - API Test View
|
|
// View test API
|
|
|
|
/// Debug view to test API endpoints
|
|
/// View debug để test các API endpoints
|
|
struct APITestView: View {
|
|
|
|
@State private var testResult: String = "Chưa có kết quả"
|
|
@State private var isLoading: Bool = false
|
|
@State private var rawJSON: String = ""
|
|
@State private var accessToken: String = ""
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
ScrollView {
|
|
VStack(spacing: 20) {
|
|
|
|
// Access Token Input
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Access Token")
|
|
.font(.headline)
|
|
|
|
TextEditor(text: $accessToken)
|
|
.frame(height: 100)
|
|
.padding(8)
|
|
.background(Color.gray.opacity(0.1))
|
|
.cornerRadius(8)
|
|
.font(.system(.caption, design: .monospaced))
|
|
|
|
Text("Paste access token từ Keychain hoặc sau khi login thành công")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.padding()
|
|
|
|
// Test Buttons
|
|
VStack(spacing: 12) {
|
|
Button("Test /users/me Endpoint") {
|
|
Task {
|
|
await testUsersMeEndpoint()
|
|
}
|
|
}
|
|
.buttonStyle(TestButtonStyle(color: .blue))
|
|
|
|
Button("Test với Mock Token") {
|
|
Task {
|
|
await testWithMockToken()
|
|
}
|
|
}
|
|
.buttonStyle(TestButtonStyle(color: .orange))
|
|
|
|
Button("Clear Results") {
|
|
testResult = "Đã xóa kết quả"
|
|
rawJSON = ""
|
|
}
|
|
.buttonStyle(TestButtonStyle(color: .red))
|
|
}
|
|
.padding()
|
|
.disabled(isLoading)
|
|
|
|
// Loading Indicator
|
|
if isLoading {
|
|
ProgressView("Đang test API...")
|
|
.padding()
|
|
}
|
|
|
|
// Test Result
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Kết quả:")
|
|
.font(.headline)
|
|
|
|
ScrollView {
|
|
Text(testResult)
|
|
.font(.system(.caption, design: .monospaced))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding()
|
|
.background(Color.gray.opacity(0.1))
|
|
.cornerRadius(8)
|
|
}
|
|
.frame(maxHeight: 200)
|
|
}
|
|
.padding()
|
|
|
|
// Raw JSON
|
|
if !rawJSON.isEmpty {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Raw JSON Response:")
|
|
.font(.headline)
|
|
|
|
ScrollView {
|
|
Text(rawJSON)
|
|
.font(.system(.caption, design: .monospaced))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding()
|
|
.background(Color.green.opacity(0.1))
|
|
.cornerRadius(8)
|
|
}
|
|
.frame(maxHeight: 300)
|
|
|
|
Button("Copy JSON") {
|
|
UIPasteboard.general.string = rawJSON
|
|
testResult = "✅ Đã copy JSON vào clipboard!"
|
|
}
|
|
.buttonStyle(TestButtonStyle(color: .green))
|
|
}
|
|
.padding()
|
|
}
|
|
|
|
// Instructions
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Hướng dẫn:")
|
|
.font(.headline)
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("1. Login thành công trước")
|
|
Text("2. Access token sẽ tự động được lấy từ Keychain")
|
|
Text("3. Nhấn 'Test /users/me Endpoint'")
|
|
Text("4. Xem Raw JSON để biết server trả về gì")
|
|
Text("5. So sánh với User model trong code")
|
|
}
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.padding()
|
|
.background(Color.blue.opacity(0.1))
|
|
.cornerRadius(8)
|
|
.padding()
|
|
}
|
|
}
|
|
.navigationTitle("API Debugger")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
.onAppear {
|
|
// Load current access token
|
|
if let token = AuthManager.shared.accessToken {
|
|
accessToken = token
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Test Methods
|
|
|
|
/// Test /users/me endpoint
|
|
private func testUsersMeEndpoint() async {
|
|
isLoading = true
|
|
testResult = "🔄 Đang gọi API..."
|
|
|
|
do {
|
|
// Build URL
|
|
guard let url = URL(string: APIConfig.baseURL + APIConfig.apiVersion + "/users/me") else {
|
|
testResult = "❌ Invalid URL"
|
|
isLoading = false
|
|
return
|
|
}
|
|
|
|
// Create request
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
|
|
// Use token from input or Keychain
|
|
let token = accessToken.isEmpty ? AuthManager.shared.accessToken : accessToken
|
|
if let token = token, !token.isEmpty {
|
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
}
|
|
|
|
testResult = """
|
|
📤 Request Details:
|
|
URL: \(url.absoluteString)
|
|
Method: GET
|
|
Token: \(token?.prefix(20) ?? "None")...
|
|
|
|
⏳ Waiting for response...
|
|
"""
|
|
|
|
// Perform request
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
testResult = "❌ Invalid response"
|
|
isLoading = false
|
|
return
|
|
}
|
|
|
|
// Get raw JSON
|
|
if let jsonString = String(data: data, encoding: .utf8) {
|
|
rawJSON = jsonString.prettyPrintedJSON ?? jsonString
|
|
}
|
|
|
|
// Parse result
|
|
testResult = """
|
|
📥 Response Details:
|
|
Status Code: \(httpResponse.statusCode)
|
|
|
|
"""
|
|
|
|
if httpResponse.statusCode == 200 {
|
|
testResult += "✅ Status: SUCCESS\n\n"
|
|
|
|
// Try to decode as User
|
|
let decoder = JSONDecoder()
|
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
decoder.dateDecodingStrategy = .iso8601
|
|
|
|
do {
|
|
// First try to decode with wrapper
|
|
// Thử decode với wrapper trước
|
|
let response = try decoder.decode(APIResponse<User>.self, from: data)
|
|
|
|
if response.success, let user = response.data {
|
|
testResult += """
|
|
✅ Decoded User Successfully (with wrapper):
|
|
Success: \(response.success)
|
|
ID: \(user.id)
|
|
Email: \(user.email)
|
|
Name: \(user.name)
|
|
Avatar: \(user.avatarUrl ?? "nil")
|
|
Phone: \(user.phoneNumber ?? "nil")
|
|
Email Verified: \(user.isEmailVerified)
|
|
Roles: \(user.roles?.joined(separator: ", ") ?? "none")
|
|
"""
|
|
} else {
|
|
testResult += """
|
|
⚠️ Response decoded but no user data:
|
|
Success: \(response.success)
|
|
Error: \(response.error ?? "nil")
|
|
"""
|
|
}
|
|
} catch let wrapperError {
|
|
testResult += """
|
|
❌ Decoding with wrapper failed, trying direct decode...
|
|
Wrapper Error: \(wrapperError.localizedDescription)
|
|
|
|
"""
|
|
|
|
// Try direct decode without wrapper
|
|
// Thử decode trực tiếp không có wrapper
|
|
do {
|
|
let user = try decoder.decode(User.self, from: data)
|
|
testResult += """
|
|
✅ Decoded User Successfully (direct, no wrapper):
|
|
ID: \(user.id)
|
|
Email: \(user.email)
|
|
Name: \(user.name)
|
|
Avatar: \(user.avatarUrl ?? "nil")
|
|
Phone: \(user.phoneNumber ?? "nil")
|
|
Email Verified: \(user.isEmailVerified)
|
|
Roles: \(user.roles?.joined(separator: ", ") ?? "none")
|
|
"""
|
|
} catch let directError {
|
|
testResult += """
|
|
❌ Both decoding methods failed:
|
|
Direct decode error: \(directError.localizedDescription)
|
|
|
|
💡 Check Raw JSON below để xem field nào bị thiếu hoặc sai tên
|
|
"""
|
|
}
|
|
}
|
|
} else if httpResponse.statusCode == 401 {
|
|
testResult += "❌ Status: UNAUTHORIZED (Token invalid or expired)\n"
|
|
} else {
|
|
testResult += "❌ Status: ERROR\n"
|
|
if let errorMessage = String(data: data, encoding: .utf8) {
|
|
testResult += "Error: \(errorMessage)\n"
|
|
}
|
|
}
|
|
|
|
} catch {
|
|
testResult = """
|
|
❌ Request Failed:
|
|
\(error.localizedDescription)
|
|
"""
|
|
rawJSON = ""
|
|
}
|
|
|
|
isLoading = false
|
|
}
|
|
|
|
/// Test with mock token
|
|
private func testWithMockToken() async {
|
|
testResult = """
|
|
⚠️ Testing với mock token...
|
|
|
|
Lưu ý: Request này sẽ fail với 401 Unauthorized
|
|
Mục đích: Kiểm tra server response format khi unauthorized
|
|
"""
|
|
|
|
accessToken = "mock_token_for_testing"
|
|
|
|
await testUsersMeEndpoint()
|
|
}
|
|
}
|
|
|
|
// MARK: - Test Button Style
|
|
|
|
struct TestButtonStyle: ButtonStyle {
|
|
let color: Color
|
|
|
|
func makeBody(configuration: Configuration) -> some View {
|
|
configuration.label
|
|
.font(.headline)
|
|
.foregroundColor(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
.background(
|
|
color.opacity(configuration.isPressed ? 0.7 : 1.0)
|
|
)
|
|
.cornerRadius(10)
|
|
}
|
|
}
|
|
|
|
// MARK: - String Extension for Pretty JSON
|
|
|
|
extension String {
|
|
var prettyPrintedJSON: String? {
|
|
guard let data = self.data(using: .utf8),
|
|
let jsonObject = try? JSONSerialization.jsonObject(with: data),
|
|
let prettyData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
|
|
let prettyString = String(data: prettyData, encoding: .utf8) else {
|
|
return nil
|
|
}
|
|
return prettyString
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
APITestView()
|
|
}
|