// // 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.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() }