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>
213 lines
6.2 KiB
Dart
213 lines
6.2 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:tasq/models/task.model.dart';
|
|
import 'package:tasq/models/task_assignment.dart';
|
|
import 'package:tasq/models/notification_item.model.dart';
|
|
import 'package:tasq/utils/app_time.dart';
|
|
|
|
void main() {
|
|
setUp(() {
|
|
AppTime.initialize();
|
|
});
|
|
|
|
group('Task model IT Job fields', () {
|
|
test('fromMap parses it_job_printed correctly', () {
|
|
final map = {
|
|
'id': 'task-1',
|
|
'ticket_id': null,
|
|
'task_number': 'T-2026-0001',
|
|
'title': 'Fix printer',
|
|
'description': 'Printer is broken',
|
|
'office_id': 'office-1',
|
|
'status': 'completed',
|
|
'priority': 1,
|
|
'queue_order': null,
|
|
'created_at': '2026-03-21T08:00:00.000Z',
|
|
'creator_id': 'user-1',
|
|
'started_at': '2026-03-21T08:30:00.000Z',
|
|
'completed_at': '2026-03-21T10:00:00.000Z',
|
|
'it_job_printed': true,
|
|
'it_job_printed_at': '2026-03-21T10:30:00.000Z',
|
|
};
|
|
|
|
final task = Task.fromMap(map);
|
|
|
|
expect(task.itJobPrinted, true);
|
|
expect(task.itJobPrintedAt, isNotNull);
|
|
});
|
|
|
|
test('fromMap defaults it_job_printed to false', () {
|
|
final map = {
|
|
'id': 'task-2',
|
|
'ticket_id': null,
|
|
'task_number': 'T-2026-0002',
|
|
'title': 'Replace cable',
|
|
'description': '',
|
|
'office_id': null,
|
|
'status': 'queued',
|
|
'priority': 1,
|
|
'queue_order': null,
|
|
'created_at': '2026-03-21T08:00:00.000Z',
|
|
'creator_id': 'user-1',
|
|
'started_at': null,
|
|
'completed_at': null,
|
|
// it_job_printed fields not present
|
|
};
|
|
|
|
final task = Task.fromMap(map);
|
|
|
|
expect(task.itJobPrinted, false);
|
|
expect(task.itJobPrintedAt, isNull);
|
|
});
|
|
|
|
test('equality includes it_job_printed fields', () {
|
|
final base = {
|
|
'id': 'task-3',
|
|
'ticket_id': null,
|
|
'task_number': null,
|
|
'title': 'Test',
|
|
'description': '',
|
|
'office_id': null,
|
|
'status': 'completed',
|
|
'priority': 1,
|
|
'queue_order': null,
|
|
'created_at': '2026-03-21T08:00:00.000Z',
|
|
'creator_id': null,
|
|
'started_at': null,
|
|
'completed_at': null,
|
|
'it_job_printed': false,
|
|
'it_job_printed_at': null,
|
|
};
|
|
|
|
final t1 = Task.fromMap(base);
|
|
final t2 = Task.fromMap({...base, 'it_job_printed': true});
|
|
|
|
expect(t1, isNot(equals(t2)));
|
|
});
|
|
});
|
|
|
|
group('IT staff checklist filtering', () {
|
|
Task makeTask(String id, {bool completed = true, bool printed = false}) {
|
|
return Task.fromMap({
|
|
'id': id,
|
|
'ticket_id': null,
|
|
'task_number': 'T-2026-000${id.hashCode % 9}',
|
|
'title': 'Task $id',
|
|
'description': '',
|
|
'office_id': null,
|
|
'status': completed ? 'completed' : 'in_progress',
|
|
'priority': 1,
|
|
'queue_order': null,
|
|
'created_at': '2026-04-01T08:00:00.000Z',
|
|
'creator_id': 'user-0',
|
|
'started_at': null,
|
|
'completed_at': completed ? '2026-04-01T09:00:00.000Z' : null,
|
|
'it_job_printed': printed,
|
|
'it_job_printed_at': null,
|
|
});
|
|
}
|
|
|
|
TaskAssignment makeAssign(String taskId, String userId) => TaskAssignment(
|
|
taskId: taskId,
|
|
userId: userId,
|
|
createdAt: DateTime(2026, 4, 1),
|
|
);
|
|
|
|
test('IT staff sees only their assigned completed tasks', () {
|
|
final tasks = [
|
|
makeTask('t1'), // assigned to it_staff_1
|
|
makeTask('t2'), // assigned to it_staff_2
|
|
makeTask('t3'), // assigned to both
|
|
makeTask('t4', completed: false), // not completed — excluded
|
|
];
|
|
|
|
final assignments = [
|
|
makeAssign('t1', 'it_staff_1'),
|
|
makeAssign('t2', 'it_staff_2'),
|
|
makeAssign('t3', 'it_staff_1'),
|
|
makeAssign('t3', 'it_staff_2'),
|
|
makeAssign('t4', 'it_staff_1'),
|
|
];
|
|
|
|
const currentUserId = 'it_staff_1';
|
|
|
|
final myCompleted = tasks
|
|
.where((t) => t.status == 'completed')
|
|
.where((t) => assignments
|
|
.any((a) => a.taskId == t.id && a.userId == currentUserId))
|
|
.toList();
|
|
|
|
expect(myCompleted.map((t) => t.id), containsAll(['t1', 't3']));
|
|
expect(myCompleted.map((t) => t.id), isNot(contains('t2')));
|
|
expect(myCompleted.map((t) => t.id), isNot(contains('t4')));
|
|
});
|
|
|
|
test('IT staff not_submitted filter excludes already-printed tasks', () {
|
|
final tasks = [
|
|
makeTask('t1', printed: false),
|
|
makeTask('t2', printed: true),
|
|
];
|
|
|
|
final assignments = [
|
|
makeAssign('t1', 'it_staff_1'),
|
|
makeAssign('t2', 'it_staff_1'),
|
|
];
|
|
|
|
const currentUserId = 'it_staff_1';
|
|
|
|
final myPending = tasks
|
|
.where((t) => t.status == 'completed')
|
|
.where((t) => assignments
|
|
.any((a) => a.taskId == t.id && a.userId == currentUserId))
|
|
.where((t) => !t.itJobPrinted)
|
|
.toList();
|
|
|
|
expect(myPending.map((t) => t.id), equals(['t1']));
|
|
});
|
|
});
|
|
|
|
group('NotificationItem announcement field', () {
|
|
test('fromMap parses announcement_id', () {
|
|
final map = {
|
|
'id': 'notif-1',
|
|
'user_id': 'user-1',
|
|
'actor_id': 'user-2',
|
|
'ticket_id': null,
|
|
'task_id': null,
|
|
'it_service_request_id': null,
|
|
'announcement_id': 'ann-1',
|
|
'message_id': null,
|
|
'type': 'announcement',
|
|
'created_at': '2026-03-21T10:00:00.000Z',
|
|
'read_at': null,
|
|
};
|
|
|
|
final item = NotificationItem.fromMap(map);
|
|
|
|
expect(item.announcementId, 'ann-1');
|
|
expect(item.type, 'announcement');
|
|
expect(item.isUnread, true);
|
|
});
|
|
|
|
test('fromMap defaults announcement_id to null', () {
|
|
final map = {
|
|
'id': 'notif-2',
|
|
'user_id': 'user-1',
|
|
'actor_id': null,
|
|
'ticket_id': null,
|
|
'task_id': 'task-1',
|
|
'it_service_request_id': null,
|
|
'message_id': null,
|
|
'type': 'it_job_reminder',
|
|
'created_at': '2026-03-21T10:00:00.000Z',
|
|
'read_at': '2026-03-21T10:05:00.000Z',
|
|
};
|
|
|
|
final item = NotificationItem.fromMap(map);
|
|
|
|
expect(item.announcementId, isNull);
|
|
expect(item.type, 'it_job_reminder');
|
|
expect(item.isUnread, false);
|
|
});
|
|
});
|
|
}
|