From 3f20a1c0347a0892cfa1e4528432d558386518d9 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 8 May 2026 15:44:42 +0200 Subject: [PATCH 1/4] fix: pass HTTP status code to ApiException preventing null cast crash ApiException.fromJson() crashed with "type 'Null' is not a subtype of type 'int' in type cast" when the API error response did not include a statusCode field in the JSON body (e.g. TFA_REQUIRED 403 responses). Add httpStatusCode parameter to fromJson() as fallback and pass response.statusCode from all call sites. --- .gitignore | 3 +++ lib/packages/service/dfx/dfx_kyc_service.dart | 20 +++++++++---------- .../service/dfx/exceptions/api_exception.dart | 4 ++-- .../real_unit_buy_payment_info_service.dart | 4 ++-- .../service/dfx/real_unit_pdf_service.dart | 6 +++--- .../dfx/real_unit_registration_service.dart | 4 ++-- .../real_unit_sell_payment_info_service.dart | 2 +- .../service/dfx/real_unit_wallet_service.dart | 2 +- 8 files changed, 24 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 41d9be21..d21fec3c 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,9 @@ app.*.map.json **/*.g.dart /lib/generated/** +# Environment +.env* + # FVM Version Cache .fvm/ .fvmrc \ No newline at end of file diff --git a/lib/packages/service/dfx/dfx_kyc_service.dart b/lib/packages/service/dfx/dfx_kyc_service.dart index 24c5149b..c05f5e3f 100644 --- a/lib/packages/service/dfx/dfx_kyc_service.dart +++ b/lib/packages/service/dfx/dfx_kyc_service.dart @@ -50,7 +50,7 @@ class DfxKycService extends DFXAuthService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final json = jsonDecode(response.body) as Map; @@ -73,7 +73,7 @@ class DfxKycService extends DFXAuthService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final json = jsonDecode(response.body); @@ -96,7 +96,7 @@ class DfxKycService extends DFXAuthService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final json = jsonDecode(response.body); @@ -120,7 +120,7 @@ class DfxKycService extends DFXAuthService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final json = jsonDecode(response.body); @@ -142,7 +142,7 @@ class DfxKycService extends DFXAuthService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } } @@ -164,7 +164,7 @@ class DfxKycService extends DFXAuthService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } return; } @@ -185,7 +185,7 @@ class DfxKycService extends DFXAuthService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } return; } @@ -207,7 +207,7 @@ class DfxKycService extends DFXAuthService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } return; } @@ -231,7 +231,7 @@ class DfxKycService extends DFXAuthService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final json = jsonDecode(response.body) as Map; @@ -257,7 +257,7 @@ class DfxKycService extends DFXAuthService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } } } diff --git a/lib/packages/service/dfx/exceptions/api_exception.dart b/lib/packages/service/dfx/exceptions/api_exception.dart index f05e2242..4d315b94 100644 --- a/lib/packages/service/dfx/exceptions/api_exception.dart +++ b/lib/packages/service/dfx/exceptions/api_exception.dart @@ -11,7 +11,7 @@ class ApiException implements Exception { required this.message, }); - factory ApiException.fromJson(Map json) { + factory ApiException.fromJson(Map json, {int? httpStatusCode}) { final code = json['code'] as String?; switch (code) { @@ -22,7 +22,7 @@ class ApiException implements Exception { default: final message = json['message']; return ApiException( - statusCode: json['statusCode'] as int, + statusCode: json['statusCode'] as int? ?? httpStatusCode, code: code ?? 'UNKNOWN', message: message is List ? message.join(', ') : message?.toString() ?? 'Unknown error', ); diff --git a/lib/packages/service/dfx/real_unit_buy_payment_info_service.dart b/lib/packages/service/dfx/real_unit_buy_payment_info_service.dart index a23c2088..78521bb4 100644 --- a/lib/packages/service/dfx/real_unit_buy_payment_info_service.dart +++ b/lib/packages/service/dfx/real_unit_buy_payment_info_service.dart @@ -53,7 +53,7 @@ class RealUnitBuyPaymentInfoService { ); } else if (response.statusCode == 403) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } else { throw Exception('Unexpected status code: ${response.statusCode}'); } @@ -72,7 +72,7 @@ class RealUnitBuyPaymentInfoService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final json = jsonDecode(response.body) as Map; diff --git a/lib/packages/service/dfx/real_unit_pdf_service.dart b/lib/packages/service/dfx/real_unit_pdf_service.dart index b2459a0d..120b4917 100644 --- a/lib/packages/service/dfx/real_unit_pdf_service.dart +++ b/lib/packages/service/dfx/real_unit_pdf_service.dart @@ -47,7 +47,7 @@ class RealUnitPdfService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } return PdfDto.fromJson(jsonDecode(response.body)); @@ -71,7 +71,7 @@ class RealUnitPdfService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } return PdfDto.fromJson(jsonDecode(response.body)); @@ -92,7 +92,7 @@ class RealUnitPdfService { if (response.statusCode != 200 && response.statusCode != 201) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } return PdfDto.fromJson(jsonDecode(response.body)); diff --git a/lib/packages/service/dfx/real_unit_registration_service.dart b/lib/packages/service/dfx/real_unit_registration_service.dart index ff5cb198..11d9af5d 100644 --- a/lib/packages/service/dfx/real_unit_registration_service.dart +++ b/lib/packages/service/dfx/real_unit_registration_service.dart @@ -51,7 +51,7 @@ class RealUnitRegistrationService { if (response.statusCode != 201 && response.statusCode != 202) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final responseDto = RealUnitRegistrationEmailResponseDto.fromJson(jsonDecode(response.body)); return responseDto.status; @@ -184,7 +184,7 @@ class RealUnitRegistrationService { if (response.statusCode != 201 && response.statusCode != 202) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final responseDto = RealUnitRegistrationResponseDto.fromJson(jsonDecode(response.body)); diff --git a/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart b/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart index a64d092a..ebb9f89f 100644 --- a/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart +++ b/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart @@ -75,7 +75,7 @@ class RealUnitSellPaymentInfoService { ); } else if (response.statusCode == 403) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } else { throw Exception('Unexpected status code: ${response.body}'); } diff --git a/lib/packages/service/dfx/real_unit_wallet_service.dart b/lib/packages/service/dfx/real_unit_wallet_service.dart index b02b928f..0cd5ab8d 100644 --- a/lib/packages/service/dfx/real_unit_wallet_service.dart +++ b/lib/packages/service/dfx/real_unit_wallet_service.dart @@ -28,7 +28,7 @@ class RealUnitWalletService { if (response.statusCode != 200) { final errorJson = jsonDecode(response.body) as Map; - throw ApiException.fromJson(errorJson); + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final dto = RealUnitWalletStatusDto.fromJson(jsonDecode(response.body)); From 915382c85bc36672710d4c0e95320fd22f66f20e Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 8 May 2026 15:48:03 +0200 Subject: [PATCH 2/4] fix: propagate httpStatusCode to ApiException subclasses KycLevelRequiredException and RegistrationRequiredException now receive and apply the HTTP status code fallback, consistent with the parent ApiException.fromJson() pattern. --- lib/packages/service/dfx/exceptions/api_exception.dart | 4 ++-- .../service/dfx/exceptions/payment/buy_exceptions.dart | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/packages/service/dfx/exceptions/api_exception.dart b/lib/packages/service/dfx/exceptions/api_exception.dart index 4d315b94..caa70dec 100644 --- a/lib/packages/service/dfx/exceptions/api_exception.dart +++ b/lib/packages/service/dfx/exceptions/api_exception.dart @@ -16,9 +16,9 @@ class ApiException implements Exception { switch (code) { case 'KYC_LEVEL_REQUIRED': - return KycLevelRequiredException.fromJson(json); + return KycLevelRequiredException.fromJson(json, httpStatusCode: httpStatusCode); case 'REGISTRATION_REQUIRED': - return RegistrationRequiredException.fromJson(json); + return RegistrationRequiredException.fromJson(json, httpStatusCode: httpStatusCode); default: final message = json['message']; return ApiException( diff --git a/lib/packages/service/dfx/exceptions/payment/buy_exceptions.dart b/lib/packages/service/dfx/exceptions/payment/buy_exceptions.dart index b982a3b7..08104467 100644 --- a/lib/packages/service/dfx/exceptions/payment/buy_exceptions.dart +++ b/lib/packages/service/dfx/exceptions/payment/buy_exceptions.dart @@ -2,12 +2,14 @@ import 'package:realunit_wallet/packages/service/dfx/exceptions/api_exception.da class RegistrationRequiredException extends ApiException { const RegistrationRequiredException({ + super.statusCode, required super.code, required super.message, }); - factory RegistrationRequiredException.fromJson(Map json) { + factory RegistrationRequiredException.fromJson(Map json, {int? httpStatusCode}) { return RegistrationRequiredException( + statusCode: json['statusCode'] as int? ?? httpStatusCode, code: json['code'] as String, message: json['message'] as String, ); @@ -22,14 +24,16 @@ class KycLevelRequiredException extends ApiException { final int currentLevel; const KycLevelRequiredException({ + super.statusCode, required super.code, required super.message, required this.requiredLevel, required this.currentLevel, }); - factory KycLevelRequiredException.fromJson(Map json) { + factory KycLevelRequiredException.fromJson(Map json, {int? httpStatusCode}) { return KycLevelRequiredException( + statusCode: json['statusCode'] as int? ?? httpStatusCode, code: json['code'] as String, message: json['message'] as String, requiredLevel: json['requiredLevel'] as int, From 7c8e6119ad86a790d104ae7e38ab356e6fc0dc36 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 8 May 2026 15:57:57 +0200 Subject: [PATCH 3/4] refactor: consistent ApiException usage across all authenticated services - Add regression tests for ApiException.fromJson() covering null statusCode, httpStatusCode fallback, and subclass creation - Migrate all authenticated services to throw ApiException instead of generic Exception: dfx_support_service, dfx_bank_account_service, dfx_blockchain_api_service, dfx_faucet_service, dfx_brokerbot_service (sell endpoints) - Revert unrelated .gitignore change from previous commit --- .gitignore | 3 - .../service/dfx/dfx_bank_account_service.dart | 10 ++- .../dfx/dfx_blockchain_api_service.dart | 4 +- .../service/dfx/dfx_brokerbot_service.dart | 7 +- .../service/dfx/dfx_faucet_service.dart | 5 +- .../service/dfx/dfx_support_service.dart | 41 +++++----- .../dfx/exceptions/api_exception_test.dart | 79 +++++++++++++++++++ 7 files changed, 120 insertions(+), 29 deletions(-) create mode 100644 test/packages/service/dfx/exceptions/api_exception_test.dart diff --git a/.gitignore b/.gitignore index d21fec3c..41d9be21 100644 --- a/.gitignore +++ b/.gitignore @@ -51,9 +51,6 @@ app.*.map.json **/*.g.dart /lib/generated/** -# Environment -.env* - # FVM Version Cache .fvm/ .fvmrc \ No newline at end of file diff --git a/lib/packages/service/dfx/dfx_bank_account_service.dart b/lib/packages/service/dfx/dfx_bank_account_service.dart index 8d09607c..eacc5576 100644 --- a/lib/packages/service/dfx/dfx_bank_account_service.dart +++ b/lib/packages/service/dfx/dfx_bank_account_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:realunit_wallet/packages/config/api_config.dart'; import 'package:realunit_wallet/packages/service/app_store.dart'; +import 'package:realunit_wallet/packages/service/dfx/exceptions/api_exception.dart'; import 'package:realunit_wallet/packages/service/dfx/models/bank_account/dto/bank_account_dto.dart'; class DfxBankAccountService { @@ -26,7 +27,8 @@ class DfxBankAccountService { ); if (response.statusCode != 200 && response.statusCode != 201) { - throw Exception('Failed to add bank account: ${response.body}'); + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final List jsonList = jsonDecode(response.body); @@ -51,7 +53,8 @@ class DfxBankAccountService { ); if (response.statusCode != 200 && response.statusCode != 201) { - throw Exception('Failed to add bank account: ${response.body}'); + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } return BankAccountDto.fromJson(jsonDecode(response.body)); @@ -80,7 +83,8 @@ class DfxBankAccountService { ); if (response.statusCode != 200 && response.statusCode != 201) { - throw Exception('Failed to update bank account: ${response.body}'); + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } return BankAccountDto.fromJson(jsonDecode(response.body)); diff --git a/lib/packages/service/dfx/dfx_blockchain_api_service.dart b/lib/packages/service/dfx/dfx_blockchain_api_service.dart index 671cdb7a..03d3f524 100644 --- a/lib/packages/service/dfx/dfx_blockchain_api_service.dart +++ b/lib/packages/service/dfx/dfx_blockchain_api_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:realunit_wallet/packages/config/api_config.dart'; import 'package:realunit_wallet/packages/service/app_store.dart'; +import 'package:realunit_wallet/packages/service/dfx/exceptions/api_exception.dart'; class DfxBlockchainApiService { static const _balancesPath = 'v1/blockchain/balances'; @@ -31,7 +32,8 @@ class DfxBlockchainApiService { ); if (response.statusCode != 200 && response.statusCode != 201) { - throw Exception('Failed to get balances: ${response.statusCode}'); + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final json = jsonDecode(response.body) as Map; diff --git a/lib/packages/service/dfx/dfx_brokerbot_service.dart b/lib/packages/service/dfx/dfx_brokerbot_service.dart index abecdafb..002e3c1f 100644 --- a/lib/packages/service/dfx/dfx_brokerbot_service.dart +++ b/lib/packages/service/dfx/dfx_brokerbot_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:realunit_wallet/packages/config/api_config.dart'; import 'package:realunit_wallet/packages/service/app_store.dart'; +import 'package:realunit_wallet/packages/service/dfx/exceptions/api_exception.dart'; import 'package:realunit_wallet/packages/service/dfx/models/brokerbot/dfx_buy_price_dto.dart'; import 'package:realunit_wallet/packages/service/dfx/models/brokerbot/dfx_buy_shares_dto.dart'; import 'package:realunit_wallet/packages/service/dfx/models/brokerbot/dfx_sell_price_dto.dart'; @@ -78,7 +79,8 @@ class DfxBrokerbotService { ); if (res.statusCode != 200) { - throw Exception('SellPrice request failed: ${res.body}'); + final errorJson = jsonDecode(res.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: res.statusCode); } return BrokerbotSellPriceDto.fromJson(jsonDecode(res.body)); @@ -102,7 +104,8 @@ class DfxBrokerbotService { ); if (res.statusCode != 200) { - throw Exception('SellShares request failed: ${res.body}'); + final errorJson = jsonDecode(res.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: res.statusCode); } return BrokerbotSellSharesDto.fromJson(jsonDecode(res.body)); diff --git a/lib/packages/service/dfx/dfx_faucet_service.dart b/lib/packages/service/dfx/dfx_faucet_service.dart index d130dbf3..9c733f1f 100644 --- a/lib/packages/service/dfx/dfx_faucet_service.dart +++ b/lib/packages/service/dfx/dfx_faucet_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:realunit_wallet/packages/config/api_config.dart'; import 'package:realunit_wallet/packages/service/app_store.dart'; +import 'package:realunit_wallet/packages/service/dfx/exceptions/api_exception.dart'; import 'package:realunit_wallet/packages/service/dfx/models/faucet/faucet_response_dto.dart'; class DfxFaucetService { @@ -28,7 +29,7 @@ class DfxFaucetService { return FaucetResponseDto.fromJson(jsonDecode(response.body) as Map); } - final body = jsonDecode(response.body) as Map; - throw Exception(body['message'] ?? 'Faucet request failed'); + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } } diff --git a/lib/packages/service/dfx/dfx_support_service.dart b/lib/packages/service/dfx/dfx_support_service.dart index c6c3f17c..17a2c9e4 100644 --- a/lib/packages/service/dfx/dfx_support_service.dart +++ b/lib/packages/service/dfx/dfx_support_service.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:realunit_wallet/packages/config/api_config.dart'; import 'package:realunit_wallet/packages/service/dfx/dfx_auth_service.dart'; +import 'package:realunit_wallet/packages/service/dfx/exceptions/api_exception.dart'; import 'package:realunit_wallet/packages/service/dfx/models/support/dto/support_issue_dto.dart'; import 'package:realunit_wallet/packages/service/dfx/models/support/support_issue_reason.dart'; import 'package:realunit_wallet/packages/service/dfx/models/support/support_issue_type.dart'; @@ -29,12 +30,13 @@ class DfxSupportService extends DFXAuthService { }, ); - if (response.statusCode == 200) { - final List jsonList = jsonDecode(response.body) as List; - return jsonList.map((e) => SupportIssueDto.fromJson(e as Map)).toList(); - } else { - throw Exception('Failed to load tickets: ${response.statusCode}'); + if (response.statusCode != 200) { + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } + + final List jsonList = jsonDecode(response.body) as List; + return jsonList.map((e) => SupportIssueDto.fromJson(e as Map)).toList(); } Future getTicket(String uid) async { @@ -49,13 +51,14 @@ class DfxSupportService extends DFXAuthService { }, ); - if (response.statusCode == 200) { - return SupportIssueDto.fromJson( - jsonDecode(response.body) as Map, - ); - } else { - throw Exception('Failed to load ticket: ${response.statusCode}'); + if (response.statusCode != 200) { + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } + + return SupportIssueDto.fromJson( + jsonDecode(response.body) as Map, + ); } Future createTicket({ @@ -83,13 +86,14 @@ class DfxSupportService extends DFXAuthService { body: body, ); - if (response.statusCode == 201) { - return SupportIssueDto.fromJson( - jsonDecode(response.body) as Map, - ); - } else { - throw Exception('Failed to create ticket: ${response.statusCode}'); + if (response.statusCode != 201) { + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } + + return SupportIssueDto.fromJson( + jsonDecode(response.body) as Map, + ); } Future sendMessage(String ticketUid, String message) async { @@ -108,7 +112,8 @@ class DfxSupportService extends DFXAuthService { ); if (response.statusCode != 201) { - throw Exception('Failed to send message: ${response.statusCode}'); + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } } } diff --git a/test/packages/service/dfx/exceptions/api_exception_test.dart b/test/packages/service/dfx/exceptions/api_exception_test.dart new file mode 100644 index 00000000..36bfa1cb --- /dev/null +++ b/test/packages/service/dfx/exceptions/api_exception_test.dart @@ -0,0 +1,79 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:realunit_wallet/packages/service/dfx/exceptions/api_exception.dart'; +import 'package:realunit_wallet/packages/service/dfx/exceptions/payment/buy_exceptions.dart'; + +void main() { + group('$ApiException', () { + group('fromJson', () { + test('uses statusCode from JSON body when present', () { + final exception = ApiException.fromJson( + {'statusCode': 404, 'message': 'Not found'}, + httpStatusCode: 500, + ); + + expect(exception.statusCode, 404); + expect(exception.message, 'Not found'); + expect(exception.code, 'UNKNOWN'); + }); + + test('falls back to httpStatusCode when JSON body has no statusCode', () { + final exception = ApiException.fromJson( + {'code': 'TFA_REQUIRED', 'message': '2FA required (strict)', 'level': 'strict'}, + httpStatusCode: 403, + ); + + expect(exception.statusCode, 403); + expect(exception.code, 'TFA_REQUIRED'); + expect(exception.message, '2FA required (strict)'); + }); + + test('statusCode is null when neither JSON body nor httpStatusCode provide it', () { + final exception = ApiException.fromJson( + {'code': 'SOME_ERROR', 'message': 'Something went wrong'}, + ); + + expect(exception.statusCode, isNull); + expect(exception.code, 'SOME_ERROR'); + }); + + test('handles message as List', () { + final exception = ApiException.fromJson( + {'message': ['error1', 'error2']}, + httpStatusCode: 400, + ); + + expect(exception.message, 'error1, error2'); + expect(exception.statusCode, 400); + }); + + test('creates KycLevelRequiredException with httpStatusCode', () { + final exception = ApiException.fromJson( + { + 'code': 'KYC_LEVEL_REQUIRED', + 'message': 'KYC level too low', + 'requiredLevel': 30, + 'currentLevel': 20, + }, + httpStatusCode: 403, + ); + + expect(exception, isA()); + expect(exception.statusCode, 403); + final kyc = exception as KycLevelRequiredException; + expect(kyc.requiredLevel, 30); + expect(kyc.currentLevel, 20); + }); + + test('creates RegistrationRequiredException with httpStatusCode', () { + final exception = ApiException.fromJson( + {'code': 'REGISTRATION_REQUIRED', 'message': 'Please register first'}, + httpStatusCode: 403, + ); + + expect(exception, isA()); + expect(exception.statusCode, 403); + expect(exception.message, 'Please register first'); + }); + }); + }); +} From 798fb5a21107de74ec97a6abe62c1ef558f2075c Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 8 May 2026 16:04:31 +0200 Subject: [PATCH 4/4] fix: migrate remaining authenticated error throws to ApiException Replace generic Exception with ApiException.fromJson() in: - real_unit_sell_payment_info_service (4 error paths) - real_unit_buy_payment_info_service (catch-all else branch) - real_unit_registration_service (completeRegistration) All authenticated DFX API error responses now consistently use ApiException with httpStatusCode fallback. --- .../dfx/real_unit_buy_payment_info_service.dart | 3 ++- .../service/dfx/real_unit_registration_service.dart | 7 ++----- .../dfx/real_unit_sell_payment_info_service.dart | 12 ++++++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/packages/service/dfx/real_unit_buy_payment_info_service.dart b/lib/packages/service/dfx/real_unit_buy_payment_info_service.dart index 78521bb4..a700fc07 100644 --- a/lib/packages/service/dfx/real_unit_buy_payment_info_service.dart +++ b/lib/packages/service/dfx/real_unit_buy_payment_info_service.dart @@ -55,7 +55,8 @@ class RealUnitBuyPaymentInfoService { final errorJson = jsonDecode(response.body) as Map; throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } else { - throw Exception('Unexpected status code: ${response.statusCode}'); + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } } diff --git a/lib/packages/service/dfx/real_unit_registration_service.dart b/lib/packages/service/dfx/real_unit_registration_service.dart index 11d9af5d..ce801b28 100644 --- a/lib/packages/service/dfx/real_unit_registration_service.dart +++ b/lib/packages/service/dfx/real_unit_registration_service.dart @@ -127,11 +127,8 @@ class RealUnitRegistrationService { ); if (response.statusCode != 201 && response.statusCode != 202) { - final messages = jsonDecode(response.body)['message'] is List - ? List.from(jsonDecode(response.body)['message']) - : [jsonDecode(response.body)['message']]; - - throw Exception(messages.join('\n')); + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final responseDto = RealUnitRegistrationResponseDto.fromJson(jsonDecode(response.body)); diff --git a/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart b/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart index ebb9f89f..d27a9809 100644 --- a/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart +++ b/lib/packages/service/dfx/real_unit_sell_payment_info_service.dart @@ -77,7 +77,8 @@ class RealUnitSellPaymentInfoService { final errorJson = jsonDecode(response.body) as Map; throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } else { - throw Exception('Unexpected status code: ${response.body}'); + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } } @@ -129,7 +130,8 @@ class RealUnitSellPaymentInfoService { }, ); if (response.statusCode != 200 && response.statusCode != 201) { - throw Exception('Failed to create unsigned transactions: ${response.body}'); + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final json = jsonDecode(response.body) as Map; return RealUnitUnsignedTransactionsRequestDto.fromJson(json); @@ -150,7 +152,8 @@ class RealUnitSellPaymentInfoService { body: jsonEncode(dto.toJson()), ); if (response.statusCode != 200 && response.statusCode != 201) { - throw Exception('Failed to broadcast transaction: ${response.statusCode} ${response.body}'); + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } final responseDto = BroadcastTransactionResponseDto.fromJson( jsonDecode(response.body) as Map, @@ -176,7 +179,8 @@ class RealUnitSellPaymentInfoService { ); if (response.statusCode != 200 && response.statusCode != 201) { - throw Exception('Failed to confirm payment: ${response.statusCode}'); + final errorJson = jsonDecode(response.body) as Map; + throw ApiException.fromJson(errorJson, httpStatusCode: response.statusCode); } }