inventory_store_test.dart 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import 'dart:convert';
  2. import 'package:flutter_test/flutter_test.dart';
  3. import 'package:http/http.dart' as http;
  4. import 'package:http/testing.dart';
  5. import 'package:rackpeek_mobile/data/api_client.dart';
  6. import 'package:rackpeek_mobile/data/settings_store.dart';
  7. import 'package:rackpeek_mobile/state/inventory_store.dart';
  8. import 'support/memory_storage.dart';
  9. void main() {
  10. test(
  11. 'offline edits persist across store restarts and preserve unknown fields',
  12. () async {
  13. final storage = MemoryKeyValueStorage();
  14. final settings = SettingsStore(storage: storage);
  15. await settings.saveOfflineState({
  16. 'document': {
  17. 'version': 5,
  18. 'resources': [
  19. {
  20. 'kind': 'Service',
  21. 'name': 'media',
  22. 'customField': {'owner': 'lab'},
  23. 'network': {
  24. 'ip': '192.168.1.20',
  25. 'port': 8096,
  26. 'protocol': 'TCP',
  27. },
  28. },
  29. ],
  30. 'connections': [],
  31. },
  32. 'pendingChanges': <Map<String, dynamic>>[],
  33. });
  34. final first = InventoryStore(null, settings: settings);
  35. await first.refresh();
  36. await first.applyPatch({
  37. 'kind': 'Service',
  38. 'name': 'media',
  39. 'network': {'port': 8920},
  40. });
  41. expect(first.document!.byName('media')!.network!.ip, '192.168.1.20');
  42. expect(
  43. first.document!.toJson()['resources'][0]['customField']['owner'],
  44. 'lab',
  45. );
  46. expect(first.pendingChangeCount, 1);
  47. first.dispose();
  48. final reopened = InventoryStore(null, settings: settings);
  49. await reopened.refresh();
  50. expect(reopened.document!.byName('media')!.network!.port, 8920);
  51. expect(reopened.pendingChangeCount, 1);
  52. reopened.dispose();
  53. },
  54. );
  55. test(
  56. 'a pending offline change uploads when the configured server returns',
  57. () async {
  58. final storage = MemoryKeyValueStorage();
  59. final settings = SettingsStore(storage: storage);
  60. await settings.saveOfflineState({
  61. 'document': {
  62. 'version': 5,
  63. 'resources': [
  64. {'kind': 'Server', 'name': 'nas01'},
  65. ],
  66. 'connections': [],
  67. },
  68. 'pendingChanges': [
  69. {'kind': 'Server', 'name': 'nas01', 'model': 'NAS'},
  70. ],
  71. });
  72. var uploaded = false;
  73. final client = MockClient((request) async {
  74. if (request.method == 'POST' && request.url.path == '/api/inventory') {
  75. uploaded = true;
  76. return http.Response('{}', 200);
  77. }
  78. if (request.url.path == '/api/inventory') {
  79. return http.Response(
  80. jsonEncode({
  81. 'version': 5,
  82. 'resources': [
  83. {'kind': 'Server', 'name': 'nas01', 'model': 'NAS'},
  84. ],
  85. 'connections': [],
  86. }),
  87. 200,
  88. );
  89. }
  90. if (request.url.path == '/api/inventory/summary') {
  91. return http.Response('{}', 200);
  92. }
  93. return http.Response('{}', 404);
  94. });
  95. final store = InventoryStore(
  96. RackPeekApi(
  97. baseUrl: 'https://server.example.test',
  98. apiKey: 'key',
  99. client: client,
  100. ),
  101. settings: settings,
  102. );
  103. await store.refresh();
  104. expect(uploaded, isTrue);
  105. expect(store.isOnline, isTrue);
  106. expect(store.hasPendingChanges, isFalse);
  107. expect(store.document!.byName('nas01')!.model, 'NAS');
  108. store.dispose();
  109. },
  110. );
  111. test('a failed upload keeps local edits and refresh retries them', () async {
  112. final storage = MemoryKeyValueStorage();
  113. final settings = SettingsStore(storage: storage);
  114. var failFirstPost = true;
  115. var postCount = 0;
  116. final client = MockClient((request) async {
  117. if (request.method == 'POST') {
  118. postCount++;
  119. if (failFirstPost) {
  120. failFirstPost = false;
  121. return http.Response('{"error":"offline"}', 503);
  122. }
  123. return http.Response('{}', 200);
  124. }
  125. if (request.url.path == '/api/inventory') {
  126. return http.Response(
  127. jsonEncode({
  128. 'version': 5,
  129. 'resources': [
  130. {'kind': 'Server', 'name': 'nas01', 'model': 'NAS'},
  131. ],
  132. 'connections': [],
  133. }),
  134. 200,
  135. );
  136. }
  137. return http.Response('{}', 200);
  138. });
  139. final store = InventoryStore(
  140. RackPeekApi(
  141. baseUrl: 'https://server.example.test',
  142. apiKey: 'key',
  143. client: client,
  144. ),
  145. settings: settings,
  146. );
  147. await store.refresh();
  148. await store.addResource({
  149. 'kind': 'Server',
  150. 'name': 'nas01',
  151. 'model': 'NAS',
  152. });
  153. expect(store.pendingChangeCount, 1);
  154. expect(store.document!.byName('nas01')!.model, 'NAS');
  155. expect(store.isOnline, isFalse);
  156. await store.refresh();
  157. expect(postCount, 2);
  158. expect(store.pendingChangeCount, 0);
  159. expect(store.isOnline, isTrue);
  160. store.dispose();
  161. });
  162. }