feat: 切换邮箱认证并重构前后端启动与门禁

This commit is contained in:
qzl
2026-04-02 18:39:35 +08:00
parent 92cdfd9fca
commit 31594558eb
116 changed files with 5608 additions and 628 deletions
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meeyao_qianwen/core/network/api_problem.dart';
import 'package:meeyao_qianwen/core/network/api_problem_mapper.dart';
import 'package:meeyao_qianwen/l10n/app_localizations.dart';
void main() {
testWidgets('map by code uses localized message', (tester) async {
late AppLocalizations l10n;
await tester.pumpWidget(
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (context) {
l10n = AppLocalizations.of(context)!;
return const SizedBox.shrink();
},
),
),
);
final message = mapApiProblemToMessage(
ApiProblem(
status: 401,
title: 'Unauthorized',
detail: 'Invalid verification code',
code: 'AUTH_VERIFICATION_CODE_INVALID',
),
l10n,
);
expect(message, l10n.errorInvalidVerificationCode);
});
}
@@ -0,0 +1,87 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:meeyao_qianwen/features/auth/data/models/auth_user.dart';
import 'package:meeyao_qianwen/features/auth/data/repositories/auth_repository.dart';
import 'package:meeyao_qianwen/features/auth/presentation/bloc/auth_bloc.dart';
import 'package:meeyao_qianwen/features/auth/presentation/bloc/auth_state.dart';
class _FakeAuthRepository implements AuthRepository {
_FakeAuthRepository({this.recoveredUser, this.throwOnLogout = false});
AuthUser? recoveredUser;
bool clearCalled = false;
bool logoutCalled = false;
bool throwOnLogout;
@override
Future<void> clearLocalSession() async {
clearCalled = true;
}
@override
Future<AuthUser> loginWithEmailOtp({
required String email,
required String otp,
}) async {
return AuthUser(id: 'u1', email: email);
}
@override
Future<void> logout() async {
logoutCalled = true;
if (throwOnLogout) {
throw Exception('logout failed');
}
}
@override
Future<AuthUser?> recoverSession() async {
return recoveredUser;
}
@override
Future<void> sendOtp(String email) async {}
}
void main() {
test('start should become authenticated when recover success', () async {
final repo = _FakeAuthRepository(
recoveredUser: const AuthUser(id: 'u1', email: 'a@b.com'),
);
final bloc = AuthBloc(repository: repo);
await bloc.start();
expect(bloc.state.status, AuthStatus.authenticated);
expect(bloc.state.user?.email, 'a@b.com');
});
test('handleUnauthorized401 should clear local session and unauth', () async {
final repo = _FakeAuthRepository(
recoveredUser: const AuthUser(id: 'u1', email: 'a@b.com'),
);
final bloc = AuthBloc(repository: repo);
await bloc.start();
await bloc.handleUnauthorized401();
expect(repo.clearCalled, isTrue);
expect(bloc.state.status, AuthStatus.unauthenticated);
});
test(
'logout should set unauthenticated even when repository throws',
() async {
final repo = _FakeAuthRepository(
recoveredUser: const AuthUser(id: 'u1', email: 'a@b.com'),
throwOnLogout: true,
);
final bloc = AuthBloc(repository: repo);
await bloc.start();
await expectLater(bloc.logout(), throwsA(isA<Exception>()));
expect(repo.logoutCalled, isTrue);
expect(bloc.state.status, AuthStatus.unauthenticated);
},
);
}
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meeyao_qianwen/core/auth/session_store.dart';
import 'package:meeyao_qianwen/app/app_theme.dart';
import 'package:meeyao_qianwen/data/storage/local_kv_store.dart';
import 'package:meeyao_qianwen/features/home/presentation/screens/home_screen.dart';
import 'package:meeyao_qianwen/l10n/app_localizations.dart';
class _FakeSessionStore extends SessionStore {
_FakeSessionStore({required this.hasReadWelcomeValue})
: super(LocalKvStore());
bool hasReadWelcomeValue;
bool setWelcomeReadCalled = false;
@override
Future<bool> hasReadWelcome() async {
return hasReadWelcomeValue;
}
@override
Future<void> setWelcomeRead(bool value) async {
setWelcomeReadCalled = value;
}
}
void main() {
testWidgets('history cards should use full available width', (tester) async {
final sessionStore = _FakeSessionStore(hasReadWelcomeValue: true);
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: HomeScreen(
account: 'user@example.com',
sessionStore: sessionStore,
onLogout: () async {},
),
),
);
await tester.pumpAndSettle();
final historyCard = find.byType(Card).first;
final cardWidth = tester.getSize(historyCard).width;
final viewportWidth =
tester.view.physicalSize.width / tester.view.devicePixelRatio;
expect(cardWidth, viewportWidth);
});
}
+4 -24
View File
@@ -1,30 +1,10 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meeyao_qianwen/main.dart';
import 'package:meeyao_qianwen/app/app.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
testWidgets('app bootstraps', (WidgetTester tester) async {
await tester.pumpWidget(const EryaoApp());
expect(find.byType(EryaoApp), findsOneWidget);
});
}