refactor(api): 统一响应解析并修复下载错误文案空白
各 *Api 类此前各自实现 data 结构判断,download_api 在结构异常时抛出空 message,界面会显示空白提示。抽出 ResponseDecoders 提供对象、可空对象、 对象数组三种语义,七个文件改为复用;下载侧异常文案与其他接口对齐。
This commit is contained in:
1 parent
70be941755
commit
1be8ebf1f9
9 files changed
+188
-67
No files matched your search
@@ -4,6 +4,7 @@ import '../models/account_device.dart';
|
|||||||
import '../models/account_export.dart';
|
import '../models/account_export.dart';
|
||||||
import '../models/account_profile.dart';
|
import '../models/account_profile.dart';
|
||||||
import 'api_client.dart';
|
import 'api_client.dart';
|
||||||
|
import 'response_decoders.dart';
|
||||||
|
|
||||||
/// 个人中心接口:`/app/me*`。
|
/// 个人中心接口:`/app/me*`。
|
||||||
class AccountApi {
|
class AccountApi {
|
||||||
@@ -23,11 +24,9 @@ class AccountApi {
|
|||||||
|
|
||||||
Future<List<AccountDevice>> devices() async {
|
Future<List<AccountDevice>> devices() async {
|
||||||
final data = await _client.get('/app/me/devices');
|
final data = await _client.get('/app/me/devices');
|
||||||
if (data is! List) return const [];
|
return ResponseDecoders.asMapList(
|
||||||
return [
|
data,
|
||||||
for (final item in data)
|
).map(AccountDevice.fromJson).toList();
|
||||||
if (item is Map) AccountDevice.fromJson(item.cast<String, dynamic>()),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<AccountExport> export() async {
|
Future<AccountExport> export() async {
|
||||||
@@ -47,6 +46,7 @@ class AccountApi {
|
|||||||
await _client.post('/app/me/deactivate/cancel');
|
await _client.post('/app/me/deactivate/cancel');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 个人资料允许缺失:后端未返回对象时按空对象解析,页面用默认值兜底。
|
||||||
Map<String, dynamic> _map(Object? data) =>
|
Map<String, dynamic> _map(Object? data) =>
|
||||||
data is Map ? data.cast<String, dynamic>() : const {};
|
ResponseDecoders.tryMap(data) ?? const {};
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import '../models/page_result.dart';
|
|||||||
import '../models/playlist_detail.dart';
|
import '../models/playlist_detail.dart';
|
||||||
import '../models/search_result.dart';
|
import '../models/search_result.dart';
|
||||||
import 'api_client.dart';
|
import 'api_client.dart';
|
||||||
|
import 'response_decoders.dart';
|
||||||
|
|
||||||
/// 客户端目录接口,对应后端 `/app/**`。
|
/// 客户端目录接口,对应后端 `/app/**`。
|
||||||
class CatalogApi {
|
class CatalogApi {
|
||||||
@@ -14,11 +15,15 @@ class CatalogApi {
|
|||||||
final ApiClient _client;
|
final ApiClient _client;
|
||||||
|
|
||||||
Future<HomeFeed> homeFeed() async {
|
Future<HomeFeed> homeFeed() async {
|
||||||
return HomeFeed.fromJson(_asMap(await _client.get('/app/home/feed')));
|
return HomeFeed.fromJson(
|
||||||
|
ResponseDecoders.asMap(await _client.get('/app/home/feed')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<HomeFeed> discoverFeed() async {
|
Future<HomeFeed> discoverFeed() async {
|
||||||
return HomeFeed.fromJson(_asMap(await _client.get('/app/discover/feed')));
|
return HomeFeed.fromJson(
|
||||||
|
ResponseDecoders.asMap(await _client.get('/app/discover/feed')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 推荐流;types/categoryId 为空表示不过滤,cursor 为上一页末条。
|
/// 推荐流;types/categoryId 为空表示不过滤,cursor 为上一页末条。
|
||||||
@@ -29,7 +34,7 @@ class CatalogApi {
|
|||||||
int size = 10,
|
int size = 10,
|
||||||
}) async {
|
}) async {
|
||||||
return FeedPage.fromJson(
|
return FeedPage.fromJson(
|
||||||
_asMap(
|
ResponseDecoders.asMap(
|
||||||
await _client.get(
|
await _client.get(
|
||||||
'/app/feed',
|
'/app/feed',
|
||||||
query: {
|
query: {
|
||||||
@@ -58,7 +63,7 @@ class CatalogApi {
|
|||||||
'pageSize': '$pageSize',
|
'pageSize': '$pageSize',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return SearchResult.fromJson(_asMap(data));
|
return SearchResult.fromJson(ResponseDecoders.asMap(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
static const int playlistPageSize = 10;
|
static const int playlistPageSize = 10;
|
||||||
@@ -86,7 +91,7 @@ class CatalogApi {
|
|||||||
int pageSize = playlistDramaPageSize,
|
int pageSize = playlistDramaPageSize,
|
||||||
}) async {
|
}) async {
|
||||||
return PlaylistDetail.fromJson(
|
return PlaylistDetail.fromJson(
|
||||||
_asMap(
|
ResponseDecoders.asMap(
|
||||||
await _client.get(
|
await _client.get(
|
||||||
'/app/playlists/$id',
|
'/app/playlists/$id',
|
||||||
query: {
|
query: {
|
||||||
@@ -138,20 +143,13 @@ class CatalogApi {
|
|||||||
|
|
||||||
Future<Drama> dramaDetail(String dramaId) async {
|
Future<Drama> dramaDetail(String dramaId) async {
|
||||||
return Drama.fromDetailJson(
|
return Drama.fromDetailJson(
|
||||||
_asMap(await _client.get('/app/dramas/$dramaId')),
|
ResponseDecoders.asMap(await _client.get('/app/dramas/$dramaId')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ContentCategory> _categories(Object? data) {
|
List<ContentCategory> _categories(Object? data) {
|
||||||
if (data is! List) return const [];
|
return ResponseDecoders.asMapList(
|
||||||
return [
|
data,
|
||||||
for (final item in data)
|
).map(ContentCategory.fromJson).toList();
|
||||||
if (item is Map<String, dynamic>) ContentCategory.fromJson(item),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> _asMap(Object? data) {
|
|
||||||
if (data is Map<String, dynamic>) return data;
|
|
||||||
throw const ApiException(-1, '返回数据格式异常');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import '../models/content_category.dart';
|
|||||||
import '../models/drama.dart';
|
import '../models/drama.dart';
|
||||||
import '../models/page_result.dart';
|
import '../models/page_result.dart';
|
||||||
import 'api_client.dart';
|
import 'api_client.dart';
|
||||||
|
import 'response_decoders.dart';
|
||||||
|
|
||||||
/// 内容分类接口,对应契约 `GET /app/categories`。
|
/// 内容分类接口,对应契约 `GET /app/categories`。
|
||||||
class CategoryApi {
|
class CategoryApi {
|
||||||
@@ -16,11 +17,9 @@ class CategoryApi {
|
|||||||
/// 拉取已启用分类,id 全程按字符串解析。
|
/// 拉取已启用分类,id 全程按字符串解析。
|
||||||
Future<List<ContentCategory>> list() async {
|
Future<List<ContentCategory>> list() async {
|
||||||
final data = await _client.get('/app/categories');
|
final data = await _client.get('/app/categories');
|
||||||
if (data is! List) return const [];
|
return ResponseDecoders.asMapList(
|
||||||
return [
|
data,
|
||||||
for (final item in data)
|
).map(ContentCategory.fromJson).toList();
|
||||||
if (item is Map<String, dynamic>) ContentCategory.fromJson(item),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 分类下已上线短剧,pageIndex 从 1 开始。
|
/// 分类下已上线短剧,pageIndex 从 1 开始。
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import '../models/order.dart';
|
|||||||
import '../models/product.dart';
|
import '../models/product.dart';
|
||||||
import '../models/wallet.dart';
|
import '../models/wallet.dart';
|
||||||
import 'api_client.dart';
|
import 'api_client.dart';
|
||||||
|
import 'response_decoders.dart';
|
||||||
|
|
||||||
/// 金币与解锁接口,对应契约 §1.8。
|
/// 金币与解锁接口,对应契约 §1.8。
|
||||||
class CommerceApi {
|
class CommerceApi {
|
||||||
@@ -13,8 +14,7 @@ class CommerceApi {
|
|||||||
|
|
||||||
Future<WalletSnapshot> wallet() async {
|
Future<WalletSnapshot> wallet() async {
|
||||||
final data = await _client.get('/app/wallet');
|
final data = await _client.get('/app/wallet');
|
||||||
if (data is Map<String, dynamic>) return WalletSnapshot.fromJson(data);
|
return WalletSnapshot.fromJson(ResponseDecoders.asMap(data));
|
||||||
throw const ApiException(-1, '返回数据格式异常');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 上架商品列表;`type` 传 `COIN_PACK` 或 `VIP`。
|
/// 上架商品列表;`type` 传 `COIN_PACK` 或 `VIP`。
|
||||||
@@ -23,13 +23,9 @@ class CommerceApi {
|
|||||||
'/app/products',
|
'/app/products',
|
||||||
query: type == null ? null : {'type': type},
|
query: type == null ? null : {'type': type},
|
||||||
);
|
);
|
||||||
if (data is List) {
|
return ResponseDecoders.requiredMapList(
|
||||||
return data
|
data,
|
||||||
.whereType<Map<String, dynamic>>()
|
).map(AppProduct.fromJson).toList();
|
||||||
.map(AppProduct.fromJson)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
throw const ApiException(-1, '返回数据格式异常');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 创建充值订单,返回 `CREATED` 状态订单。
|
/// 创建充值订单,返回 `CREATED` 状态订单。
|
||||||
@@ -38,13 +34,11 @@ class CommerceApi {
|
|||||||
'/app/orders',
|
'/app/orders',
|
||||||
body: {'productId': productId},
|
body: {'productId': productId},
|
||||||
);
|
);
|
||||||
if (data is Map<String, dynamic>) return AppOrder.fromJson(data);
|
return AppOrder.fromJson(ResponseDecoders.asMap(data));
|
||||||
throw const ApiException(-1, '返回数据格式异常');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UnlockResult> unlock(String episodeId) async {
|
Future<UnlockResult> unlock(String episodeId) async {
|
||||||
final data = await _client.post('/app/episodes/$episodeId/unlock');
|
final data = await _client.post('/app/episodes/$episodeId/unlock');
|
||||||
if (data is Map<String, dynamic>) return UnlockResult.fromJson(data);
|
return UnlockResult.fromJson(ResponseDecoders.asMap(data));
|
||||||
throw const ApiException(-1, '返回数据格式异常');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import '../models/download_license.dart';
|
import '../models/download_license.dart';
|
||||||
import '../models/download_policy.dart';
|
import '../models/download_policy.dart';
|
||||||
import 'api_client.dart';
|
import 'api_client.dart';
|
||||||
|
import 'response_decoders.dart';
|
||||||
|
|
||||||
/// 离线下载授权接口,对应契约 §1.6 的全部 8 个接口。
|
/// 离线下载授权接口,对应契约 §1.6 的全部 8 个接口。
|
||||||
///
|
///
|
||||||
@@ -15,7 +16,7 @@ class DownloadApi {
|
|||||||
/// 拉取配额和偏好策略。
|
/// 拉取配额和偏好策略。
|
||||||
Future<DownloadPolicy> policy() async {
|
Future<DownloadPolicy> policy() async {
|
||||||
return DownloadPolicy.fromJson(
|
return DownloadPolicy.fromJson(
|
||||||
_asMap(await _client.get('/app/downloads/policy')),
|
ResponseDecoders.asMap(await _client.get('/app/downloads/policy')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ class DownloadApi {
|
|||||||
/// 申请下载授权;同集重复申请由服务端保证幂等。
|
/// 申请下载授权;同集重复申请由服务端保证幂等。
|
||||||
Future<DownloadLicense> grant(String episodeId, {String? quality}) async {
|
Future<DownloadLicense> grant(String episodeId, {String? quality}) async {
|
||||||
return DownloadLicense.fromJson(
|
return DownloadLicense.fromJson(
|
||||||
_asMap(
|
ResponseDecoders.asMap(
|
||||||
await _client.post(
|
await _client.post(
|
||||||
'/app/downloads',
|
'/app/downloads',
|
||||||
body: {'episodeId': episodeId, 'quality': ?quality},
|
body: {'episodeId': episodeId, 'quality': ?quality},
|
||||||
@@ -42,17 +43,17 @@ class DownloadApi {
|
|||||||
/// 拉取当前设备的授权列表。
|
/// 拉取当前设备的授权列表。
|
||||||
Future<List<DownloadLicense>> list() async {
|
Future<List<DownloadLicense>> list() async {
|
||||||
final data = await _client.get('/app/downloads');
|
final data = await _client.get('/app/downloads');
|
||||||
if (data is! List) return const [];
|
return ResponseDecoders.asMapList(
|
||||||
return [
|
data,
|
||||||
for (final item in data)
|
).map(DownloadLicense.fromJson).toList();
|
||||||
if (item is Map<String, dynamic>) DownloadLicense.fromJson(item),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 续传前刷新短时效下载地址。
|
/// 续传前刷新短时效下载地址。
|
||||||
Future<DownloadLicense> refreshUrl(String episodeId) async {
|
Future<DownloadLicense> refreshUrl(String episodeId) async {
|
||||||
return DownloadLicense.fromJson(
|
return DownloadLicense.fromJson(
|
||||||
_asMap(await _client.get('/app/downloads/$episodeId/url')),
|
ResponseDecoders.asMap(
|
||||||
|
await _client.get('/app/downloads/$episodeId/url'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ class DownloadApi {
|
|||||||
String? errorMsg,
|
String? errorMsg,
|
||||||
}) async {
|
}) async {
|
||||||
return DownloadLicense.fromJson(
|
return DownloadLicense.fromJson(
|
||||||
_asMap(
|
ResponseDecoders.asMap(
|
||||||
await _client.post(
|
await _client.post(
|
||||||
'/app/downloads/$episodeId/progress',
|
'/app/downloads/$episodeId/progress',
|
||||||
body: {
|
body: {
|
||||||
@@ -93,9 +94,4 @@ class DownloadApi {
|
|||||||
final data = await _client.delete('/app/downloads');
|
final data = await _client.delete('/app/downloads');
|
||||||
return data is int ? data : 0;
|
return data is int ? data : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _asMap(Object? data) {
|
|
||||||
if (data is Map<String, dynamic>) return data;
|
|
||||||
throw const ApiException(-1, '');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import '../models/episode_stats.dart';
|
import '../models/episode_stats.dart';
|
||||||
import '../models/play_ticket.dart';
|
import '../models/play_ticket.dart';
|
||||||
import 'api_client.dart';
|
import 'api_client.dart';
|
||||||
|
import 'response_decoders.dart';
|
||||||
|
|
||||||
/// 播放票据接口:`GET /app/episodes/{id}/play`(契约 §1.4)。
|
/// 播放票据接口:`GET /app/episodes/{id}/play`(契约 §1.4)。
|
||||||
///
|
///
|
||||||
@@ -15,14 +16,12 @@ class PlaybackApi {
|
|||||||
|
|
||||||
Future<PlayTicket> ticket(String episodeId) async {
|
Future<PlayTicket> ticket(String episodeId) async {
|
||||||
final data = await _client.get('/app/episodes/$episodeId/play');
|
final data = await _client.get('/app/episodes/$episodeId/play');
|
||||||
if (data is Map<String, dynamic>) return PlayTicket.fromJson(data);
|
return PlayTicket.fromJson(ResponseDecoders.asMap(data));
|
||||||
throw const ApiException(-1, '返回数据格式异常');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 轻量刷新互动计数:不签发地址,也不占用播放票据缓存。
|
/// 轻量刷新互动计数:不签发地址,也不占用播放票据缓存。
|
||||||
Future<EpisodeStats> episodeStats(String episodeId) async {
|
Future<EpisodeStats> episodeStats(String episodeId) async {
|
||||||
final data = await _client.get('/app/episodes/$episodeId/stats');
|
final data = await _client.get('/app/episodes/$episodeId/stats');
|
||||||
if (data is Map<String, dynamic>) return EpisodeStats.fromJson(data);
|
return EpisodeStats.fromJson(ResponseDecoders.asMap(data));
|
||||||
throw const ApiException(-1, '返回数据格式异常');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'api_exception.dart';
|
||||||
|
|
||||||
|
/// 接口响应 `data` 字段的统一解析。
|
||||||
|
///
|
||||||
|
/// 此前每个 `*_api.dart` 各写一份 `is Map` 判断与错误文案,还出现过抛空
|
||||||
|
/// message 导致界面显示空白的情况;统一收口到这里,语义只有三种:
|
||||||
|
/// 必须为对象、可为空对象、对象数组。
|
||||||
|
abstract final class ResponseDecoders {
|
||||||
|
/// 业务失败码:HTTP 成功但 `data` 结构不符合约定。
|
||||||
|
static const int malformedCode = -1;
|
||||||
|
|
||||||
|
/// `data` 结构异常时的提示,可直接展示给用户。
|
||||||
|
static const String malformedMessage = '返回数据格式异常';
|
||||||
|
|
||||||
|
/// 要求 `data` 是对象,否则抛 [ApiException]。
|
||||||
|
static Map<String, dynamic> asMap(Object? data) {
|
||||||
|
final map = tryMap(data);
|
||||||
|
if (map == null) {
|
||||||
|
throw const ApiException(malformedCode, malformedMessage);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 允许缺失:`data` 为空或不是对象时返回 null。
|
||||||
|
static Map<String, dynamic>? tryMap(Object? data) {
|
||||||
|
if (data is Map<String, dynamic>) return data;
|
||||||
|
if (data is Map) return data.cast<String, dynamic>();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 对象数组;不是数组时按空列表处理,避免列表页因结构异常白屏。
|
||||||
|
static List<Map<String, dynamic>> asMapList(Object? data) {
|
||||||
|
if (data is! List) return const [];
|
||||||
|
return data
|
||||||
|
.whereType<Map>()
|
||||||
|
.map((item) => item.cast<String, dynamic>())
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 对象数组;不是数组时抛 [ApiException],用于不能静默为空的数据(如商品)。
|
||||||
|
static List<Map<String, dynamic>> requiredMapList(Object? data) {
|
||||||
|
if (data is! List) {
|
||||||
|
throw const ApiException(malformedCode, malformedMessage);
|
||||||
|
}
|
||||||
|
return asMapList(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import '../models/page_result.dart';
|
import '../models/page_result.dart';
|
||||||
import '../models/watch_progress.dart';
|
import '../models/watch_progress.dart';
|
||||||
import 'api_client.dart';
|
import 'api_client.dart';
|
||||||
|
import 'response_decoders.dart';
|
||||||
|
|
||||||
/// 观看进度接口:`/app/watch/*`(契约 §1.5)。支持账号或设备身份。
|
/// 观看进度接口:`/app/watch/*`(契约 §1.5)。支持账号或设备身份。
|
||||||
class WatchApi {
|
class WatchApi {
|
||||||
@@ -28,7 +29,8 @@ class WatchApi {
|
|||||||
'source': source,
|
'source': source,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return data is Map<String, dynamic> ? WatchProgress.fromJson(data) : null;
|
final map = ResponseDecoders.tryMap(data);
|
||||||
|
return map == null ? null : WatchProgress.fromJson(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 某部剧的续看位置;没有记录时后端返回空 data。
|
/// 某部剧的续看位置;没有记录时后端返回空 data。
|
||||||
@@ -37,7 +39,8 @@ class WatchApi {
|
|||||||
'/app/watch/progress',
|
'/app/watch/progress',
|
||||||
query: {'dramaId': dramaId},
|
query: {'dramaId': dramaId},
|
||||||
);
|
);
|
||||||
return data is Map<String, dynamic> ? WatchProgress.fromJson(data) : null;
|
final map = ResponseDecoders.tryMap(data);
|
||||||
|
return map == null ? null : WatchProgress.fromJson(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 最近观看分页:`pageIndex`/`pageSize`,解包 `PageResult`。
|
/// 最近观看分页:`pageIndex`/`pageSize`,解包 `PageResult`。
|
||||||
@@ -49,13 +52,9 @@ class WatchApi {
|
|||||||
'/app/watch/recent',
|
'/app/watch/recent',
|
||||||
query: {'pageIndex': '$pageIndex', 'pageSize': '$pageSize'},
|
query: {'pageIndex': '$pageIndex', 'pageSize': '$pageSize'},
|
||||||
);
|
);
|
||||||
if (data is! Map) {
|
final map = ResponseDecoders.tryMap(data);
|
||||||
return const PageResult();
|
if (map == null) return const PageResult();
|
||||||
}
|
return PageResult.fromJson(map, WatchProgress.fromJson);
|
||||||
return PageResult.fromJson(
|
|
||||||
Map<String, dynamic>.from(data),
|
|
||||||
WatchProgress.fromJson,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 删除某部剧的观看记录(记录页清单条用)。
|
/// 删除某部剧的观看记录(记录页清单条用)。
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:fruitshorts/core/data/remote/api_client.dart';
|
||||||
|
import 'package:fruitshorts/core/data/remote/download_api.dart';
|
||||||
|
import 'package:fruitshorts/core/data/remote/response_decoders.dart';
|
||||||
|
|
||||||
|
import '../support/fake_http_adapter.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('ResponseDecoders', () {
|
||||||
|
test('should_return_same_map_when_data_is_object', () {
|
||||||
|
const source = {'id': '1'};
|
||||||
|
expect(ResponseDecoders.asMap(source), same(source));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should_throw_with_readable_message_when_data_is_not_object', () {
|
||||||
|
expect(
|
||||||
|
() => ResponseDecoders.asMap(const []),
|
||||||
|
throwsA(
|
||||||
|
isA<ApiException>()
|
||||||
|
.having((e) => e.code, 'code', ResponseDecoders.malformedCode)
|
||||||
|
.having(
|
||||||
|
(e) => e.message,
|
||||||
|
'message',
|
||||||
|
ResponseDecoders.malformedMessage,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(ResponseDecoders.malformedMessage, isNotEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should_return_null_when_try_map_receives_non_object', () {
|
||||||
|
expect(ResponseDecoders.tryMap(null), isNull);
|
||||||
|
expect(ResponseDecoders.tryMap('text'), isNull);
|
||||||
|
expect(ResponseDecoders.tryMap({'a': 1})?['a'], 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should_skip_non_object_items_when_decoding_object_list', () {
|
||||||
|
final decoded = ResponseDecoders.asMapList([
|
||||||
|
{'id': '1'},
|
||||||
|
'bad',
|
||||||
|
{'id': '2'},
|
||||||
|
]);
|
||||||
|
expect(decoded.map((item) => item['id']), ['1', '2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should_return_empty_list_when_data_is_not_list', () {
|
||||||
|
expect(ResponseDecoders.asMapList(const {}), isEmpty);
|
||||||
|
expect(ResponseDecoders.asMapList(null), isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should_throw_when_required_list_receives_non_list', () {
|
||||||
|
expect(
|
||||||
|
() => ResponseDecoders.requiredMapList(const {}),
|
||||||
|
throwsA(isA<ApiException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'should_raise_readable_error_when_download_policy_data_is_malformed',
|
||||||
|
() async {
|
||||||
|
final adapter = FakeHttpAdapter(
|
||||||
|
(options) async => FakeHttpResponse(
|
||||||
|
jsonEncode({'code': 0, 'message': 'ok', 'data': null}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final client = ApiClient(adapter: adapter);
|
||||||
|
final api = DownloadApi(client);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
api.policy(),
|
||||||
|
throwsA(
|
||||||
|
isA<ApiException>().having(
|
||||||
|
(error) => error.message,
|
||||||
|
'message',
|
||||||
|
ResponseDecoders.malformedMessage,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
client.close();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user