import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:glp1_companion/features/nutrition/data/datasources/usda_fdc_client.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; /// Representative USDA FoodData Central `foods/search` payload with the six /// nutrient IDs the client maps. String _fdcSearchResponse() => jsonEncode({ 'totalHits': 1, 'foods': [ { 'fdcId': 12345, 'description': 'YOGURT, PLAIN, WHOLE MILK', 'dataType': 'Foundation', 'foodNutrients': [ {'nutrientId': 1008, 'value': 61}, {'nutrientId': 1003, 'value': 3.5}, {'nutrientId': 1004, 'value': 3.3}, {'nutrientId': 1005, 'value': 4.7}, {'nutrientId': 1079, 'value': 0}, // USDA reports sodium (1093) in mg directly — unlike OFF, which // reports grams; the client passes it through unchanged. {'nutrientId': 1093, 'value': 46}, ], }, ], }); void main() { group('UsdaFdcClient request shape', () { test('sends api_key as a QUERY parameter, never in the JSON body', () async { late http.Request captured; final client = MockClient((request) async { captured = request; return http.Response(_fdcSearchResponse(), 200); }); final usda = UsdaFdcClient(client: client, apiKey: 'TEST_KEY'); await usda.searchByText('yogurt'); // Regression (field-reported 2026-10-17): the FDC API only validates // the key from the query string — a body-only `apiKey` yields // HTTP 403 API_KEY_MISSING. expect(captured.url.queryParameters['api_key'], 'TEST_KEY'); expect(captured.url.queryParameters.containsKey('apiKey'), isFalse); final body = jsonDecode(captured.body) as Map; expect(body.containsKey('apiKey'), isFalse, reason: 'the JSON body must not carry the key'); expect(body['query'], 'yogurt'); }); test('falls back to DEMO_KEY when no override or .env entry exists', () async { late Uri capturedUrl; final client = MockClient((request) async { capturedUrl = request.url; return http.Response(_fdcSearchResponse(), 200); }); // No constructor key; dotenv is uninitialized in tests, so the client // must degrade to the public DEMO_KEY. await UsdaFdcClient(client: client).searchByText('yogurt'); expect(capturedUrl.queryParameters['api_key'], 'DEMO_KEY'); }); test('maps nutrient IDs onto the FoodProduct entity', () async { final client = MockClient((request) async { return http.Response(_fdcSearchResponse(), 200); }); final results = await UsdaFdcClient(client: client, apiKey: 'k').searchByText('yogurt'); expect(results, hasLength(1)); final product = results.single; expect(product.id, 'fdc:12345'); expect(product.productName, 'YOGURT, PLAIN, WHOLE MILK'); expect(product.calories, 61); expect(product.proteinGrams, 3.5); expect(product.fatGrams, 3.3); expect(product.carbsGrams, 4.7); expect(product.fiberGrams, 0); expect(product.sodiumMg, 46, reason: 'grams are converted to mg'); expect(product.servingSizeGrams, 100); expect(product.servingSizeUnit, 'g', reason: 'FDC defaults to a per-100 g basis when no unit is given'); }); test('barcode lookups restrict the query to Branded foods', () async { late Map capturedBody; final client = MockClient((request) async { capturedBody = jsonDecode(request.body) as Map; return http.Response(_fdcSearchResponse(), 200); }); await UsdaFdcClient(client: client, apiKey: 'k') .lookupBarcode('012345678905'); expect(capturedBody['query'], '012345678905'); expect(capturedBody['dataType'], ['Branded']); }); test('non-200 responses throw UsdaFdcException', () async { final client = MockClient( (request) async => http.Response('{"error":{"code":"API_KEY_MISSING"}}', 403), ); final usda = UsdaFdcClient(client: client, apiKey: 'bad'); expect( usda.searchByText('yogurt'), throwsA(isA()), ); }); }); }