ThemeTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. <?php
  2. use Illuminate\Http\UploadedFile;
  3. use Webkul\Theme\Models\ThemeCustomization;
  4. use function Pest\Laravel\deleteJson;
  5. use function Pest\Laravel\get;
  6. use function Pest\Laravel\postJson;
  7. it('should returns the theme index page', function () {
  8. // Act and Assert.
  9. $this->loginAsAdmin();
  10. get(route('admin.settings.themes.index'))
  11. ->assertOk()
  12. ->assertSeeText(trans('admin::app.settings.themes.index.title'))
  13. ->assertSeeText(trans('admin::app.settings.themes.index.create-btn'));
  14. });
  15. it('should fail the validation with errors when certain field not provided when store the theme', function () {
  16. // Act and Assert.
  17. $this->loginAsAdmin();
  18. postJson(route('admin.settings.themes.store'))
  19. ->assertJsonValidationErrorFor('name')
  20. ->assertJsonValidationErrorFor('sort_order')
  21. ->assertJsonValidationErrorFor('type')
  22. ->assertJsonValidationErrorFor('channel_id')
  23. ->assertJsonValidationErrorFor('theme_code')
  24. ->assertUnprocessable();
  25. });
  26. it('should fail the validation with errors when correct type not provided when store the theme', function () {
  27. // Act and Assert.
  28. $this->loginAsAdmin();
  29. postJson(route('admin.settings.themes.store'), [
  30. 'type' => 'INVALID_TYPE',
  31. ])
  32. ->assertJsonValidationErrorFor('name')
  33. ->assertJsonValidationErrorFor('sort_order')
  34. ->assertJsonValidationErrorFor('type')
  35. ->assertJsonValidationErrorFor('channel_id')
  36. ->assertJsonValidationErrorFor('theme_code')
  37. ->assertUnprocessable();
  38. });
  39. it('should store the newly created theme', function () {
  40. // Arrange.
  41. $lastThemeId = ThemeCustomization::factory()->create()->id + 1;
  42. // Act and Assert.
  43. $this->loginAsAdmin();
  44. postJson(route('admin.settings.themes.store'), [
  45. 'type' => $type = fake()->randomElement([
  46. 'product_carousel',
  47. 'category_carousel',
  48. 'image_carousel',
  49. 'footer_links',
  50. 'services_content',
  51. ]),
  52. 'name' => $name = fake()->name(),
  53. 'sort_order' => $lastThemeId,
  54. 'channel_id' => $channelId = core()->getCurrentChannel()->id,
  55. 'theme_code' => $themeCode = core()->getCurrentChannel()->theme,
  56. ])
  57. ->assertOk()
  58. ->assertJsonPath('redirect_url', route('admin.settings.themes.edit', $lastThemeId));
  59. $this->assertModelWise([
  60. ThemeCustomization::class => [
  61. [
  62. 'id' => $lastThemeId,
  63. 'type' => $type,
  64. 'name' => $name,
  65. 'channel_id' => $channelId,
  66. 'theme_code' => $themeCode,
  67. ],
  68. ],
  69. ]);
  70. });
  71. it('should fail the validation with errors when correct type not provided when update the theme', function () {
  72. // Arrange.
  73. $theme = ThemeCustomization::factory()->create();
  74. // Act and Assert.
  75. $this->loginAsAdmin();
  76. postJson(route('admin.settings.themes.update', $theme->id))
  77. ->assertJsonValidationErrorFor('name')
  78. ->assertJsonValidationErrorFor('sort_order')
  79. ->assertJsonValidationErrorFor('type')
  80. ->assertJsonValidationErrorFor('channel_id')
  81. ->assertJsonValidationErrorFor('theme_code')
  82. ->assertUnprocessable();
  83. });
  84. it('should update the theme customizations', function () {
  85. $theme = ThemeCustomization::factory()->create();
  86. $data = [];
  87. switch ($theme->type) {
  88. case ThemeCustomization::PRODUCT_CAROUSEL:
  89. $data[app()->getLocale()] = [
  90. 'options' => [
  91. 'title' => fake()->title(),
  92. 'filters' => [
  93. 'sort' => 'name-desc',
  94. 'limit' => '12',
  95. 'new' => '1',
  96. ],
  97. ],
  98. ];
  99. break;
  100. case ThemeCustomization::CATEGORY_CAROUSEL:
  101. $data[app()->getLocale()] = [
  102. 'options' => [
  103. 'title' => fake()->title(),
  104. 'filters' => [
  105. 'sort' => 'desc',
  106. 'limit' => '10',
  107. 'parent_id' => '1',
  108. ],
  109. ],
  110. ];
  111. break;
  112. case ThemeCustomization::IMAGE_CAROUSEL:
  113. $data[app()->getLocale()] = [
  114. 'options' => [
  115. [
  116. 'title' => fake()->title(),
  117. 'link' => fake()->url(),
  118. 'image' => UploadedFile::fake()->image(fake()->word().'.png', 640, 480, 'png'),
  119. ],
  120. ],
  121. ];
  122. break;
  123. case ThemeCustomization::FOOTER_LINKS:
  124. $data[app()->getLocale()] = [
  125. 'options' => [
  126. 'column_1' => [
  127. [
  128. 'url' => fake()->url(),
  129. 'title' => fake()->title(),
  130. 'sort_order' => '1',
  131. ],
  132. ],
  133. ],
  134. ];
  135. break;
  136. case ThemeCustomization::SERVICES_CONTENT:
  137. $data[app()->getLocale()] = [
  138. 'options' => [
  139. [
  140. 'title' => fake()->title(),
  141. 'description' => fake()->paragraph(),
  142. 'service_icon' => 'icon-truck',
  143. ],
  144. ],
  145. ];
  146. break;
  147. }
  148. $data['locale'] = app()->getLocale();
  149. $data['type'] = $theme->type;
  150. $data['name'] = $name = fake()->name();
  151. $data['sort_order'] = '1';
  152. $data['channel_id'] = core()->getCurrentChannel()->id;
  153. $data['theme_code'] = core()->getCurrentChannel()->theme;
  154. $data['status'] = 'on';
  155. // Act and Assert.
  156. $this->loginAsAdmin();
  157. postJson(route('admin.settings.themes.update', $theme->id), $data)
  158. ->assertRedirect(route('admin.settings.themes.index'))
  159. ->isRedirection();
  160. $this->assertModelWise([
  161. ThemeCustomization::class => [
  162. [
  163. 'id' => $theme->id,
  164. 'type' => $theme->type,
  165. 'name' => $name,
  166. ],
  167. ],
  168. ]);
  169. });
  170. it('should sanitize malicious script tags from static content HTML when updating theme', function () {
  171. // Arrange.
  172. $theme = ThemeCustomization::factory()->create([
  173. 'type' => 'static_content',
  174. ]);
  175. $maliciousHtml = '<div>Safe content</div><script>alert("XSS")</script><p>More safe content</p>';
  176. $safeCss = 'body { color: red; }';
  177. $data = [
  178. app()->getLocale() => [
  179. 'options' => [
  180. 'html' => $maliciousHtml,
  181. 'css' => $safeCss,
  182. ],
  183. ],
  184. 'locale' => app()->getLocale(),
  185. 'type' => 'static_content',
  186. 'name' => $name = fake()->name(),
  187. 'sort_order' => '1',
  188. 'channel_id' => core()->getCurrentChannel()->id,
  189. 'theme_code' => core()->getCurrentChannel()->theme,
  190. 'status' => 'on',
  191. ];
  192. // Act and Assert.
  193. $this->loginAsAdmin();
  194. postJson(route('admin.settings.themes.update', $theme->id), $data)
  195. ->assertRedirect(route('admin.settings.themes.index'))
  196. ->isRedirection();
  197. $theme->refresh();
  198. $translation = $theme->translate(app()->getLocale());
  199. // Assert that script tag was removed.
  200. expect($translation->options['html'])->not->toContain('<script>');
  201. expect($translation->options['html'])->not->toContain('alert("XSS")');
  202. expect($translation->options['html'])->toContain('Safe content');
  203. expect($translation->options['html'])->toContain('More safe content');
  204. });
  205. it('should sanitize iframe tags from static content HTML when updating theme', function () {
  206. // Arrange.
  207. $theme = ThemeCustomization::factory()->create([
  208. 'type' => 'static_content',
  209. ]);
  210. $maliciousHtml = '<div>Content</div><iframe src="https://malicious.com"></iframe><p>More content</p>';
  211. $data = [
  212. app()->getLocale() => [
  213. 'options' => [
  214. 'html' => $maliciousHtml,
  215. 'css' => '',
  216. ],
  217. ],
  218. 'locale' => app()->getLocale(),
  219. 'type' => 'static_content',
  220. 'name' => fake()->name(),
  221. 'sort_order' => '1',
  222. 'channel_id' => core()->getCurrentChannel()->id,
  223. 'theme_code' => core()->getCurrentChannel()->theme,
  224. 'status' => 'on',
  225. ];
  226. // Act and Assert.
  227. $this->loginAsAdmin();
  228. postJson(route('admin.settings.themes.update', $theme->id), $data)
  229. ->assertRedirect(route('admin.settings.themes.index'))
  230. ->isRedirection();
  231. $theme->refresh();
  232. $translation = $theme->translate(app()->getLocale());
  233. // Assert that iframe tag was removed.
  234. expect($translation->options['html'])->not->toContain('<iframe');
  235. expect($translation->options['html'])->not->toContain('malicious.com');
  236. expect($translation->options['html'])->toContain('Content');
  237. expect($translation->options['html'])->toContain('More content');
  238. });
  239. it('should sanitize form tags from static content HTML when updating theme', function () {
  240. // Arrange.
  241. $theme = ThemeCustomization::factory()->create([
  242. 'type' => 'static_content',
  243. ]);
  244. $maliciousHtml = '<div>Safe content</div><form action="/submit" method="post"><input name="data"></form><p>More content</p>';
  245. $data = [
  246. app()->getLocale() => [
  247. 'options' => [
  248. 'html' => $maliciousHtml,
  249. 'css' => '',
  250. ],
  251. ],
  252. 'locale' => app()->getLocale(),
  253. 'type' => 'static_content',
  254. 'name' => fake()->name(),
  255. 'sort_order' => '1',
  256. 'channel_id' => core()->getCurrentChannel()->id,
  257. 'theme_code' => core()->getCurrentChannel()->theme,
  258. 'status' => 'on',
  259. ];
  260. // Act and Assert.
  261. $this->loginAsAdmin();
  262. postJson(route('admin.settings.themes.update', $theme->id), $data)
  263. ->assertRedirect(route('admin.settings.themes.index'))
  264. ->isRedirection();
  265. $theme->refresh();
  266. $translation = $theme->translate(app()->getLocale());
  267. // Assert that form tag was removed.
  268. expect($translation->options['html'])->not->toContain('<form');
  269. expect($translation->options['html'])->not->toContain('</form>');
  270. expect($translation->options['html'])->toContain('Safe content');
  271. expect($translation->options['html'])->toContain('More content');
  272. });
  273. it('should preserve safe HTML content in static content when updating theme', function () {
  274. // Arrange.
  275. $theme = ThemeCustomization::factory()->create([
  276. 'type' => 'static_content',
  277. ]);
  278. $safeHtml = '<div class="container"><h1>Title</h1><p>Paragraph with <strong>bold</strong> and <em>italic</em> text.</p><ul><li>Item 1</li><li>Item 2</li></ul></div>';
  279. $safeCss = 'body { color: blue; font-size: 14px; }';
  280. $data = [
  281. app()->getLocale() => [
  282. 'options' => [
  283. 'html' => $safeHtml,
  284. 'css' => $safeCss,
  285. ],
  286. ],
  287. 'locale' => app()->getLocale(),
  288. 'type' => 'static_content',
  289. 'name' => fake()->name(),
  290. 'sort_order' => '1',
  291. 'channel_id' => core()->getCurrentChannel()->id,
  292. 'theme_code' => core()->getCurrentChannel()->theme,
  293. 'status' => 'on',
  294. ];
  295. // Act and Assert.
  296. $this->loginAsAdmin();
  297. postJson(route('admin.settings.themes.update', $theme->id), $data)
  298. ->assertRedirect(route('admin.settings.themes.index'))
  299. ->isRedirection();
  300. $theme->refresh();
  301. $translation = $theme->translate(app()->getLocale());
  302. // Assert that safe HTML elements are preserved.
  303. expect($translation->options['html'])->toContain('<div');
  304. expect($translation->options['html'])->toContain('<h1>');
  305. expect($translation->options['html'])->toContain('<p>');
  306. expect($translation->options['html'])->toContain('<strong>');
  307. expect($translation->options['html'])->toContain('<em>');
  308. expect($translation->options['html'])->toContain('<ul>');
  309. expect($translation->options['html'])->toContain('<li>');
  310. });
  311. it('should sanitize malicious event handlers from static content HTML when updating theme', function () {
  312. // Arrange.
  313. $theme = ThemeCustomization::factory()->create([
  314. 'type' => 'static_content',
  315. ]);
  316. $maliciousHtml = '<div onclick="alert(\'XSS\')">Click me</div><img src="x" onerror="alert(\'XSS\')">';
  317. $data = [
  318. app()->getLocale() => [
  319. 'options' => [
  320. 'html' => $maliciousHtml,
  321. 'css' => '',
  322. ],
  323. ],
  324. 'locale' => app()->getLocale(),
  325. 'type' => 'static_content',
  326. 'name' => fake()->name(),
  327. 'sort_order' => '1',
  328. 'channel_id' => core()->getCurrentChannel()->id,
  329. 'theme_code' => core()->getCurrentChannel()->theme,
  330. 'status' => 'on',
  331. ];
  332. // Act and Assert.
  333. $this->loginAsAdmin();
  334. postJson(route('admin.settings.themes.update', $theme->id), $data)
  335. ->assertRedirect(route('admin.settings.themes.index'))
  336. ->isRedirection();
  337. $theme->refresh();
  338. $translation = $theme->translate(app()->getLocale());
  339. // Assert that malicious event handlers were removed.
  340. expect($translation->options['html'])->not->toContain('onclick');
  341. expect($translation->options['html'])->not->toContain('onerror');
  342. expect($translation->options['html'])->not->toContain('alert(');
  343. expect($translation->options['html'])->toContain('Click me');
  344. });
  345. it('should not sanitize HTML for non-static content theme types', function () {
  346. // Arrange.
  347. $theme = ThemeCustomization::factory()->create([
  348. 'type' => 'product_carousel',
  349. ]);
  350. $data = [
  351. app()->getLocale() => [
  352. 'options' => [
  353. 'title' => 'Test Title',
  354. 'filters' => [
  355. 'sort' => 'name-desc',
  356. 'limit' => '12',
  357. 'new' => '1',
  358. ],
  359. ],
  360. ],
  361. 'locale' => app()->getLocale(),
  362. 'type' => 'product_carousel',
  363. 'name' => $name = fake()->name(),
  364. 'sort_order' => '1',
  365. 'channel_id' => core()->getCurrentChannel()->id,
  366. 'theme_code' => core()->getCurrentChannel()->theme,
  367. 'status' => 'on',
  368. ];
  369. // Act and Assert.
  370. $this->loginAsAdmin();
  371. postJson(route('admin.settings.themes.update', $theme->id), $data)
  372. ->assertRedirect(route('admin.settings.themes.index'))
  373. ->isRedirection();
  374. $theme->refresh();
  375. // Assert theme was updated successfully.
  376. $this->assertModelWise([
  377. ThemeCustomization::class => [
  378. [
  379. 'id' => $theme->id,
  380. 'type' => 'product_carousel',
  381. 'name' => $name,
  382. ],
  383. ],
  384. ]);
  385. });
  386. it('should delete the theme', function () {
  387. // Arrange.
  388. $theme = ThemeCustomization::factory()->create();
  389. // Act and Assert.
  390. $this->loginAsAdmin();
  391. deleteJson(route('admin.settings.themes.delete', $theme->id))
  392. ->assertOk()
  393. ->assertJsonPath('message', trans('admin::app.settings.themes.delete-success'));
  394. $this->assertDatabaseMissing('theme_customizations', [
  395. 'id' => $theme->id,
  396. ]);
  397. $this->assertDatabaseMissing('theme_customization_translations', [
  398. 'theme_customization_id' => $theme->id,
  399. ]);
  400. });