135 lines
6.5 KiB
Dart
135 lines
6.5 KiB
Dart
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:fancake/core/data/backend.dart';
|
|
import 'package:firebase_core/firebase_core.dart';
|
|
import 'package:fancake/features/home/home_repository_firestore.dart';
|
|
import 'package:fancake/features/my/my_data.dart';
|
|
import 'package:fancake/features/my/my_repository_firestore.dart';
|
|
import 'package:firebase_auth_mocks/firebase_auth_mocks.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:integration_test/integration_test.dart';
|
|
|
|
/// POP → 행성 반영 실증 — 별첨 1 ② 완료 시나리오 1 의 **읽는 쪽**.
|
|
///
|
|
/// **무엇을 재는가.** 서버가 원장을 지나 쓴 값을 앱이 그대로 읽어 HOME·MY 수치로
|
|
/// 옮기는지를 잰다. 가짜 Firestore 가 아니라 **실제 Firestore 에뮬레이터**를 물고,
|
|
/// 문서도 앱이 만든 것이 아니라 **Cloud Functions 가 쓴 것**을 읽는다. 두 단계가
|
|
/// 서로 다른 프로세스라, 필드 이름이 한 글자만 어긋나도 여기서 0 이 나온다.
|
|
///
|
|
/// 실행 — 두 단계를 순서대로 돈다.
|
|
///
|
|
/// ```bash
|
|
/// # 1. 서버가 상태를 만든다(출석 지급 → 원장 → 기여 차감 → 등급·멤버 갱신)
|
|
/// java -jar ~/.cache/firebase/emulators/cloud-firestore-emulator-v1.22.0.jar \
|
|
/// --host=0.0.0.0 --port=8080 &
|
|
/// CI=1 FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 GCLOUD_PROJECT=demo-fanuzu \
|
|
/// npm --prefix firebase/functions test
|
|
///
|
|
/// # 2. 앱이 그 상태를 읽는다
|
|
/// fvm flutter test integration_test/pop_loop_emulator_test.dart \
|
|
/// -d emulator-5554 --dart-define=FANUZU_EMULATOR_HOST=10.0.2.2
|
|
/// ```
|
|
///
|
|
/// `10.0.2.2` 는 Android 에뮬레이터가 보는 호스트 PC 주소다.
|
|
///
|
|
/// **로그인만 흉내 낸다.** Auth 에뮬레이터는 `firebase` CLI 안에만 있어 이 환경에서
|
|
/// 띄우지 못한다(BUILD.md 2-1). UID 는 서버 테스트가 쓴 것과 같은 값을 넣고, 읽는
|
|
/// 경로는 실제 구현을 그대로 지난다.
|
|
void main() {
|
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
/// `firebase/functions/test/quest_e2e.test.ts` 가 쓰는 UID.
|
|
const uid = 'test-quest-e2e';
|
|
|
|
/// 출석 퀘스트의 **문서 ID**. 진행 문서도 같은 ID 로 선다(설계서 §2.1).
|
|
const attendQuest = 'daily-checkin';
|
|
|
|
/// 호스트 PC 주소. Android 에뮬레이터는 `10.0.2.2` 로 본다.
|
|
const host = String.fromEnvironment('FANUZU_EMULATOR_HOST', defaultValue: '10.0.2.2');
|
|
|
|
late Backend backend;
|
|
|
|
setUpAll(() async {
|
|
// `FirebaseBoot.init()` 을 쓰지 않는다. 그 경로는 Auth 에뮬레이터(9099)에도
|
|
// 붙는데, Auth 는 `firebase` CLI 안에만 있어 이 환경에서 뜨지 않는다(BUILD.md 2-1).
|
|
// 거기서 실패하면 Firestore 연결까지 건너뛰어 앱이 실 서버를 바라본다.
|
|
//
|
|
// 이름 있는 앱을 따로 띄운다. Android 는 `google-services.json` 으로 `[DEFAULT]`
|
|
// 앱을 자동 초기화하므로 그 자리를 덮을 수 없고, 덮지 않으면 프로젝트 ID 가
|
|
// 달라 에뮬레이터의 다른 칸을 읽는다.
|
|
final app = await Firebase.initializeApp(
|
|
name: 'fanuzu-emulator-check',
|
|
options: const FirebaseOptions(
|
|
apiKey: 'demo-key',
|
|
appId: '1:0:android:0',
|
|
messagingSenderId: '0',
|
|
projectId: 'demo-fanuzu',
|
|
),
|
|
);
|
|
final db = FirebaseFirestore.instanceFor(app: app)
|
|
..useFirestoreEmulator(host, 8080);
|
|
backend = Backend(
|
|
firestore: db,
|
|
auth: MockFirebaseAuth(
|
|
signedIn: true,
|
|
mockUser: MockUser(uid: uid, email: 'fan@fanuzu.co.kr'),
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('에뮬레이터를 물고 서버가 쓴 문서가 보인다', (tester) async {
|
|
final user = await backend.db.doc('users/$uid').get();
|
|
expect(user.exists, isTrue,
|
|
reason: '서버 E2E(quest_e2e.test.ts)를 먼저 돌려 상태를 만드십시오');
|
|
// 출석·후원을 지난 뒤의 서버 값이다. 앱이 쓴 값이 아니다.
|
|
expect(user.data()!['totalContributedPop'], 3000);
|
|
// 레벨은 누적 기여 POP 이 아니라 User EXP 에서 나온다(C-18) — 출석 1건 × 100(후원은 EXP 대상 아님).
|
|
expect(user.data()!['userExp'], 100);
|
|
expect(user.data()!['userLevel'], 1);
|
|
// Passport Tier 는 사용자 문서가 아니라 팬덤별 Passport 문서다(10/3 #10).
|
|
expect(user.data()!['passportTier'], isNull);
|
|
});
|
|
|
|
testWidgets('HOME 이 출석 보상 잔액과 행성 성장 수치를 서버 값에서 읽는다', (tester) async {
|
|
final snapshot = await FirestoreHomeRepository(backend).load();
|
|
|
|
// 출석 1회 보상 10 POP — `fn-quest-attend` 가 원장을 지나 올린 활동 잔액.
|
|
expect(snapshot.popBalance, 10);
|
|
// 공동 성장 = 출석 보상 10 + 후원 3,000. 개인 기여와 구별한다.
|
|
expect(snapshot.growth.currentPop, 3010);
|
|
// 공동 Stage가 아직 없는 시나리오이므로 진행률은 확정하지 않는다.
|
|
expect(snapshot.growth.growthPercent, isNull);
|
|
expect(snapshot.growth.fandomName, '퀘스트 루프 팬덤');
|
|
});
|
|
|
|
testWidgets('MY 가 누적 기여와 등급을 서버 값에서 읽는다', (tester) async {
|
|
final profile = await FirestoreMyRepository(backend).profile();
|
|
|
|
expect(profile.contributedPop, 3000);
|
|
// 레벨은 User EXP 기반이다(C-18). 기여 POP 3,000 이 칭호를 올리지 않는다.
|
|
expect(profile.userLevel, greaterThanOrEqualTo(1));
|
|
expect(profile.passportTier, PassportTier.silver);
|
|
expect(profile.balancePop, 10);
|
|
expect(profile.planetTiers.single.fandomName, '퀘스트 루프 팬덤');
|
|
expect(profile.planetTiers.single.contributedPop, 3000);
|
|
});
|
|
|
|
testWidgets('같은 퀘스트를 다시 참여하면 서버가 막은 자국이 남아 있다', (tester) async {
|
|
// 중복 참여 거부(E-POP-03)는 서버가 판정한다. 앱에서 보이는 자국은 진행 카운터다 —
|
|
// 같은 날 두 번 눌러도 1 을 넘지 않는다.
|
|
//
|
|
// **경로가 퀘스트 문서 ID 다.** 앱이 목록을 그릴 때 읽는 자리와 서버가 쓰는 자리가
|
|
// 같아야 화면이 「보상완료」로 넘어간다.
|
|
final progress =
|
|
await backend.db.doc('users/$uid/questProgress/$attendQuest').get();
|
|
expect(progress.exists, isTrue);
|
|
expect(progress.data()!['count'], 1);
|
|
expect(progress.data()!['questId'], attendQuest);
|
|
|
|
final ledger = await backend.db
|
|
.collection('users/$uid/popTransactions')
|
|
.where('reasonCode', isEqualTo: 'QUEST_ATTEND')
|
|
.get();
|
|
expect(ledger.size, 1);
|
|
});
|
|
}
|