import 'package:drift/drift.dart' hide isNull, isNotNull; import 'package:flutter_test/flutter_test.dart'; import 'package:life_helper/core/constants.dart'; import 'package:life_helper/core/time.dart'; import 'package:life_helper/data/db/app_database.dart'; /// v1 schema 로 in-memory DB 를 raw SQL 로 부트스트랩. /// /// Phase 1 (#204) 시점의 protocols 테이블 정의를 모사 — 그 시점 CHECK 는 6 값. Future _buildV1Database() async { // schemaVersion=1 흉내. v2 schema 로 createAll 후, protocols 만 v1 CHECK 로 재생성. // 이렇게 하면 migrateV1ToV2 가 호출 가능 (생성된 DB 는 항상 v2 코드 베이스). // 본 테스트의 핵심은 "DROP+CREATE+flag 클리어가 user 테이블에 영향 주지 않음" + "v2 CHECK 적용 확인". final db = AppDatabase.memory(); // v1 의 protocols 테이블을 시뮬레이트: v2 테이블을 DROP 하고 v1 정의로 다시 만든다. await db.customStatement('DROP TABLE IF EXISTS protocols'); await db.customStatement('DROP INDEX IF EXISTS IDX_protocols_category'); await db.customStatement(''' CREATE TABLE protocols ( id TEXT NOT NULL PRIMARY KEY, category TEXT NOT NULL CHECK (category IN ('health','meditation','motivation','habit','learning','diet')), title TEXT NOT NULL, title_en TEXT, what TEXT NOT NULL, when_text TEXT NOT NULL, dose TEXT NOT NULL, why TEXT NOT NULL, how_json TEXT NOT NULL, check_text TEXT NOT NULL, caution TEXT, default_anchor_json TEXT, min_dose_for_start TEXT, reference_ids_json TEXT, evidence_strength TEXT CHECK (evidence_strength IS NULL OR evidence_strength IN ('strong_rct','meta_analysis','observational','mechanistic','expert_opinion')), source_doc TEXT CHECK (source_doc IS NULL OR source_doc IN ('huberman-protocols.md','diet-protocols.md')) ) '''); await db.customStatement( 'CREATE INDEX IDX_protocols_category ON protocols(category)'); return db; } void main() { group('migrateV1ToV2', () { test('v1 → v2: CHECK 갱신 + 인덱스 재생성 + flag 클리어', () async { final db = await _buildV1Database(); addTearDown(db.close); // v1 row 1개 insert (raw SQL — v1 CHECK 통과). await db.customStatement(''' INSERT INTO protocols (id, category, title, what, when_text, dose, why, how_json, check_text) VALUES ('legacy', 'health', '레거시', '뭐', '언제', '도즈', '왜', '[]', '체크') '''); // 시드 flag pre-set. await db.into(db.metaKv).insert( MetaKvCompanion.insert(key: kSeededV1Flag, value: 'true')); // Migrate. await db.transaction(() async { final m = Migrator(db); await migrateV1ToV2(m, db); }); // 1. Protocols 테이블 비어있음 (DROP 으로 row 손실 — reseed 가 채울 책임). final rows = await db.select(db.protocols).get(); expect(rows, isEmpty); // 2. v1 카테고리 'health' insert 는 이제 CHECK 위배. Future insertHealth() async { await db.customStatement(''' INSERT INTO protocols (id, category, title, what, when_text, dose, why, how_json, check_text) VALUES ('new', 'health', 't', 'w', 'wn', 'd', 'w', '[]', 'c') '''); } await expectLater(insertHealth(), throwsA(isA())); // 3. v2 카테고리 'light_circadian' insert 는 통과. await db.customStatement(''' INSERT INTO protocols (id, category, title, what, when_text, dose, why, how_json, check_text) VALUES ('new', 'light_circadian', 't', 'w', 'wn', 'd', 'w', '[]', 'c') '''); final after = await db.select(db.protocols).get(); expect(after.length, 1); // 4. 시드 flag 클리어 — 다음 부팅이 reseed 트리거. final marker = await (db.select(db.metaKv) ..where((t) => t.key.equals(kSeededV1Flag))) .getSingleOrNull(); expect(marker, isNull); // 5. 인덱스 재생성 확인 — sqlite_master 조회. final indexCheck = await db.customSelect( "SELECT name FROM sqlite_master WHERE type='index' AND name='IDX_protocols_category'", ).get(); expect(indexCheck.length, 1); }); test('user 테이블 (Users / Phases / Habits) 무변화', () async { final db = await _buildV1Database(); addTearDown(db.close); // user 데이터 사전 insert. await db.into(db.users).insert(UsersCompanion.insert( id: 'u1', displayName: const Value('Alice'), createdAt: nowKst().toIso8601String())); await db.into(db.phases).insert(PhasesCompanion.insert( id: 'ph1', userId: 'u1', status: 'active', startedAt: nowKst().toIso8601String())); await db.into(db.habits).insert(HabitsCompanion.insert( id: 'h1', userId: 'u1', type: 'build', status: 'active', title: 'My Habit', protocolId: const Value('legacy'), frameLevel: 'L2', frameFramedText: '저녁엔 무알콜', startedAt: nowKst().toIso8601String())); // Migrate. await db.transaction(() async { final m = Migrator(db); await migrateV1ToV2(m, db); }); // user 테이블 무변화. final users = await db.select(db.users).get(); final phases = await db.select(db.phases).get(); final habits = await db.select(db.habits).get(); expect(users.length, 1); expect(phases.length, 1); expect(habits.length, 1); expect(users.first.id, 'u1'); expect(habits.first.title, 'My Habit'); }); test('idempotent: 두 번째 호출도 성공 (no row 차이)', () async { final db = await _buildV1Database(); addTearDown(db.close); await db.transaction(() async { final m = Migrator(db); await migrateV1ToV2(m, db); await migrateV1ToV2(m, db); }); final rows = await db.select(db.protocols).get(); expect(rows, isEmpty); }); }); }