3cb980b629
Security — enforce write authorization server-side (was UI/RPC-only): - it_service_requests RLS: block cross-office read/edit + self-approve (QA-015) - pass_slips RLS: owner can complete but not self-approve (QA-046) - swap_requests RLS: scope select/update to participants + admin (QA-047) - storage: tighten it_service_attachments + task_attachments write/delete (QA-027) - admin_user_management edge function: allow programmers to manage users (QA-016) Fixes: - workforce generator "uncovered shifts" false alarms (QA-043/044) - network-map VLAN + New-location dialog validation, disabled-until-valid (QA-048) - de-flake time-of-day-dependent dashboard metrics test (QA-045) Toolchain: - upgrade to Flutter 3.47.5 / Dart 3.13.4; font_awesome_flutter 11.0.0, flutter_quill 11.6.0, pdfrx 2.6.5; clear resulting deprecations (QA-002) analyze clean; 139 tests pass; web build succeeds. Report + evidence in docs/qa/. Note: also carries the in-progress Brick model cleanup already present in the working tree. QA-001 (AI keys public in the build) is deferred by owner decision. Co-Authored-By: claude-flow <ruv@ruv.net>
216 lines
7.3 KiB
Dart
216 lines
7.3 KiB
Dart
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.model.dart';
|
|
import 'package:tasq/models/profile.model.dart';
|
|
import 'package:tasq/models/ticket_message.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',
|
|
authOptions: const AuthClientOptions(autoRefreshToken: false)));
|
|
|
|
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: Scaffold(body: 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.binding.setSurfaceSize(const Size(1280, 900));
|
|
addTearDown(() async => await tester.binding.setSurfaceSize(null));
|
|
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) => Stream.value(const <TicketMessage>[])),
|
|
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));
|
|
for (var i = 0; i < 20; i++) {
|
|
if (find.text('Alice Admin').evaluate().isNotEmpty) break;
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
}
|
|
|
|
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.binding.setSurfaceSize(const Size(1280, 900));
|
|
addTearDown(() async => await tester.binding.setSurfaceSize(null));
|
|
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) => Stream.value(const <TicketMessage>[])),
|
|
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));
|
|
for (var i = 0; i < 20; i++) {
|
|
if (find.text('Alice Admin').evaluate().isNotEmpty) break;
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
}
|
|
|
|
// 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);
|
|
});
|
|
}
|