030-file-explorer.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. /*
  2. Critical path: file explorer (File Manager).
  3. Renders the real File Manager UI against a live server, then walks
  4. the whole file lifecycle through the same endpoints the UI calls:
  5. list roots, list directory, create folder/file, rename, copy, move,
  6. properties, recycle to trash, restore from trash and empty trash.
  7. Mutating calls carry a CSRF token exactly like the front end does.
  8. */
  9. "use strict";
  10. const h = require("../lib/system-harness");
  11. h.run("FILE-EXPLORER", async function (env) {
  12. const base = env.baseURL;
  13. const page = await h.newPage(env.browser);
  14. await h.loginViaAPI(page, base, env.admin.username, env.admin.password);
  15. // ── 1. File Manager UI renders ──
  16. await page.goto(base + "/SystemAO/file_system/file_explorer.html", { waitUntil: "domcontentloaded" });
  17. await page.waitForSelector("#navibar", { state: "visible", timeout: 20000 });
  18. await page.waitForSelector("#folderView", { state: "attached", timeout: 20000 });
  19. // The sidebar lists the user's roots once listRoots returns.
  20. await page.waitForFunction(function () {
  21. var el = document.getElementById("userroot");
  22. return el && el.textContent.trim().length > 0;
  23. }, { timeout: 20000 });
  24. h.ok("File Manager UI renders navibar, folder view and storage roots");
  25. // ── 2. Storage roots include the user home ──
  26. const roots = await h.getJSON(page, base + "/system/file_system/listRoots");
  27. if (JSON.stringify(roots).indexOf("user") === -1) {
  28. h.fail("listRoots does not include the user root: " + JSON.stringify(roots));
  29. }
  30. h.ok("listRoots includes the user: home root");
  31. // ── 3. Create a folder ──
  32. let csrft = await h.csrfToken(page, base);
  33. let resp = await h.postForm(page, base + "/system/file_system/newItem", {
  34. type: "folder", src: "user:/", filename: "e2e-folder", csrft: csrft
  35. });
  36. if (resp.toLowerCase().indexOf("ok") === -1) h.fail("newItem folder failed: " + resp);
  37. let listing = await h.postForm(page, base + "/system/file_system/listDir", { dir: "user:/" });
  38. if (listing.indexOf("e2e-folder") === -1) h.fail("created folder missing from listDir: " + listing.slice(0, 300));
  39. h.ok("newItem creates a folder that shows up in listDir");
  40. // ── 4. Create a file inside it ──
  41. csrft = await h.csrfToken(page, base);
  42. resp = await h.postForm(page, base + "/system/file_system/newItem", {
  43. type: "file", src: "user:/e2e-folder", filename: "e2e-note.txt", csrft: csrft
  44. });
  45. if (resp.toLowerCase().indexOf("ok") === -1) h.fail("newItem file failed: " + resp);
  46. listing = await h.postForm(page, base + "/system/file_system/listDir", { dir: "user:/e2e-folder" });
  47. if (listing.indexOf("e2e-note.txt") === -1) h.fail("created file missing from listDir: " + listing.slice(0, 300));
  48. h.ok("newItem creates a file inside the new folder");
  49. // ── 5. Rename the file ──
  50. csrft = await h.csrfToken(page, base);
  51. resp = await h.postForm(page, base + "/system/file_system/fileOpr", {
  52. opr: "rename",
  53. src: JSON.stringify(["user:/e2e-folder/e2e-note.txt"]),
  54. new: JSON.stringify(["e2e-renamed.txt"]),
  55. csrft: csrft
  56. });
  57. if (resp.toLowerCase().indexOf("ok") === -1) h.fail("rename failed: " + resp);
  58. listing = await h.postForm(page, base + "/system/file_system/listDir", { dir: "user:/e2e-folder" });
  59. if (listing.indexOf("e2e-renamed.txt") === -1 || listing.indexOf("e2e-note.txt") !== -1) {
  60. h.fail("rename result wrong: " + listing.slice(0, 300));
  61. }
  62. h.ok("fileOpr rename renames the file");
  63. // ── 6. Copy the file to the home root ──
  64. csrft = await h.csrfToken(page, base);
  65. resp = await h.postForm(page, base + "/system/file_system/fileOpr", {
  66. opr: "copy",
  67. src: JSON.stringify(["user:/e2e-folder/e2e-renamed.txt"]),
  68. dest: "user:/",
  69. csrft: csrft
  70. });
  71. if (resp.toLowerCase().indexOf("ok") === -1) h.fail("copy failed: " + resp);
  72. listing = await h.postForm(page, base + "/system/file_system/listDir", { dir: "user:/" });
  73. if (listing.indexOf("e2e-renamed.txt") === -1) h.fail("copied file missing at destination: " + listing.slice(0, 300));
  74. h.ok("fileOpr copy duplicates the file to user:/");
  75. // ── 7. Move the copy into a second folder ──
  76. csrft = await h.csrfToken(page, base);
  77. await h.postForm(page, base + "/system/file_system/newItem", {
  78. type: "folder", src: "user:/", filename: "e2e-folder2", csrft: csrft
  79. });
  80. csrft = await h.csrfToken(page, base);
  81. resp = await h.postForm(page, base + "/system/file_system/fileOpr", {
  82. opr: "move",
  83. src: JSON.stringify(["user:/e2e-renamed.txt"]),
  84. dest: "user:/e2e-folder2",
  85. csrft: csrft
  86. });
  87. if (resp.toLowerCase().indexOf("ok") === -1) h.fail("move failed: " + resp);
  88. listing = await h.postForm(page, base + "/system/file_system/listDir", { dir: "user:/" });
  89. if (listing.indexOf("e2e-renamed.txt") !== -1) h.fail("moved file still present at source root");
  90. listing = await h.postForm(page, base + "/system/file_system/listDir", { dir: "user:/e2e-folder2" });
  91. if (listing.indexOf("e2e-renamed.txt") === -1) h.fail("moved file missing at destination");
  92. h.ok("fileOpr move relocates the file into the second folder");
  93. // ── 8. File properties ──
  94. const props = await h.getJSON(page, base + "/system/file_system/getProperties?path=" + encodeURIComponent("user:/e2e-folder"));
  95. if (!props.IsDirectory || props.Basename !== "e2e-folder") {
  96. h.fail("getProperties returned unexpected data: " + JSON.stringify(props));
  97. }
  98. h.ok("getProperties reports the folder correctly");
  99. // ── 9. Recycle (delete to trash) ──
  100. csrft = await h.csrfToken(page, base);
  101. resp = await h.postForm(page, base + "/system/file_system/fileOpr", {
  102. opr: "recycle",
  103. src: JSON.stringify(["user:/e2e-folder/e2e-renamed.txt"]),
  104. csrft: csrft
  105. });
  106. if (resp.toLowerCase().indexOf("ok") === -1) h.fail("recycle failed: " + resp);
  107. let trash = await h.getJSON(page, base + "/system/file_system/listTrash");
  108. let entry = trash.find(function (t) { return t.OriginalFilename === "e2e-renamed.txt"; });
  109. if (!entry) h.fail("recycled file not found in trash: " + JSON.stringify(trash).slice(0, 300));
  110. h.ok("recycle moves the file into the trash bin");
  111. // ── 10. Restore from trash ──
  112. resp = await h.postForm(page, base + "/system/file_system/restoreTrash", { src: entry.Filepath });
  113. if (resp.toLowerCase().indexOf("ok") === -1) h.fail("restoreTrash failed: " + resp);
  114. listing = await h.postForm(page, base + "/system/file_system/listDir", { dir: "user:/e2e-folder" });
  115. if (listing.indexOf("e2e-renamed.txt") === -1) h.fail("restored file missing from original folder");
  116. h.ok("restoreTrash puts the file back where it came from");
  117. // ── 11. Recycle again and empty the trash ──
  118. csrft = await h.csrfToken(page, base);
  119. await h.postForm(page, base + "/system/file_system/fileOpr", {
  120. opr: "recycle",
  121. src: JSON.stringify(["user:/e2e-folder/e2e-renamed.txt"]),
  122. csrft: csrft
  123. });
  124. resp = await h.postForm(page, base + "/system/file_system/clearTrash", {});
  125. trash = await h.getJSON(page, base + "/system/file_system/listTrash");
  126. entry = trash.find(function (t) { return t.OriginalFilename === "e2e-renamed.txt"; });
  127. if (entry) h.fail("trash still contains the file after clearTrash");
  128. h.ok("clearTrash empties the trash bin");
  129. // ── 12. Permanent delete of the working folders (cleanup + coverage) ──
  130. csrft = await h.csrfToken(page, base);
  131. resp = await h.postForm(page, base + "/system/file_system/fileOpr", {
  132. opr: "delete",
  133. src: JSON.stringify(["user:/e2e-folder", "user:/e2e-folder2"]),
  134. csrft: csrft
  135. });
  136. if (resp.toLowerCase().indexOf("ok") === -1) h.fail("permanent delete failed: " + resp);
  137. listing = await h.postForm(page, base + "/system/file_system/listDir", { dir: "user:/" });
  138. if (listing.indexOf("e2e-folder") !== -1) h.fail("folder still present after permanent delete");
  139. h.ok("fileOpr delete permanently removes the folders");
  140. // ── 13. Mutations without a CSRF token are refused ──
  141. // (Match on "csrf" - the rejection text "Invalid CSRF token" itself
  142. // contains the letters "ok", so a plain ok-check would misfire.)
  143. resp = await h.postForm(page, base + "/system/file_system/newItem", {
  144. type: "folder", src: "user:/", filename: "no-csrf-folder"
  145. });
  146. if (resp.toLowerCase().indexOf("csrf") === -1) h.fail("newItem without CSRF token was not refused: " + resp);
  147. listing = await h.postForm(page, base + "/system/file_system/listDir", { dir: "user:/" });
  148. if (listing.indexOf("no-csrf-folder") !== -1) h.fail("folder was created despite missing CSRF token");
  149. h.ok("mutating file API refuses requests without a CSRF token");
  150. await page.close();
  151. });