diff --git a/lib/services/api_client.dart b/lib/services/api_client.dart index 5a1eec8..6e81662 100644 --- a/lib/services/api_client.dart +++ b/lib/services/api_client.dart @@ -3,7 +3,9 @@ import 'package:dio/dio.dart'; /// HTTP-клиент для одного сервера Ignis. /// Покрывает все эндпоинты из openapi.json. class IgnisApi { - final Dio _dio = Dio(); + IgnisApi({Dio? dio}) : _dio = dio ?? Dio(); + + final Dio _dio; Dio get dioInstance => _dio; static String normalizeBaseUrl(String baseUrl) { @@ -70,11 +72,11 @@ class IgnisApi { /// Управление группой: state, brightness, temp, scene, r/g/b Future controlGroup(String id, Map params) => - _dio.post('/control/group/$id', queryParameters: params); + _dio.post('/control/group/$id', data: params); /// Управление одной лампой Future controlDevice(String id, Map params) => - _dio.post('/control/device/$id', queryParameters: params); + _dio.post('/control/device/$id', data: params); /// Мигнуть лампой (для идентификации) Future blinkDevice(String id) => @@ -92,11 +94,11 @@ class IgnisApi { /// Одноразовое расписание (таймер) Future scheduleOnce(Map params) => - _dio.post('/schedules/once', queryParameters: params); + _dio.post('/schedules/once', data: params); /// Cron-расписание (повторяющееся) Future scheduleCron(Map params) => - _dio.post('/schedules/cron', queryParameters: params); + _dio.post('/schedules/cron', data: params); /// Все активные задачи расписания Future getTasks() => _dio.get('/schedules/tasks'); diff --git a/test/api_client_test.dart b/test/api_client_test.dart new file mode 100644 index 0000000..fde5c08 --- /dev/null +++ b/test/api_client_test.dart @@ -0,0 +1,86 @@ +import 'dart:typed_data'; + +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:ignis_app/services/api_client.dart'; + +class RecordingAdapter implements HttpClientAdapter { + RequestOptions? lastRequest; + + @override + Future fetch( + RequestOptions options, + Stream? requestStream, + Future? cancelFuture, + ) async { + lastRequest = options; + return ResponseBody.fromString( + '{}', + 200, + headers: { + Headers.contentTypeHeader: ['application/json'], + }, + ); + } + + @override + void close({bool force = false}) {} +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('controlGroup sends command payload in request body', () async { + final adapter = RecordingAdapter(); + final dio = Dio()..httpClientAdapter = adapter; + final api = IgnisApi(dio: dio)..init('http://localhost:8000', 'secret'); + + await api.controlGroup('kitchen', {'state': true, 'brightness': 42}); + + expect(adapter.lastRequest, isNotNull); + expect(adapter.lastRequest?.path, '/control/group/kitchen'); + expect(adapter.lastRequest?.queryParameters, isEmpty); + expect(adapter.lastRequest?.data, {'state': true, 'brightness': 42}); + }); + + test('scheduleOnce sends schedule payload in request body', () async { + final adapter = RecordingAdapter(); + final dio = Dio()..httpClientAdapter = adapter; + final api = IgnisApi(dio: dio)..init('http://localhost:8000', 'secret'); + + await api.scheduleOnce({ + 'target_id': 'hall', + 'hours_from_now': 4, + 'state': false, + 'is_group': true, + }); + + expect(adapter.lastRequest, isNotNull); + expect(adapter.lastRequest?.path, '/schedules/once'); + expect(adapter.lastRequest?.queryParameters, isEmpty); + expect(adapter.lastRequest?.data, { + 'target_id': 'hall', + 'hours_from_now': 4, + 'state': false, + 'is_group': true, + }); + }); + + test( + 'createApiKey keeps query-based contract until backend is changed', + () async { + final adapter = RecordingAdapter(); + final dio = Dio()..httpClientAdapter = adapter; + final api = IgnisApi(dio: dio)..init('http://localhost:8000', 'secret'); + + await api.createApiKey('Guest', isAdmin: true); + + expect(adapter.lastRequest, isNotNull); + expect(adapter.lastRequest?.path, '/api-keys'); + expect(adapter.lastRequest?.queryParameters, { + 'name': 'Guest', + 'is_admin': true, + }); + }, + ); +}