Files
tasq/test/programmer_day_sheets_controller_test.dart
T
2026-09-28 14:00:29 +08:00

319 lines
9.2 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:tasq/models/programmer_day_sheet.model.dart';
import 'package:tasq/models/programmer_day_sheet_event.model.dart';
import 'package:tasq/providers/programmer_day_sheets_provider.dart';
// ---------------------------------------------------------------------------
// Fake push recorder
// ---------------------------------------------------------------------------
class _PushCall {
final List<String> userIds;
final String title;
final String body;
final Map<String, dynamic>? data;
_PushCall({
required this.userIds,
required this.title,
required this.body,
this.data,
});
}
class _RecordingPush {
final calls = <_PushCall>[];
Future<void> call({
required List<String> userIds,
required String title,
required String body,
Map<String, dynamic>? data,
}) async {
calls.add(_PushCall(userIds: userIds, title: title, body: body, data: data));
}
}
// ---------------------------------------------------------------------------
// Fake Supabase-like client — only exposes rpc()
// ---------------------------------------------------------------------------
class _RpcCall {
final String name;
final Map<String, dynamic>? params;
_RpcCall(this.name, this.params);
}
class _FakeClient {
final calls = <_RpcCall>[];
Map<String, dynamic>? _nextResult;
Object? _nextError;
void setResult(Map<String, dynamic> result) => _nextResult = result;
void setError(Object error) => _nextError = error;
Future<dynamic> rpc(String name, {Map<String, dynamic>? params}) async {
calls.add(_RpcCall(name, params));
if (_nextError != null) {
final err = _nextError!;
_nextError = null;
throw err;
}
final result = _nextResult;
_nextResult = null;
return result;
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
void main() {
group('DaySheetsController', () {
late _FakeClient fake;
late _RecordingPush push;
late DaySheetsController controller;
setUp(() {
fake = _FakeClient();
push = _RecordingPush();
controller = DaySheetsController(fake, push: push.call);
});
test('submit calls submit_day_sheet with p_date and pushes admins', () async {
fake.setResult({
'sheet_id': 's1',
'work_date': '2026-09-28',
'programmer_name': 'Paola Ross',
'notify_user_ids': ['adm'],
});
final sheetId = await controller.submit(DateTime.utc(2026, 9, 28));
expect(sheetId, 's1');
expect(fake.calls.single.name, 'submit_day_sheet');
expect(fake.calls.single.params, {'p_date': '2026-09-28'});
expect(push.calls.single.title, 'Day sheet submitted');
expect(
push.calls.single.body,
'Paola Ross submitted their day sheet for Sep 28, 2026',
);
});
test('blank remarks fail fast without an rpc call', () async {
await expectLater(
controller.disapprove(
sheetId: 'sh1',
remarks: ' ',
flaggedTaskIds: [],
),
throwsA(
isA<DaySheetActionException>().having(
(e) => e.code,
'code',
DaySheetError.remarksRequired,
),
),
);
expect(fake.calls, isEmpty);
});
test('already_reviewed maps to a friendly message', () async {
fake.setError(Exception('already_reviewed'));
DaySheetActionException? caught;
try {
await controller.submit(DateTime.utc(2026, 9, 28));
} on DaySheetActionException catch (e) {
caught = e;
}
expect(caught, isNotNull);
expect(caught!.code, DaySheetError.alreadyReviewed);
expect(caught.message, 'This sheet was already reviewed.');
});
test('approve reports approved and skipped ids and pushes per approved sheet',
() async {
fake.setResult({
'approved': [
{
'sheet_id': 'sh1',
'work_date': '2026-09-28',
'programmer_id': 'prog1',
},
{
'sheet_id': 'sh2',
'work_date': '2026-09-27',
'programmer_id': 'prog2',
},
],
'skipped_ids': ['sh3'],
});
final snapshot = DaySheetSnapshot(totalSeconds: 3600, rows: []);
final items = [
(sheetId: 'sh1', snapshot: snapshot),
(sheetId: 'sh2', snapshot: snapshot),
(sheetId: 'sh3', snapshot: snapshot),
];
final result = await controller.approve(items);
expect(result.approvedIds, ['sh1', 'sh2']);
expect(result.skippedIds, ['sh3']);
expect(push.calls.length, 2);
expect(push.calls[0].title, 'Day sheet approved');
expect(
push.calls[0].body,
'Your day sheet for Sep 28, 2026 was approved',
);
expect(
push.calls[1].body,
'Your day sheet for Sep 27, 2026 was approved',
);
});
test('a failing push does not fail the action', () async {
fake.setResult({
'sheet_id': 's1',
'work_date': '2026-09-28',
'programmer_name': 'Paola Ross',
'notify_user_ids': ['adm'],
});
final throwingController = DaySheetsController(
fake,
push: ({
required List<String> userIds,
required String title,
required String body,
Map<String, dynamic>? data,
}) async {
throw Exception('push failed');
},
);
// Should succeed even though push throws.
final sheetId = await throwingController.submit(DateTime.utc(2026, 9, 28));
expect(sheetId, 's1');
});
});
// -------------------------------------------------------------------------
group('mapDaySheetError', () {
test('maps known substrings to the correct error code', () {
expect(
mapDaySheetError(Exception('not_authorized')),
DaySheetError.notAuthorized,
);
expect(
mapDaySheetError(Exception('already_reviewed')),
DaySheetError.alreadyReviewed,
);
expect(
mapDaySheetError(Exception('already_submitted')),
DaySheetError.alreadySubmitted,
);
expect(
mapDaySheetError(Exception('remarks_required')),
DaySheetError.remarksRequired,
);
expect(
mapDaySheetError(Exception('body_required')),
DaySheetError.bodyRequired,
);
expect(
mapDaySheetError(Exception('work_date_locked')),
DaySheetError.workDateLocked,
);
expect(
mapDaySheetError(Exception('invalid_snapshot')),
DaySheetError.invalidSnapshot,
);
expect(
mapDaySheetError(Exception('something else entirely')),
DaySheetError.unknown,
);
});
});
// -------------------------------------------------------------------------
group('DaySheetActionException.message', () {
test('returns exact strings for every error code', () {
expect(
DaySheetActionException(DaySheetError.alreadyReviewed).message,
'This sheet was already reviewed.',
);
expect(
DaySheetActionException(DaySheetError.alreadySubmitted).message,
'This day sheet was already submitted.',
);
expect(
DaySheetActionException(DaySheetError.notAuthorized).message,
'You are not allowed to do that.',
);
expect(
DaySheetActionException(DaySheetError.remarksRequired).message,
'Remarks are required to disapprove.',
);
expect(
DaySheetActionException(DaySheetError.bodyRequired).message,
'Please write a justification.',
);
expect(
DaySheetActionException(DaySheetError.workDateLocked).message,
'That day is locked — you can only add entries to pending or disapproved days.',
);
expect(
DaySheetActionException(DaySheetError.invalidSnapshot).message,
'Could not approve: the sheet data was invalid.',
);
expect(
DaySheetActionException(DaySheetError.unknown).message,
'Something went wrong. Please try again.',
);
});
});
// -------------------------------------------------------------------------
group('ProgrammerDaySheetEvent', () {
test('fromMap parses all fields correctly', () {
final event = ProgrammerDaySheetEvent.fromMap({
'id': 'ev1',
'sheet_id': 'sh1',
'kind': 'submitted',
'actor_id': 'u1',
'body': 'some text',
'flagged_task_ids': ['t1', 't2'],
'created_at': '2026-09-28T10:00:00.000Z',
});
expect(event.id, 'ev1');
expect(event.sheetId, 'sh1');
expect(event.kind, 'submitted');
expect(event.actorId, 'u1');
expect(event.body, 'some text');
expect(event.flaggedTaskIds, ['t1', 't2']);
expect(event.createdAt, DateTime.utc(2026, 9, 28, 10, 0, 0));
});
test('fromMap handles null optional fields', () {
final event = ProgrammerDaySheetEvent.fromMap({
'id': 'ev2',
'sheet_id': 'sh2',
'kind': 'approved',
'created_at': '2026-09-28T10:00:00.000Z',
});
expect(event.actorId, isNull);
expect(event.body, isNull);
expect(event.flaggedTaskIds, isEmpty);
});
});
}