Files
recipe-app/flutter/test/admin_aliases_panel_test.dart
Nils-Johan Gynther 67a7590525
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Successful in 12m45s
Test Suite / flutter-quality (push) Failing after 7m24s
feat(ai): add AI trace tracking and admin panel
- Add AiTrace model to Prisma schema with relations to User
- Implement AiTraceService with CRUD operations for AI traces
- Add new admin panel for AI traces with filtering and detail views
- Integrate trace persistence in receipt import flow
- Add API endpoints for listing and retrieving AI traces
- Update Flutter admin UI with new AI tab and navigation
- Add new domain models for AI traces and details
- Add migration for AiTrace table creation

BREAKING CHANGE: None
2026-05-21 17:33:21 +02:00

352 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:recipe_flutter/features/admin/data/admin_repository.dart';
import 'package:recipe_flutter/features/admin/domain/admin_ai_categorize_result.dart';
import 'package:recipe_flutter/features/admin/domain/admin_ai_trace.dart';
import 'package:recipe_flutter/features/admin/domain/admin_ai_trace_detail.dart';
import 'package:recipe_flutter/features/admin/domain/admin_category_node.dart';
import 'package:recipe_flutter/features/admin/domain/admin_inventory_item.dart';
import 'package:recipe_flutter/features/admin/domain/admin_pantry_item.dart';
import 'package:recipe_flutter/features/admin/domain/admin_product.dart';
import 'package:recipe_flutter/features/admin/domain/ai_model_info.dart';
import 'package:recipe_flutter/features/admin/domain/pending_product.dart';
import 'package:recipe_flutter/features/admin/domain/receipt_alias.dart';
import 'package:recipe_flutter/features/admin/domain/user_admin.dart';
import 'package:recipe_flutter/features/admin/presentation/admin_aliases_panel.dart';
// Test wrapper that implements AdminRepository with minimal methods
class TestAdminRepositoryWrapper implements AdminRepository {
final FakeAdminRepository _fakeRepo;
TestAdminRepositoryWrapper(this._fakeRepo);
@override
Future<List<ReceiptAlias>> listReceiptAliases() =>
_fakeRepo.listReceiptAliases();
@override
Future<List<AdminProduct>> listGlobalProducts() =>
_fakeRepo.listGlobalProducts();
@override
Future<void> updateReceiptAlias(int id,
{String? receiptName, int? productId, bool? isGlobal}) =>
_fakeRepo.updateReceiptAlias(id,
receiptName: receiptName, productId: productId, isGlobal: isGlobal);
// Stub implementations for other required methods
@override
Future<List<AdminAiCategorizeResult>> aiCategorizeBulk(
{List<int>? productIds}) =>
throw UnimplementedError();
@override
Future<int> bulkSetCategory(List<int> ids, {required int? categoryId}) =>
throw UnimplementedError();
@override
Future<AdminInventoryItem> createAdminInventory(
{int? userId,
required int productId,
required double quantity,
required String unit,
String? location,
String? brand,
String? receiptName,
String? suitableFor,
String? comment}) =>
throw UnimplementedError();
@override
Future<AdminPantryItem> createAdminPantry(
{int? userId, required int productId, String? location}) =>
throw UnimplementedError();
@override
Future<Map<String, dynamic>> createProduct(String name, {int? categoryId}) =>
throw UnimplementedError();
@override
Future<UserAdmin> createUser(
{required String username,
required String email,
required String password,
String role = 'user'}) =>
throw UnimplementedError();
@override
Future<void> deleteUser(int userId) => throw UnimplementedError();
@override
Future<List<AdminInventoryItem>> listAdminInventory(
{int? userId, String? sort}) =>
throw UnimplementedError();
@override
Future<List<AdminPantryItem>> listAdminPantry({int? userId}) =>
throw UnimplementedError();
@override
Future<List<AiModelInfo>> listAiModels() => throw UnimplementedError();
@override
Future<AdminAiTraceDetail> getAiTraceById(String traceId) =>
throw UnimplementedError();
@override
Future<AdminAiTraceListResponse> listAiTraces(
{required AdminAiTraceSource source,
int limit = 25,
String? cursor,
String? period,
bool onlyErrors = false}) =>
throw UnimplementedError();
@override
Future<List<AdminCategoryNode>> listCategoryTree() =>
throw UnimplementedError();
@override
Future<List<AdminProduct>> listDeletedProducts() =>
throw UnimplementedError();
@override
Future<List<PendingProduct>> listPendingProducts() =>
throw UnimplementedError();
@override
Future<List<PendingProduct>> listPrivateProducts() =>
throw UnimplementedError();
@override
Future<List<AdminProduct>> listProducts() => throw UnimplementedError();
@override
Future<List<AdminProduct>> listSelectableProductsForAdmin(
{bool forceRefresh = false}) =>
throw UnimplementedError();
@override
Future<List<UserAdmin>> listUsers() => throw UnimplementedError();
@override
Future<void> mergeAdminInventory(
{required int sourceInventoryId, required int targetInventoryId}) =>
throw UnimplementedError();
@override
Future<void> mergeProducts(
{required int sourceProductId, required int targetProductId}) =>
throw UnimplementedError();
@override
Future<void> mergeProductsPrivate(
{required int sourceProductId, required int targetProductId}) =>
throw UnimplementedError();
@override
Future<void> moveAdminInventoryToPantry(int inventoryId) =>
throw UnimplementedError();
@override
Future<void> moveAdminPantryToInventory(
int pantryItemId, Map<String, dynamic> body) =>
throw UnimplementedError();
@override
Future<Map<String, dynamic>> previewAdminInventoryMerge(
{required int sourceInventoryId, required int targetInventoryId}) =>
throw UnimplementedError();
@override
Future<Map<String, dynamic>> previewMerge(
{required int sourceProductId, required int targetProductId}) =>
throw UnimplementedError();
@override
Future<AdminProduct> promotePrivateProduct(int productId) =>
throw UnimplementedError();
@override
Future<void> removeAdminInventory(int inventoryId) =>
throw UnimplementedError();
@override
Future<void> removeAdminPantryItem(int pantryItemId) =>
throw UnimplementedError();
@override
Future<void> removeProduct(int productId) => throw UnimplementedError();
@override
Future<void> removeReceiptAlias(int id) => throw UnimplementedError();
@override
Future<Map<String, dynamic>> resetPassword(int userId) =>
throw UnimplementedError();
@override
Future<void> restoreProduct(int productId) => throw UnimplementedError();
@override
Future<UserAdmin> setPremium(int userId, {required bool isPremium}) =>
throw UnimplementedError();
@override
Future<void> setProductCategory(int productId, {required int? categoryId}) =>
throw UnimplementedError();
@override
Future<void> setProductStatus(int productId, String status) =>
throw UnimplementedError();
@override
Future<UserAdmin> setRecipeSharing(int userId,
{required bool canShareRecipes}) =>
throw UnimplementedError();
@override
Future<UserAdmin> setRole(int userId, String newRole) =>
throw UnimplementedError();
@override
Future<AdminInventoryItem> updateAdminInventory(int inventoryId,
{int? productId,
double? quantity,
String? unit,
String? location,
String? brand,
String? receiptName,
String? suitableFor,
String? comment}) =>
throw UnimplementedError();
@override
Future<AdminPantryItem> updateAdminPantry(int pantryItemId,
{int? productId, String? location}) =>
throw UnimplementedError();
@override
Future<void> updateCanonicalName(int productId, String canonicalName) =>
throw UnimplementedError();
@override
Future<void> updateCanonicalNamePrivate(
int productId, String canonicalName) =>
throw UnimplementedError();
@override
Future<void> updateEmail(int userId, String email) =>
throw UnimplementedError();
@override
Future<void> upsertReceiptAlias(
{required String receiptName,
required int productId,
bool isGlobal = false}) =>
throw UnimplementedError();
}
// Simple fake that only implements the methods we need
class FakeAdminRepository {
List<ReceiptAlias> _aliases = [];
List<AdminProduct> _products = [];
Future<List<ReceiptAlias>> listReceiptAliases() async {
return _aliases;
}
Future<List<AdminProduct>> listGlobalProducts() async {
return _products;
}
Future<void> updateReceiptAlias(int id,
{String? receiptName, int? productId, bool? isGlobal}) async {
// Find and update alias
final index = _aliases.indexWhere((a) => a.id == id);
if (index >= 0) {
_aliases[index] = ReceiptAlias(
id: id,
receiptName: receiptName ?? _aliases[index].receiptName,
productId: productId ?? _aliases[index].productId,
ownerId: _aliases[index].ownerId,
productName: _products
.firstWhere((p) => p.id == (productId ?? _aliases[index].productId))
.displayName,
isGlobal: isGlobal ?? _aliases[index].isGlobal,
);
}
}
void setAliases(List<ReceiptAlias> aliases) {
_aliases = aliases;
}
void setProducts(List<AdminProduct> products) {
_products = products;
}
}
void main() {
group('AdminAliasesPanel - Switch Tests', () {
late FakeAdminRepository fakeRepo;
late TestAdminRepositoryWrapper wrapper;
late List<ReceiptAlias> mockAliases;
late List<AdminProduct> mockProducts;
setUp(() {
fakeRepo = FakeAdminRepository();
wrapper = TestAdminRepositoryWrapper(fakeRepo);
mockProducts = [
AdminProduct(
id: 1,
name: 'Test Product',
canonicalName: 'Test Product',
categoryPath: 'Test > Category',
),
];
fakeRepo.setProducts(mockProducts);
});
testWidgets('Switch should be disabled for global alias', (tester) async {
mockAliases = [
ReceiptAlias(
id: 1,
receiptName: 'Global Alias',
productId: 1,
ownerId: null,
productName: 'Test Product',
isGlobal: true,
),
];
fakeRepo.setAliases(mockAliases);
await tester.pumpWidget(
ProviderScope(
overrides: [
adminRepositoryProvider.overrideWithValue(wrapper),
],
child: MaterialApp(
home: Scaffold(
body: AdminAliasesPanel(embedded: true),
),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.edit_outlined).first);
await tester.pumpAndSettle();
final switchListTileFinder = find.byType(SwitchListTile);
expect(switchListTileFinder, findsOneWidget);
final switchTile = tester.widget<SwitchListTile>(switchListTileFinder);
expect(switchTile.value, isTrue);
expect(switchTile.onChanged, isNull); // Disabled
expect(find.text('Aliaset är redan globalt.'), findsOneWidget);
});
testWidgets('Switch should be enabled for private alias', (tester) async {
mockAliases = [
ReceiptAlias(
id: 1,
receiptName: 'Private Alias',
productId: 1,
ownerId: 123,
productName: 'Test Product',
isGlobal: false,
),
];
fakeRepo.setAliases(mockAliases);
await tester.pumpWidget(
ProviderScope(
overrides: [
adminRepositoryProvider.overrideWithValue(wrapper),
],
child: MaterialApp(
home: Scaffold(
body: AdminAliasesPanel(embedded: true),
),
),
),
);
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.edit_outlined).first);
await tester.pumpAndSettle();
final switchListTileFinder = find.byType(SwitchListTile);
expect(switchListTileFinder, findsOneWidget);
final switchTile = tester.widget<SwitchListTile>(switchListTileFinder);
expect(switchTile.value, isFalse);
expect(switchTile.onChanged, isNotNull); // Enabled
expect(find.text('Du kan göra privata alias globala.'), findsOneWidget);
});
});
}