DataTransferTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. <?php
  2. use Illuminate\Http\UploadedFile;
  3. use Illuminate\Support\Facades\Storage;
  4. use Webkul\DataTransfer\Models\Import;
  5. use function Pest\Laravel\deleteJson;
  6. use function Pest\Laravel\get;
  7. use function Pest\Laravel\postJson;
  8. use function Pest\Laravel\putJson;
  9. beforeEach(function () {
  10. Storage::fake('private');
  11. });
  12. it('should return the data transfer imports index page', function () {
  13. // Act and Assert.
  14. $this->loginAsAdmin();
  15. get(route('admin.settings.data_transfer.imports.index'))
  16. ->assertOk()
  17. ->assertSeeText(trans('admin::app.settings.data-transfer.imports.index.title'));
  18. });
  19. it('should return the create page of data transfer import', function () {
  20. // Act and Assert.
  21. $this->loginAsAdmin();
  22. get(route('admin.settings.data_transfer.imports.create'))
  23. ->assertOk()
  24. ->assertSeeText(trans('admin::app.settings.data-transfer.imports.create.title'));
  25. });
  26. it('should fail validation when required fields are not provided when storing import', function () {
  27. // Act and Assert.
  28. $this->loginAsAdmin();
  29. postJson(route('admin.settings.data_transfer.imports.store'))
  30. ->assertJsonValidationErrorFor('type')
  31. ->assertJsonValidationErrorFor('action')
  32. ->assertJsonValidationErrorFor('validation_strategy')
  33. ->assertJsonValidationErrorFor('allowed_errors')
  34. ->assertJsonValidationErrorFor('field_separator')
  35. ->assertJsonValidationErrorFor('file')
  36. ->assertUnprocessable();
  37. });
  38. it('should fail validation with invalid file type when storing import', function () {
  39. // Arrange
  40. $file = UploadedFile::fake()->create('invalid.txt', 100, 'text/plain');
  41. // Act and Assert.
  42. $this->loginAsAdmin();
  43. postJson(route('admin.settings.data_transfer.imports.store'), [
  44. 'type' => 'products',
  45. 'action' => 'append',
  46. 'validation_strategy' => 'stop-on-errors',
  47. 'allowed_errors' => 10,
  48. 'field_separator' => ',',
  49. 'file' => $file,
  50. ])
  51. ->assertJsonValidationErrorFor('file')
  52. ->assertUnprocessable();
  53. });
  54. it('should successfully store import with CSV file', function () {
  55. // Arrange
  56. Storage::fake('private');
  57. $csvContent = "email,first_name,last_name\njohn@example.com,John,Doe";
  58. $file = UploadedFile::fake()->createWithContent('customers.csv', $csvContent);
  59. // Act and Assert.
  60. $this->loginAsAdmin();
  61. postJson(route('admin.settings.data_transfer.imports.store'), [
  62. 'type' => 'customers',
  63. 'action' => 'append',
  64. 'validation_strategy' => 'stop-on-errors',
  65. 'allowed_errors' => 10,
  66. 'field_separator' => ',',
  67. 'file' => $file,
  68. ])
  69. ->assertRedirect()
  70. ->assertSessionHas('success');
  71. $this->assertDatabaseHas('imports', [
  72. 'type' => 'customers',
  73. 'action' => 'append',
  74. 'state' => 'pending',
  75. ]);
  76. });
  77. it('should accept CSV file with text/plain MIME type (small files)', function () {
  78. // Arrange
  79. Storage::fake('private');
  80. $csvContent = "id,name\n1,Test";
  81. $file = UploadedFile::fake()->createWithContent('test.csv', $csvContent);
  82. // Act and Assert.
  83. $this->loginAsAdmin();
  84. postJson(route('admin.settings.data_transfer.imports.store'), [
  85. 'type' => 'products',
  86. 'action' => 'append',
  87. 'validation_strategy' => 'stop-on-errors',
  88. 'allowed_errors' => 0,
  89. 'field_separator' => ',',
  90. 'file' => $file,
  91. ])
  92. ->assertRedirect()
  93. ->assertSessionHas('success');
  94. });
  95. it('should successfully store import with XML file', function () {
  96. // Arrange
  97. Storage::fake('private');
  98. $xmlContent = '<?xml version="1.0"?><customers><customer email="test@example.com"/></customers>';
  99. $file = UploadedFile::fake()->createWithContent('customers.xml', $xmlContent);
  100. // Act and Assert.
  101. $this->loginAsAdmin();
  102. postJson(route('admin.settings.data_transfer.imports.store'), [
  103. 'type' => 'customers',
  104. 'action' => 'delete',
  105. 'validation_strategy' => 'skip-errors',
  106. 'allowed_errors' => 5,
  107. 'field_separator' => ',',
  108. 'file' => $file,
  109. ])
  110. ->assertRedirect()
  111. ->assertSessionHas('success');
  112. $this->assertDatabaseHas('imports', [
  113. 'type' => 'customers',
  114. 'action' => 'delete',
  115. ]);
  116. });
  117. it('should return the edit page for an existing import', function () {
  118. // Arrange
  119. $import = Import::create([
  120. 'type' => 'products',
  121. 'state' => 'pending',
  122. 'file_path' => 'imports/test.csv',
  123. 'action' => 'append',
  124. 'validation_strategy' => 'stop-on-errors',
  125. 'allowed_errors' => 10,
  126. 'field_separator' => ',',
  127. ]);
  128. // Act and Assert.
  129. $this->loginAsAdmin();
  130. get(route('admin.settings.data_transfer.imports.edit', $import->id))
  131. ->assertOk()
  132. ->assertSeeText(trans('admin::app.settings.data-transfer.imports.edit.title'));
  133. });
  134. it('should successfully update an existing import', function () {
  135. // Arrange
  136. Storage::fake('private');
  137. $import = Import::create([
  138. 'type' => 'products',
  139. 'state' => 'pending',
  140. 'file_path' => 'imports/old.csv',
  141. 'action' => 'append',
  142. 'validation_strategy' => 'stop-on-errors',
  143. 'allowed_errors' => 10,
  144. 'field_separator' => ',',
  145. ]);
  146. $csvContent = "sku,name\nSKU-001,Product 1";
  147. $newFile = UploadedFile::fake()->createWithContent('products.csv', $csvContent);
  148. // Act and Assert.
  149. $this->loginAsAdmin();
  150. putJson(route('admin.settings.data_transfer.imports.update', $import->id), [
  151. 'type' => 'products',
  152. 'action' => 'append',
  153. 'validation_strategy' => 'stop-on-errors',
  154. 'allowed_errors' => 15,
  155. 'field_separator' => ',',
  156. 'file' => $newFile,
  157. ])
  158. ->assertRedirect()
  159. ->assertSessionHas('success');
  160. $import->refresh();
  161. expect($import->allowed_errors)->toBe(15);
  162. expect($import->state)->toBe('pending');
  163. });
  164. it('should allow updating import without providing a new file', function () {
  165. // Arrange
  166. Storage::fake('private');
  167. $import = Import::create([
  168. 'type' => 'customers',
  169. 'state' => 'pending',
  170. 'file_path' => 'imports/existing.csv',
  171. 'allowed_errors' => 10,
  172. 'action' => 'append',
  173. 'validation_strategy' => 'stop-on-errors',
  174. 'field_separator' => ',',
  175. ]);
  176. // Act and Assert.
  177. $this->loginAsAdmin();
  178. putJson(route('admin.settings.data_transfer.imports.update', $import->id), [
  179. 'type' => 'customers',
  180. 'action' => 'append',
  181. 'validation_strategy' => 'stop-on-errors',
  182. 'allowed_errors' => 20,
  183. 'field_separator' => ',',
  184. ])
  185. ->assertRedirect()
  186. ->assertSessionHas('success');
  187. $import->refresh();
  188. expect($import->allowed_errors)->toBe(20);
  189. expect($import->file_path)->toBe('imports/existing.csv');
  190. });
  191. it('should successfully delete an import', function () {
  192. // Arrange
  193. Storage::fake('private');
  194. $import = Import::create([
  195. 'type' => 'products',
  196. 'file_path' => 'imports/test.csv',
  197. 'state' => 'pending',
  198. 'action' => 'append',
  199. 'validation_strategy' => 'stop-on-errors',
  200. 'allowed_errors' => 10,
  201. 'field_separator' => ',',
  202. ]);
  203. Storage::disk('private')->put($import->file_path, 'test content');
  204. // Act and Assert.
  205. $this->loginAsAdmin();
  206. deleteJson(route('admin.settings.data_transfer.imports.delete', $import->id))
  207. ->assertOk()
  208. ->assertJsonFragment(['message' => trans('admin::app.settings.data-transfer.imports.delete-success')]);
  209. $this->assertDatabaseMissing('imports', [
  210. 'id' => $import->id,
  211. ]);
  212. });
  213. it('should return import page with stats', function () {
  214. // Arrange
  215. $import = Import::create([
  216. 'type' => 'customers',
  217. 'state' => 'pending',
  218. 'file_path' => 'imports/customers.csv',
  219. 'processed_rows_count' => 0,
  220. 'action' => 'append',
  221. 'validation_strategy' => 'stop-on-errors',
  222. 'allowed_errors' => 10,
  223. 'field_separator' => ',',
  224. ]);
  225. // Act and Assert.
  226. $this->loginAsAdmin();
  227. get(route('admin.settings.data_transfer.imports.import', $import->id))
  228. ->assertOk()
  229. ->assertSeeText($import->type);
  230. });
  231. it('should validate action field with correct syntax', function () {
  232. // Arrange
  233. Storage::fake('private');
  234. $file = UploadedFile::fake()->createWithContent('test.csv', 'data');
  235. // Act and Assert
  236. $this->loginAsAdmin();
  237. // Valid action values should pass
  238. postJson(route('admin.settings.data_transfer.imports.store'), [
  239. 'type' => 'products',
  240. 'action' => 'append',
  241. 'validation_strategy' => 'stop-on-errors',
  242. 'allowed_errors' => 10,
  243. 'field_separator' => ',',
  244. 'file' => $file,
  245. ])
  246. ->assertRedirect();
  247. // Invalid action value should fail
  248. postJson(route('admin.settings.data_transfer.imports.store'), [
  249. 'type' => 'products',
  250. 'action' => 'invalid_action',
  251. 'validation_strategy' => 'stop-on-errors',
  252. 'allowed_errors' => 10,
  253. 'field_separator' => ',',
  254. 'file' => $file,
  255. ])
  256. ->assertJsonValidationErrorFor('action')
  257. ->assertUnprocessable();
  258. });
  259. it('should validate validation_strategy field with correct syntax', function () {
  260. // Arrange
  261. Storage::fake('private');
  262. $file = UploadedFile::fake()->createWithContent('test.csv', 'data');
  263. // Act and Assert
  264. $this->loginAsAdmin();
  265. // Valid validation_strategy values should pass
  266. foreach (['stop-on-errors', 'skip-errors'] as $strategy) {
  267. postJson(route('admin.settings.data_transfer.imports.store'), [
  268. 'type' => 'products',
  269. 'action' => 'append',
  270. 'validation_strategy' => $strategy,
  271. 'allowed_errors' => 10,
  272. 'field_separator' => ',',
  273. 'file' => $file,
  274. ])
  275. ->assertRedirect();
  276. }
  277. // Invalid validation_strategy value should fail
  278. postJson(route('admin.settings.data_transfer.imports.store'), [
  279. 'type' => 'products',
  280. 'action' => 'append',
  281. 'validation_strategy' => 'invalid_strategy',
  282. 'allowed_errors' => 10,
  283. 'field_separator' => ',',
  284. 'file' => $file,
  285. ])
  286. ->assertJsonValidationErrorFor('validation_strategy')
  287. ->assertUnprocessable();
  288. });
  289. it('should accept all supported file formats', function () {
  290. // Arrange
  291. Storage::fake('private');
  292. $this->loginAsAdmin();
  293. $formats = [
  294. 'csv' => 'text/csv',
  295. 'xml' => 'text/xml',
  296. 'xls' => 'application/vnd.ms-excel',
  297. 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  298. ];
  299. foreach ($formats as $extension => $mimeType) {
  300. $file = UploadedFile::fake()->create("test.{$extension}", 100, $mimeType);
  301. postJson(route('admin.settings.data_transfer.imports.store'), [
  302. 'type' => 'products',
  303. 'action' => 'append',
  304. 'validation_strategy' => 'stop-on-errors',
  305. 'allowed_errors' => 10,
  306. 'field_separator' => ',',
  307. 'file' => $file,
  308. ])
  309. ->assertRedirect();
  310. }
  311. });
  312. it('should handle process_in_queue option correctly', function () {
  313. // Arrange
  314. Storage::fake('private');
  315. $this->loginAsAdmin();
  316. // Test with process_in_queue enabled
  317. $file = UploadedFile::fake()->createWithContent('queue_test.csv', 'test data content');
  318. postJson(route('admin.settings.data_transfer.imports.store'), [
  319. 'type' => 'customers',
  320. 'action' => 'append',
  321. 'validation_strategy' => 'stop-on-errors',
  322. 'allowed_errors' => 10,
  323. 'field_separator' => ',',
  324. 'file' => $file,
  325. 'process_in_queue' => 1,
  326. ])
  327. ->assertRedirect()
  328. ->assertSessionHas('success');
  329. // Verify the import with process_in_queue = 1 exists
  330. $importWithQueue = Import::where('process_in_queue', 1)->latest()->first();
  331. expect($importWithQueue)->not->toBeNull();
  332. expect($importWithQueue->process_in_queue)->toBe(1);
  333. expect($importWithQueue->type)->toBe('customers');
  334. // Test without process_in_queue (should default to 0)
  335. $file2 = UploadedFile::fake()->createWithContent('no_queue_test.csv', 'different test data');
  336. postJson(route('admin.settings.data_transfer.imports.store'), [
  337. 'type' => 'products',
  338. 'action' => 'delete',
  339. 'validation_strategy' => 'skip-errors',
  340. 'allowed_errors' => 5,
  341. 'field_separator' => ';',
  342. 'file' => $file2,
  343. ])
  344. ->assertRedirect();
  345. // Verify the import without process_in_queue defaults to 0
  346. $importWithoutQueue = Import::where('type', 'products')
  347. ->where('action', 'delete')
  348. ->where('field_separator', ';')
  349. ->latest()
  350. ->first();
  351. expect($importWithoutQueue)->not->toBeNull();
  352. expect($importWithoutQueue->process_in_queue)->toBe(0);
  353. });
  354. it('should sanitize filename when storing import', function () {
  355. // Arrange
  356. Storage::fake('private');
  357. $file = UploadedFile::fake()->createWithContent('../../malicious.csv', 'data');
  358. // Act and Assert
  359. $this->loginAsAdmin();
  360. postJson(route('admin.settings.data_transfer.imports.store'), [
  361. 'type' => 'products',
  362. 'action' => 'append',
  363. 'validation_strategy' => 'stop-on-errors',
  364. 'allowed_errors' => 10,
  365. 'field_separator' => ',',
  366. 'file' => $file,
  367. ])
  368. ->assertRedirect();
  369. $import = Import::latest()->first();
  370. // Verify the filename is sanitized (contains hash and unique ID)
  371. expect($import->file_path)->toContain('imports/');
  372. expect($import->file_path)->not()->toContain('../');
  373. });