Files
tasq/test/dashboard_metrics_provider_test.dart
redz1029 3cb980b629 QA hardening: security RLS fixes, Flutter 3.47.5 upgrade, UI/validation fixes
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>
2026-09-26 11:52:16 +08:00

413 lines
14 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ignore_for_file: deprecated_member_use
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tasq/models/it_service_request.model.dart';
import 'package:tasq/models/it_service_request_assignment.model.dart';
import 'package:tasq/models/profile.model.dart';
import 'package:tasq/models/ticket.model.dart';
import 'package:tasq/models/task.model.dart';
import 'package:tasq/models/task_assignment.dart';
import 'package:tasq/models/ticket_message.dart';
import 'package:tasq/models/duty_schedule.model.dart';
import 'package:tasq/models/attendance_log.model.dart';
import 'package:tasq/models/live_position.dart';
import 'package:tasq/models/leave_of_absence.model.dart';
import 'package:tasq/models/pass_slip.model.dart';
import 'package:tasq/models/team.model.dart';
import 'package:tasq/models/team_member.dart';
import 'package:tasq/screens/dashboard/dashboard_screen.dart';
import 'package:tasq/utils/app_time.dart';
// underlying providers needed by the dashboard metrics logic
import 'package:tasq/providers/it_service_request_provider.dart';
import 'package:tasq/providers/profile_provider.dart';
import 'package:tasq/providers/tickets_provider.dart';
import 'package:tasq/providers/tasks_provider.dart';
import 'package:tasq/providers/workforce_provider.dart'; // dutySchedulesProvider
import 'package:tasq/providers/attendance_provider.dart';
import 'package:tasq/providers/whereabouts_provider.dart';
import 'package:tasq/providers/leave_provider.dart';
import 'package:tasq/providers/pass_slip_provider.dart';
import 'package:tasq/providers/teams_provider.dart';
void main() {
group('dashboardMetricsProvider', () {
test('service request in progress causes status to become On task', () async {
AppTime.initialize();
final now = AppTime.now();
// create a single IT staff profile
final profile = Profile(
id: 'u1',
fullName: 'Staff One',
role: 'it_staff',
avatarUrl: null,
allowTracking: false,
);
final service = ItServiceRequest(
id: 'r1',
services: ['wifi'],
eventName: 'Event',
status: ItServiceRequestStatus.inProgress,
outsidePremiseAllowed: false,
createdAt: now,
updatedAt: now,
);
final assignment = ItServiceRequestAssignment(
id: 'a1',
requestId: 'r1',
userId: 'u1',
createdAt: now,
);
final container = ProviderContainer(
overrides: [
profilesProvider.overrideWith((ref) => Stream.value([profile])),
itServiceRequestsProvider.overrideWith(
(ref) => Stream.value([service]),
),
itServiceRequestAssignmentsProvider.overrideWith(
(ref) => Stream.value([assignment]),
),
// everything else can be empty streams so dashboard metrics doesn't crash
ticketsProvider.overrideWith((ref) => Stream.value(const <Ticket>[])),
tasksProvider.overrideWith((ref) => Stream.value(const <Task>[])),
taskAssignmentsProvider.overrideWith(
(ref) => Stream.value(const <TaskAssignment>[]),
),
ticketMessagesAllProvider.overrideWith(
(ref) => Stream.value(const <TicketMessage>[]),
),
dutySchedulesProvider.overrideWith(
(ref) => Stream.value(const <DutySchedule>[]),
),
attendanceLogsProvider.overrideWith(
(ref) => Stream.value(const <AttendanceLog>[]),
),
livePositionsProvider.overrideWith(
(ref) => Stream.value(const <LivePosition>[]),
),
leavesProvider.overrideWith(
(ref) => Stream.value(const <LeaveOfAbsence>[]),
),
passSlipsProvider.overrideWithProvider(
StreamProvider<List<PassSlip>>(
(ref) => Stream.value(const <PassSlip>[]),
),
),
teamsProvider.overrideWithProvider(
StreamProvider<List<Team>>((ref) => Stream.value(const <Team>[])),
),
teamMembersProvider.overrideWithProvider(
StreamProvider<List<TeamMember>>(
(ref) => Stream.value(const <TeamMember>[]),
),
),
],
);
// wait for the dashboard metrics provider to emit AsyncData
final completer = Completer<DashboardMetrics>();
container.listen<AsyncValue<DashboardMetrics>>(dashboardMetricsProvider, (
prev,
next,
) {
if (next is AsyncData<DashboardMetrics>) {
completer.complete(next.value);
}
}, fireImmediately: true);
final data = await completer.future;
expect(data.staffRows.length, 1);
expect(data.staffRows.first.status, 'On event');
});
test('service request not in progress does not affect status', () async {
AppTime.initialize();
final now = AppTime.now();
final profile = Profile(
id: 'u2',
fullName: 'Staff Two',
role: 'it_staff',
avatarUrl: null,
allowTracking: false,
);
final service = ItServiceRequest(
id: 'r2',
services: ['wifi'],
eventName: 'Event',
status: ItServiceRequestStatus.scheduled,
outsidePremiseAllowed: false,
createdAt: now,
updatedAt: now,
);
final assignment = ItServiceRequestAssignment(
id: 'a2',
requestId: 'r2',
userId: 'u2',
createdAt: now,
);
final container = ProviderContainer(
overrides: [
profilesProvider.overrideWith((ref) => Stream.value([profile])),
itServiceRequestsProvider.overrideWith(
(ref) => Stream.value([service]),
),
itServiceRequestAssignmentsProvider.overrideWith(
(ref) => Stream.value([assignment]),
),
ticketsProvider.overrideWith((ref) => Stream.value(const <Ticket>[])),
tasksProvider.overrideWith((ref) => Stream.value(const <Task>[])),
taskAssignmentsProvider.overrideWith(
(ref) => Stream.value(const <TaskAssignment>[]),
),
ticketMessagesAllProvider.overrideWith(
(ref) => Stream.value(const <TicketMessage>[]),
),
dutySchedulesProvider.overrideWith(
(ref) => Stream.value(const <DutySchedule>[]),
),
attendanceLogsProvider.overrideWith(
(ref) => Stream.value(const <AttendanceLog>[]),
),
livePositionsProvider.overrideWith(
(ref) => Stream.value(const <LivePosition>[]),
),
leavesProvider.overrideWith(
(ref) => Stream.value(const <LeaveOfAbsence>[]),
),
passSlipsProvider.overrideWithProvider(
StreamProvider<List<PassSlip>>(
(ref) => Stream.value(const <PassSlip>[]),
),
),
teamsProvider.overrideWithProvider(
StreamProvider<List<Team>>((ref) => Stream.value(const <Team>[])),
),
teamMembersProvider.overrideWithProvider(
StreamProvider<List<TeamMember>>(
(ref) => Stream.value(const <TeamMember>[]),
),
),
],
);
final completer = Completer<DashboardMetrics>();
container.listen<AsyncValue<DashboardMetrics>>(dashboardMetricsProvider, (
prev,
next,
) {
if (next is AsyncData<DashboardMetrics>) {
completer.complete(next.value);
}
}, fireImmediately: true);
final data = await completer.future;
expect(data.staffRows.length, 1);
// since user has no attendance or task data they should default to Off duty
expect(data.staffRows.first.status.toLowerCase(), contains('off'));
});
});
test(
'approved leave should force On leave status and not mark Off duty',
() async {
AppTime.initialize();
final now = AppTime.now();
final profile = Profile(
id: 'u3',
fullName: 'Staff Three',
role: 'it_staff',
avatarUrl: null,
allowTracking: false,
);
// create a schedule for today
final schedule = DutySchedule(
id: 's1',
userId: profile.id,
shiftType: 'am',
startTime: DateTime(now.year, now.month, now.day, 8),
endTime: DateTime(now.year, now.month, now.day, 16),
status: 'scheduled',
createdAt: now,
checkInAt: null,
checkInLocation: null,
relieverIds: [],
);
final leave = LeaveOfAbsence(
id: 'l1',
userId: profile.id,
leaveType: 'sick_leave',
justification: 'ill',
startTime: now.subtract(const Duration(hours: 1)),
endTime: now.add(const Duration(hours: 4)),
status: 'approved',
filedBy: profile.id,
createdAt: now,
);
final container = ProviderContainer(
overrides: [
profilesProvider.overrideWith((ref) => Stream.value([profile])),
dutySchedulesProvider.overrideWith((ref) => Stream.value([schedule])),
leavesProvider.overrideWith((ref) => Stream.value([leave])),
// stub the rest with empties
ticketsProvider.overrideWith((ref) => Stream.value(const <Ticket>[])),
tasksProvider.overrideWith((ref) => Stream.value(const <Task>[])),
taskAssignmentsProvider.overrideWith(
(ref) => Stream.value(const <TaskAssignment>[]),
),
ticketMessagesAllProvider.overrideWith(
(ref) => Stream.value(const <TicketMessage>[]),
),
attendanceLogsProvider.overrideWith(
(ref) => Stream.value(const <AttendanceLog>[]),
),
livePositionsProvider.overrideWith(
(ref) => Stream.value(const <LivePosition>[]),
),
passSlipsProvider.overrideWithProvider(
StreamProvider<List<PassSlip>>(
(ref) => Stream.value(const <PassSlip>[]),
),
),
teamsProvider.overrideWithProvider(
StreamProvider<List<Team>>((ref) => Stream.value(const <Team>[])),
),
teamMembersProvider.overrideWithProvider(
StreamProvider<List<TeamMember>>(
(ref) => Stream.value(const <TeamMember>[]),
),
),
itServiceRequestsProvider.overrideWith(
(ref) => Stream.value(const <ItServiceRequest>[]),
),
itServiceRequestAssignmentsProvider.overrideWith(
(ref) => Stream.value(const <ItServiceRequestAssignment>[]),
),
],
);
final completer = Completer<DashboardMetrics>();
container.listen<AsyncValue<DashboardMetrics>>(dashboardMetricsProvider, (
prev,
next,
) {
if (next is AsyncData<DashboardMetrics>) {
completer.complete(next.value);
}
}, fireImmediately: true);
final data = await completer.future;
expect(data.staffRows.length, 1);
expect(data.staffRows.first.status, equals('On leave'));
},
);
test('rejected leave should not affect schedule status', () async {
AppTime.initialize();
final now = AppTime.now();
final profile = Profile(
id: 'u4',
fullName: 'Staff Four',
role: 'it_staff',
avatarUrl: null,
allowTracking: false,
);
final schedule = DutySchedule(
id: 's2',
// Anchor the shift around `now` so the shift is always active when the
// suite runs; a hard-coded 08:00–16:00 window made this test pass only
// during daytime and report 'Off duty' (correctly) at night.
userId: profile.id,
shiftType: 'am',
startTime: now.subtract(const Duration(hours: 1)),
endTime: now.add(const Duration(hours: 4)),
status: 'scheduled',
createdAt: now,
checkInAt: null,
checkInLocation: null,
relieverIds: [],
);
final leave = LeaveOfAbsence(
id: 'l2',
userId: profile.id,
leaveType: 'vacation_leave',
justification: 'plans',
startTime: now.subtract(const Duration(hours: 1)),
endTime: now.add(const Duration(hours: 4)),
status: 'rejected',
filedBy: profile.id,
createdAt: now,
);
final container = ProviderContainer(
overrides: [
profilesProvider.overrideWith((ref) => Stream.value([profile])),
dutySchedulesProvider.overrideWith((ref) => Stream.value([schedule])),
leavesProvider.overrideWith((ref) => Stream.value([leave])),
ticketsProvider.overrideWith((ref) => Stream.value(const <Ticket>[])),
tasksProvider.overrideWith((ref) => Stream.value(const <Task>[])),
taskAssignmentsProvider.overrideWith(
(ref) => Stream.value(const <TaskAssignment>[]),
),
ticketMessagesAllProvider.overrideWith(
(ref) => Stream.value(const <TicketMessage>[]),
),
attendanceLogsProvider.overrideWith(
(ref) => Stream.value(const <AttendanceLog>[]),
),
livePositionsProvider.overrideWith(
(ref) => Stream.value(const <LivePosition>[]),
),
passSlipsProvider.overrideWithProvider(
StreamProvider<List<PassSlip>>(
(ref) => Stream.value(const <PassSlip>[]),
),
),
teamsProvider.overrideWithProvider(
StreamProvider<List<Team>>((ref) => Stream.value(const <Team>[])),
),
teamMembersProvider.overrideWithProvider(
StreamProvider<List<TeamMember>>(
(ref) => Stream.value(const <TeamMember>[]),
),
),
itServiceRequestsProvider.overrideWith(
(ref) => Stream.value(const <ItServiceRequest>[]),
),
itServiceRequestAssignmentsProvider.overrideWith(
(ref) => Stream.value(const <ItServiceRequestAssignment>[]),
),
],
);
final completer = Completer<DashboardMetrics>();
container.listen<AsyncValue<DashboardMetrics>>(dashboardMetricsProvider, (
prev,
next,
) {
if (next is AsyncData<DashboardMetrics>) {
completer.complete(next.value);
}
}, fireImmediately: true);
final data = await completer.future;
expect(data.staffRows.length, 1);
// since leave was rejected, status should not be 'On leave' nor 'Off duty'
expect(data.staffRows.first.status, isNot('On leave'));
expect(data.staffRows.first.status.toLowerCase(), isNot(contains('off')));
});
}