Files
kooltab/test/product_service_test.dart
T

383 lines
11 KiB
Dart

import 'package:drift/drift.dart' hide isNull, isNotNull;
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:kooltap/database/app_database.dart';
import 'package:kooltap/models/product.dart';
import 'package:kooltap/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('hides the product but keeps its row', () 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');
expect(await service.getProducts(), isEmpty);
final row = await service.getProductById('delete-test');
expect(row!.active, isFalse);
await service.increaseStock('delete-test', 1);
});
});
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('goes below zero instead of refusing', () async {
await database
.into(database.products)
.insert(
ProductsCompanion.insert(
id: 'low-stock',
name: 'Low Stock',
category: 'Drinks',
stockQuantity: 3,
lowStockThreshold: 1,
priceInCents: 500,
),
);
await service.decreaseStock('low-stock', 5);
final product = await service.getProductById('low-stock');
expect(product!.stockQuantity, -2);
});
});
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>()),
);
});
test('rejects non-positive stock adjustments', () async {
await database
.into(database.products)
.insert(
ProductsCompanion.insert(
id: 'invalid-adjustment',
name: 'Invalid Adjustment',
category: 'Drinks',
stockQuantity: 10,
lowStockThreshold: 2,
priceInCents: 500,
),
);
expect(
() => service.decreaseStock('invalid-adjustment', 0),
throwsA(isA<ArgumentError>()),
);
expect(
() => service.increaseStock('invalid-adjustment', -1),
throwsA(isA<ArgumentError>()),
);
});
test('counts every concurrent decrement', () async {
await database
.into(database.products)
.insert(
ProductsCompanion.insert(
id: 'concurrent-stock',
name: 'Concurrent Stock',
category: 'Drinks',
stockQuantity: 1,
lowStockThreshold: 0,
priceInCents: 500,
),
);
await Future.wait([
service.decreaseStock('concurrent-stock', 1),
service.decreaseStock('concurrent-stock', 1),
]);
final product = await service.getProductById('concurrent-stock');
expect(product!.stockQuantity, -1);
});
});
group('infinite stock', () {
setUp(() async {
await database
.into(database.products)
.insert(
ProductsCompanion.insert(
id: 'water',
name: 'Water',
category: 'Drinks',
stockQuantity: 0,
lowStockThreshold: 0,
infiniteStock: const Value(true),
priceInCents: 100,
),
);
});
test('sells without stock and leaves the count alone', () async {
await service.decreaseStock('water', 5);
await service.increaseStock('water', 2);
final product = await service.getProductById('water');
expect(product!.stockQuantity, 0);
expect(product.infiniteStock, true);
expect(product.isOutOfStock, false);
expect(product.isLowStock, false);
});
test('still reports a missing product', () async {
await expectLater(
service.increaseStock('nonexistent', 1),
throwsA(isA<ProductNotFoundException>()),
);
});
test('is saved by create and update', () async {
await service.createProduct(
name: 'Tap water',
category: 'Drinks',
stockQuantity: 0,
lowStockThreshold: 0,
infiniteStock: true,
priceInCents: 0,
imagePath: null,
);
final created = (await service.getProducts()).firstWhere(
(product) => product.name == 'Tap water',
);
expect(created.infiniteStock, true);
await service.updateProduct(
created.copyWith(infiniteStock: false, stockQuantity: 4),
);
final updated = await service.getProductById(created.id);
expect(updated!.infiniteStock, false);
expect(updated.stockQuantity, 4);
});
});
});
}