feat: add tests
This commit is contained in:
@@ -600,6 +600,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
mocktail:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: mocktail
|
||||||
|
sha256: "5e1bf53cc7baa8062a33b84424deb61513858ea05c601b8509e683815b5914aa"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.5"
|
||||||
native_toolchain_c:
|
native_toolchain_c:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ dev_dependencies:
|
|||||||
flutter_lints: ^6.0.0
|
flutter_lints: ^6.0.0
|
||||||
drift_dev: ^2.34.2+1
|
drift_dev: ^2.34.2+1
|
||||||
build_runner: ^2.15.0
|
build_runner: ^2.15.0
|
||||||
|
mocktail: ^1.0.4
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:kooltab2/models/bar_tab.dart';
|
||||||
|
import 'package:kooltab2/models/tab_item.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('BarTab', () {
|
||||||
|
group('totalInCents', () {
|
||||||
|
test('calculates total from items', () {
|
||||||
|
final tab = BarTab(
|
||||||
|
id: 'tab-1',
|
||||||
|
customerName: 'John',
|
||||||
|
status: 'open',
|
||||||
|
openedAt: DateTime.now(),
|
||||||
|
items: [
|
||||||
|
const TabItem(
|
||||||
|
id: 'item-1',
|
||||||
|
tabId: 'tab-1',
|
||||||
|
productId: 'prod-1',
|
||||||
|
productName: 'Beer',
|
||||||
|
quantity: 2,
|
||||||
|
unitPriceInCents: 400,
|
||||||
|
),
|
||||||
|
const TabItem(
|
||||||
|
id: 'item-2',
|
||||||
|
tabId: 'tab-1',
|
||||||
|
productId: 'prod-2',
|
||||||
|
productName: 'Wine',
|
||||||
|
quantity: 1,
|
||||||
|
unitPriceInCents: 600,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tab.totalInCents, 1400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns 0 for empty tab', () {
|
||||||
|
final tab = BarTab(
|
||||||
|
id: 'tab-1',
|
||||||
|
customerName: 'John',
|
||||||
|
status: 'open',
|
||||||
|
openedAt: DateTime.now(),
|
||||||
|
items: const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tab.totalInCents, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('itemCount', () {
|
||||||
|
test('sums quantities of all items', () {
|
||||||
|
final tab = BarTab(
|
||||||
|
id: 'tab-1',
|
||||||
|
customerName: 'John',
|
||||||
|
status: 'open',
|
||||||
|
openedAt: DateTime.now(),
|
||||||
|
items: [
|
||||||
|
const TabItem(
|
||||||
|
id: 'item-1',
|
||||||
|
tabId: 'tab-1',
|
||||||
|
productId: 'prod-1',
|
||||||
|
productName: 'Beer',
|
||||||
|
quantity: 3,
|
||||||
|
unitPriceInCents: 400,
|
||||||
|
),
|
||||||
|
const TabItem(
|
||||||
|
id: 'item-2',
|
||||||
|
tabId: 'tab-1',
|
||||||
|
productId: 'prod-2',
|
||||||
|
productName: 'Wine',
|
||||||
|
quantity: 2,
|
||||||
|
unitPriceInCents: 600,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tab.itemCount, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns 0 for empty tab', () {
|
||||||
|
final tab = BarTab(
|
||||||
|
id: 'tab-1',
|
||||||
|
customerName: 'John',
|
||||||
|
status: 'open',
|
||||||
|
openedAt: DateTime.now(),
|
||||||
|
items: const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tab.itemCount, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('formattedTotal', () {
|
||||||
|
test('formats total with euro symbol', () {
|
||||||
|
final tab = BarTab(
|
||||||
|
id: 'tab-1',
|
||||||
|
customerName: 'John',
|
||||||
|
status: 'open',
|
||||||
|
openedAt: DateTime.now(),
|
||||||
|
items: const [
|
||||||
|
TabItem(
|
||||||
|
id: 'item-1',
|
||||||
|
tabId: 'tab-1',
|
||||||
|
productId: 'prod-1',
|
||||||
|
productName: 'Beer',
|
||||||
|
quantity: 2,
|
||||||
|
unitPriceInCents: 425,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tab.formattedTotal, '€8.50');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('isOpen', () {
|
||||||
|
test('returns true for open status', () {
|
||||||
|
final tab = BarTab(
|
||||||
|
id: 'tab-1',
|
||||||
|
customerName: 'John',
|
||||||
|
status: 'open',
|
||||||
|
openedAt: DateTime.now(),
|
||||||
|
items: const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tab.isOpen, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false for closed status', () {
|
||||||
|
final tab = BarTab(
|
||||||
|
id: 'tab-1',
|
||||||
|
customerName: 'John',
|
||||||
|
status: 'closed',
|
||||||
|
openedAt: DateTime.now(),
|
||||||
|
items: const [],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tab.isOpen, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import 'package:drift/drift.dart' hide isNull, isNotNull;
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:kooltab2/database/app_database.dart';
|
||||||
|
import 'package:kooltab2/models/product.dart';
|
||||||
|
import 'package:kooltab2/services/bar_tab_service.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late AppDatabase database;
|
||||||
|
late DriftBarTabService service;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
database = AppDatabase(NativeDatabase.memory());
|
||||||
|
service = DriftBarTabService(database: database);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await database.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
Product createTestProduct({
|
||||||
|
String id = 'prod-1',
|
||||||
|
String name = 'Test Beer',
|
||||||
|
int priceInCents = 400,
|
||||||
|
}) {
|
||||||
|
return Product(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 100,
|
||||||
|
lowStockThreshold: 10,
|
||||||
|
priceInCents: priceInCents,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('DriftBarTabService', () {
|
||||||
|
group('createTab', () {
|
||||||
|
test('creates tab with open status', () async {
|
||||||
|
final tab = await service.createTab(customerName: 'John');
|
||||||
|
|
||||||
|
expect(tab.customerName, 'John');
|
||||||
|
expect(tab.status, 'open');
|
||||||
|
expect(tab.items, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('creates tab with unique id', () async {
|
||||||
|
final tab1 = await service.createTab(customerName: 'John');
|
||||||
|
final tab2 = await service.createTab(customerName: 'Jane');
|
||||||
|
|
||||||
|
expect(tab1.id, isNot(equals(tab2.id)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('getOpenTabs', () {
|
||||||
|
test('returns empty list when no tabs', () async {
|
||||||
|
final tabs = await service.getOpenTabs();
|
||||||
|
expect(tabs, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns only open tabs', () async {
|
||||||
|
await service.createTab(customerName: 'Open Tab');
|
||||||
|
await database.into(database.barTabs).insert(
|
||||||
|
BarTabsCompanion.insert(
|
||||||
|
id: 'closed-tab',
|
||||||
|
customerName: 'Closed Tab',
|
||||||
|
status: const Value('closed'),
|
||||||
|
openedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final tabs = await service.getOpenTabs();
|
||||||
|
|
||||||
|
expect(tabs.length, 1);
|
||||||
|
expect(tabs.first.customerName, 'Open Tab');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tabs sorted by openedAt descending', () async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
await database.into(database.barTabs).insert(
|
||||||
|
BarTabsCompanion.insert(
|
||||||
|
id: 'tab-first',
|
||||||
|
customerName: 'First',
|
||||||
|
status: const Value('open'),
|
||||||
|
openedAt: now.subtract(const Duration(hours: 1)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await database.into(database.barTabs).insert(
|
||||||
|
BarTabsCompanion.insert(
|
||||||
|
id: 'tab-second',
|
||||||
|
customerName: 'Second',
|
||||||
|
status: const Value('open'),
|
||||||
|
openedAt: now,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final tabs = await service.getOpenTabs();
|
||||||
|
|
||||||
|
expect(tabs.first.customerName, 'Second');
|
||||||
|
expect(tabs.last.customerName, 'First');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('getTabById', () {
|
||||||
|
test('returns tab when exists', () async {
|
||||||
|
final created = await service.createTab(customerName: 'Test');
|
||||||
|
|
||||||
|
final tab = await service.getTabById(created.id);
|
||||||
|
|
||||||
|
expect(tab, isNotNull);
|
||||||
|
expect(tab!.customerName, 'Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null when not exists', () async {
|
||||||
|
final tab = await service.getTabById('nonexistent');
|
||||||
|
expect(tab, isNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('addProductToTab', () {
|
||||||
|
test('adds new product to tab', () async {
|
||||||
|
final tab = await service.createTab(customerName: 'John');
|
||||||
|
final product = createTestProduct();
|
||||||
|
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
|
||||||
|
final updatedTab = await service.getTabById(tab.id);
|
||||||
|
expect(updatedTab!.items.length, 1);
|
||||||
|
expect(updatedTab.items.first.productName, 'Test Beer');
|
||||||
|
expect(updatedTab.items.first.quantity, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('increments quantity when product already in tab', () async {
|
||||||
|
final tab = await service.createTab(customerName: 'John');
|
||||||
|
final product = createTestProduct();
|
||||||
|
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
|
||||||
|
final updatedTab = await service.getTabById(tab.id);
|
||||||
|
expect(updatedTab!.items.length, 1);
|
||||||
|
expect(updatedTab.items.first.quantity, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adding product calculates correct line total', () async {
|
||||||
|
final tab = await service.createTab(customerName: 'John');
|
||||||
|
final product = createTestProduct(priceInCents: 450);
|
||||||
|
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
|
||||||
|
final updatedTab = await service.getTabById(tab.id);
|
||||||
|
expect(updatedTab!.items.first.lineTotalInCents, 900);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('updateTabItemQuantity', () {
|
||||||
|
test('updates item quantity', () async {
|
||||||
|
final tab = await service.createTab(customerName: 'John');
|
||||||
|
final product = createTestProduct();
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
|
||||||
|
final tabItemId = (await service.getTabById(tab.id))!.items.first.id;
|
||||||
|
|
||||||
|
await service.updateTabItemQuantity(
|
||||||
|
tabItemId: tabItemId,
|
||||||
|
quantity: 5,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedTab = await service.getTabById(tab.id);
|
||||||
|
expect(updatedTab!.items.first.quantity, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deletes item when quantity is zero', () async {
|
||||||
|
final tab = await service.createTab(customerName: 'John');
|
||||||
|
final product = createTestProduct();
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
|
||||||
|
final tabItemId = (await service.getTabById(tab.id))!.items.first.id;
|
||||||
|
|
||||||
|
await service.updateTabItemQuantity(tabItemId: tabItemId, quantity: 0);
|
||||||
|
|
||||||
|
final updatedTab = await service.getTabById(tab.id);
|
||||||
|
expect(updatedTab!.items, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deletes item when quantity is negative', () async {
|
||||||
|
final tab = await service.createTab(customerName: 'John');
|
||||||
|
final product = createTestProduct();
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
|
||||||
|
final tabItemId = (await service.getTabById(tab.id))!.items.first.id;
|
||||||
|
|
||||||
|
await service.updateTabItemQuantity(tabItemId: tabItemId, quantity: -1);
|
||||||
|
|
||||||
|
final updatedTab = await service.getTabById(tab.id);
|
||||||
|
expect(updatedTab!.items, isEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('closeTab', () {
|
||||||
|
test('archives tab items to closed tabs', () async {
|
||||||
|
final tab = await service.createTab(customerName: 'John');
|
||||||
|
final product = createTestProduct();
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
|
||||||
|
await service.closeTab(tab.id);
|
||||||
|
|
||||||
|
final closedTabs = await service.getClosedTabs();
|
||||||
|
expect(closedTabs.length, 1);
|
||||||
|
expect(closedTabs.first.customerName, 'John');
|
||||||
|
expect(closedTabs.first.items.length, 1);
|
||||||
|
expect(closedTabs.first.items.first.quantity, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clears tab items after closing', () async {
|
||||||
|
final tab = await service.createTab(customerName: 'John');
|
||||||
|
final product = createTestProduct();
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
|
||||||
|
await service.closeTab(tab.id);
|
||||||
|
|
||||||
|
final updatedTab = await service.getTabById(tab.id);
|
||||||
|
expect(updatedTab!.items, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tab remains open but items cleared', () async {
|
||||||
|
final tab = await service.createTab(customerName: 'John');
|
||||||
|
final product = createTestProduct();
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
|
||||||
|
await service.closeTab(tab.id);
|
||||||
|
|
||||||
|
final updatedTab = await service.getTabById(tab.id);
|
||||||
|
expect(updatedTab!.isOpen, true);
|
||||||
|
expect(updatedTab.items, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('closed tab preserves item prices', () async {
|
||||||
|
final tab = await service.createTab(customerName: 'John');
|
||||||
|
final product = createTestProduct(priceInCents: 550);
|
||||||
|
await service.addProductToTab(tabId: tab.id, product: product);
|
||||||
|
|
||||||
|
await service.closeTab(tab.id);
|
||||||
|
|
||||||
|
final closedTabs = await service.getClosedTabs();
|
||||||
|
expect(closedTabs.first.items.first.unitPriceInCents, 550);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('getClosedTabs', () {
|
||||||
|
test('returns empty when no closed tabs', () async {
|
||||||
|
final tabs = await service.getClosedTabs();
|
||||||
|
expect(tabs, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns closed tabs sorted by closedAt descending', () async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
await database.into(database.closedTabs).insert(
|
||||||
|
ClosedTabsCompanion.insert(
|
||||||
|
id: 'closed-first',
|
||||||
|
originalTabId: 'original-1',
|
||||||
|
customerName: 'First',
|
||||||
|
closedAt: now.subtract(const Duration(hours: 1)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await database.into(database.closedTabs).insert(
|
||||||
|
ClosedTabsCompanion.insert(
|
||||||
|
id: 'closed-second',
|
||||||
|
originalTabId: 'original-2',
|
||||||
|
customerName: 'Second',
|
||||||
|
closedAt: now,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final closedTabs = await service.getClosedTabs();
|
||||||
|
|
||||||
|
expect(closedTabs.first.customerName, 'Second');
|
||||||
|
expect(closedTabs.last.customerName, 'First');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:kooltab2/models/product.dart';
|
||||||
|
import 'package:kooltab2/services/product_service.dart';
|
||||||
|
import 'package:kooltab2/viewmodels/inventory_view_model.dart';
|
||||||
|
|
||||||
|
class MockProductService extends Mock implements ProductService {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late InventoryViewModel viewModel;
|
||||||
|
late MockProductService mockService;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockService = MockProductService();
|
||||||
|
viewModel = InventoryViewModel(productService: mockService);
|
||||||
|
});
|
||||||
|
|
||||||
|
setUpAll(() {
|
||||||
|
registerFallbackValue(const Product(
|
||||||
|
id: 'fallback',
|
||||||
|
name: 'Fallback',
|
||||||
|
category: 'Test',
|
||||||
|
stockQuantity: 0,
|
||||||
|
lowStockThreshold: 0,
|
||||||
|
priceInCents: 0,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
final testProducts = [
|
||||||
|
const Product(
|
||||||
|
id: 'prod-1',
|
||||||
|
name: 'Beer',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 20,
|
||||||
|
lowStockThreshold: 5,
|
||||||
|
priceInCents: 400,
|
||||||
|
),
|
||||||
|
const Product(
|
||||||
|
id: 'prod-2',
|
||||||
|
name: 'Wine',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 3,
|
||||||
|
lowStockThreshold: 5,
|
||||||
|
priceInCents: 600,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
group('InventoryViewModel', () {
|
||||||
|
group('load', () {
|
||||||
|
test('loads products from service', () async {
|
||||||
|
when(() => mockService.getProducts())
|
||||||
|
.thenAnswer((_) async => testProducts);
|
||||||
|
|
||||||
|
await viewModel.load();
|
||||||
|
|
||||||
|
expect(viewModel.products, testProducts);
|
||||||
|
verify(() => mockService.getProducts()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('notifies listeners after load', () async {
|
||||||
|
var notifyCount = 0;
|
||||||
|
viewModel.addListener(() => notifyCount++);
|
||||||
|
when(() => mockService.getProducts())
|
||||||
|
.thenAnswer((_) async => testProducts);
|
||||||
|
|
||||||
|
await viewModel.load();
|
||||||
|
|
||||||
|
expect(notifyCount, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('decreaseStock', () {
|
||||||
|
test('decreases stock via service', () async {
|
||||||
|
when(() => mockService.decreaseStock('prod-1', 3))
|
||||||
|
.thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await viewModel.decreaseStock('prod-1', 3);
|
||||||
|
|
||||||
|
verify(() => mockService.decreaseStock('prod-1', 3)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updates local product cache', () async {
|
||||||
|
when(() => mockService.getProducts())
|
||||||
|
.thenAnswer((_) async => testProducts);
|
||||||
|
when(() => mockService.decreaseStock('prod-1', 5))
|
||||||
|
.thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await viewModel.load();
|
||||||
|
await viewModel.decreaseStock('prod-1', 5);
|
||||||
|
|
||||||
|
final product = viewModel.products.firstWhere((p) => p.id == 'prod-1');
|
||||||
|
expect(product.stockQuantity, 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('notifies listeners after decrease', () async {
|
||||||
|
var notifyCount = 0;
|
||||||
|
viewModel.addListener(() => notifyCount++);
|
||||||
|
when(() => mockService.decreaseStock('prod-1', 1))
|
||||||
|
.thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await viewModel.decreaseStock('prod-1', 1);
|
||||||
|
|
||||||
|
expect(notifyCount, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('increaseStock', () {
|
||||||
|
test('increases stock via service', () async {
|
||||||
|
when(() => mockService.increaseStock('prod-1', 5))
|
||||||
|
.thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await viewModel.increaseStock('prod-1', 5);
|
||||||
|
|
||||||
|
verify(() => mockService.increaseStock('prod-1', 5)).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updates local product cache', () async {
|
||||||
|
when(() => mockService.getProducts())
|
||||||
|
.thenAnswer((_) async => testProducts);
|
||||||
|
when(() => mockService.increaseStock('prod-2', 2))
|
||||||
|
.thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await viewModel.load();
|
||||||
|
await viewModel.increaseStock('prod-2', 2);
|
||||||
|
|
||||||
|
final product = viewModel.products.firstWhere((p) => p.id == 'prod-2');
|
||||||
|
expect(product.stockQuantity, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('notifies listeners after increase', () async {
|
||||||
|
var notifyCount = 0;
|
||||||
|
viewModel.addListener(() => notifyCount++);
|
||||||
|
when(() => mockService.increaseStock('prod-1', 1))
|
||||||
|
.thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await viewModel.increaseStock('prod-1', 1);
|
||||||
|
|
||||||
|
expect(notifyCount, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('edge cases', () {
|
||||||
|
test('handles decrease for product not in local cache', () async {
|
||||||
|
when(() => mockService.getProducts())
|
||||||
|
.thenAnswer((_) async => testProducts);
|
||||||
|
when(() => mockService.decreaseStock('unknown', 1))
|
||||||
|
.thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await viewModel.load();
|
||||||
|
await viewModel.decreaseStock('unknown', 1);
|
||||||
|
|
||||||
|
verify(() => mockService.decreaseStock('unknown', 1)).called(1);
|
||||||
|
expect(viewModel.products.length, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:kooltab2/services/pin_lock_service.dart';
|
||||||
|
|
||||||
|
class MockFlutterSecureStorage extends Mock
|
||||||
|
implements FlutterSecureStorage {}
|
||||||
|
|
||||||
|
String _computeHash(String pin, String salt) {
|
||||||
|
final bytes = utf8.encode('$salt:$pin');
|
||||||
|
return sha256.convert(bytes).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late PinLockService service;
|
||||||
|
late MockFlutterSecureStorage mockStorage;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockStorage = MockFlutterSecureStorage();
|
||||||
|
service = PinLockService(storage: mockStorage);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('PinLockService', () {
|
||||||
|
group('hasPin', () {
|
||||||
|
test('returns false when no hash stored', () async {
|
||||||
|
when(() => mockStorage.read(key: 'pin_hash'))
|
||||||
|
.thenAnswer((_) async => null);
|
||||||
|
|
||||||
|
final result = await service.hasPin();
|
||||||
|
|
||||||
|
expect(result, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when hash is empty', () async {
|
||||||
|
when(() => mockStorage.read(key: 'pin_hash'))
|
||||||
|
.thenAnswer((_) async => '');
|
||||||
|
|
||||||
|
final result = await service.hasPin();
|
||||||
|
|
||||||
|
expect(result, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true when hash exists', () async {
|
||||||
|
when(() => mockStorage.read(key: 'pin_hash'))
|
||||||
|
.thenAnswer((_) async => 'somehash');
|
||||||
|
|
||||||
|
final result = await service.hasPin();
|
||||||
|
|
||||||
|
expect(result, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('setPin', () {
|
||||||
|
test('stores salt and hash', () async {
|
||||||
|
when(() => mockStorage.write(key: any(named: 'key'), value: any(named: 'value')))
|
||||||
|
.thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await service.setPin('1234');
|
||||||
|
|
||||||
|
verify(() => mockStorage.write(key: 'pin_salt', value: any(named: 'value'))).called(1);
|
||||||
|
verify(() => mockStorage.write(key: 'pin_hash', value: any(named: 'value'))).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('generates unique salts for different pins', () async {
|
||||||
|
when(() => mockStorage.write(key: any(named: 'key'), value: any(named: 'value')))
|
||||||
|
.thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await service.setPin('1234');
|
||||||
|
final firstSaltCapture = verify(() => mockStorage.write(
|
||||||
|
key: 'pin_salt',
|
||||||
|
value: captureAny(named: 'value'),
|
||||||
|
)).captured.first;
|
||||||
|
|
||||||
|
await service.setPin('5678');
|
||||||
|
final secondSaltCapture = verify(() => mockStorage.write(
|
||||||
|
key: 'pin_salt',
|
||||||
|
value: captureAny(named: 'value'),
|
||||||
|
)).captured.last;
|
||||||
|
|
||||||
|
expect(firstSaltCapture, isNot(equals(secondSaltCapture)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('verifyPin', () {
|
||||||
|
test('returns false when salt is missing', () async {
|
||||||
|
when(() => mockStorage.read(key: 'pin_salt'))
|
||||||
|
.thenAnswer((_) async => null);
|
||||||
|
when(() => mockStorage.read(key: 'pin_hash'))
|
||||||
|
.thenAnswer((_) async => 'somehash');
|
||||||
|
|
||||||
|
final result = await service.verifyPin('1234');
|
||||||
|
|
||||||
|
expect(result, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when hash is missing', () async {
|
||||||
|
when(() => mockStorage.read(key: 'pin_salt'))
|
||||||
|
.thenAnswer((_) async => 'somesalt');
|
||||||
|
when(() => mockStorage.read(key: 'pin_hash'))
|
||||||
|
.thenAnswer((_) async => null);
|
||||||
|
|
||||||
|
final result = await service.verifyPin('1234');
|
||||||
|
|
||||||
|
expect(result, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true for correct pin', () async {
|
||||||
|
const salt = 'test_salt';
|
||||||
|
const pin = '1234';
|
||||||
|
final expectedHash = _computeHash(pin, salt);
|
||||||
|
when(() => mockStorage.read(key: 'pin_salt'))
|
||||||
|
.thenAnswer((_) async => salt);
|
||||||
|
when(() => mockStorage.read(key: 'pin_hash'))
|
||||||
|
.thenAnswer((_) async => expectedHash);
|
||||||
|
|
||||||
|
final result = await service.verifyPin('1234');
|
||||||
|
|
||||||
|
expect(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false for incorrect pin', () async {
|
||||||
|
const salt = 'test_salt';
|
||||||
|
final correctHash = _computeHash('1234', salt);
|
||||||
|
when(() => mockStorage.read(key: 'pin_salt'))
|
||||||
|
.thenAnswer((_) async => salt);
|
||||||
|
when(() => mockStorage.read(key: 'pin_hash'))
|
||||||
|
.thenAnswer((_) async => correctHash);
|
||||||
|
|
||||||
|
final result = await service.verifyPin('5678');
|
||||||
|
|
||||||
|
expect(result, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('clearPin', () {
|
||||||
|
test('deletes both salt and hash', () async {
|
||||||
|
when(() => mockStorage.delete(key: any(named: 'key')))
|
||||||
|
.thenAnswer((_) async {});
|
||||||
|
|
||||||
|
await service.clearPin();
|
||||||
|
|
||||||
|
verify(() => mockStorage.delete(key: 'pin_salt')).called(1);
|
||||||
|
verify(() => mockStorage.delete(key: 'pin_hash')).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:kooltab2/models/product.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Product', () {
|
||||||
|
group('isLowStock', () {
|
||||||
|
test('returns true when stock equals threshold', () {
|
||||||
|
const product = Product(
|
||||||
|
id: '1',
|
||||||
|
name: 'Test',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 5,
|
||||||
|
lowStockThreshold: 5,
|
||||||
|
priceInCents: 100,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(product.isLowStock, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true when stock is below threshold', () {
|
||||||
|
const product = Product(
|
||||||
|
id: '1',
|
||||||
|
name: 'Test',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 3,
|
||||||
|
lowStockThreshold: 5,
|
||||||
|
priceInCents: 100,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(product.isLowStock, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false when stock is above threshold', () {
|
||||||
|
const product = Product(
|
||||||
|
id: '1',
|
||||||
|
name: 'Test',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 10,
|
||||||
|
lowStockThreshold: 5,
|
||||||
|
priceInCents: 100,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(product.isLowStock, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('formattedPrice', () {
|
||||||
|
test('formats price with euro symbol and 2 decimals', () {
|
||||||
|
const product = Product(
|
||||||
|
id: '1',
|
||||||
|
name: 'Test',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 10,
|
||||||
|
lowStockThreshold: 5,
|
||||||
|
priceInCents: 1234,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(product.formattedPrice, '€12.34');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats zero price correctly', () {
|
||||||
|
const product = Product(
|
||||||
|
id: '1',
|
||||||
|
name: 'Test',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 10,
|
||||||
|
lowStockThreshold: 5,
|
||||||
|
priceInCents: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(product.formattedPrice, '€0.00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats whole euros correctly', () {
|
||||||
|
const product = Product(
|
||||||
|
id: '1',
|
||||||
|
name: 'Test',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 10,
|
||||||
|
lowStockThreshold: 5,
|
||||||
|
priceInCents: 500,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(product.formattedPrice, '€5.00');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('copyWith', () {
|
||||||
|
test('creates copy with updated fields', () {
|
||||||
|
const original = Product(
|
||||||
|
id: '1',
|
||||||
|
name: 'Original',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 10,
|
||||||
|
lowStockThreshold: 5,
|
||||||
|
priceInCents: 100,
|
||||||
|
);
|
||||||
|
|
||||||
|
final copy = original.copyWith(name: 'Updated', stockQuantity: 20);
|
||||||
|
|
||||||
|
expect(copy.name, 'Updated');
|
||||||
|
expect(copy.stockQuantity, 20);
|
||||||
|
expect(copy.id, '1');
|
||||||
|
expect(copy.category, 'Drinks');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves original values when not specified', () {
|
||||||
|
const original = Product(
|
||||||
|
id: '1',
|
||||||
|
name: 'Test',
|
||||||
|
category: 'Food',
|
||||||
|
stockQuantity: 15,
|
||||||
|
lowStockThreshold: 3,
|
||||||
|
priceInCents: 750,
|
||||||
|
imagePath: '/path/to/image.jpg',
|
||||||
|
active: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final copy = original.copyWith();
|
||||||
|
|
||||||
|
expect(copy.id, '1');
|
||||||
|
expect(copy.name, 'Test');
|
||||||
|
expect(copy.category, 'Food');
|
||||||
|
expect(copy.stockQuantity, 15);
|
||||||
|
expect(copy.lowStockThreshold, 3);
|
||||||
|
expect(copy.priceInCents, 750);
|
||||||
|
expect(copy.imagePath, '/path/to/image.jpg');
|
||||||
|
expect(copy.active, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import 'package:drift/drift.dart' hide isNull, isNotNull;
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:kooltab2/database/app_database.dart';
|
||||||
|
import 'package:kooltab2/models/product.dart';
|
||||||
|
import 'package:kooltab2/services/product_service.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late AppDatabase database;
|
||||||
|
late DriftProductService service;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
database = AppDatabase(NativeDatabase.memory());
|
||||||
|
service = DriftProductService(database: database);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await database.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
group('DriftProductService', () {
|
||||||
|
group('getProducts', () {
|
||||||
|
test('returns empty list when no products', () async {
|
||||||
|
final products = await service.getProducts();
|
||||||
|
expect(products, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns only active products', () async {
|
||||||
|
await database.into(database.products).insert(
|
||||||
|
ProductsCompanion.insert(
|
||||||
|
id: '1',
|
||||||
|
name: 'Active Product',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 10,
|
||||||
|
lowStockThreshold: 2,
|
||||||
|
priceInCents: 500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await database.into(database.products).insert(
|
||||||
|
ProductsCompanion.insert(
|
||||||
|
id: '2',
|
||||||
|
name: 'Inactive Product',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 5,
|
||||||
|
lowStockThreshold: 1,
|
||||||
|
priceInCents: 300,
|
||||||
|
active: const Value(false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final products = await service.getProducts();
|
||||||
|
|
||||||
|
expect(products.length, 1);
|
||||||
|
expect(products.first.name, 'Active Product');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns products sorted by name', () async {
|
||||||
|
await database.into(database.products).insert(
|
||||||
|
ProductsCompanion.insert(
|
||||||
|
id: '1',
|
||||||
|
name: 'Zebra',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 10,
|
||||||
|
lowStockThreshold: 2,
|
||||||
|
priceInCents: 500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await database.into(database.products).insert(
|
||||||
|
ProductsCompanion.insert(
|
||||||
|
id: '2',
|
||||||
|
name: 'Apple',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 5,
|
||||||
|
lowStockThreshold: 1,
|
||||||
|
priceInCents: 300,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final products = await service.getProducts();
|
||||||
|
|
||||||
|
expect(products.first.name, 'Apple');
|
||||||
|
expect(products.last.name, 'Zebra');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('getProductById', () {
|
||||||
|
test('returns product when exists', () async {
|
||||||
|
await database.into(database.products).insert(
|
||||||
|
ProductsCompanion.insert(
|
||||||
|
id: 'prod-123',
|
||||||
|
name: 'Test Product',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 10,
|
||||||
|
lowStockThreshold: 2,
|
||||||
|
priceInCents: 500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final product = await service.getProductById('prod-123');
|
||||||
|
|
||||||
|
expect(product, isNotNull);
|
||||||
|
expect(product!.name, 'Test Product');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null when not exists', () async {
|
||||||
|
final product = await service.getProductById('nonexistent');
|
||||||
|
expect(product, isNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('createProduct', () {
|
||||||
|
test('creates product successfully', () async {
|
||||||
|
await service.createProduct(
|
||||||
|
name: 'New Product',
|
||||||
|
category: 'Food',
|
||||||
|
stockQuantity: 20,
|
||||||
|
lowStockThreshold: 5,
|
||||||
|
priceInCents: 850,
|
||||||
|
imagePath: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
final products = await service.getProducts();
|
||||||
|
expect(products.length, 1);
|
||||||
|
expect(products.first.name, 'New Product');
|
||||||
|
expect(products.first.stockQuantity, 20);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('updateProduct', () {
|
||||||
|
test('updates product fields', () async {
|
||||||
|
await database.into(database.products).insert(
|
||||||
|
ProductsCompanion.insert(
|
||||||
|
id: 'update-test',
|
||||||
|
name: 'Original',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 10,
|
||||||
|
lowStockThreshold: 2,
|
||||||
|
priceInCents: 500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await service.updateProduct(
|
||||||
|
const Product(
|
||||||
|
id: 'update-test',
|
||||||
|
name: 'Updated',
|
||||||
|
category: 'Food',
|
||||||
|
stockQuantity: 15,
|
||||||
|
lowStockThreshold: 3,
|
||||||
|
priceInCents: 750,
|
||||||
|
active: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final product = await service.getProductById('update-test');
|
||||||
|
expect(product!.name, 'Updated');
|
||||||
|
expect(product.stockQuantity, 15);
|
||||||
|
expect(product.priceInCents, 750);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('deleteProduct', () {
|
||||||
|
test('removes product from database', () async {
|
||||||
|
await database.into(database.products).insert(
|
||||||
|
ProductsCompanion.insert(
|
||||||
|
id: 'delete-test',
|
||||||
|
name: 'To Delete',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 10,
|
||||||
|
lowStockThreshold: 2,
|
||||||
|
priceInCents: 500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await service.deleteProduct('delete-test');
|
||||||
|
|
||||||
|
final products = await database.select(database.products).get();
|
||||||
|
expect(products, isEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('decreaseStock', () {
|
||||||
|
test('decreases stock quantity', () async {
|
||||||
|
await database.into(database.products).insert(
|
||||||
|
ProductsCompanion.insert(
|
||||||
|
id: 'stock-test',
|
||||||
|
name: 'Stock Test',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 10,
|
||||||
|
lowStockThreshold: 2,
|
||||||
|
priceInCents: 500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await service.decreaseStock('stock-test', 3);
|
||||||
|
|
||||||
|
final product = await service.getProductById('stock-test');
|
||||||
|
expect(product!.stockQuantity, 7);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when product not found', () async {
|
||||||
|
expect(
|
||||||
|
() => service.decreaseStock('nonexistent', 1),
|
||||||
|
throwsA(isA<Exception>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when not enough stock', () async {
|
||||||
|
await database.into(database.products).insert(
|
||||||
|
ProductsCompanion.insert(
|
||||||
|
id: 'low-stock',
|
||||||
|
name: 'Low Stock',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 3,
|
||||||
|
lowStockThreshold: 1,
|
||||||
|
priceInCents: 500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => service.decreaseStock('low-stock', 5),
|
||||||
|
throwsA(isA<Exception>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('increaseStock', () {
|
||||||
|
test('increases stock quantity', () async {
|
||||||
|
await database.into(database.products).insert(
|
||||||
|
ProductsCompanion.insert(
|
||||||
|
id: 'increase-test',
|
||||||
|
name: 'Increase Test',
|
||||||
|
category: 'Drinks',
|
||||||
|
stockQuantity: 10,
|
||||||
|
lowStockThreshold: 2,
|
||||||
|
priceInCents: 500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await service.increaseStock('increase-test', 5);
|
||||||
|
|
||||||
|
final product = await service.getProductById('increase-test');
|
||||||
|
expect(product!.stockQuantity, 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when product not found', () async {
|
||||||
|
expect(
|
||||||
|
() => service.increaseStock('nonexistent', 1),
|
||||||
|
throwsA(isA<Exception>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user