- pet_models.dart:pets 域 DTO 逐字段对齐冻结契约(宠物/品种/体重/ 疫苗目录/疫苗/健康事件/照护提醒/聚合摘要 + cursor 分页信封), 枚举严格解析,请求体部分更新语义(缺席字段不出现) - pets_repository.dart:PetsRepository 抽象 + ApiPetsRepository, 12 路径 18 操作全覆盖;四个 POST 携带 Idempotency-Key; 复用既有 ApiClient token 拦截与单飞刷新重放 - pet_exceptions.dart:新 8 错误码映射为类型化异常 (40300/40401/40402/40902/40903/40904/42201/42202), 仍可按 ApiBusinessException 基类捕获 - pets_controller.dart:宠物档案状态自 AppState 拆出 (Controller → Repository → API Client 分层,四态就绪供 T2-12) - money.dart:元/分换算工具(DTO 层保持整数分,UI 层 T2-14 使用) - api_client.dart:新增 patbondPetApiBaseUrl(:8083,--dart-define 可覆盖)与 query 参数支持;api_exception.dart 补 pets 域错误码 - 测试 64 → 126:DTO 映射、错误类型化映射、请求线路 (路径/方法/鉴权/分页/幂等键/tz)、控制器状态机、金额换算 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:patbond_flutter/features/pets/money.dart';
|
||||
|
||||
void main() {
|
||||
group('formatCentsAsYuan', () {
|
||||
test('整元不带小数', () {
|
||||
expect(formatCentsAsYuan(0), '0');
|
||||
expect(formatCentsAsYuan(100), '1');
|
||||
expect(formatCentsAsYuan(12800), '128');
|
||||
});
|
||||
|
||||
test('非整元固定两位小数', () {
|
||||
expect(formatCentsAsYuan(12850), '128.50');
|
||||
expect(formatCentsAsYuan(12805), '128.05');
|
||||
expect(formatCentsAsYuan(5), '0.05');
|
||||
expect(formatCentsAsYuan(50), '0.50');
|
||||
});
|
||||
|
||||
test('负数入参抛 ArgumentError(契约金额非负)', () {
|
||||
expect(() => formatCentsAsYuan(-1), throwsArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
group('parseYuanToCents', () {
|
||||
test('整数与一/两位小数', () {
|
||||
expect(parseYuanToCents('128'), 12800);
|
||||
expect(parseYuanToCents('128.5'), 12850);
|
||||
expect(parseYuanToCents('128.50'), 12850);
|
||||
expect(parseYuanToCents('0.05'), 5);
|
||||
expect(parseYuanToCents('0'), 0);
|
||||
expect(parseYuanToCents(' 12.30 '), 1230);
|
||||
});
|
||||
|
||||
test('非法输入返回 null', () {
|
||||
expect(parseYuanToCents(''), isNull);
|
||||
expect(parseYuanToCents('abc'), isNull);
|
||||
expect(parseYuanToCents('12.345'), isNull);
|
||||
expect(parseYuanToCents('-5'), isNull);
|
||||
expect(parseYuanToCents('12.'), isNull);
|
||||
expect(parseYuanToCents('.5'), isNull);
|
||||
expect(parseYuanToCents('1,200'), isNull);
|
||||
});
|
||||
|
||||
test('往返一致:format(parse(x)) 保持数值', () {
|
||||
for (final input in ['128', '128.50', '0.05', '99.90']) {
|
||||
final cents = parseYuanToCents(input)!;
|
||||
expect(parseYuanToCents(formatCentsAsYuan(cents)), cents);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,537 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:patbond_flutter/features/pets/pet_models.dart';
|
||||
|
||||
/// 契约样本(openapi.yaml v1.2.0 各 schema 的全字段实例)。
|
||||
Map<String, dynamic> petJson({Map<String, Object?> overrides = const {}}) => {
|
||||
'id': '019212aa-0000-7000-8000-00000000p001',
|
||||
'name': '豆豆',
|
||||
'species': 'dog',
|
||||
'breedId': '019212aa-0000-7000-8000-00000000b001',
|
||||
'breedDisplayName': '柴犬',
|
||||
'customBreedName': null,
|
||||
'sex': 'male',
|
||||
'birthDate': '2024-03-15',
|
||||
'birthDateEstimated': true,
|
||||
'personality': '粘人',
|
||||
'microchipNo': '900123456789012',
|
||||
'sterilizedOn': '2025-06-01',
|
||||
'status': 'active',
|
||||
'myRole': 'owner',
|
||||
'createdAt': '2026-09-01T10:00:00+08:00',
|
||||
'updatedAt': '2026-09-02T10:00:00+08:00',
|
||||
'version': 3,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
void main() {
|
||||
group('Pet', () {
|
||||
test('全字段映射', () {
|
||||
final pet = Pet.fromJson(petJson());
|
||||
|
||||
expect(pet.id, '019212aa-0000-7000-8000-00000000p001');
|
||||
expect(pet.name, '豆豆');
|
||||
expect(pet.species, PetSpecies.dog);
|
||||
expect(pet.breedId, '019212aa-0000-7000-8000-00000000b001');
|
||||
expect(pet.breedDisplayName, '柴犬');
|
||||
expect(pet.customBreedName, isNull);
|
||||
expect(pet.sex, PetSex.male);
|
||||
expect(pet.birthDate, DateTime.parse('2024-03-15'));
|
||||
expect(pet.birthDateEstimated, isTrue);
|
||||
expect(pet.personality, '粘人');
|
||||
expect(pet.microchipNo, '900123456789012');
|
||||
expect(pet.sterilizedOn, DateTime.parse('2025-06-01'));
|
||||
expect(pet.status, PetStatus.active);
|
||||
expect(pet.myRole, PetRole.owner);
|
||||
expect(pet.version, 3);
|
||||
});
|
||||
|
||||
test('可空字段缺席 / null 均映射为 null(自定义品种侧)', () {
|
||||
final pet = Pet.fromJson(
|
||||
petJson(
|
||||
overrides: {
|
||||
'breedId': null,
|
||||
'breedDisplayName': null,
|
||||
'customBreedName': '串串',
|
||||
'birthDate': null,
|
||||
'personality': null,
|
||||
'microchipNo': null,
|
||||
'sterilizedOn': null,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(pet.breedId, isNull);
|
||||
expect(pet.breedDisplayName, isNull);
|
||||
expect(pet.customBreedName, '串串');
|
||||
expect(pet.birthDate, isNull);
|
||||
expect(pet.personality, isNull);
|
||||
expect(pet.microchipNo, isNull);
|
||||
expect(pet.sterilizedOn, isNull);
|
||||
});
|
||||
|
||||
test('未知枚举取值抛 FormatException(契约漂移显式暴露)', () {
|
||||
expect(
|
||||
() => Pet.fromJson(petJson(overrides: {'status': 'deleted'})),
|
||||
throwsFormatException,
|
||||
);
|
||||
expect(
|
||||
() => Pet.fromJson(petJson(overrides: {'myRole': 'admin'})),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('CreatePetRequest / UpdatePetRequest', () {
|
||||
test('创建请求:必填 + 可选字段序列化,日期为 YYYY-MM-DD', () {
|
||||
final json = CreatePetRequest(
|
||||
name: '豆豆',
|
||||
species: PetSpecies.dog,
|
||||
sex: PetSex.male,
|
||||
breedId: 'b-1',
|
||||
birthDate: DateTime(2024, 3, 5),
|
||||
birthDateEstimated: false,
|
||||
microchipNo: '900123456789012',
|
||||
sterilizedOn: DateTime(2025, 6, 1),
|
||||
).toJson();
|
||||
|
||||
expect(json, {
|
||||
'name': '豆豆',
|
||||
'species': 'dog',
|
||||
'sex': 'male',
|
||||
'breedId': 'b-1',
|
||||
'birthDate': '2024-03-05',
|
||||
'birthDateEstimated': false,
|
||||
'microchipNo': '900123456789012',
|
||||
'sterilizedOn': '2025-06-01',
|
||||
});
|
||||
});
|
||||
|
||||
test('创建请求:可选字段缺席不出现在请求体(部分语义)', () {
|
||||
final json = const CreatePetRequest(
|
||||
name: '咪咪',
|
||||
species: PetSpecies.cat,
|
||||
sex: PetSex.female,
|
||||
customBreedName: '狸花',
|
||||
).toJson();
|
||||
|
||||
expect(json, {
|
||||
'name': '咪咪',
|
||||
'species': 'cat',
|
||||
'sex': 'female',
|
||||
'customBreedName': '狸花',
|
||||
});
|
||||
});
|
||||
|
||||
test('更新请求:version 必带,缺席字段不出现(不支持清空回 null)', () {
|
||||
final json = const UpdatePetRequest(
|
||||
version: 3,
|
||||
name: '豆豆二世',
|
||||
status: PetStatus.lost,
|
||||
).toJson();
|
||||
|
||||
expect(json, {'version': 3, 'name': '豆豆二世', 'status': 'lost'});
|
||||
expect(json.containsKey('species'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('Breed / VaccineCatalogItem', () {
|
||||
test('品种映射', () {
|
||||
final breed = Breed.fromJson({
|
||||
'id': 'b-1',
|
||||
'species': 'cat',
|
||||
'code': 'ragdoll',
|
||||
'displayName': '布偶猫',
|
||||
});
|
||||
|
||||
expect(breed.id, 'b-1');
|
||||
expect(breed.species, PetSpecies.cat);
|
||||
expect(breed.code, 'ragdoll');
|
||||
expect(breed.displayName, '布偶猫');
|
||||
});
|
||||
|
||||
test('疫苗目录映射(description 可空)', () {
|
||||
final item = VaccineCatalogItem.fromJson({
|
||||
'id': 'v-1',
|
||||
'code': 'rabies',
|
||||
'name': '狂犬疫苗',
|
||||
'species': 'dog',
|
||||
'description': null,
|
||||
});
|
||||
|
||||
expect(item.name, '狂犬疫苗');
|
||||
expect(item.description, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('WeightRecord', () {
|
||||
test('全字段映射;weightKg 接受 int 与 double 下发', () {
|
||||
final record = WeightRecord.fromJson({
|
||||
'id': 'w-1',
|
||||
'petId': 'p-1',
|
||||
'weightKg': 12,
|
||||
'measuredAt': '2026-09-07T09:00:00+08:00',
|
||||
'source': 'clinic',
|
||||
'note': '年检称重',
|
||||
'createdAt': '2026-09-07T09:01:00+08:00',
|
||||
});
|
||||
|
||||
expect(record.weightKg, 12.0);
|
||||
expect(record.source, WeightSource.clinic);
|
||||
expect(record.note, '年检称重');
|
||||
|
||||
final decimal = WeightRecord.fromJson({
|
||||
'id': 'w-2',
|
||||
'petId': 'p-1',
|
||||
'weightKg': 4.35,
|
||||
'measuredAt': '2026-09-07T09:00:00Z',
|
||||
'source': 'manual',
|
||||
'note': null,
|
||||
'createdAt': '2026-09-07T09:01:00Z',
|
||||
});
|
||||
expect(decimal.weightKg, 4.35);
|
||||
expect(decimal.note, isNull);
|
||||
});
|
||||
|
||||
test('创建请求序列化(source/note 可选缺席)', () {
|
||||
final json = CreateWeightRequest(
|
||||
weightKg: 4.35,
|
||||
measuredAt: DateTime.utc(2026, 9, 7, 1, 0),
|
||||
).toJson();
|
||||
|
||||
expect(json['weightKg'], 4.35);
|
||||
expect(json['measuredAt'], '2026-09-07T01:00:00.000Z');
|
||||
expect(json.containsKey('source'), isFalse);
|
||||
expect(json.containsKey('note'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('Vaccination', () {
|
||||
Map<String, dynamic> vaccinationJson({
|
||||
Map<String, Object?> overrides = const {},
|
||||
}) => {
|
||||
'id': 'vx-1',
|
||||
'petId': 'p-1',
|
||||
'vaccineId': 'v-1',
|
||||
'vaccineName': '狂犬疫苗',
|
||||
'seriesKey': 'rabies-initial',
|
||||
'doseNo': 2,
|
||||
'doseLabel': '第二针',
|
||||
'status': 'completed',
|
||||
'plannedOn': '2026-08-01',
|
||||
'administeredOn': '2026-08-03',
|
||||
'nextDueOn': '2027-08-03',
|
||||
'manufacturer': '硕腾',
|
||||
'batchNo': 'B20260801',
|
||||
'notes': '无不良反应',
|
||||
'createdAt': '2026-08-03T10:00:00+08:00',
|
||||
'updatedAt': '2026-08-03T10:00:00+08:00',
|
||||
'version': 1,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
test('全字段映射', () {
|
||||
final record = Vaccination.fromJson(vaccinationJson());
|
||||
|
||||
expect(record.vaccineName, '狂犬疫苗');
|
||||
expect(record.seriesKey, 'rabies-initial');
|
||||
expect(record.doseNo, 2);
|
||||
expect(record.status, VaccinationStatus.completed);
|
||||
expect(record.administeredOn, DateTime.parse('2026-08-03'));
|
||||
expect(record.nextDueOn, DateTime.parse('2027-08-03'));
|
||||
expect(record.version, 1);
|
||||
});
|
||||
|
||||
test('scheduled 态:日期可空字段为 null', () {
|
||||
final record = Vaccination.fromJson(
|
||||
vaccinationJson(
|
||||
overrides: {
|
||||
'status': 'scheduled',
|
||||
'administeredOn': null,
|
||||
'nextDueOn': null,
|
||||
'doseLabel': null,
|
||||
'manufacturer': null,
|
||||
'batchNo': null,
|
||||
'notes': null,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(record.status, VaccinationStatus.scheduled);
|
||||
expect(record.administeredOn, isNull);
|
||||
expect(record.nextDueOn, isNull);
|
||||
expect(record.doseLabel, isNull);
|
||||
});
|
||||
|
||||
test('创建请求:completed 携带 administeredOn,日期 YYYY-MM-DD', () {
|
||||
final json = CreateVaccinationRequest(
|
||||
vaccineId: 'v-1',
|
||||
seriesKey: 'rabies-initial',
|
||||
doseNo: 1,
|
||||
status: VaccinationStatus.completed,
|
||||
administeredOn: DateTime(2026, 8, 3),
|
||||
nextDueOn: DateTime(2027, 8, 3),
|
||||
).toJson();
|
||||
|
||||
expect(json, {
|
||||
'vaccineId': 'v-1',
|
||||
'seriesKey': 'rabies-initial',
|
||||
'doseNo': 1,
|
||||
'status': 'completed',
|
||||
'administeredOn': '2026-08-03',
|
||||
'nextDueOn': '2027-08-03',
|
||||
});
|
||||
});
|
||||
|
||||
test('更新请求:version 必带,仅提交的字段出现', () {
|
||||
final json = const UpdateVaccinationRequest(
|
||||
version: 1,
|
||||
status: VaccinationStatus.cancelled,
|
||||
).toJson();
|
||||
|
||||
expect(json, {'version': 1, 'status': 'cancelled'});
|
||||
expect(json.containsKey('vaccineId'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('HealthEvent', () {
|
||||
test('全字段映射;amountCents 保持整数分不换算', () {
|
||||
final event = HealthEvent.fromJson({
|
||||
'id': 'e-1',
|
||||
'petId': 'p-1',
|
||||
'eventType': 'medical',
|
||||
'occurredAt': '2026-09-05T14:00:00+08:00',
|
||||
'title': '皮肤检查',
|
||||
'notes': '轻微湿疹',
|
||||
'amountCents': 12850,
|
||||
'createdByUserId': 'u-1',
|
||||
'createdAt': '2026-09-05T14:05:00+08:00',
|
||||
'updatedAt': '2026-09-05T14:05:00+08:00',
|
||||
'version': 1,
|
||||
});
|
||||
|
||||
expect(event.eventType, HealthEventType.medical);
|
||||
expect(event.title, '皮肤检查');
|
||||
expect(event.amountCents, 12850);
|
||||
expect(event.createdByUserId, 'u-1');
|
||||
});
|
||||
|
||||
test('amountCents / notes 可空', () {
|
||||
final event = HealthEvent.fromJson({
|
||||
'id': 'e-2',
|
||||
'petId': 'p-1',
|
||||
'eventType': 'note',
|
||||
'occurredAt': '2026-09-05T14:00:00Z',
|
||||
'title': '记录',
|
||||
'notes': null,
|
||||
'amountCents': null,
|
||||
'createdByUserId': 'u-1',
|
||||
'createdAt': '2026-09-05T14:05:00Z',
|
||||
'updatedAt': '2026-09-05T14:05:00Z',
|
||||
'version': 1,
|
||||
});
|
||||
|
||||
expect(event.amountCents, isNull);
|
||||
expect(event.notes, isNull);
|
||||
});
|
||||
|
||||
test('创建 / 更新请求序列化(amountCents 整数分)', () {
|
||||
final create = CreateHealthEventRequest(
|
||||
eventType: HealthEventType.deworming,
|
||||
occurredAt: DateTime.utc(2026, 9, 5, 6, 0),
|
||||
title: '体内驱虫',
|
||||
amountCents: 6800,
|
||||
).toJson();
|
||||
|
||||
expect(create, {
|
||||
'eventType': 'deworming',
|
||||
'occurredAt': '2026-09-05T06:00:00.000Z',
|
||||
'title': '体内驱虫',
|
||||
'amountCents': 6800,
|
||||
});
|
||||
|
||||
final update = const UpdateHealthEventRequest(
|
||||
version: 2,
|
||||
title: '体内外驱虫',
|
||||
amountCents: 9800,
|
||||
).toJson();
|
||||
|
||||
expect(update, {'version': 2, 'title': '体内外驱虫', 'amountCents': 9800});
|
||||
});
|
||||
});
|
||||
|
||||
group('CareReminder', () {
|
||||
test('全字段映射;completedAt 非空当且仅当 completed', () {
|
||||
final reminder = CareReminder.fromJson({
|
||||
'id': 'r-1',
|
||||
'petId': 'p-1',
|
||||
'reminderType': 'deworming',
|
||||
'title': '体外驱虫',
|
||||
'dueAt': '2026-09-15T00:00:00+08:00',
|
||||
'status': 'completed',
|
||||
'completedAt': '2026-09-14T20:00:00+08:00',
|
||||
'createdAt': '2026-09-01T10:00:00+08:00',
|
||||
'updatedAt': '2026-09-14T20:00:00+08:00',
|
||||
});
|
||||
|
||||
expect(reminder.reminderType, CareReminderType.deworming);
|
||||
expect(reminder.status, CareReminderStatus.completed);
|
||||
expect(reminder.completedAt, isNotNull);
|
||||
});
|
||||
|
||||
test('pending 态 completedAt 为 null', () {
|
||||
final reminder = CareReminder.fromJson({
|
||||
'id': 'r-2',
|
||||
'petId': 'p-1',
|
||||
'reminderType': 'checkup',
|
||||
'title': '年度体检',
|
||||
'dueAt': '2026-10-01T00:00:00Z',
|
||||
'status': 'pending',
|
||||
'completedAt': null,
|
||||
'createdAt': '2026-09-01T10:00:00Z',
|
||||
'updatedAt': '2026-09-01T10:00:00Z',
|
||||
});
|
||||
|
||||
expect(reminder.status, CareReminderStatus.pending);
|
||||
expect(reminder.completedAt, isNull);
|
||||
});
|
||||
|
||||
test('创建请求恒不含 status;流转请求 dismissed 不带 completedAt', () {
|
||||
final create = CreateCareReminderRequest(
|
||||
reminderType: CareReminderType.medication,
|
||||
title: '心丝虫药',
|
||||
dueAt: DateTime.utc(2026, 10, 1),
|
||||
).toJson();
|
||||
|
||||
expect(create, {
|
||||
'reminderType': 'medication',
|
||||
'title': '心丝虫药',
|
||||
'dueAt': '2026-10-01T00:00:00.000Z',
|
||||
});
|
||||
expect(create.containsKey('status'), isFalse);
|
||||
|
||||
final complete = UpdateCareReminderRequest(
|
||||
status: CareReminderStatus.completed,
|
||||
completedAt: DateTime.utc(2026, 9, 14, 12),
|
||||
).toJson();
|
||||
expect(complete, {
|
||||
'status': 'completed',
|
||||
'completedAt': '2026-09-14T12:00:00.000Z',
|
||||
});
|
||||
|
||||
final dismiss = const UpdateCareReminderRequest(
|
||||
status: CareReminderStatus.dismissed,
|
||||
).toJson();
|
||||
expect(dismiss, {'status': 'dismissed'});
|
||||
expect(dismiss.containsKey('completedAt'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('CursorPage', () {
|
||||
test('分页信封 {items, nextCursor, hasMore} 映射', () {
|
||||
final page = CursorPage.fromJson({
|
||||
'items': [
|
||||
{
|
||||
'id': 'w-1',
|
||||
'petId': 'p-1',
|
||||
'weightKg': 4.2,
|
||||
'measuredAt': '2026-09-07T09:00:00Z',
|
||||
'source': 'manual',
|
||||
'note': null,
|
||||
'createdAt': '2026-09-07T09:01:00Z',
|
||||
},
|
||||
],
|
||||
'nextCursor': 'b64cursor',
|
||||
'hasMore': true,
|
||||
}, WeightRecord.fromJson);
|
||||
|
||||
expect(page.items.single.weightKg, 4.2);
|
||||
expect(page.nextCursor, 'b64cursor');
|
||||
expect(page.hasMore, isTrue);
|
||||
});
|
||||
|
||||
test('末页:hasMore=false 且 nextCursor 为 null(缺席同义)', () {
|
||||
final page = CursorPage.fromJson({
|
||||
'items': <Object?>[],
|
||||
'hasMore': false,
|
||||
}, WeightRecord.fromJson);
|
||||
|
||||
expect(page.items, isEmpty);
|
||||
expect(page.nextCursor, isNull);
|
||||
expect(page.hasMore, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('PetSummary', () {
|
||||
test('四聚合全量映射', () {
|
||||
final summary = PetSummary.fromJson({
|
||||
'petId': 'p-1',
|
||||
'latestWeight': {
|
||||
'weightKg': 4.35,
|
||||
'measuredAt': '2026-09-07T09:00:00+08:00',
|
||||
},
|
||||
'vaccinationProgress': {'completedDoses': 2, 'totalDoses': 3},
|
||||
'nextVaccination': {
|
||||
'vaccinationId': 'vx-3',
|
||||
'vaccineId': 'v-1',
|
||||
'vaccineName': '狂犬疫苗',
|
||||
'doseNo': 3,
|
||||
'doseLabel': null,
|
||||
'dueOn': '2026-08-01',
|
||||
'source': 'planned',
|
||||
},
|
||||
'monthlyExpense': {
|
||||
'month': '2026-09',
|
||||
'timezone': 'Asia/Shanghai',
|
||||
'amountCents': 12850,
|
||||
},
|
||||
});
|
||||
|
||||
expect(summary.petId, 'p-1');
|
||||
expect(summary.latestWeight!.weightKg, 4.35);
|
||||
expect(summary.vaccinationProgress!.completedDoses, 2);
|
||||
expect(summary.vaccinationProgress!.totalDoses, 3);
|
||||
expect(summary.nextVaccination!.vaccinationId, 'vx-3');
|
||||
expect(summary.nextVaccination!.doseLabel, isNull);
|
||||
// dueOn 可为过去日期(逾期针仍是下一针)
|
||||
expect(summary.nextVaccination!.dueOn, DateTime.parse('2026-08-01'));
|
||||
expect(summary.nextVaccination!.source, NextVaccinationSource.planned);
|
||||
expect(summary.monthlyExpense.month, '2026-09');
|
||||
expect(summary.monthlyExpense.timezone, 'Asia/Shanghai');
|
||||
expect(summary.monthlyExpense.amountCents, 12850);
|
||||
});
|
||||
|
||||
test('null 语义:前三项无记录为 null,monthlyExpense 恒非 null、无支出为 0', () {
|
||||
final summary = PetSummary.fromJson({
|
||||
'petId': 'p-1',
|
||||
'latestWeight': null,
|
||||
'vaccinationProgress': null,
|
||||
'nextVaccination': null,
|
||||
'monthlyExpense': {
|
||||
'month': '2026-09',
|
||||
'timezone': 'UTC',
|
||||
'amountCents': 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(summary.latestWeight, isNull);
|
||||
expect(summary.vaccinationProgress, isNull);
|
||||
expect(summary.nextVaccination, isNull);
|
||||
expect(summary.monthlyExpense.amountCents, 0);
|
||||
});
|
||||
|
||||
test('nextDue 来源枚举', () {
|
||||
final next = SummaryNextVaccination.fromJson({
|
||||
'vaccinationId': 'vx-1',
|
||||
'vaccineId': 'v-1',
|
||||
'vaccineName': '猫三联',
|
||||
'doseNo': 1,
|
||||
'doseLabel': '首针',
|
||||
'dueOn': '2027-08-03',
|
||||
'source': 'nextDue',
|
||||
});
|
||||
|
||||
expect(next.source, NextVaccinationSource.nextDue);
|
||||
expect(next.doseLabel, '首针');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:patbond_flutter/core/network/api_exception.dart';
|
||||
import 'package:patbond_flutter/features/pets/pet_exceptions.dart';
|
||||
import 'package:patbond_flutter/features/pets/pet_models.dart';
|
||||
import 'package:patbond_flutter/features/pets/pets_controller.dart';
|
||||
import 'package:patbond_flutter/features/pets/pets_repository.dart';
|
||||
|
||||
import '../../helpers/pet_test_helpers.dart';
|
||||
|
||||
/// 假仓库:各方法可注入行为,未注入的方法抛 UnimplementedError。
|
||||
class FakePetsRepository implements PetsRepository {
|
||||
Future<List<Pet>> Function()? listPetsHandler;
|
||||
Future<Pet> Function(CreatePetRequest)? createPetHandler;
|
||||
Future<Pet> Function(String)? getPetHandler;
|
||||
Future<Pet> Function(String, UpdatePetRequest)? updatePetHandler;
|
||||
|
||||
@override
|
||||
Future<List<Pet>> listPets() =>
|
||||
listPetsHandler?.call() ?? Future.value(const []);
|
||||
|
||||
@override
|
||||
Future<Pet> createPet(CreatePetRequest request) => createPetHandler!(request);
|
||||
|
||||
@override
|
||||
Future<Pet> getPet(String petId) => getPetHandler!(petId);
|
||||
|
||||
@override
|
||||
Future<Pet> updatePet(String petId, UpdatePetRequest request) =>
|
||||
updatePetHandler!(petId, request);
|
||||
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) =>
|
||||
throw UnimplementedError('${invocation.memberName}');
|
||||
}
|
||||
|
||||
Pet pet(String id, {String name = '豆豆', int version = 1}) => Pet.fromJson(
|
||||
samplePetJson(overrides: {'id': id, 'name': name, 'version': version}),
|
||||
);
|
||||
|
||||
void main() {
|
||||
late FakePetsRepository repository;
|
||||
late PetsController controller;
|
||||
|
||||
setUp(() {
|
||||
repository = FakePetsRepository();
|
||||
controller = PetsController(repository: repository);
|
||||
});
|
||||
|
||||
test('初始态为 initial,不发请求', () {
|
||||
expect(controller.phase, PetsLoadPhase.initial);
|
||||
expect(controller.pets, isEmpty);
|
||||
expect(controller.isEmpty, isFalse);
|
||||
});
|
||||
|
||||
test('refresh 成功:loading → ready,列表就位', () async {
|
||||
repository.listPetsHandler = () async => [pet('p-1'), pet('p-2')];
|
||||
final phases = <PetsLoadPhase>[];
|
||||
controller.addListener(() => phases.add(controller.phase));
|
||||
|
||||
await controller.refresh();
|
||||
|
||||
expect(phases, [PetsLoadPhase.loading, PetsLoadPhase.ready]);
|
||||
expect(controller.pets.map((p) => p.id), ['p-1', 'p-2']);
|
||||
expect(controller.isEmpty, isFalse);
|
||||
expect(controller.lastError, isNull);
|
||||
});
|
||||
|
||||
test('refresh 空列表:ready 且 isEmpty(新用户空态)', () async {
|
||||
repository.listPetsHandler = () async => [];
|
||||
|
||||
await controller.refresh();
|
||||
|
||||
expect(controller.phase, PetsLoadPhase.ready);
|
||||
expect(controller.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('refresh 失败:error 态保留类型化错误,重试可恢复', () async {
|
||||
repository.listPetsHandler = () async =>
|
||||
throw const ApiNetworkException('断网');
|
||||
|
||||
await controller.refresh();
|
||||
|
||||
expect(controller.phase, PetsLoadPhase.error);
|
||||
expect(controller.lastError, isA<ApiNetworkException>());
|
||||
|
||||
repository.listPetsHandler = () async => [pet('p-1')];
|
||||
await controller.refresh();
|
||||
|
||||
expect(controller.phase, PetsLoadPhase.ready);
|
||||
expect(controller.lastError, isNull);
|
||||
expect(controller.pets, hasLength(1));
|
||||
});
|
||||
|
||||
test('createPet:成功后插入列表头', () async {
|
||||
repository.listPetsHandler = () async => [pet('p-1')];
|
||||
await controller.refresh();
|
||||
repository.createPetHandler = (request) async => pet('p-2', name: '咪咪');
|
||||
|
||||
final created = await controller.createPet(
|
||||
const CreatePetRequest(
|
||||
name: '咪咪',
|
||||
species: PetSpecies.cat,
|
||||
sex: PetSex.female,
|
||||
customBreedName: '狸花',
|
||||
),
|
||||
);
|
||||
|
||||
expect(created.id, 'p-2');
|
||||
expect(controller.pets.map((p) => p.id), ['p-2', 'p-1']);
|
||||
});
|
||||
|
||||
test('createPet 失败(40903):类型化异常外抛,列表不变', () async {
|
||||
repository.listPetsHandler = () async => [pet('p-1')];
|
||||
await controller.refresh();
|
||||
repository.createPetHandler = (request) async =>
|
||||
throw const MicrochipTakenException(message: '芯片号已被登记');
|
||||
|
||||
await expectLater(
|
||||
controller.createPet(
|
||||
const CreatePetRequest(
|
||||
name: '咪咪',
|
||||
species: PetSpecies.cat,
|
||||
sex: PetSex.female,
|
||||
customBreedName: '狸花',
|
||||
),
|
||||
),
|
||||
throwsA(isA<MicrochipTakenException>()),
|
||||
);
|
||||
expect(controller.pets, hasLength(1));
|
||||
});
|
||||
|
||||
test('updatePet:成功后同步列表内存副本', () async {
|
||||
repository.listPetsHandler = () async => [pet('p-1', version: 1)];
|
||||
await controller.refresh();
|
||||
repository.updatePetHandler = (petId, request) async =>
|
||||
pet('p-1', name: '豆豆二世', version: 2);
|
||||
|
||||
await controller.updatePet(
|
||||
'p-1',
|
||||
const UpdatePetRequest(version: 1, name: '豆豆二世'),
|
||||
);
|
||||
|
||||
expect(controller.pets.single.name, '豆豆二世');
|
||||
expect(controller.pets.single.version, 2);
|
||||
});
|
||||
|
||||
test('updatePet 版本冲突(40902):异常外抛供页面提示刷新重提', () async {
|
||||
repository.updatePetHandler = (petId, request) async =>
|
||||
throw const PetVersionConflictException(message: '数据已被修改');
|
||||
|
||||
await expectLater(
|
||||
controller.updatePet('p-1', const UpdatePetRequest(version: 1)),
|
||||
throwsA(isA<PetVersionConflictException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('getPet:详情结果回写列表副本', () async {
|
||||
repository.listPetsHandler = () async => [pet('p-1', version: 1)];
|
||||
await controller.refresh();
|
||||
repository.getPetHandler = (petId) async => pet('p-1', version: 5);
|
||||
|
||||
final detail = await controller.getPet('p-1');
|
||||
|
||||
expect(detail.version, 5);
|
||||
expect(controller.pets.single.version, 5);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:patbond_flutter/core/network/api_client.dart';
|
||||
import 'package:patbond_flutter/core/network/api_exception.dart';
|
||||
import 'package:patbond_flutter/core/network/token_refresher.dart';
|
||||
import 'package:patbond_flutter/features/auth/session_manager.dart';
|
||||
import 'package:patbond_flutter/features/pets/pet_exceptions.dart';
|
||||
import 'package:patbond_flutter/features/pets/pet_models.dart';
|
||||
import 'package:patbond_flutter/features/pets/pets_repository.dart';
|
||||
|
||||
import '../../helpers/auth_test_helpers.dart';
|
||||
import '../../helpers/pet_test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
late SessionManager session;
|
||||
late FakeHttpAdapter adapter;
|
||||
late ApiPetsRepository repository;
|
||||
|
||||
Future<void> setUpWith(
|
||||
Future<ResponseBody> Function(RequestOptions) handler,
|
||||
) async {
|
||||
session = SessionManager(store: InMemoryTokenStore());
|
||||
await session.updateTokens(sampleTokens(access: 'pet-access'));
|
||||
final dio = buildPatbondDio(session: session, baseUrl: 'http://pet.local');
|
||||
adapter = FakeHttpAdapter(handler);
|
||||
dio.httpClientAdapter = adapter;
|
||||
final refresher = TokenRefresher(dio: dio, session: session);
|
||||
repository = ApiPetsRepository(
|
||||
api: ApiClient(dio: dio, session: session, refresher: refresher),
|
||||
);
|
||||
}
|
||||
|
||||
group('请求线路(路径 / 方法 / 鉴权 / 参数)', () {
|
||||
test('listPets:GET /api/v1/pets,携带 Bearer', () async {
|
||||
await setUpWith(
|
||||
(options) async => jsonResponse(200, okListEnvelope([samplePetJson()])),
|
||||
);
|
||||
|
||||
final pets = await repository.listPets();
|
||||
|
||||
final request = adapter.requests.single;
|
||||
expect(request.path, '/api/v1/pets');
|
||||
expect(request.method, 'GET');
|
||||
expect(request.headers['Authorization'], 'Bearer pet-access');
|
||||
expect(pets.single.name, '豆豆');
|
||||
expect(pets.single.myRole, PetRole.owner);
|
||||
});
|
||||
|
||||
test('createPet:POST 201,不带 Idempotency-Key(契约:唯一约束兜底)', () async {
|
||||
await setUpWith(
|
||||
(options) async => jsonResponse(201, okEnvelope(samplePetJson())),
|
||||
);
|
||||
|
||||
final pet = await repository.createPet(
|
||||
const CreatePetRequest(
|
||||
name: '豆豆',
|
||||
species: PetSpecies.dog,
|
||||
sex: PetSex.male,
|
||||
breedId: 'b-1',
|
||||
),
|
||||
);
|
||||
|
||||
final request = adapter.requests.single;
|
||||
expect(request.method, 'POST');
|
||||
expect(request.data, {
|
||||
'name': '豆豆',
|
||||
'species': 'dog',
|
||||
'sex': 'male',
|
||||
'breedId': 'b-1',
|
||||
});
|
||||
expect(request.headers.containsKey('Idempotency-Key'), isFalse);
|
||||
expect(pet.id, isNotEmpty);
|
||||
});
|
||||
|
||||
test('getPet / updatePet:路径带 petId,PATCH 提交 version', () async {
|
||||
await setUpWith(
|
||||
(options) async => jsonResponse(200, okEnvelope(samplePetJson())),
|
||||
);
|
||||
|
||||
await repository.getPet('p-1');
|
||||
await repository.updatePet(
|
||||
'p-1',
|
||||
const UpdatePetRequest(version: 3, name: '豆豆二世'),
|
||||
);
|
||||
|
||||
expect(adapter.requests[0].path, '/api/v1/pets/p-1');
|
||||
expect(adapter.requests[0].method, 'GET');
|
||||
expect(adapter.requests[1].method, 'PATCH');
|
||||
expect(adapter.requests[1].data, {'version': 3, 'name': '豆豆二世'});
|
||||
});
|
||||
|
||||
test('字典:species 过滤参数按枚举名传递,缺省不传', () async {
|
||||
await setUpWith((options) async => jsonResponse(200, okListEnvelope([])));
|
||||
|
||||
await repository.listBreeds(species: PetSpecies.cat);
|
||||
await repository.listBreeds();
|
||||
await repository.listVaccineCatalog(species: PetSpecies.dog);
|
||||
|
||||
expect(adapter.requests[0].path, '/api/v1/breeds');
|
||||
expect(adapter.requests[0].queryParameters, {'species': 'cat'});
|
||||
expect(adapter.requests[1].queryParameters, isEmpty);
|
||||
expect(adapter.requests[2].path, '/api/v1/vaccine-catalog');
|
||||
expect(adapter.requests[2].queryParameters, {'species': 'dog'});
|
||||
});
|
||||
|
||||
test('listWeights:cursor 分页参数与信封解析', () async {
|
||||
await setUpWith(
|
||||
(options) async => jsonResponse(
|
||||
200,
|
||||
okEnvelope({
|
||||
'items': [sampleWeightJson()],
|
||||
'nextCursor': 'c2',
|
||||
'hasMore': true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
final page = await repository.listWeights('p-1', limit: 50, cursor: 'c1');
|
||||
|
||||
final request = adapter.requests.single;
|
||||
expect(request.path, '/api/v1/pets/p-1/weights');
|
||||
expect(request.queryParameters, {'limit': 50, 'cursor': 'c1'});
|
||||
expect(page.items.single.weightKg, 4.35);
|
||||
expect(page.nextCursor, 'c2');
|
||||
expect(page.hasMore, isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
'四个 POST 携带 Idempotency-Key(weights/vaccinations/health-events/care-reminders)',
|
||||
() async {
|
||||
var call = 0;
|
||||
await setUpWith((options) async {
|
||||
call += 1;
|
||||
return switch (call) {
|
||||
1 => jsonResponse(201, okEnvelope(sampleWeightJson())),
|
||||
2 => jsonResponse(201, okEnvelope(sampleVaccinationJson())),
|
||||
3 => jsonResponse(201, okEnvelope(sampleHealthEventJson())),
|
||||
_ => jsonResponse(201, okEnvelope(sampleCareReminderJson())),
|
||||
};
|
||||
});
|
||||
|
||||
await repository.createWeight(
|
||||
'p-1',
|
||||
CreateWeightRequest(
|
||||
weightKg: 4.35,
|
||||
measuredAt: DateTime.utc(2026, 9, 7),
|
||||
),
|
||||
);
|
||||
await repository.createVaccination(
|
||||
'p-1',
|
||||
const CreateVaccinationRequest(
|
||||
vaccineId: 'v-1',
|
||||
seriesKey: 'rabies-initial',
|
||||
doseNo: 1,
|
||||
status: VaccinationStatus.scheduled,
|
||||
),
|
||||
);
|
||||
await repository.createHealthEvent(
|
||||
'p-1',
|
||||
CreateHealthEventRequest(
|
||||
eventType: HealthEventType.note,
|
||||
occurredAt: DateTime.utc(2026, 9, 7),
|
||||
title: '记录',
|
||||
),
|
||||
);
|
||||
await repository.createCareReminder(
|
||||
'p-1',
|
||||
CreateCareReminderRequest(
|
||||
reminderType: CareReminderType.checkup,
|
||||
title: '体检',
|
||||
dueAt: DateTime.utc(2026, 10, 1),
|
||||
),
|
||||
);
|
||||
|
||||
final keys = adapter.requests
|
||||
.map((r) => r.headers['Idempotency-Key'] as String?)
|
||||
.toList();
|
||||
expect(keys, everyElement(isNotNull));
|
||||
expect(keys, everyElement(isNotEmpty));
|
||||
// 每次逻辑提交换新键
|
||||
expect(keys.toSet().length, 4);
|
||||
},
|
||||
);
|
||||
|
||||
test('疫苗 / 事件 / 提醒 PATCH:顶层短路径(不含 petId)', () async {
|
||||
var call = 0;
|
||||
await setUpWith((options) async {
|
||||
call += 1;
|
||||
return switch (call) {
|
||||
1 => jsonResponse(200, okEnvelope(sampleVaccinationJson())),
|
||||
2 => jsonResponse(200, okEnvelope(sampleHealthEventJson())),
|
||||
_ => jsonResponse(200, okEnvelope(sampleCareReminderJson())),
|
||||
};
|
||||
});
|
||||
|
||||
await repository.updateVaccination(
|
||||
'vx-1',
|
||||
const UpdateVaccinationRequest(
|
||||
version: 1,
|
||||
status: VaccinationStatus.cancelled,
|
||||
),
|
||||
);
|
||||
await repository.updateHealthEvent(
|
||||
'e-1',
|
||||
const UpdateHealthEventRequest(version: 1, title: '复查'),
|
||||
);
|
||||
await repository.updateCareReminder(
|
||||
'r-1',
|
||||
const UpdateCareReminderRequest(status: CareReminderStatus.dismissed),
|
||||
);
|
||||
|
||||
expect(adapter.requests[0].path, '/api/v1/vaccinations/vx-1');
|
||||
expect(adapter.requests[1].path, '/api/v1/health-events/e-1');
|
||||
expect(adapter.requests[2].path, '/api/v1/care-reminders/r-1');
|
||||
expect(adapter.requests.map((r) => r.method), everyElement('PATCH'));
|
||||
});
|
||||
|
||||
test('listVaccinations / listCareReminders:不分页数组 + status 过滤', () async {
|
||||
var call = 0;
|
||||
await setUpWith((options) async {
|
||||
call += 1;
|
||||
return call == 1
|
||||
? jsonResponse(200, okListEnvelope([sampleVaccinationJson()]))
|
||||
: jsonResponse(200, okListEnvelope([sampleCareReminderJson()]));
|
||||
});
|
||||
|
||||
final vaccinations = await repository.listVaccinations('p-1');
|
||||
final reminders = await repository.listCareReminders(
|
||||
'p-1',
|
||||
status: CareReminderStatus.pending,
|
||||
);
|
||||
|
||||
expect(adapter.requests[0].path, '/api/v1/pets/p-1/vaccinations');
|
||||
expect(adapter.requests[0].queryParameters, isEmpty);
|
||||
expect(adapter.requests[1].path, '/api/v1/pets/p-1/care-reminders');
|
||||
expect(adapter.requests[1].queryParameters, {'status': 'pending'});
|
||||
expect(vaccinations.single.vaccineName, '狂犬疫苗');
|
||||
expect(reminders.single.status, CareReminderStatus.pending);
|
||||
});
|
||||
|
||||
test('listHealthEvents:分页参数缺省不传', () async {
|
||||
await setUpWith(
|
||||
(options) async => jsonResponse(
|
||||
200,
|
||||
okEnvelope({'items': <Object?>[], 'hasMore': false}),
|
||||
),
|
||||
);
|
||||
|
||||
final page = await repository.listHealthEvents('p-1');
|
||||
|
||||
expect(adapter.requests.single.path, '/api/v1/pets/p-1/health-events');
|
||||
expect(adapter.requests.single.queryParameters, isEmpty);
|
||||
expect(page.items, isEmpty);
|
||||
expect(page.hasMore, isFalse);
|
||||
});
|
||||
|
||||
test('getPetSummary:tz 参数传递与四聚合解析', () async {
|
||||
await setUpWith(
|
||||
(options) async =>
|
||||
jsonResponse(200, okEnvelope(samplePetSummaryJson())),
|
||||
);
|
||||
|
||||
final summary = await repository.getPetSummary(
|
||||
'p-1',
|
||||
tz: 'Asia/Shanghai',
|
||||
);
|
||||
|
||||
final request = adapter.requests.single;
|
||||
expect(request.path, '/api/v1/pets/p-1/summary');
|
||||
expect(request.queryParameters, {'tz': 'Asia/Shanghai'});
|
||||
expect(summary.monthlyExpense.timezone, 'Asia/Shanghai');
|
||||
expect(summary.latestWeight!.weightKg, 4.35);
|
||||
});
|
||||
});
|
||||
|
||||
group('错误映射(新 8 码 → 类型化异常)', () {
|
||||
Future<void> expectMapped(
|
||||
int httpStatus,
|
||||
int code,
|
||||
TypeMatcher<ApiBusinessException> matcher,
|
||||
) async {
|
||||
await setUpWith(
|
||||
(options) async => jsonResponse(httpStatus, errorEnvelope(code, 'err')),
|
||||
);
|
||||
await expectLater(
|
||||
repository.getPet('p-1'),
|
||||
throwsA(matcher.having((e) => e.code, 'code', code)),
|
||||
);
|
||||
}
|
||||
|
||||
test('40300 → PetAccessDeniedException', () async {
|
||||
await expectMapped(403, 40300, isA<PetAccessDeniedException>());
|
||||
});
|
||||
|
||||
test('40401 → PetNotFoundException(防枚举 404)', () async {
|
||||
await expectMapped(404, 40401, isA<PetNotFoundException>());
|
||||
});
|
||||
|
||||
test('40402 → PetRecordNotFoundException(记录级防枚举)', () async {
|
||||
await expectMapped(404, 40402, isA<PetRecordNotFoundException>());
|
||||
});
|
||||
|
||||
test('40902 → PetVersionConflictException', () async {
|
||||
await expectMapped(409, 40902, isA<PetVersionConflictException>());
|
||||
});
|
||||
|
||||
test('40903 → MicrochipTakenException', () async {
|
||||
await expectMapped(409, 40903, isA<MicrochipTakenException>());
|
||||
});
|
||||
|
||||
test('40904 → VaccinationDoseExistsException', () async {
|
||||
await expectMapped(409, 40904, isA<VaccinationDoseExistsException>());
|
||||
});
|
||||
|
||||
test('42201 → VaccinationRuleException', () async {
|
||||
await expectMapped(422, 42201, isA<VaccinationRuleException>());
|
||||
});
|
||||
|
||||
test('42202 → CareReminderRuleException', () async {
|
||||
await expectMapped(422, 42202, isA<CareReminderRuleException>());
|
||||
});
|
||||
|
||||
test('未覆盖码(40000)保持通用 ApiBusinessException', () async {
|
||||
await setUpWith(
|
||||
(options) async => jsonResponse(400, errorEnvelope(40000, '参数校验失败')),
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
repository.getPet('p-1'),
|
||||
throwsA(
|
||||
isA<ApiBusinessException>()
|
||||
.having((e) => e.code, 'code', 40000)
|
||||
.having(
|
||||
(e) => e,
|
||||
'runtimeType',
|
||||
isNot(isA<PetNotFoundException>()),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('类型化异常仍可按基类 ApiBusinessException 捕获', () {
|
||||
const error = PetVersionConflictException(message: '数据已被修改');
|
||||
expect(error, isA<ApiBusinessException>());
|
||||
expect(error.code, ApiCodes.versionConflict);
|
||||
});
|
||||
});
|
||||
|
||||
group('token 拦截复用', () {
|
||||
test('40101:单飞刷新后重放,重放沿用同一 Idempotency-Key', () async {
|
||||
await setUpWith((options) async {
|
||||
if (options.path == '/api/v1/auth/refresh') {
|
||||
return jsonResponse(
|
||||
200,
|
||||
okEnvelope(tokenDataJson(access: 'new-access')),
|
||||
);
|
||||
}
|
||||
if (options.headers['Authorization'] == 'Bearer pet-access') {
|
||||
return jsonResponse(401, errorEnvelope(40101, 'token 过期'));
|
||||
}
|
||||
return jsonResponse(201, okEnvelope(sampleWeightJson()));
|
||||
});
|
||||
|
||||
await repository.createWeight(
|
||||
'p-1',
|
||||
CreateWeightRequest(
|
||||
weightKg: 4.35,
|
||||
measuredAt: DateTime.utc(2026, 9, 7),
|
||||
),
|
||||
);
|
||||
|
||||
final weightRequests = adapter.requests
|
||||
.where((r) => r.path == '/api/v1/pets/p-1/weights')
|
||||
.toList();
|
||||
expect(weightRequests, hasLength(2));
|
||||
expect(weightRequests.last.headers['Authorization'], 'Bearer new-access');
|
||||
expect(
|
||||
weightRequests.first.headers['Idempotency-Key'],
|
||||
weightRequests.last.headers['Idempotency-Key'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('pet 服务基地址常量存在且默认指向 :8083', () {
|
||||
expect(patbondPetApiBaseUrl, 'http://127.0.0.1:8083');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user