test: 저장 무결성 + Model 직렬화 라운드트립 테스트 추가
- save_integrity_test: 13개 (sign/verify 라운드트립, 변조 감지, 레거시 호환) - save_data_roundtrip_test: 16개 (toJson/fromJson, v2→v4 마이그레이션, 기본값)
This commit is contained in:
191
test/core/storage/save_integrity_test.dart
Normal file
191
test/core/storage/save_integrity_test.dart
Normal file
@@ -0,0 +1,191 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:asciineverdie/src/core/storage/save_integrity.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('SaveIntegrity', () {
|
||||
// =========================================================================
|
||||
// sign() → verify() 라운드트립(roundtrip) 테스트
|
||||
// =========================================================================
|
||||
|
||||
group('sign → verify 라운드트립', () {
|
||||
test('서명 후 검증 성공 — 원본 데이터 보존', () {
|
||||
final original = Uint8List.fromList([10, 20, 30, 40, 50]);
|
||||
final signed = SaveIntegrity.sign(original);
|
||||
|
||||
// 서명된 데이터 = HMAC(32바이트) + 원본
|
||||
expect(signed.length, equals(SaveIntegrity.hmacLength + original.length));
|
||||
|
||||
final result = SaveIntegrity.verify(signed);
|
||||
|
||||
expect(result.isLegacy, isFalse);
|
||||
expect(result.gzipBytes, equals(original));
|
||||
});
|
||||
|
||||
test('큰 데이터에도 라운드트립 성공', () {
|
||||
// 1KB 데이터
|
||||
final original = Uint8List.fromList(
|
||||
List.generate(1024, (i) => i % 256),
|
||||
);
|
||||
final signed = SaveIntegrity.sign(original);
|
||||
|
||||
final result = SaveIntegrity.verify(signed);
|
||||
|
||||
expect(result.isLegacy, isFalse);
|
||||
expect(result.gzipBytes, equals(original));
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// 변조(tampered) 데이터 검증 실패 테스트
|
||||
// =========================================================================
|
||||
|
||||
group('변조된 데이터 verify 실패', () {
|
||||
test('데이터 영역 변조 시 SaveIntegrityException 발생', () {
|
||||
final original = Uint8List.fromList([10, 20, 30, 40, 50]);
|
||||
final signed = SaveIntegrity.sign(original);
|
||||
|
||||
// 데이터 영역(HMAC 이후) 변조
|
||||
signed[SaveIntegrity.hmacLength] = 0xFF;
|
||||
|
||||
expect(
|
||||
() => SaveIntegrity.verify(signed),
|
||||
throwsA(isA<SaveIntegrityException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('HMAC 영역 변조 시 SaveIntegrityException 발생', () {
|
||||
final original = Uint8List.fromList([10, 20, 30, 40, 50]);
|
||||
final signed = SaveIntegrity.sign(original);
|
||||
|
||||
// HMAC 영역 변조
|
||||
signed[0] ^= 0xFF;
|
||||
|
||||
expect(
|
||||
() => SaveIntegrity.verify(signed),
|
||||
throwsA(isA<SaveIntegrityException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('HMAC과 데이터 모두 변조 시 예외 발생', () {
|
||||
final original = Uint8List.fromList([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
final signed = SaveIntegrity.sign(original);
|
||||
|
||||
// 양쪽 변조
|
||||
signed[0] ^= 0x01;
|
||||
signed[SaveIntegrity.hmacLength + 2] ^= 0x01;
|
||||
|
||||
expect(
|
||||
() => SaveIntegrity.verify(signed),
|
||||
throwsA(isA<SaveIntegrityException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// 레거시 포맷(legacy format) 감지 테스트
|
||||
// =========================================================================
|
||||
|
||||
group('레거시 포맷 감지', () {
|
||||
test('GZip 매직 바이트(0x1f 0x8b)로 시작하면 isLegacy=true', () {
|
||||
// GZip 매직 바이트 + 임의 데이터
|
||||
final legacyData = Uint8List.fromList([0x1f, 0x8b, 0x08, 0x00, 0x00]);
|
||||
|
||||
final result = SaveIntegrity.verify(legacyData);
|
||||
|
||||
expect(result.isLegacy, isTrue);
|
||||
expect(result.gzipBytes, equals(legacyData));
|
||||
});
|
||||
|
||||
test('레거시 포맷은 HMAC 검증을 건너뛰고 원본 데이터 반환', () {
|
||||
// GZip 매직 바이트로 시작하는 큰 데이터
|
||||
final legacyData = Uint8List.fromList([
|
||||
0x1f, 0x8b, ...List.generate(100, (i) => i % 256),
|
||||
]);
|
||||
|
||||
final result = SaveIntegrity.verify(legacyData);
|
||||
|
||||
expect(result.isLegacy, isTrue);
|
||||
expect(result.gzipBytes.length, equals(legacyData.length));
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// 너무 작은 데이터 verify 실패 테스트
|
||||
// =========================================================================
|
||||
|
||||
group('너무 작은 데이터', () {
|
||||
test('HMAC 길이 미만 데이터 시 SaveIntegrityException 발생', () {
|
||||
// HMAC 32바이트 미만이면서 GZip 매직 바이트가 아닌 데이터
|
||||
final tooSmall = Uint8List.fromList([0x00, 0x01, 0x02]);
|
||||
|
||||
expect(
|
||||
() => SaveIntegrity.verify(tooSmall),
|
||||
throwsA(isA<SaveIntegrityException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('정확히 HMAC 길이(32바이트) 데이터 — 빈 페이로드로 검증 시도', () {
|
||||
// 32바이트 = HMAC만 있고 데이터 없음 → HMAC 불일치로 실패
|
||||
final exactHmacLength = Uint8List(SaveIntegrity.hmacLength);
|
||||
|
||||
// HMAC이 맞지 않으므로 예외 발생
|
||||
expect(
|
||||
() => SaveIntegrity.verify(exactHmacLength),
|
||||
throwsA(isA<SaveIntegrityException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('1바이트 데이터 — 레거시도 아니고 너무 작음', () {
|
||||
final oneByte = Uint8List.fromList([0x42]);
|
||||
|
||||
expect(
|
||||
() => SaveIntegrity.verify(oneByte),
|
||||
throwsA(isA<SaveIntegrityException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// 빈 데이터(empty data) 처리 테스트
|
||||
// =========================================================================
|
||||
|
||||
group('빈 데이터 처리', () {
|
||||
test('빈 바이트 배열 verify 시 예외 발생', () {
|
||||
final empty = Uint8List(0);
|
||||
|
||||
expect(
|
||||
() => SaveIntegrity.verify(empty),
|
||||
throwsA(isA<SaveIntegrityException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('빈 데이터에 sign() 후 verify() 라운드트립 성공', () {
|
||||
// 빈 페이로드(payload)도 서명/검증 가능
|
||||
final emptyPayload = Uint8List(0);
|
||||
final signed = SaveIntegrity.sign(emptyPayload);
|
||||
|
||||
expect(signed.length, equals(SaveIntegrity.hmacLength));
|
||||
|
||||
final result = SaveIntegrity.verify(signed);
|
||||
|
||||
expect(result.isLegacy, isFalse);
|
||||
expect(result.gzipBytes, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// SaveIntegrityException 테스트
|
||||
// =========================================================================
|
||||
|
||||
group('SaveIntegrityException', () {
|
||||
test('toString()에 메시지 포함', () {
|
||||
const exception = SaveIntegrityException('테스트 메시지');
|
||||
|
||||
expect(exception.toString(), contains('테스트 메시지'));
|
||||
expect(exception.message, equals('테스트 메시지'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user