import 'dart:async'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:nearle_pos/core/config/sync_config.dart'; import 'package:nearle_pos/core/services/cash_drawer_service.dart'; import 'package:nearle_pos/data/datasources/local_store.dart'; import 'package:nearle_pos/data/datasources/seed_data.dart'; import 'package:nearle_pos/data/local/sync_config_store.dart'; void main() { setUpAll(() { LocalStore.registerSeed( products: SeedData.products, customers: SeedData.customers, ); }); group('cash drawer', () { test('sends the ESC/POS kick to the printer', () async { // Previously a debugPrint, so the drawer never opened. The PDF pipeline // cannot carry these bytes — the platform driver renders a document, it // does not pass raw commands through — so this goes over a socket. final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); final received = Completer>(); server.listen((socket) { socket.listen((bytes) { if (!received.isCompleted) received.complete(bytes); }); }); final result = await CashDrawerService() .open(host: server.address.address, port: server.port); expect(result, DrawerResult.opened); expect(await received.future, CashDrawerService.kickCommand); // ESC p 0 25 250 — pin 2, the near-universal wiring. expect(CashDrawerService.kickCommand, [27, 112, 0, 25, 250]); await server.close(); }); test('a blank address is not an error', () async { // Plenty of shops take card only, or open the drawer by hand. A USB // printer also has no raw path from Flutter, so this is the honest state // rather than a failure to report. expect(await CashDrawerService().open(), DrawerResult.notConfigured); expect( await CashDrawerService().open(host: ' '), DrawerResult.notConfigured, ); }); test('an unreachable printer reports it instead of hanging the sale', () async { // Port 1 on loopback refuses immediately. final result = await CashDrawerService().open(host: '127.0.0.1', port: 1); expect(result, DrawerResult.unreachable); expect(result.isSuccess, isFalse); expect(result.message, contains('Could not reach')); }); test('every outcome explains itself to the cashier', () { for (final result in DrawerResult.values) { expect(result.message, isNotEmpty); } expect(DrawerResult.opened.isSuccess, isTrue); }); }); group('sync configuration', () { late LocalStore store; setUp(() async { store = LocalStore.instance; await store.reset(withCatalogue: true); }); test('the route survives a restart', () async { // Held only in memory these had to be retyped after every restart, which // on a shop-floor terminal means they end up on a sticky note instead. const configured = SyncConfig( transport: TransportKind.mqtt, brokerHost: 'nats.example.com', brokerPort: 1883, useTls: false, storeId: 'store-77', terminalId: 'T9A1', ); await store.syncConfig.save(configured); // A fresh store object, as if the app had been relaunched. final reloaded = await SyncConfigStore(store.catalogue) .load(const SyncConfig(storeId: 'store-77', terminalId: 'T9A1')); expect(reloaded.transport, TransportKind.mqtt); expect(reloaded.brokerHost, 'nats.example.com'); expect(reloaded.brokerPort, 1883); expect(reloaded.useTls, isFalse); }); test('no credential is written to the database', () async { // The whole reason they go to the platform keystore. SQLite holds the // bills, on a machine behind a shop counter. await store.syncConfig.save(const SyncConfig( transport: TransportKind.mqtt, brokerHost: 'nats.example.com', username: 'till-04', password: 'sup3rs3cret', apiKey: 'ak_live_9f21', ),); final rows = await store.catalogue.allMeta(); final everything = rows.entries.map((e) => '${e.key}=${e.value}').join('|'); expect(everything, isNot(contains('sup3rs3cret'))); expect(everything, isNot(contains('ak_live_9f21'))); // Non-secret settings are expected to be there — that is the point of // the split. expect(everything, contains('nats.example.com')); }); test('the terminal identity is never overwritten by a saved route', () async { // Store and terminal ids belong to the device. Re-pointing a till at a // different broker must not change who it is, or its bills and its // presence records stop lining up. await store.syncConfig.save(const SyncConfig( transport: TransportKind.http, httpBaseUrl: 'https://api.example.com', storeId: 'wrong-store', terminalId: 'WRONG', ),); final reloaded = await SyncConfigStore(store.catalogue).load( const SyncConfig(storeId: 'store-01', terminalId: 'T4A9'), ); expect(reloaded.storeId, 'store-01'); expect(reloaded.terminalId, 'T4A9'); expect(reloaded.httpBaseUrl, 'https://api.example.com'); }); test('an unconfigured terminal falls back rather than failing', () async { final loaded = await SyncConfigStore(store.catalogue) .load(const SyncConfig()); expect(loaded.transport, TransportKind.simulated); expect(loaded.isConfigured, isTrue); }); }); }