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>
271 lines
7.8 KiB
Dart
271 lines
7.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'dart:async';
|
|
|
|
import 'package:tasq/models/profile.model.dart';
|
|
import 'package:tasq/models/task.model.dart';
|
|
import 'package:tasq/models/task_assignment.dart';
|
|
import 'package:tasq/providers/profile_provider.dart';
|
|
import 'package:tasq/providers/tasks_provider.dart';
|
|
import 'package:tasq/utils/app_time.dart';
|
|
import 'package:tasq/widgets/task_assignment_section.dart';
|
|
|
|
void main() {
|
|
setUp(() {
|
|
AppTime.initialize();
|
|
});
|
|
|
|
testWidgets(
|
|
'allows assigning IT staff to a task even when they are already on another active task',
|
|
(tester) async {
|
|
// prepare two it_staff profiles
|
|
final profile1 = Profile(
|
|
id: 'u1',
|
|
fullName: 'User One',
|
|
role: 'it_staff',
|
|
);
|
|
final profile2 = Profile(
|
|
id: 'u2',
|
|
fullName: 'User Two',
|
|
role: 'it_staff',
|
|
);
|
|
|
|
// create two active tasks; staff u1 is already assigned to the first
|
|
final now = DateTime.now();
|
|
final task1 = Task.fromMap({
|
|
'id': 't1',
|
|
'status': 'queued',
|
|
'title': 'First task',
|
|
'description': '',
|
|
'created_at': now.toIso8601String(),
|
|
'priority': 1,
|
|
});
|
|
final task2 = Task.fromMap({
|
|
'id': 't2',
|
|
'status': 'queued',
|
|
'title': 'Second task',
|
|
'description': '',
|
|
'created_at': now.toIso8601String(),
|
|
'priority': 1,
|
|
});
|
|
|
|
final assignment1 = TaskAssignment(
|
|
taskId: 't1',
|
|
userId: 'u1',
|
|
createdAt: now,
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
profilesProvider.overrideWith(
|
|
(ref) => Stream.value([profile1, profile2]),
|
|
),
|
|
tasksProvider.overrideWith((ref) => Stream.value([task1, task2])),
|
|
taskAssignmentsProvider.overrideWith(
|
|
(ref) => Stream.value([assignment1]),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: TaskAssignmentSection(taskId: 't2', canAssign: true),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
// allow provider streams to deliver their first value
|
|
await tester.pumpAndSettle();
|
|
|
|
// open the assignment dialog
|
|
await tester.tap(find.text('Assign'));
|
|
await tester.pumpAndSettle();
|
|
|
|
// both IT staff users should be listed even though u1 is already busy
|
|
expect(find.byType(CheckboxListTile), findsNWidgets(2));
|
|
expect(find.widgetWithText(CheckboxListTile, 'User One'), findsOneWidget);
|
|
expect(find.widgetWithText(CheckboxListTile, 'User Two'), findsOneWidget);
|
|
},
|
|
);
|
|
|
|
testWidgets('dialog shows message when there are no IT staff at all', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
profilesProvider.overrideWith(
|
|
(ref) => Stream.value(const <Profile>[]),
|
|
),
|
|
tasksProvider.overrideWith(
|
|
(ref) => Stream.value(<Task>[
|
|
Task.fromMap({
|
|
'id': 't',
|
|
'status': 'queued',
|
|
'title': 't',
|
|
'description': '',
|
|
'created_at': DateTime.now().toIso8601String(),
|
|
'priority': 1,
|
|
}),
|
|
]),
|
|
),
|
|
taskAssignmentsProvider.overrideWith(
|
|
(ref) => Stream.value(const <TaskAssignment>[]),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: TaskAssignmentSection(taskId: 't', canAssign: true),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('Assign'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('No IT staff available.'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('save button shows spinner and snack on success', (tester) async {
|
|
final profile = Profile(id: 'u1', fullName: 'User One', role: 'it_staff');
|
|
final task = Task.fromMap({
|
|
'id': 't',
|
|
'status': 'queued',
|
|
'title': 't',
|
|
'description': '',
|
|
'created_at': DateTime.now().toIso8601String(),
|
|
'priority': 1,
|
|
});
|
|
final completer = Completer<void>();
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
profilesProvider.overrideWith((ref) => Stream.value([profile])),
|
|
tasksProvider.overrideWith((ref) => Stream.value([task])),
|
|
taskAssignmentsProvider.overrideWith(
|
|
(ref) => Stream.value(const <TaskAssignment>[]),
|
|
),
|
|
taskAssignmentsControllerProvider.overrideWith(
|
|
(ref) => _DelayedController(completer),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: TaskAssignmentSection(taskId: 't', canAssign: true),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
// open dialog and select the only staff
|
|
await tester.tap(find.text('Assign'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.byType(CheckboxListTile));
|
|
await tester.pump();
|
|
|
|
// tap save, spinner should appear
|
|
await tester.tap(find.text('Save'));
|
|
await tester.pump();
|
|
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
|
|
|
// resolve future and let dialog close
|
|
completer.complete();
|
|
await tester.pumpAndSettle();
|
|
|
|
// snackbar with success message should be visible
|
|
expect(find.text('Assignment saved successfully'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('error snackbar shows when save fails', (tester) async {
|
|
final profile = Profile(id: 'u1', fullName: 'User One', role: 'it_staff');
|
|
final task = Task.fromMap({
|
|
'id': 't',
|
|
'status': 'queued',
|
|
'title': 't',
|
|
'description': '',
|
|
'created_at': DateTime.now().toIso8601String(),
|
|
'priority': 1,
|
|
});
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
profilesProvider.overrideWith((ref) => Stream.value([profile])),
|
|
tasksProvider.overrideWith((ref) => Stream.value([task])),
|
|
taskAssignmentsProvider.overrideWith(
|
|
(ref) => Stream.value(const <TaskAssignment>[]),
|
|
),
|
|
taskAssignmentsControllerProvider.overrideWith(
|
|
(ref) => _FailingController(),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: TaskAssignmentSection(taskId: 't', canAssign: true),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.text('Assign'));
|
|
await tester.pumpAndSettle();
|
|
await tester.tap(find.byType(CheckboxListTile));
|
|
await tester.pump();
|
|
|
|
await tester.tap(find.text('Save'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Failed to save assignment'), findsOneWidget);
|
|
});
|
|
}
|
|
|
|
// helper fakes used by the tests below
|
|
class _DelayedController implements TaskAssignmentsController {
|
|
_DelayedController(this._completer);
|
|
final Completer<void> _completer;
|
|
|
|
@override
|
|
Future<void> replaceAssignments({
|
|
required String taskId,
|
|
required String? ticketId,
|
|
required List<String> newUserIds,
|
|
required List<String> currentUserIds,
|
|
}) {
|
|
return _completer.future;
|
|
}
|
|
|
|
@override
|
|
Future<void> removeAssignment({
|
|
required String taskId,
|
|
required String userId,
|
|
}) {
|
|
// not needed for these tests
|
|
return Future.value();
|
|
}
|
|
}
|
|
|
|
class _FailingController implements TaskAssignmentsController {
|
|
@override
|
|
Future<void> replaceAssignments({
|
|
required String taskId,
|
|
required String? ticketId,
|
|
required List<String> newUserIds,
|
|
required List<String> currentUserIds,
|
|
}) async {
|
|
throw Exception('server error');
|
|
}
|
|
|
|
@override
|
|
Future<void> removeAssignment({
|
|
required String taskId,
|
|
required String userId,
|
|
}) {
|
|
return Future.value();
|
|
}
|
|
}
|