Fixed user management
This commit is contained in:
parent
af6cfe76b4
commit
8c1bb7646e
|
|
@ -75,63 +75,66 @@ class AdminUserController {
|
||||||
await _client.from('profiles').update({'role': role}).eq('id', userId);
|
await _client.from('profiles').update({'role': role}).eq('id', userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Password administration — forwarded to the admin Edge Function.
|
// Centralized helper that calls the admin Edge Function and surfaces
|
||||||
|
// a clear error for 401/bad_jwt so the UI can react (sign out/reauth).
|
||||||
|
Future<dynamic> _invokeAdminFunction(
|
||||||
|
String action,
|
||||||
|
Map<String, dynamic> payload,
|
||||||
|
) async {
|
||||||
|
final accessToken = _client.auth.currentSession?.accessToken;
|
||||||
|
final response = await _client.functions.invoke(
|
||||||
|
'admin_user_management',
|
||||||
|
body: {'action': action, ...payload},
|
||||||
|
headers: accessToken == null
|
||||||
|
? null
|
||||||
|
: {'Authorization': 'Bearer $accessToken'},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status == 401) {
|
||||||
|
// If the gateway rejects the JWT, proactively clear the local session
|
||||||
|
// so the app can re-authenticate and obtain a valid Supabase token.
|
||||||
|
try {
|
||||||
|
await _client.auth.signOut();
|
||||||
|
} catch (_) {
|
||||||
|
// ignore sign-out errors
|
||||||
|
}
|
||||||
|
throw Exception(
|
||||||
|
'Unauthorized: invalid or expired session token (bad_jwt)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status != 200) {
|
||||||
|
throw Exception(response.data ?? 'Admin request failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setPassword({
|
Future<void> setPassword({
|
||||||
required String userId,
|
required String userId,
|
||||||
required String password,
|
required String password,
|
||||||
}) async {
|
}) async {
|
||||||
final payload = {
|
if (password.length < 8) {
|
||||||
'action': 'set_password',
|
throw Exception('Password must be at least 8 characters');
|
||||||
|
}
|
||||||
|
|
||||||
|
await _invokeAdminFunction('set_password', {
|
||||||
'userId': userId,
|
'userId': userId,
|
||||||
'password': password,
|
'password': password,
|
||||||
};
|
});
|
||||||
final accessToken = _client.auth.currentSession?.accessToken;
|
|
||||||
final response = await _client.functions.invoke(
|
|
||||||
'admin_user_management',
|
|
||||||
body: payload,
|
|
||||||
headers: accessToken == null
|
|
||||||
? null
|
|
||||||
: {'Authorization': 'Bearer $accessToken'},
|
|
||||||
);
|
|
||||||
if (response.status != 200) {
|
|
||||||
throw Exception(response.data ?? 'Failed to reset password');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set/unset a user's ban/lock via the admin Edge Function (preferred).
|
|
||||||
Future<void> setLock({required String userId, required bool locked}) async {
|
Future<void> setLock({required String userId, required bool locked}) async {
|
||||||
final payload = {'action': 'set_lock', 'userId': userId, 'locked': locked};
|
await _invokeAdminFunction('set_lock', {
|
||||||
final accessToken = _client.auth.currentSession?.accessToken;
|
'userId': userId,
|
||||||
final response = await _client.functions.invoke(
|
'locked': locked,
|
||||||
'admin_user_management',
|
});
|
||||||
body: payload,
|
|
||||||
headers: accessToken == null
|
|
||||||
? null
|
|
||||||
: {'Authorization': 'Bearer $accessToken'},
|
|
||||||
);
|
|
||||||
if (response.status != 200) {
|
|
||||||
throw Exception(response.data ?? 'Failed to update lock state');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch user email + banned state from the admin Edge Function (auth.user).
|
|
||||||
Future<AdminUserStatus> fetchStatus(String userId) async {
|
Future<AdminUserStatus> fetchStatus(String userId) async {
|
||||||
final payload = {'action': 'get_user', 'userId': userId};
|
final data = await _invokeAdminFunction('get_user', {'userId': userId});
|
||||||
final accessToken = _client.auth.currentSession?.accessToken;
|
final user =
|
||||||
final response = await _client.functions.invoke(
|
(data as Map<String, dynamic>?)?['user'] as Map<String, dynamic>?;
|
||||||
'admin_user_management',
|
|
||||||
body: payload,
|
|
||||||
headers: accessToken == null
|
|
||||||
? null
|
|
||||||
: {'Authorization': 'Bearer $accessToken'},
|
|
||||||
);
|
|
||||||
if (response.status != 200) {
|
|
||||||
return AdminUserStatus(email: null, bannedUntil: null);
|
|
||||||
}
|
|
||||||
final data = response.data;
|
|
||||||
final user = (data is Map<String, dynamic>)
|
|
||||||
? (data['user'] as Map<String, dynamic>?)
|
|
||||||
: null;
|
|
||||||
final email = user?['email'] as String?;
|
final email = user?['email'] as String?;
|
||||||
DateTime? bannedUntil;
|
DateTime? bannedUntil;
|
||||||
final bannedRaw = user?['banned_until'];
|
final bannedRaw = user?['banned_until'];
|
||||||
|
|
@ -146,27 +149,15 @@ class AdminUserController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Server-side paginated listing via Edge Function (returns auth + profile light view).
|
|
||||||
Future<List<Map<String, dynamic>>> listUsers(AdminUserQuery q) async {
|
Future<List<Map<String, dynamic>>> listUsers(AdminUserQuery q) async {
|
||||||
final payload = {
|
final data = await _invokeAdminFunction('list_users', {
|
||||||
'action': 'list_users',
|
|
||||||
'offset': q.offset,
|
'offset': q.offset,
|
||||||
'limit': q.limit,
|
'limit': q.limit,
|
||||||
'searchQuery': q.searchQuery,
|
'searchQuery': q.searchQuery,
|
||||||
};
|
});
|
||||||
final accessToken = _client.auth.currentSession?.accessToken;
|
|
||||||
final response = await _client.functions.invoke(
|
final users = (data is Map && data['users'] is List)
|
||||||
'admin_user_management',
|
? (data['users'] as List).cast<Map<String, dynamic>>()
|
||||||
body: payload,
|
|
||||||
headers: accessToken == null
|
|
||||||
? null
|
|
||||||
: {'Authorization': 'Bearer $accessToken'},
|
|
||||||
);
|
|
||||||
if (response.status != 200) {
|
|
||||||
throw Exception(response.data ?? 'Failed to list users');
|
|
||||||
}
|
|
||||||
final users = (response.data is Map && response.data['users'] is List)
|
|
||||||
? (response.data['users'] as List).cast<Map<String, dynamic>>()
|
|
||||||
: <Map<String, dynamic>>[];
|
: <Map<String, dynamic>>[];
|
||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,14 +144,17 @@ final appRouterProvider = Provider<GoRouter>((ref) {
|
||||||
class RouterNotifier extends ChangeNotifier {
|
class RouterNotifier extends ChangeNotifier {
|
||||||
RouterNotifier(this.ref) {
|
RouterNotifier(this.ref) {
|
||||||
_authSub = ref.listen(authStateChangesProvider, (previous, next) {
|
_authSub = ref.listen(authStateChangesProvider, (previous, next) {
|
||||||
// Enforce app-level profile lock when a session becomes available.
|
// Enforce auth-level ban when a session becomes available.
|
||||||
next.whenData((authState) {
|
next.maybeWhen(
|
||||||
|
data: (authState) {
|
||||||
final session = authState.session;
|
final session = authState.session;
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
// Fire-and-forget enforcement (best-effort client-side sign-out)
|
// Fire-and-forget enforcement (best-effort client-side sign-out)
|
||||||
enforceLockForCurrentUser(ref.read(supabaseClientProvider));
|
enforceLockForCurrentUser(ref.read(supabaseClientProvider));
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
orElse: () {},
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
});
|
});
|
||||||
_profileSub = ref.listen(currentProfileProvider, (previous, next) {
|
_profileSub = ref.listen(currentProfileProvider, (previous, next) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import '../../models/profile.dart';
|
||||||
import '../../models/ticket_message.dart';
|
import '../../models/ticket_message.dart';
|
||||||
import '../../models/user_office.dart';
|
import '../../models/user_office.dart';
|
||||||
import '../../providers/admin_user_provider.dart';
|
import '../../providers/admin_user_provider.dart';
|
||||||
|
import '../../providers/auth_provider.dart';
|
||||||
import '../../providers/profile_provider.dart';
|
import '../../providers/profile_provider.dart';
|
||||||
import '../../providers/tickets_provider.dart';
|
import '../../providers/tickets_provider.dart';
|
||||||
import '../../theme/app_surfaces.dart';
|
import '../../theme/app_surfaces.dart';
|
||||||
|
|
@ -165,7 +166,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
||||||
return statusAsync.when(
|
return statusAsync.when(
|
||||||
data: (s) => Text(s.email ?? 'Unknown'),
|
data: (s) => Text(s.email ?? 'Unknown'),
|
||||||
loading: () => const Text('Loading...'),
|
loading: () => const Text('Loading...'),
|
||||||
error: (_, __) => const Text('Unknown'),
|
error: (error, stack) => const Text('Unknown'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -190,7 +191,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
||||||
return _StatusBadge(label: statusLabel);
|
return _StatusBadge(label: statusLabel);
|
||||||
},
|
},
|
||||||
loading: () => _StatusBadge(label: 'Loading'),
|
loading: () => _StatusBadge(label: 'Loading'),
|
||||||
error: (_, __) => _StatusBadge(label: 'Unknown'),
|
error: (error, stack) => _StatusBadge(label: 'Unknown'),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -227,7 +228,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
||||||
statusAsync.when(
|
statusAsync.when(
|
||||||
data: (s) => Text('Email: ${s.email ?? 'Unknown'}'),
|
data: (s) => Text('Email: ${s.email ?? 'Unknown'}'),
|
||||||
loading: () => const Text('Email: Loading...'),
|
loading: () => const Text('Email: Loading...'),
|
||||||
error: (_, __) => const Text('Email: Unknown'),
|
error: (error, stack) => const Text('Email: Unknown'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -235,7 +236,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
||||||
data: (s) =>
|
data: (s) =>
|
||||||
_StatusBadge(label: s.isLocked ? 'Locked' : 'Active'),
|
_StatusBadge(label: s.isLocked ? 'Locked' : 'Active'),
|
||||||
loading: () => _StatusBadge(label: 'Loading'),
|
loading: () => _StatusBadge(label: 'Loading'),
|
||||||
error: (_, __) => _StatusBadge(label: 'Unknown'),
|
error: (error, stack) => _StatusBadge(label: 'Unknown'),
|
||||||
),
|
),
|
||||||
onTap: () =>
|
onTap: () =>
|
||||||
_showUserDialog(context, profile, offices, assignments),
|
_showUserDialog(context, profile, offices, assignments),
|
||||||
|
|
@ -286,6 +287,18 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
||||||
.map((assignment) => assignment.officeId)
|
.map((assignment) => assignment.officeId)
|
||||||
.toSet();
|
.toSet();
|
||||||
|
|
||||||
|
// Populate dialog-backed state so form fields reflect the selected user.
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_selectedUserId = profile.id;
|
||||||
|
_selectedRole = profile.role;
|
||||||
|
_fullNameController.text = profile.fullName;
|
||||||
|
_selectedOfficeIds
|
||||||
|
..clear()
|
||||||
|
..addAll(currentOfficeIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
await showDialog<void>(
|
await showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -318,6 +331,17 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Clear the temporary selection state after the dialog is closed so the
|
||||||
|
// next dialog starts from a clean slate.
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_selectedUserId = null;
|
||||||
|
_selectedRole = null;
|
||||||
|
_selectedOfficeIds.clear();
|
||||||
|
_fullNameController.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUserForm(
|
Widget _buildUserForm(
|
||||||
|
|
@ -363,7 +387,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
||||||
'Email: Loading...',
|
'Email: Loading...',
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
error: (_, __) => Text(
|
error: (error, stack) => Text(
|
||||||
'Email: Unknown',
|
'Email: Unknown',
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
|
|
@ -392,7 +416,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
||||||
icon: const Icon(Icons.lock),
|
icon: const Icon(Icons.lock),
|
||||||
label: const Text('Loading...'),
|
label: const Text('Loading...'),
|
||||||
),
|
),
|
||||||
error: (_, __) => OutlinedButton.icon(
|
error: (error, stack) => OutlinedButton.icon(
|
||||||
onPressed: _isSaving
|
onPressed: _isSaving
|
||||||
? null
|
? null
|
||||||
: () => _toggleLock(profile.id, true),
|
: () => _toggleLock(profile.id, true),
|
||||||
|
|
@ -578,6 +602,22 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
||||||
const SnackBar(content: Text('Password updated.')),
|
const SnackBar(content: Text('Password updated.')),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
final msg = error.toString();
|
||||||
|
if (msg.contains('Unauthorized') ||
|
||||||
|
msg.contains('bad_jwt') ||
|
||||||
|
msg.contains('expired')) {
|
||||||
|
await ref.read(authControllerProvider).signOut();
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Session expired — please sign in again.',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Reset failed: $error')),
|
SnackBar(content: Text('Reset failed: $error')),
|
||||||
|
|
@ -613,6 +653,20 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
final msg = error.toString();
|
||||||
|
if (msg.contains('Unauthorized') ||
|
||||||
|
msg.contains('bad_jwt') ||
|
||||||
|
msg.contains('expired')) {
|
||||||
|
await ref.read(authControllerProvider).signOut();
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Session expired — please sign in again.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,42 @@ serve(async (req) => {
|
||||||
});
|
});
|
||||||
const { data: authData, error: authError } =
|
const { data: authData, error: authError } =
|
||||||
await authClient.auth.getUser();
|
await authClient.auth.getUser();
|
||||||
|
|
||||||
|
// DEBUG: log auth results to help diagnose intermittent 401s from the
|
||||||
|
// gateway / auth service. Remove these logs before deploying to production.
|
||||||
|
console.log("admin_user_management: token-snippet", token.slice(0, 16));
|
||||||
|
console.log("admin_user_management: authData", JSON.stringify(authData));
|
||||||
|
console.log("admin_user_management: authError", JSON.stringify(authError));
|
||||||
|
|
||||||
if (authError || !authData?.user) {
|
if (authError || !authData?.user) {
|
||||||
return jsonResponse({ error: "Unauthorized" }, 401);
|
// Extract token header (kid/alg) for debugging without revealing the full
|
||||||
|
// token. This helps confirm which key the gateway/runtime attempted to
|
||||||
|
// verify against.
|
||||||
|
let tokenHeader: unknown = null;
|
||||||
|
try {
|
||||||
|
const headerB64 = token.split(".")[0] ?? "";
|
||||||
|
tokenHeader = JSON.parse(atob(headerB64));
|
||||||
|
} catch (e) {
|
||||||
|
tokenHeader = { parseError: (e as Error).message };
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonResponse(
|
||||||
|
{
|
||||||
|
error: "Unauthorized",
|
||||||
|
debug: {
|
||||||
|
authError: authError?.message ?? null,
|
||||||
|
tokenHeader,
|
||||||
|
authDataUser: authData?.user
|
||||||
|
? {
|
||||||
|
id: authData.user.id,
|
||||||
|
email: authData.user.email ?? null,
|
||||||
|
banned_until: authData.user.banned_until ?? null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
401,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminClient = createClient(supabaseUrl, serviceKey);
|
const adminClient = createClient(supabaseUrl, serviceKey);
|
||||||
|
|
|
||||||
201
test/user_management_screen_test.dart
Normal file
201
test/user_management_screen_test.dart
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
import 'package:tasq/models/office.dart';
|
||||||
|
import 'package:tasq/models/profile.dart';
|
||||||
|
import 'package:tasq/models/user_office.dart';
|
||||||
|
import 'package:tasq/providers/profile_provider.dart';
|
||||||
|
|
||||||
|
import 'package:tasq/providers/user_offices_provider.dart';
|
||||||
|
import 'package:tasq/providers/admin_user_provider.dart';
|
||||||
|
import 'package:tasq/providers/tickets_provider.dart';
|
||||||
|
import 'package:tasq/screens/admin/user_management_screen.dart';
|
||||||
|
|
||||||
|
class _FakeAdminController extends AdminUserController {
|
||||||
|
_FakeAdminController()
|
||||||
|
: super(SupabaseClient('http://localhost', 'test-key'));
|
||||||
|
|
||||||
|
String? lastSetPasswordUserId;
|
||||||
|
String? lastSetPasswordValue;
|
||||||
|
|
||||||
|
String? lastSetLockUserId;
|
||||||
|
bool? lastSetLockedValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setPassword({
|
||||||
|
required String userId,
|
||||||
|
required String password,
|
||||||
|
}) async {
|
||||||
|
lastSetPasswordUserId = userId;
|
||||||
|
lastSetPasswordValue = password;
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setLock({required String userId, required bool locked}) async {
|
||||||
|
lastSetLockUserId = userId;
|
||||||
|
lastSetLockedValue = locked;
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final office = Office(id: 'office-1', name: 'HQ');
|
||||||
|
final profile = Profile(id: 'user-1', role: 'admin', fullName: 'Alice Admin');
|
||||||
|
|
||||||
|
ProviderScope buildApp({required List<Override> overrides}) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: overrides,
|
||||||
|
child: const MaterialApp(home: UserManagementScreen()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('sanity - basic Text widgets render', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(home: Scaffold(body: Text('SANITY'))),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('SANITY'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Edit dialog pre-fills fields and shows actions', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
overrides: [
|
||||||
|
currentProfileProvider.overrideWith((ref) => Stream.value(profile)),
|
||||||
|
profilesProvider.overrideWith((ref) => Stream.value([profile])),
|
||||||
|
officesProvider.overrideWith((ref) => Stream.value([office])),
|
||||||
|
userOfficesProvider.overrideWith(
|
||||||
|
(ref) => Stream.value([
|
||||||
|
UserOffice(userId: 'user-1', officeId: 'office-1'),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
ticketMessagesAllProvider.overrideWith((ref) => const Stream.empty()),
|
||||||
|
isAdminProvider.overrideWith((ref) => true),
|
||||||
|
// Provide an AdminUserStatus so the dialog can show email/status immediately.
|
||||||
|
adminUserStatusProvider.overrideWithProvider(
|
||||||
|
FutureProvider.autoDispose.family<AdminUserStatus, String>(
|
||||||
|
(ref, id) async => AdminUserStatus(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
bannedUntil: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 16));
|
||||||
|
|
||||||
|
expect(find.text('Alice Admin'), findsOneWidget);
|
||||||
|
|
||||||
|
// Open edit dialog
|
||||||
|
await tester.tap(find.text('Alice Admin'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 16));
|
||||||
|
|
||||||
|
// Dialog should show and the Full name TextField should contain the profile name
|
||||||
|
final alert = find.byType(AlertDialog);
|
||||||
|
expect(alert, findsOneWidget);
|
||||||
|
|
||||||
|
final editable = find.descendant(
|
||||||
|
of: alert,
|
||||||
|
matching: find.byType(EditableText),
|
||||||
|
);
|
||||||
|
expect(editable, findsWidgets);
|
||||||
|
|
||||||
|
// The first EditableText inside the dialog is the Full name field
|
||||||
|
final fullNameEditable = editable.first;
|
||||||
|
final et = tester.widget<EditableText>(fullNameEditable);
|
||||||
|
expect(et.controller.text, equals('Alice Admin'));
|
||||||
|
|
||||||
|
// Role should show selected value
|
||||||
|
expect(
|
||||||
|
find.descendant(of: alert, matching: find.text('admin')),
|
||||||
|
findsWidgets,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset password and Lock buttons must be visible
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: alert,
|
||||||
|
matching: find.widgetWithText(OutlinedButton, 'Reset password'),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: alert,
|
||||||
|
matching: find.widgetWithText(OutlinedButton, 'Lock'),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Reset password and lock call admin controller', (tester) async {
|
||||||
|
final fake = _FakeAdminController();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildApp(
|
||||||
|
overrides: [
|
||||||
|
currentProfileProvider.overrideWith((ref) => Stream.value(profile)),
|
||||||
|
profilesProvider.overrideWith((ref) => Stream.value([profile])),
|
||||||
|
officesProvider.overrideWith((ref) => Stream.value([office])),
|
||||||
|
userOfficesProvider.overrideWith(
|
||||||
|
(ref) => Stream.value([
|
||||||
|
UserOffice(userId: 'user-1', officeId: 'office-1'),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
ticketMessagesAllProvider.overrideWith((ref) => const Stream.empty()),
|
||||||
|
isAdminProvider.overrideWith((ref) => true),
|
||||||
|
adminUserStatusProvider.overrideWithProvider(
|
||||||
|
FutureProvider.autoDispose.family<AdminUserStatus, String>(
|
||||||
|
(ref, id) async => AdminUserStatus(
|
||||||
|
email: 'alice@example.com',
|
||||||
|
bannedUntil: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
adminUserControllerProvider.overrideWithValue(fake),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 16));
|
||||||
|
|
||||||
|
// Open edit dialog (again)
|
||||||
|
await tester.tap(find.text('Alice Admin'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 16));
|
||||||
|
|
||||||
|
// Tap Reset password
|
||||||
|
await tester.tap(find.widgetWithText(OutlinedButton, 'Reset password'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 16));
|
||||||
|
|
||||||
|
// Enter new password in the nested dialog
|
||||||
|
final pwdField = find.byType(TextFormField).last;
|
||||||
|
await tester.enterText(pwdField, 'new-pass-123');
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 16));
|
||||||
|
|
||||||
|
// Confirm update
|
||||||
|
await tester.tap(find.text('Update password'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 16));
|
||||||
|
|
||||||
|
expect(fake.lastSetPasswordUserId, equals('user-1'));
|
||||||
|
expect(fake.lastSetPasswordValue, equals('new-pass-123'));
|
||||||
|
|
||||||
|
// Back in the main dialog - press Lock
|
||||||
|
await tester.tap(find.widgetWithText(OutlinedButton, 'Lock'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(fake.lastSetLockUserId, equals('user-1'));
|
||||||
|
expect(fake.lastSetLockedValue, isTrue);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user