glp-companion/test/features/nutrition/usda_fdc_client_test.dart

120 lines
4.3 KiB
Dart

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<String, dynamic>;
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<String, dynamic> capturedBody;
final client = MockClient((request) async {
capturedBody = jsonDecode(request.body) as Map<String, dynamic>;
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<UsdaFdcException>()),
);
});
});
}