mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Backend:
- Add db::devices::delete to erase a device and its events atomically
- Expose DELETE /api/devices/{mac}/permanently, wired to OpenAPI
- Cover the new db method and endpoint with tests
Frontend:
- Delete action for not-registered devices (detail screen + list row)
and an opt-in "permanently delete" checkbox in the Forget dialog
- Navigate to the devices list after deleting from the detail screen
- Refine button emphasis to M3: single filled primary, error-colored
text buttons for destructive actions, Test demoted to filled-tonal
- Flash the backend-config Test button red on a failed connection test
- Render snackbars through a top-level ScaffoldMessenger host so they
show above dialogs; keep the built-in SnackBar (with an Overlay host)
Docs:
- CLAUDE.md: rustfmt edition 2024, don't revert formatter-only changes,
prefer built-in Flutter components, follow existing patterns + M3
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
194 lines
4.9 KiB
Dart
194 lines
4.9 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('deleteDevice DELETEs the permanently path', () async {
|
|
adapter.onDelete(
|
|
'/devices/aa:bb:cc:dd:ee:ff/permanently',
|
|
(server) => server.reply(200, null),
|
|
);
|
|
|
|
await BackendAPI.instance.deleteDevice('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);
|
|
});
|
|
}
|