Files
tasq/test/geofence_admin_screen_test.dart
T
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

255 lines
7.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';
import 'package:tasq/models/app_settings.dart';
import 'package:tasq/models/profile.model.dart';
import 'package:tasq/providers/profile_provider.dart';
import 'package:tasq/providers/workforce_provider.dart';
import 'package:tasq/providers/location_provider.dart';
import 'package:tasq/screens/admin/geofence_test_screen.dart';
void main() {
// Polygon from existing unit test (CRMC)
final polygonJson = [
{"lat": 7.2025231, "lng": 124.2339774},
{"lat": 7.2018791, "lng": 124.2334463},
{"lat": 7.2013362, "lng": 124.2332049},
{"lat": 7.2011393, "lng": 124.2332907},
{"lat": 7.2009531, "lng": 124.2334195},
{"lat": 7.200655, "lng": 124.2339344},
{"lat": 7.2000749, "lng": 124.2348465},
{"lat": 7.199501, "lng": 124.2357645},
{"lat": 7.1990597, "lng": 124.2364595},
{"lat": 7.1986557, "lng": 124.2371331},
{"lat": 7.1992252, "lng": 124.237168},
{"lat": 7.199494, "lng": 124.2372713},
{"lat": 7.1997415, "lng": 124.2374604},
{"lat": 7.1999383, "lng": 124.2377071},
{"lat": 7.2001938, "lng": 124.2380934},
{"lat": 7.2011411, "lng": 124.2364357},
{"lat": 7.2025231, "lng": 124.2339774},
];
ProviderScope buildApp({required List<Override> overrides}) {
return ProviderScope(
overrides: overrides,
child: const MaterialApp(home: GeofenceTestScreen()),
);
}
testWidgets('non-admin cannot access', (tester) async {
await tester.pumpWidget(
buildApp(overrides: [isAdminProvider.overrideWith((ref) => false)]),
);
await tester.pump();
expect(find.text('Admin access required.'), findsOneWidget);
});
testWidgets('polygon geofence - inside shown', (tester) async {
final profile = Profile(id: 'p1', role: 'admin', fullName: 'Admin');
final insidePos = Position(
latitude: 7.2009,
longitude: 124.2360,
timestamp: DateTime.now(),
accuracy: 1.0,
altitude: 0.0,
altitudeAccuracy: 0.0,
heading: 0.0,
speed: 0.0,
speedAccuracy: 0.0,
headingAccuracy: 0.0,
);
await tester.pumpWidget(
buildApp(
overrides: [
currentProfileProvider.overrideWith((ref) => Stream.value(profile)),
isAdminProvider.overrideWith((ref) => true),
geofenceProvider.overrideWith(
(ref) =>
Future.value(GeofenceConfig.fromJson({'polygon': polygonJson})),
),
currentPositionProvider.overrideWith(
(ref) => Future.value(insidePos),
),
],
),
);
await tester.pumpAndSettle();
expect(find.text('Inside geofence'), findsOneWidget);
expect(find.byIcon(Icons.check_circle), findsOneWidget);
});
testWidgets('polygon geofence - outside shown', (tester) async {
final profile = Profile(id: 'p1', role: 'admin', fullName: 'Admin');
final outsidePos = Position(
latitude: 7.2060,
longitude: 124.2360,
timestamp: DateTime.now(),
accuracy: 1.0,
altitude: 0.0,
altitudeAccuracy: 0.0,
heading: 0.0,
speed: 0.0,
speedAccuracy: 0.0,
headingAccuracy: 0.0,
);
await tester.pumpWidget(
buildApp(
overrides: [
currentProfileProvider.overrideWith((ref) => Stream.value(profile)),
isAdminProvider.overrideWith((ref) => true),
geofenceProvider.overrideWith(
(ref) =>
Future.value(GeofenceConfig.fromJson({'polygon': polygonJson})),
),
currentPositionProvider.overrideWith(
(ref) => Future.value(outsidePos),
),
],
),
);
await tester.pumpAndSettle();
expect(find.text('Outside geofence'), findsOneWidget);
expect(find.byIcon(Icons.cancel), findsOneWidget);
});
testWidgets('circle geofence - inside shown', (tester) async {
final profile = Profile(id: 'p1', role: 'admin', fullName: 'Admin');
final cfg = GeofenceConfig(lat: 0.0, lng: 0.0, radiusMeters: 1000.0);
final insidePos = Position(
latitude: 0.005,
longitude: 0.0,
timestamp: DateTime.now(),
accuracy: 1.0,
altitude: 0.0,
altitudeAccuracy: 0.0,
heading: 0.0,
speed: 0.0,
speedAccuracy: 0.0,
headingAccuracy: 0.0,
);
await tester.pumpWidget(
buildApp(
overrides: [
currentProfileProvider.overrideWith((ref) => Stream.value(profile)),
isAdminProvider.overrideWith((ref) => true),
geofenceProvider.overrideWith((ref) => Future.value(cfg)),
currentPositionProvider.overrideWith(
(ref) => Future.value(insidePos),
),
],
),
);
await tester.pumpAndSettle();
expect(find.text('Inside geofence'), findsOneWidget);
expect(find.byIcon(Icons.check_circle), findsOneWidget);
});
testWidgets('circle geofence - outside shown', (tester) async {
final profile = Profile(id: 'p1', role: 'admin', fullName: 'Admin');
final cfg = GeofenceConfig(lat: 0.0, lng: 0.0, radiusMeters: 1000.0);
final outsidePos = Position(
latitude: 0.02,
longitude: 0.0,
timestamp: DateTime.now(),
accuracy: 1.0,
altitude: 0.0,
altitudeAccuracy: 0.0,
heading: 0.0,
speed: 0.0,
speedAccuracy: 0.0,
headingAccuracy: 0.0,
);
await tester.pumpWidget(
buildApp(
overrides: [
currentProfileProvider.overrideWith((ref) => Stream.value(profile)),
isAdminProvider.overrideWith((ref) => true),
geofenceProvider.overrideWith((ref) => Future.value(cfg)),
currentPositionProvider.overrideWith(
(ref) => Future.value(outsidePos),
),
],
),
);
await tester.pumpAndSettle();
expect(find.text('Outside geofence'), findsOneWidget);
expect(find.byIcon(Icons.cancel), findsOneWidget);
});
testWidgets('live tracking toggle follows stream', (tester) async {
final profile = Profile(id: 'p1', role: 'admin', fullName: 'Admin');
final cfg = GeofenceConfig(lat: 0.0, lng: 0.0, radiusMeters: 1000.0);
final initialPos = Position(
latitude: 0.02,
longitude: 0.0,
timestamp: DateTime.now(),
accuracy: 1.0,
altitude: 0.0,
altitudeAccuracy: 0.0,
heading: 0.0,
speed: 0.0,
speedAccuracy: 0.0,
headingAccuracy: 0.0,
);
final livePos = Position(
latitude: 0.005,
longitude: 0.0,
timestamp: DateTime.now(),
accuracy: 1.0,
altitude: 0.0,
altitudeAccuracy: 0.0,
heading: 0.0,
speed: 0.0,
speedAccuracy: 0.0,
headingAccuracy: 0.0,
);
await tester.pumpWidget(
buildApp(
overrides: [
currentProfileProvider.overrideWith((ref) => Stream.value(profile)),
isAdminProvider.overrideWith((ref) => true),
geofenceProvider.overrideWith((ref) => Future.value(cfg)),
currentPositionProvider.overrideWith(
(ref) => Future.value(initialPos),
),
currentPositionStreamProvider.overrideWith(
(ref) => Stream.value(livePos),
),
],
),
);
await tester.pumpAndSettle();
// initial state is the one-shot position (outside)
expect(find.text('Outside geofence'), findsOneWidget);
// enable live tracking
await tester.tap(find.byKey(const Key('live-switch')));
await tester.pumpAndSettle();
// stream value should now be used and indicate inside
expect(find.text('Inside geofence'), findsOneWidget);
expect(find.byIcon(Icons.check_circle), findsOneWidget);
});
}