035-file-transfer.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. /*
  2. Critical path (deep): file transfer, search and sharing.
  3. Extends the file-explorer coverage in 030 with the transfer-oriented
  4. File Manager features that move real bytes in and out of the system:
  5. multipart upload, media download (round-trip verification), search,
  6. and the share-link lifecycle (create / list / public download /
  7. delete). Uses the same endpoints the File Manager front end calls.
  8. */
  9. "use strict";
  10. const h = require("../lib/system-harness");
  11. const UPLOAD_BODY = "arozos-e2e upload payload " + Date.now();
  12. const UPLOAD_NAME = "e2e-upload.txt";
  13. h.run("FILE-TRANSFER", async function (env) {
  14. const base = env.baseURL;
  15. const page = await h.newPage(env.browser);
  16. await h.loginViaAPI(page, base, env.admin.username, env.admin.password);
  17. // Work inside a dedicated folder so the test is self-contained.
  18. let csrft = await h.csrfToken(page, base);
  19. let resp = await h.postForm(page, base + "/system/file_system/newItem", {
  20. type: "folder", src: "user:/", filename: "e2e-transfer", csrft: csrft
  21. });
  22. if (resp.toLowerCase().indexOf("ok") === -1) h.fail("could not create working folder: " + resp);
  23. // ── 1. Multipart upload of a real file ──
  24. const uploadResp = await page.request.post(base + "/system/file_system/upload", {
  25. multipart: {
  26. path: "user:/e2e-transfer",
  27. file: {
  28. name: UPLOAD_NAME,
  29. mimeType: "text/plain",
  30. buffer: Buffer.from(UPLOAD_BODY)
  31. }
  32. }
  33. });
  34. const uploadText = (await uploadResp.text()).trim().toLowerCase();
  35. if (uploadText.indexOf("ok") === -1) h.fail("upload failed: " + uploadText);
  36. let listing = await h.postForm(page, base + "/system/file_system/listDir", { dir: "user:/e2e-transfer" });
  37. if (listing.indexOf(UPLOAD_NAME) === -1) h.fail("uploaded file missing from listDir: " + listing.slice(0, 300));
  38. h.ok("multipart upload stores the file in the target folder");
  39. // ── 2. Download the file back and verify the bytes round-trip ──
  40. const dlResp = await page.request.get(base + "/media/?file=" +
  41. encodeURIComponent("user:/e2e-transfer/" + UPLOAD_NAME) + "&download=true");
  42. if (!dlResp.ok()) h.fail("download request failed with HTTP " + dlResp.status());
  43. const dlBody = await dlResp.text();
  44. if (dlBody !== UPLOAD_BODY) {
  45. h.fail("downloaded bytes did not match what was uploaded (got " + dlBody.length + " bytes)");
  46. }
  47. h.ok("download returns the exact bytes that were uploaded");
  48. // ── 3. Search finds the file by name ──
  49. const searchResults = await h.getJSON(page, base + "/system/file_system/search?path=" +
  50. encodeURIComponent("user:/e2e-transfer/") + "&keyword=e2e-upload");
  51. if (JSON.stringify(searchResults).indexOf(UPLOAD_NAME) === -1) {
  52. h.fail("search did not find the uploaded file: " + JSON.stringify(searchResults).slice(0, 300));
  53. }
  54. h.ok("file search finds the uploaded file by keyword");
  55. // ── 4. Create a share link for the file ──
  56. const share = await h.getJSON(page, base + "/system/file_system/share/new?path=" +
  57. encodeURIComponent("user:/e2e-transfer/" + UPLOAD_NAME));
  58. // share/new accepts POST too, but the front end also uses GET-style; use POST for parity.
  59. let shareObj = share;
  60. if (!shareObj || !shareObj.UUID) {
  61. // Fall back to the POST form the File Manager actually submits.
  62. const shareText = await h.postForm(page, base + "/system/file_system/share/new", {
  63. path: "user:/e2e-transfer/" + UPLOAD_NAME
  64. });
  65. try { shareObj = JSON.parse(shareText); } catch (e) { shareObj = null; }
  66. }
  67. if (!shareObj || !shareObj.UUID) h.fail("share/new did not return a share UUID: " + JSON.stringify(share));
  68. const shareUUID = shareObj.UUID;
  69. h.ok("share/new creates a share link with a UUID");
  70. // ── 5. The share appears in the user's share list ──
  71. const shareList = await h.getJSON(page, base + "/system/file_system/share/list");
  72. if (JSON.stringify(shareList).indexOf(shareUUID) === -1) {
  73. h.fail("created share not present in share list");
  74. }
  75. h.ok("the share appears in the user's share list");
  76. // ── 6. The public share link serves the file without a session ──
  77. // /share/download/{uuid}/ is the direct-download endpoint (the plain
  78. // /share/{uuid} path renders the preview page instead).
  79. const anon = await h.newPage(env.browser);
  80. const anonDl = await anon.request.get(base + "/share/download/" + shareUUID + "/");
  81. if (!anonDl.ok()) h.fail("public share download failed with HTTP " + anonDl.status());
  82. const anonBody = await anonDl.text();
  83. if (anonBody !== UPLOAD_BODY) {
  84. h.fail("public share download bytes did not match the original file");
  85. }
  86. // The plain share page should also be reachable anonymously (preview).
  87. const anonPage = await anon.request.get(base + "/share/" + shareUUID + "/");
  88. if (!anonPage.ok()) h.fail("public share preview page failed with HTTP " + anonPage.status());
  89. h.ok("the public share link serves the file and preview page without a session");
  90. await anon.close();
  91. // ── 7. Deleting the share revokes public access ──
  92. resp = await h.postForm(page, base + "/system/file_system/share/delete", { uuid: shareUUID });
  93. if (resp.toLowerCase().indexOf("ok") === -1) h.fail("share delete failed: " + resp);
  94. const shareListAfter = await h.getJSON(page, base + "/system/file_system/share/list");
  95. if (JSON.stringify(shareListAfter).indexOf(shareUUID) !== -1) {
  96. h.fail("deleted share still present in share list");
  97. }
  98. h.ok("deleting the share removes it from the share list");
  99. // ── Cleanup ──
  100. csrft = await h.csrfToken(page, base);
  101. await h.postForm(page, base + "/system/file_system/fileOpr", {
  102. opr: "delete", src: JSON.stringify(["user:/e2e-transfer"]), csrft: csrft
  103. });
  104. await page.close();
  105. });