/* Any copyright is dedicated to the Public Domain. https://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { HttpServer } = ChromeUtils.importESModule( "resource://testing-common/httpd.sys.mjs" ); const { MAX_DOWNLOAD_BYTES } = ChromeUtils.importESModule( "moz-src:///toolkit/components/pageextractor/GoogleDocsExtraction.sys.mjs" ); const EDITOR_MARKUP = `
Revenue grew by twenty percent, see the full report.
The team shipped three major features.
[a]Can we add last quarter for comparison?
Start marker
End marker
"; return before + "x".repeat(totalBytes - before.length - after.length) + after; } /** * @param {string} pageUrl * @returns {string} What the extractor reads from VIEW_MARKUP. */ function viewText(pageUrl) { return [ `[Results](${pageUrl}#h.results)`, `Quarterly report[\\[a\\]](${pageUrl}#cmnt1)`, "Revenue grew by twenty percent, see the [full report](https://example.org/report).", "Results", "The team shipped three major features.", `[\\[a\\]](${pageUrl}#cmnt_ref1)Can we add last quarter for comparison?`, ].join("\n"); } add_setup(async function () { // A slow run can take longer than the default to download a view at the // download cap, and the stall test is ended by its deadline, never a timer. await SpecialPowers.pushPrefEnv({ set: [["browser.pageextractor.googledocs.timeoutMs", 10 * 60 * 1000]], }); }); add_task(async function test_reads_the_view() { const doc = await serveGoogleDoc(); try { const result = await doc.getPageExtractor().getText({ sourceUrl: doc.sourceUrl, }); is( result.text, viewText(doc.pageUrl), "The document and its comments are read from the view instead of the editor interface." ); Assert.deepEqual( result.links.toSorted(), [ `${doc.pageUrl}#cmnt1`, `${doc.pageUrl}#cmnt_ref1`, `${doc.pageUrl}#h.results`, "https://example.org/report", ], "Heading and comment links resolve against the document, and Google's redirect is unwrapped." ); Assert.deepEqual( doc.viewRequests, [""], "The view is requested exactly once." ); } finally { await doc.cleanup(); } }); add_task(async function test_view_respects_text_options() { const doc = await serveGoogleDoc(); try { const result = await doc.getPageExtractor().getText({ sourceUrl: doc.sourceUrl, sufficientLength: 1, }); is( result.text, `[Results](${doc.pageUrl}#h.results)`, "The caller's text options apply to the view, so extraction stops once the text is sufficient." ); } finally { await doc.cleanup(); } }); add_task(async function test_page_cannot_see_the_request() { const doc = await serveGoogleDoc(); try { const result = await doc.getPageExtractor().getText({ sourceUrl: doc.sourceUrl, }); is( result.text, viewText(doc.pageUrl), "The document is read from the view." ); const pageSeen = await SpecialPowers.spawn(doc.browser, [], () => content.performance .getEntriesByType("resource") .map(entry => entry.name) .filter(name => name.includes("/mobilebasic")) ); Assert.deepEqual( pageSeen, [], "The page's own resource timing does not show the view request." ); } finally { await doc.cleanup(); } }); add_task(async function test_carries_page_params() { const doc = await serveGoogleDoc({ path: "/document/u/1/d/testdoc/edit?tab=t.abc&resourcekey=k&usp=sharing", viewPath: "/document/u/1/d/testdoc/mobilebasic", }); try { const result = await doc.getPageExtractor().getText({ sourceUrl: doc.sourceUrl, }); is( result.text, viewText(doc.pageUrl), "A document under an account selector is read from its view." ); Assert.deepEqual( doc.viewRequests, ["tab=t.abc&resourcekey=k"], "The view is requested for the same document tab and sharing key the user is looking at." ); } finally { await doc.cleanup(); } }); add_task(async function test_falls_back_to_the_generic_walk() { // Like accounts.google.com, the sign-in page sends no CORS headers. let signInRequests = 0; const signIn = new HttpServer(); signIn.registerPathHandler("/signin", (_request, response) => { signInRequests++; response.setHeader("Content-Type", "text/html; charset=utf-8"); response.write("Sorry, the file you have requested does not exist.
" ); }, }, { name: "the view is not HTML", status: "not-html", viewHandler: serveView("Quarterly report", "text/plain"), }, { name: "the view redirects to a sign-in page on another origin", status: "error", viewHandler: (request, response) => { response.setStatusLine(request.httpVersion, 302, "Found"); response.setHeader("Location", signInUrl); }, }, { name: "the view has no text", status: "empty", viewHandler: serveView(""), }, ]; for (const { name, status, viewHandler } of cases) { await Services.fog.testFlushAllChildren(); Services.fog.testResetFOG(); const doc = await serveGoogleDoc({ viewHandler }); try { const result = await doc.getPageExtractor().getText({ sourceUrl: doc.sourceUrl, }); is(result.text, EDITOR_TEXT, `The generic walk is kept when ${name}.`); is(doc.viewRequests.length, 1, `The view was requested when ${name}.`); await Services.fog.testFlushAllChildren(); const phaseEvent = Glean.pageExtractor.phase .testGetValue("page-extractor") ?.findLast(e => e.extra.phase === "google-docs-extract"); is( phaseEvent?.extra.status, status, `The Google Docs phase reports "${status}" when ${name}.` ); } finally { await doc.cleanup(); } } is(signInRequests, 0, "No request follows the redirect to the sign-in page."); await new Promise(resolve => signIn.stop(resolve)); }); add_task(async function test_follows_same_origin_redirects() { // Google drops an authuser the profile isn't signed in to by redirecting to // the same view without it. const doc = await serveGoogleDoc({ path: "/document/d/testdoc/edit?authuser=1", viewHandler: (request, response) => { if (request.queryString) { response.setStatusLine(request.httpVersion, 302, "Found"); response.setHeader("Location", "/document/d/testdoc/mobilebasic"); return; } serveView(VIEW_MARKUP)(request, response); }, }); try { const result = await doc.getPageExtractor().getText({ sourceUrl: doc.sourceUrl, }); is( result.text, viewText(doc.pageUrl), "The view is read after a same-origin redirect." ); Assert.deepEqual( doc.viewRequests, ["authuser=1", ""], "The redirect was followed." ); } finally { await doc.cleanup(); } }); add_task(async function test_sends_the_tabs_cookies() { await SpecialPowers.pushPrefEnv({ set: [ [ "network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, ], ], }); const doc = await serveGoogleDoc({ pageCookies: ["strict=1; SameSite=Strict; HttpOnly", "lax=1; SameSite=Lax"], }); try { const result = await doc.getPageExtractor().getText({ sourceUrl: doc.sourceUrl, }); is( result.text, viewText(doc.pageUrl), "The document is read from the view." ); Assert.deepEqual( doc.viewCookies.map(cookies => cookies.split("; ").toSorted()), [["lax=1", "strict=1"]], "The view request carries the session's SameSite and HttpOnly cookies, so private documents can be read." ); } finally { await doc.cleanup(); } Services.cookies.removeAll(); await SpecialPowers.popPrefEnv(); }); add_task(async function test_sends_the_containers_cookies() { const defaultDoc = await serveGoogleDoc({ pageCookies: ["jar=default"] }); await defaultDoc.cleanup(); const doc = await serveGoogleDoc({ pageCookies: ["jar=container"], userContextId: 2, }); try { await doc.getPageExtractor().getText({ sourceUrl: doc.sourceUrl }); Assert.deepEqual( doc.viewCookies, ["jar=container"], "The view request uses the container's cookies, not the default ones." ); } finally { await doc.cleanup(); } Services.cookies.removeAll(); }); add_task(async function test_falls_back_when_the_view_stalls() { await Services.fog.testFlushAllChildren(); Services.fog.testResetFOG(); const viewRequested = Promise.withResolvers(); let heldResponse; const doc = await serveGoogleDoc({ viewHandler: (_request, response) => { response.processAsync(); heldResponse = response; viewRequested.resolve(); }, }); try { const resultPromise = doc.getPageExtractor().getText({ sourceUrl: doc.sourceUrl, }); await viewRequested.promise; await SpecialPowers.spawn(doc.browser, [], () => { ChromeUtils.importESModule( "moz-src:///toolkit/components/pageextractor/GoogleDocsExtraction.sys.mjs" ).expireViewReadsForTesting(); }); const result = await resultPromise; is( result.text, EDITOR_TEXT, "The generic walk is kept when the view never answers." ); is(doc.viewRequests.length, 1, "The view was requested."); await Services.fog.testFlushAllChildren(); const phaseEvent = Glean.pageExtractor.phase .testGetValue("page-extractor") ?.findLast(e => e.extra.phase === "google-docs-extract"); is(phaseEvent?.extra.status, "error", "The Google Docs phase failed."); is( phaseEvent?.extra.error_name, "TimeoutError", "The Google Docs phase failed because the view timed out." ); } finally { heldResponse.finish(); await doc.cleanup(); } }); add_task(async function test_no_view_request() { const cases = [ { name: "only the viewport is wanted", options: { justViewport: true }, }, { name: "the page is on another host", options: { sourceUrl: "https://example.com/document/d/testdoc/edit" }, }, { name: "the page is a spreadsheet", options: { sourceUrl: "https://docs.google.com/spreadsheets/d/x/edit" }, }, { name: "the document is published to the web", options: { sourceUrl: "https://docs.google.com/document/d/e/x/pub" }, }, { name: "the page is not served over https", // eslint-disable-next-line sdl/no-insecure-url options: { sourceUrl: "http://docs.google.com/document/d/testdoc/edit" }, }, ]; for (const { name, options } of cases) { const doc = await serveGoogleDoc(); try { const result = await doc.getPageExtractor().getText({ sourceUrl: doc.sourceUrl, ...options, }); is(result.text, EDITOR_TEXT, `The generic walk is used when ${name}.`); is(doc.viewRequests.length, 0, `No view is requested when ${name}.`); } finally { await doc.cleanup(); } } }); add_task(async function test_no_view_request_when_disabled() { await SpecialPowers.pushPrefEnv({ set: [["browser.pageextractor.googledocs.enabled", false]], }); const doc = await serveGoogleDoc(); try { const result = await doc.getPageExtractor().getText({ sourceUrl: doc.sourceUrl, }); is(result.text, EDITOR_TEXT, "The generic walk is used when disabled."); is(doc.viewRequests.length, 0, "No view is requested when disabled."); } finally { await doc.cleanup(); } await SpecialPowers.popPrefEnv(); }); add_task(async function test_view_at_the_cap_is_read_whole() { const doc = await serveGoogleDoc({ viewHandler: serveView(viewOfSize(MAX_DOWNLOAD_BYTES)), }); try { const result = await doc.getPageExtractor().getText({ sourceUrl: doc.sourceUrl, }); is( result.text, "Start marker\nEnd marker", "A view exactly at the download cap is read to the end." ); } finally { await doc.cleanup(); } }); add_task(async function test_download_cap() { const doc = await serveGoogleDoc({ viewHandler: serveView(viewOfSize(MAX_DOWNLOAD_BYTES + 1024)), }); try { const result = await doc.getPageExtractor().getText({ sourceUrl: doc.sourceUrl, }); is( result.text, "Start marker", "A view cut short by the download cap still yields the part that arrived." ); } finally { await doc.cleanup(); } });