210 lines
6.0 KiB
Dart
210 lines
6.0 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:tasq/providers/tasks_provider.dart';
|
|
import 'package:tasq/models/task.dart';
|
|
|
|
// Minimal fake supabase client similar to integration test work,
|
|
// only implements the methods used by TasksController.
|
|
class _FakeClient {
|
|
final Map<String, List<Map<String, dynamic>>> tables = {
|
|
'tasks': [],
|
|
'task_activity_logs': [],
|
|
};
|
|
|
|
_FakeQuery from(String table) => _FakeQuery(this, table);
|
|
}
|
|
|
|
class _FakeQuery {
|
|
final _FakeClient client;
|
|
final String table;
|
|
Map<String, dynamic>? _eq;
|
|
Map<String, dynamic>? _insertPayload;
|
|
Map<String, dynamic>? _updatePayload;
|
|
|
|
_FakeQuery(this.client, this.table);
|
|
|
|
_FakeQuery select([String? _]) => this;
|
|
|
|
Future<Map<String, dynamic>?> maybeSingle() async {
|
|
final rows = client.tables[table] ?? [];
|
|
if (_eq != null) {
|
|
final field = _eq!.keys.first;
|
|
final value = _eq![field];
|
|
for (final r in rows) {
|
|
if (r[field] == value) {
|
|
return Map<String, dynamic>.from(r);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
return rows.isEmpty ? null : Map<String, dynamic>.from(rows.first);
|
|
}
|
|
|
|
_FakeQuery insert(Map<String, dynamic> payload) {
|
|
_insertPayload = Map<String, dynamic>.from(payload);
|
|
return this;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> single() async {
|
|
if (_insertPayload != null) {
|
|
final id = 'tsk-${client.tables['tasks']!.length + 1}';
|
|
final row = Map<String, dynamic>.from(_insertPayload!);
|
|
row['id'] = id;
|
|
client.tables[table]!.add(row);
|
|
return Map<String, dynamic>.from(row);
|
|
}
|
|
throw Exception('unexpected single() call');
|
|
}
|
|
|
|
_FakeQuery update(Map<String, dynamic> payload) {
|
|
_updatePayload = payload;
|
|
// don't apply yet; wait for eq to know which row
|
|
return this;
|
|
}
|
|
|
|
_FakeQuery eq(String field, dynamic value) {
|
|
_eq = {field: value};
|
|
// apply update payload now that we know which row to target
|
|
if (_updatePayload != null) {
|
|
final idx = client.tables[table]!.indexWhere((r) => r[field] == value);
|
|
if (idx >= 0) {
|
|
client.tables[table]![idx] = {
|
|
...client.tables[table]![idx],
|
|
..._updatePayload!,
|
|
};
|
|
}
|
|
// clear payload after applying so subsequent eq calls don't reapply
|
|
_updatePayload = null;
|
|
}
|
|
return this;
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
group('TasksController business rules', () {
|
|
late _FakeClient fake;
|
|
late TasksController controller;
|
|
|
|
setUp(() {
|
|
fake = _FakeClient();
|
|
controller = TasksController(fake as dynamic);
|
|
// ignore: avoid_dynamic_calls
|
|
// note: controller expects SupabaseClient; using dynamic bypass.
|
|
});
|
|
|
|
test('cannot complete a task without required metadata', () async {
|
|
// insert a task with no metadata at all
|
|
final row = {'id': 'tsk-1', 'status': 'queued'};
|
|
fake.tables['tasks']!.add(row);
|
|
|
|
expect(
|
|
() => controller.updateTaskStatus(taskId: 'tsk-1', status: 'completed'),
|
|
throwsA(isA<Exception>()),
|
|
);
|
|
});
|
|
|
|
test('cannot complete when action taken is missing', () async {
|
|
// insert a task that has the basic request metadata but nothing else
|
|
final row = {
|
|
'id': 'tsk-3',
|
|
'status': 'queued',
|
|
'request_type': 'Repair',
|
|
'request_category': 'Hardware',
|
|
};
|
|
fake.tables['tasks']!.add(row);
|
|
|
|
// missing actionTaken should still prevent completion even though
|
|
// signatories are not required.
|
|
expect(
|
|
() => controller.updateTaskStatus(taskId: 'tsk-3', status: 'completed'),
|
|
throwsA(isA<Exception>()),
|
|
);
|
|
|
|
// add some signatories but no actionTaken yet
|
|
await controller.updateTask(
|
|
taskId: 'tsk-3',
|
|
requestedBy: 'Alice',
|
|
notedBy: 'Bob',
|
|
receivedBy: 'Carol',
|
|
);
|
|
expect(
|
|
() => controller.updateTaskStatus(taskId: 'tsk-3', status: 'completed'),
|
|
throwsA(isA<Exception>()),
|
|
);
|
|
|
|
// once action taken is provided completion should succeed even if
|
|
// signatories remain empty (they already have values here, but the
|
|
// previous checks show they aren't required).
|
|
await controller.updateTask(taskId: 'tsk-3', actionTaken: '{}');
|
|
await controller.updateTaskStatus(taskId: 'tsk-3', status: 'completed');
|
|
expect(
|
|
fake.tables['tasks']!.firstWhere((t) => t['id'] == 'tsk-3')['status'],
|
|
'completed',
|
|
);
|
|
});
|
|
|
|
test('completing after adding metadata succeeds', () async {
|
|
// insert a task
|
|
final row = {'id': 'tsk-2', 'status': 'queued'};
|
|
fake.tables['tasks']!.add(row);
|
|
|
|
// update metadata via updateTask including actionTaken
|
|
await controller.updateTask(
|
|
taskId: 'tsk-2',
|
|
requestType: 'Repair',
|
|
requestCategory: 'Hardware',
|
|
actionTaken: '{}',
|
|
);
|
|
|
|
await controller.updateTaskStatus(taskId: 'tsk-2', status: 'completed');
|
|
expect(
|
|
fake.tables['tasks']!.firstWhere((t) => t['id'] == 'tsk-2')['status'],
|
|
'completed',
|
|
);
|
|
});
|
|
|
|
test('Task.hasIncompleteDetails flag works correctly', () {
|
|
final now = DateTime.now();
|
|
final base = Task(
|
|
id: 'x',
|
|
ticketId: null,
|
|
taskNumber: null,
|
|
title: 't',
|
|
description: '',
|
|
officeId: null,
|
|
status: 'completed',
|
|
priority: 1,
|
|
queueOrder: null,
|
|
createdAt: now,
|
|
creatorId: null,
|
|
startedAt: null,
|
|
completedAt: now,
|
|
// leave all optional metadata null
|
|
);
|
|
expect(base.hasIncompleteDetails, isTrue);
|
|
|
|
final full = Task(
|
|
id: 'x',
|
|
ticketId: null,
|
|
taskNumber: null,
|
|
title: 't',
|
|
description: '',
|
|
officeId: null,
|
|
status: 'completed',
|
|
priority: 1,
|
|
queueOrder: null,
|
|
createdAt: now,
|
|
creatorId: null,
|
|
startedAt: null,
|
|
completedAt: now,
|
|
requestedBy: 'A',
|
|
notedBy: 'B',
|
|
receivedBy: 'C',
|
|
requestType: 'foo',
|
|
requestCategory: 'bar',
|
|
actionTaken: '{}',
|
|
);
|
|
expect(full.hasIncompleteDetails, isFalse);
|
|
});
|
|
});
|
|
}
|