generated from ttipo/project-base
fix(flutter): 토큰 갱신 429 로 로그아웃되지 않게, 갱신 뒤 사진 다시 보내기
리뷰에서 나온 앱 쪽 문제를 고친다. - 토큰 갱신이 429 를 받으면 세션을 끊던 문제 — 거절(401·400)만 세션을 끊고, 429 는 REQ_001·그 밖은 SYS_001 로 알린다 (서버는 갱신을 IP 당 분당 10회로 막아 통신사 망처럼 여럿이 한 IP 를 쓰면 실제로 생긴다) - 액세스 토큰이 만료된 뒤 사진을 올리면 다시 보낼 때 FormData 가 이미 쓰여 실패하던 문제 — 복사본으로 다시 보낸다 - 에러 사전 계약 테스트에 앱이 직접 쓰는 도메인 코드(FILE·DIARY·CARD·SCHED)를 더한다 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -169,6 +169,34 @@ void main() {
|
||||
expect(t.session.hasSession, isTrue);
|
||||
});
|
||||
|
||||
test('갱신이 너무 잦아 막히면(429) 세션은 두고 요청 과다(REQ_001)로 알린다', () async {
|
||||
final t = build((o) => o.path == kRefreshPath
|
||||
? (status: 429, body: _fail('REQ_001'))
|
||||
: (status: 401, body: _fail('AUTH_001')));
|
||||
|
||||
final error = await errorOf(t.client.dio.get<Object?>('/pets'));
|
||||
|
||||
expect(error.code, 'REQ_001');
|
||||
expect(t.session.hasSession, isTrue, reason: '한 IP 를 여럿이 쓰는 망에서도 생기므로 로그아웃시키지 않는다');
|
||||
});
|
||||
|
||||
test('사진 올리기(FormData)도 토큰을 갱신한 뒤 다시 보낼 수 있다', () async {
|
||||
final t = build((o) {
|
||||
if (o.path == kRefreshPath) return (status: 201, body: _newTokens());
|
||||
return _auth(o) == 'Bearer new-access-1'
|
||||
? (status: 201, body: _ok({'id': 'f1'}))
|
||||
: (status: 401, body: _fail('AUTH_001'));
|
||||
});
|
||||
|
||||
final res = await t.client.dio.post<Map<String, dynamic>>(
|
||||
'/files',
|
||||
data: FormData.fromMap({'file': MultipartFile.fromBytes(const [1, 2, 3], filename: 'a.png')}),
|
||||
);
|
||||
|
||||
expect(res.data, {'id': 'f1'});
|
||||
expect(reported, isEmpty);
|
||||
});
|
||||
|
||||
test('로그인 전(토큰 없음)의 401 은 그대로 AUTH_001 이다', () async {
|
||||
final t = build((o) => (status: 401, body: _fail('AUTH_001')), tokens: null);
|
||||
|
||||
|
||||
@@ -40,6 +40,21 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
test('화면이 분기하거나 같은 문구로 안내하는 도메인 코드가 사전에 존재한다', () {
|
||||
// 이름이 바뀌면 조용히 SYS_001 문구로 떨어지므로, 앱 코드가 직접 쓰는 도메인 코드를 지킨다
|
||||
// (checklist 3절 — 클라이언트가 그 코드로 분기하면 목록에 추가한다)
|
||||
const domainUsedByClient = [
|
||||
'FILE_001', 'FILE_002', // 사진 올리기
|
||||
'DIARY_001', 'DIARY_002', 'DIARY_003', 'DIARY_004', 'DIARY_005', // 다이어리 책장·하루 한 번·초대
|
||||
'CARD_001', 'CARD_002', 'CARD_003', // 명함 한도·받기
|
||||
'SCHED_001', 'SCHED_002', 'SCHED_003', 'SCHED_004', 'SCHED_005', // 일정 연동
|
||||
];
|
||||
|
||||
for (final code in domainUsedByClient) {
|
||||
expect(ErrorLexicon.contains(code), isTrue, reason: '$code 누락');
|
||||
}
|
||||
});
|
||||
|
||||
test('모든 코드는 DOMAIN_NNN 네이밍 규약을 따른다', () {
|
||||
final pattern = RegExp(r'^[A-Z]+_\d{3}$');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user