mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
The list endpoints now return a total count alongside the page so the
front-end can show how many pages exist and offer a last-page jump.
Backend: add count(is_new) and count_devices(...) (sharing a WHERE-builder
with list_devices so page and count can't drift), wrap both list responses
in {items, total_count} structs, and register them with utoipa.
Front-end: parse the wrapper shape (dropping the fetch-one-extra trick),
add a Last-page button and a responsive "Page X of Y" / "X / Y" label to
the shared PaginationBar, and track the total in both lists. Notifications
re-sync the count on every fetch and decrement it locally on mark-read/
unread removals so the count stays accurate without a re-fetch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
185 lines
4.6 KiB
Dart
185 lines
4.6 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:frontend/model/device_type.dart';
|
|
import 'package:frontend/utils/oott_api.dart';
|
|
import 'package:http_mock_adapter/http_mock_adapter.dart';
|
|
|
|
import '../helpers/backend_test_harness.dart';
|
|
import '../helpers/fixtures.dart';
|
|
|
|
void main() {
|
|
late DioAdapter adapter;
|
|
|
|
setUp(() async {
|
|
adapter = await setUpBackendForTest();
|
|
});
|
|
|
|
test('getDevice fetches and decodes a single device', () async {
|
|
adapter.onGet(
|
|
'/devices/aa:bb:cc:dd:ee:ff',
|
|
(server) => server.reply(200, deviceJson(owner: 'carol')),
|
|
);
|
|
|
|
final device = await BackendAPI.instance.getDevice('aa:bb:cc:dd:ee:ff');
|
|
|
|
expect(device.owner, 'carol');
|
|
});
|
|
|
|
test('getDeviceSummary GETs /devices/summary', () async {
|
|
adapter.onGet(
|
|
'/devices/summary',
|
|
(server) => server.reply(200, deviceSummaryJson(totalRegistered: 7)),
|
|
);
|
|
|
|
final summary = await BackendAPI.instance.getDeviceSummary();
|
|
|
|
expect(summary.totalRegistered, 7);
|
|
});
|
|
|
|
test(
|
|
'listDevices sends filter + pagination params and reports the total',
|
|
() async {
|
|
adapter.onGet(
|
|
'/devices',
|
|
(server) => server.reply(
|
|
200,
|
|
pagedListJson([
|
|
deviceJson(macAddress: '00:00:00:00:00:01'),
|
|
deviceJson(macAddress: '00:00:00:00:00:02'),
|
|
], totalCount: 12),
|
|
),
|
|
queryParameters: {
|
|
'is_registered': false,
|
|
'device_type': 'laptop',
|
|
'sort_by': 'last_seen',
|
|
'sort_order': 'asc',
|
|
'page_offset': 0,
|
|
'page_limit': 2,
|
|
},
|
|
);
|
|
|
|
final result = await BackendAPI.instance.listDevices(
|
|
isRegistered: false,
|
|
deviceType: DeviceType.laptop,
|
|
sortBy: 'last_seen',
|
|
sortAscending: true,
|
|
page: 0,
|
|
perPage: 2,
|
|
);
|
|
|
|
expect(result.items, hasLength(2));
|
|
expect(result.totalCount, 12);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'listDevices reports the total when a single page is returned',
|
|
() async {
|
|
adapter.onGet(
|
|
'/devices',
|
|
(server) => server.reply(200, pagedListJson([deviceJson()])),
|
|
);
|
|
|
|
final result = await BackendAPI.instance.listDevices(perPage: 2);
|
|
|
|
expect(result.items, hasLength(1));
|
|
expect(result.totalCount, 1);
|
|
},
|
|
);
|
|
|
|
test('listDevices maps the unknown device type to an empty filter', () async {
|
|
adapter.onGet(
|
|
'/devices',
|
|
(server) => server.reply(200, pagedListJson([])),
|
|
queryParameters: {'device_type': '', 'page_offset': 0, 'page_limit': 10},
|
|
);
|
|
|
|
final result = await BackendAPI.instance.listDevices(
|
|
deviceType: DeviceType.unknown,
|
|
);
|
|
|
|
expect(result.items, isEmpty);
|
|
});
|
|
|
|
test('registerDevice PUTs the device payload', () async {
|
|
adapter.onPut(
|
|
'/devices',
|
|
(server) => server.reply(200, null),
|
|
data: {
|
|
'mac_address': 'aa:bb:cc:dd:ee:ff',
|
|
'owner': 'dave',
|
|
'device_type': 'phone',
|
|
'name': 'Dave Phone',
|
|
},
|
|
);
|
|
|
|
await BackendAPI.instance.registerDevice(
|
|
'aa:bb:cc:dd:ee:ff',
|
|
'dave',
|
|
'phone',
|
|
name: 'Dave Phone',
|
|
);
|
|
});
|
|
|
|
test('updateDevice PUTs to the device path', () async {
|
|
adapter.onPut(
|
|
'/devices/aa:bb:cc:dd:ee:ff',
|
|
(server) => server.reply(200, null),
|
|
data: {
|
|
'owner': 'erin',
|
|
'device_type': 'tv',
|
|
'vendor': 'Globex',
|
|
'name': null,
|
|
},
|
|
);
|
|
|
|
await BackendAPI.instance.updateDevice(
|
|
'aa:bb:cc:dd:ee:ff',
|
|
'erin',
|
|
'tv',
|
|
'Globex',
|
|
);
|
|
});
|
|
|
|
test('forgetDevice DELETEs the device path', () async {
|
|
adapter.onDelete(
|
|
'/devices/aa:bb:cc:dd:ee:ff',
|
|
(server) => server.reply(200, null),
|
|
);
|
|
|
|
await BackendAPI.instance.forgetDevice('aa:bb:cc:dd:ee:ff');
|
|
});
|
|
|
|
test('getDeviceEvents decodes a list of events', () async {
|
|
adapter.onGet(
|
|
'/devices/aa:bb:cc:dd:ee:ff/events',
|
|
(server) => server.reply(200, [
|
|
deviceEventJson(id: 1, scanner: 'Arp'),
|
|
deviceEventJson(id: 2, scanner: 'Mdns'),
|
|
]),
|
|
);
|
|
|
|
final events = await BackendAPI.instance.getDeviceEvents(
|
|
'aa:bb:cc:dd:ee:ff',
|
|
);
|
|
|
|
expect(events, hasLength(2));
|
|
expect(events.first.scannerLabel, 'ARP');
|
|
});
|
|
|
|
test('getDeviceEvents sends created_from when provided', () async {
|
|
final from = DateTime.utc(2026, 5, 1, 0, 0, 0);
|
|
adapter.onGet(
|
|
'/devices/aa:bb:cc:dd:ee:ff/events',
|
|
(server) => server.reply(200, <dynamic>[]),
|
|
queryParameters: {'created_from': from.toIso8601String()},
|
|
);
|
|
|
|
final events = await BackendAPI.instance.getDeviceEvents(
|
|
'aa:bb:cc:dd:ee:ff',
|
|
createdFrom: from,
|
|
);
|
|
|
|
expect(events, isEmpty);
|
|
});
|
|
}
|