From 3d84014116a3f07010131ef9989073e763e3031d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:01:18 -0400 Subject: [PATCH 1/4] automated update to translation keys (#598) Co-authored-by: Paul's Grist Bot --- static/locales/en.client.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/static/locales/en.client.json b/static/locales/en.client.json index d95c6ffbf5..6624c08d6f 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -76,7 +76,10 @@ "Activation": "Activation", "Billing Account": "Billing Account", "Support Grist": "Support Grist", - "Upgrade Plan": "Upgrade Plan" + "Upgrade Plan": "Upgrade Plan", + "Sign In": "Sign In", + "Sign Up": "Sign Up", + "Use This Template": "Use This Template" }, "ViewAsDropdown": { "View As": "View As", @@ -112,7 +115,8 @@ "Home Page": "Home Page", "Legacy": "Legacy", "Personal Site": "Personal Site", - "Team Site": "Team Site" + "Team Site": "Team Site", + "Grist Templates": "Grist Templates" }, "AppModel": { "This team site is suspended. Documents can be read, but not modified.": "This team site is suspended. Documents can be read, but not modified." From 7a6464ae5a0851858a03b6d9336e223ee6f5d5e9 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Sat, 29 Jul 2023 00:24:09 +0200 Subject: [PATCH 2/4] Run tests on Python 3.9, 3.10, and 3.11 in GHA (#597) --- .github/workflows/main.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0538be7d98..98db1d33cb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,6 +28,13 @@ jobs: - ':nbrowser-^[M-O]:' - ':nbrowser-^[P-S]:' - ':nbrowser-^[^A-S]:' + include: + - tests: ':lint:python:client:common:smoke:' + node-version: 14.x + python-version: '3.10' + - tests: ':lint:python:client:common:smoke:' + node-version: 14.x + python-version: '3.11' steps: - uses: actions/checkout@v3 From e1df6039c2e047ebecadc226b2a363bd1412b61d Mon Sep 17 00:00:00 2001 From: John Cant Date: Sun, 30 Jul 2023 20:13:43 +0100 Subject: [PATCH 3/4] REQUEST now supports POST (#588) * REQUEST now supports POST * Add extra flag for enabling REQUEST, also update README and comments Co-authored-by: John Cant Co-authored-by: Alex Hall --- README.md | 1 + app/common/ActionBundle.ts | 2 ++ app/server/devServerMain.ts | 5 +++ app/server/lib/Requests.ts | 11 ++++-- sandbox/grist/functions/info.py | 63 +++++++++++++++++++++++++++++---- sandbox/grist/test_requests.py | 44 ++++++++++++++++++++++- 6 files changed, 116 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ca000a78ea..1167171615 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,7 @@ GRIST_DEFAULT_PRODUCT | if set, this controls enabled features and limits of ne GRIST_DEFAULT_LOCALE | Locale to use as fallback when Grist cannot honour the browser locale. GRIST_DOMAIN | in hosted Grist, Grist is served from subdomains of this domain. Defaults to "getgrist.com". GRIST_EXPERIMENTAL_PLUGINS | enables experimental plugins +GRIST_ENABLE_REQUEST_FUNCTION | enables the REQUEST function. This function performs HTTP requests in a similar way to `requests.request`. This function presents a significant security risk, since it can let users call internal endpoints when Grist is available publicly. This function can also cause performance issues. Unset by default. GRIST_HIDE_UI_ELEMENTS | comma-separated list of UI features to disable. Allowed names of parts: `helpCenter,billing,templates,multiSite,multiAccounts,sendToDrive,tutorials`. If a part also exists in GRIST_UI_FEATURES, it will still be disabled. GRIST_HOME_INCLUDE_STATIC | if set, home server also serves static resources GRIST_HOST | hostname to use when listening on a port. diff --git a/app/common/ActionBundle.ts b/app/common/ActionBundle.ts index f48e584ae8..35fe34d155 100644 --- a/app/common/ActionBundle.ts +++ b/app/common/ActionBundle.ts @@ -72,6 +72,8 @@ export interface SandboxActionBundle { // Represents a unique call to the Python REQUEST function export interface SandboxRequest { url: string; + method: string; + body?: string; params: Record | null; headers: Record | null; deps: unknown; // pass back to the sandbox unchanged in the response diff --git a/app/server/devServerMain.ts b/app/server/devServerMain.ts index a7f20a23d1..7e7012a5d9 100644 --- a/app/server/devServerMain.ts +++ b/app/server/devServerMain.ts @@ -66,6 +66,11 @@ export async function main() { process.env.GRIST_EXPERIMENTAL_PLUGINS = "1"; } + // Experimental plugins are enabled by default for devs + if (!process.env.GRIST_ENABLE_REQUEST_FUNCTION) { + process.env.GRIST_ENABLE_REQUEST_FUNCTION = "1"; + } + // For tests, it is useful to start with the database in a known state. // If TEST_CLEAN_DATABASE is set, we reset the database before starting. if (process.env.TEST_CLEAN_DATABASE) { diff --git a/app/server/lib/Requests.ts b/app/server/lib/Requests.ts index 4cee6a2e96..69f5dd9e9d 100644 --- a/app/server/lib/Requests.ts +++ b/app/server/lib/Requests.ts @@ -80,16 +80,21 @@ export class DocRequests { private async _handleSingleRequestRaw(request: SandboxRequest): Promise { try { - if (process.env.GRIST_EXPERIMENTAL_PLUGINS != '1') { + if (process.env.GRIST_ENABLE_REQUEST_FUNCTION != '1') { throw new Error("REQUEST is not enabled"); } - const {url, params, headers} = request; + const {url, method, body, params, headers} = request; const urlObj = new URL(url); log.rawInfo("Handling sandbox request", {host: urlObj.host, docId: this._activeDoc.docName}); for (const [param, value] of Object.entries(params || {})) { urlObj.searchParams.append(param, value); } - const response = await fetch(urlObj.toString(), {headers: headers || {}, agent: proxyAgent(urlObj)}); + const response = await fetch(urlObj.toString(), { + headers: headers || {}, + agent: proxyAgent(urlObj), + method, + body + }); const content = await response.buffer(); const {status, statusText} = response; const encoding = httpEncoding(response.headers.get('content-type'), content); diff --git a/sandbox/grist/functions/info.py b/sandbox/grist/functions/info.py index 87640ef2da..99ab6424cf 100644 --- a/sandbox/grist/functions/info.py +++ b/sandbox/grist/functions/info.py @@ -4,13 +4,14 @@ from __future__ import absolute_import import datetime import hashlib -import json +import json as json_module import math import numbers import re import chardet import six +from six.moves import urllib_parse import column import docmodel @@ -653,20 +654,70 @@ def is_error(value): or (isinstance(value, float) and math.isnan(value))) +def _replicate_requests_body_args(data=None, json=None): + """ + Replicate some of the behaviour of requests.post, specifically the data and + json args. + + Returns a tuple of (body, extra_headers) + """ + if data is None and json is None: + return None, {} + + elif data is not None and json is None: + if isinstance(data, str): + body = data + extra_headers = {} + else: + body = urllib_parse.urlencode(data) + extra_headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + return body, extra_headers + + elif json is not None and data is None: + if isinstance(json, str): + body = json + else: + body = json_module.dumps(json) + extra_headers = { + "Content-Type": "application/json", + } + return body, extra_headers + + elif data is not None and json is not None: + # From testing manually with requests 2.28.2, data overrides json if both + # supplied. However, this is probably a mistake on behalf of the caller, so + # we choose to throw an error instead + raise ValueError("`data` and `json` cannot be supplied to REQUEST at the same time") + + @unimplemented # ^ This excludes this function from autocomplete while in beta # and marks it as unimplemented in the docs. # It also makes grist-help expect to see the string 'raise NotImplemented' in the function source, # which it does now, because of this comment. Removing this comment will currently break the docs. -def REQUEST(url, params=None, headers=None): - # Makes a GET HTTP request with an API similar to `requests.get`. +def REQUEST(url, params=None, headers=None, method="GET", data=None, json=None): + # Makes an HTTP request with an API similar to `requests.request`. # Actually jumps through hoops internally to make the request asynchronously (usually) # while feeling synchronous to the formula writer. + # When making a POST or PUT request, REQUEST supports `data` and `json` args, from `requests.request`: + # - `args` as str: Used as the request body + # - `args` as other types: Form encoded and used as the request body. The correct header is also set. + # - `json` as str: Used as the request body. The correct header is also set. + # - `json` as other types: JSON encoded and set as the request body. The correct header is also set. + body, _headers = _replicate_requests_body_args(data=data, json=json) + + # Extra headers that make us consistent with requests.post must not override + # user-supplied headers. + _headers.update(headers or {}) + # Requests are identified by a string key in various places. # The same arguments should produce the same key so the request is only made once. - args = dict(url=url, params=params, headers=headers) - args_json = json.dumps(args, sort_keys=True) + args = dict(url=url, params=params, headers=_headers, method=method, body=body) + + args_json = json_module.dumps(args, sort_keys=True) key = hashlib.sha256(args_json.encode()).hexdigest() # This may either return the raw response data or it may raise a special exception @@ -701,7 +752,7 @@ def text(self): return self.content.decode(self.encoding) def json(self, **kwargs): - return json.loads(self.text, **kwargs) + return json_module.loads(self.text, **kwargs) @property def ok(self): diff --git a/sandbox/grist/test_requests.py b/sandbox/grist/test_requests.py index 4a57c16831..d5960f6bb9 100644 --- a/sandbox/grist/test_requests.py +++ b/sandbox/grist/test_requests.py @@ -4,6 +4,7 @@ import test_engine import testutil from functions import CaseInsensitiveDict, Response, HTTPError +from functions.info import _replicate_requests_body_args class TestCaseInsensitiveDict(unittest.TestCase): @@ -73,6 +74,45 @@ def test_apparent_encoding(self): self.assertEqual(r.text, text) +class TestRequestsPostInterface(unittest.TestCase): + def test_no_post_args(self): + body, headers = _replicate_requests_body_args() + + assert body is None + assert headers == {} + + def test_data_as_dict(self): + body, headers = _replicate_requests_body_args(data={"foo": "bar"}) + + assert body == "foo=bar" + assert headers == {"Content-Type": "application/x-www-form-urlencoded"} + + def test_data_as_string(self): + body, headers = _replicate_requests_body_args(data="some_content") + + assert body == "some_content" + assert headers == {} + + def test_json_as_dict(self): + body, headers = _replicate_requests_body_args(json={"foo": "bar"}) + + assert body == '{"foo": "bar"}' + assert headers == {"Content-Type": "application/json"} + + def test_json_as_string(self): + body, headers = _replicate_requests_body_args(json="invalid_but_ignored") + + assert body == "invalid_but_ignored" + assert headers == {"Content-Type": "application/json"} + + def test_data_and_json_together(self): + with self.assertRaises(ValueError): + body, headers = _replicate_requests_body_args( + json={"foo": "bar"}, + data={"quux": "jazz"} + ) + + class TestRequestFunction(test_engine.EngineTestCase): sample = testutil.parse_test_sample({ "SCHEMA": [ @@ -98,12 +138,14 @@ def test_request_function(self): r.__dict__ """ out_actions = self.modify_column("Table1", "Request", formula=formula) - key = '9d305be9664924aaaf7ebb0bab2e4155d1fa1b9dcde53e417f1a9f9a2c7e09b9' + key = 'd7f8cedf177ab538bf7dadf66e77a525486a29a41ce4520b2c89a33e39095fed' deps = {'Table1': {'Request': [1, 2]}} args = { 'url': 'my_url', 'headers': {'foo': 'bar'}, 'params': {'a': 2, 'b': 1}, + 'method': 'GET', + 'body': None, 'deps': deps, } self.assertEqual(out_actions.requests, {key: args}) From 61f954ff05ae183154522664a1c3d9421591a792 Mon Sep 17 00:00:00 2001 From: Paul Fitzpatrick Date: Mon, 31 Jul 2023 21:10:59 +0100 Subject: [PATCH 4/4] move getTemplateOrg method; enable template org in docker tests (#602) * move getTemplateOrg method; enable template org in docker tests This moves the `getTemplateOrg` method to a neutral venue for the convenience of `grist-static`, otherwise a lot of awkward dependencies get pulled in needlessly in new parts of the app. This also fixes docker tests using the template org. --- app/gen-server/ApiServer.ts | 2 +- app/server/lib/ActiveDoc.ts | 2 +- app/server/lib/AppEndpoint.ts | 3 ++- app/server/lib/gristSettings.ts | 13 ++++++++++++ app/server/lib/sendAppPage.ts | 14 +------------ test/nbrowser/CellColor.ts | 1 + test/nbrowser/SelectByRefList.ts | 2 +- test/nbrowser/SelectBySummary.ts | 36 +++++++++++++++++++------------- test/nbrowser/gristUtils.ts | 8 +++++++ test/nbrowser/homeUtil.ts | 7 ++++++- test/test_under_docker.sh | 1 + 11 files changed, 57 insertions(+), 32 deletions(-) create mode 100644 app/server/lib/gristSettings.ts diff --git a/app/gen-server/ApiServer.ts b/app/gen-server/ApiServer.ts index 542f2bf3dd..9373825313 100644 --- a/app/gen-server/ApiServer.ts +++ b/app/gen-server/ApiServer.ts @@ -11,10 +11,10 @@ import {getAuthorizedUserId, getUserId, getUserProfiles, RequestWithLogin} from import {getSessionUser, linkOrgWithEmail} from 'app/server/lib/BrowserSession'; import {expressWrap} from 'app/server/lib/expressWrap'; import {RequestWithOrg} from 'app/server/lib/extractOrg'; +import {getTemplateOrg} from 'app/server/lib/gristSettings'; import log from 'app/server/lib/log'; import {addPermit, clearSessionCacheIfNeeded, getDocScope, getScope, integerParam, isParameterOn, optStringParam, sendOkReply, sendReply, stringParam} from 'app/server/lib/requestUtils'; -import {getTemplateOrg} from 'app/server/lib/sendAppPage'; import {IWidgetRepository} from 'app/server/lib/WidgetRepository'; import {User} from './entity/User'; diff --git a/app/server/lib/ActiveDoc.ts b/app/server/lib/ActiveDoc.ts index 9d30976e60..34e7f53ccb 100644 --- a/app/server/lib/ActiveDoc.ts +++ b/app/server/lib/ActiveDoc.ts @@ -89,6 +89,7 @@ import {Authorizer} from 'app/server/lib/Authorizer'; import {checksumFile} from 'app/server/lib/checksumFile'; import {Client} from 'app/server/lib/Client'; import {DEFAULT_CACHE_TTL, DocManager} from 'app/server/lib/DocManager'; +import {getTemplateOrg} from 'app/server/lib/gristSettings'; import {ICreateActiveDocOptions} from 'app/server/lib/ICreate'; import {makeForkIds} from 'app/server/lib/idUtils'; import {GRIST_DOC_SQL, GRIST_DOC_WITH_TABLE1_SQL} from 'app/server/lib/initialDocSql'; @@ -97,7 +98,6 @@ import log from 'app/server/lib/log'; import {LogMethods} from "app/server/lib/LogMethods"; import {NullSandbox, UnavailableSandboxMethodError} from 'app/server/lib/NullSandbox'; import {DocRequests} from 'app/server/lib/Requests'; -import {getTemplateOrg} from 'app/server/lib/sendAppPage'; import {shortDesc} from 'app/server/lib/shortDesc'; import {TableMetadataLoader} from 'app/server/lib/TableMetadataLoader'; import {DocTriggers} from "app/server/lib/Triggers"; diff --git a/app/server/lib/AppEndpoint.ts b/app/server/lib/AppEndpoint.ts index a29ed2b8d8..4196827103 100644 --- a/app/server/lib/AppEndpoint.ts +++ b/app/server/lib/AppEndpoint.ts @@ -20,10 +20,11 @@ import {DocStatus, IDocWorkerMap} from 'app/server/lib/DocWorkerMap'; import {expressWrap} from 'app/server/lib/expressWrap'; import {DocTemplate, GristServer} from 'app/server/lib/GristServer'; import {getCookieDomain} from 'app/server/lib/gristSessions'; +import {getTemplateOrg} from 'app/server/lib/gristSettings'; import {getAssignmentId} from 'app/server/lib/idUtils'; import log from 'app/server/lib/log'; import {adaptServerUrl, addOrgToPathIfNeeded, pruneAPIResult, trustOrigin} from 'app/server/lib/requestUtils'; -import {getTemplateOrg, ISendAppPageOptions} from 'app/server/lib/sendAppPage'; +import {ISendAppPageOptions} from 'app/server/lib/sendAppPage'; export interface AttachOptions { app: express.Application; // Express app to which to add endpoints diff --git a/app/server/lib/gristSettings.ts b/app/server/lib/gristSettings.ts new file mode 100644 index 0000000000..3ffb8197ad --- /dev/null +++ b/app/server/lib/gristSettings.ts @@ -0,0 +1,13 @@ +import {appSettings} from 'app/server/lib/AppSettings'; + +export function getTemplateOrg() { + let org = appSettings.section('templates').flag('org').readString({ + envVar: 'GRIST_TEMPLATE_ORG', + }); + if (!org) { return null; } + + if (process.env.GRIST_ID_PREFIX) { + org += `-${process.env.GRIST_ID_PREFIX}`; + } + return org; +} diff --git a/app/server/lib/sendAppPage.ts b/app/server/lib/sendAppPage.ts index 1933698274..8aaf48bea9 100644 --- a/app/server/lib/sendAppPage.ts +++ b/app/server/lib/sendAppPage.ts @@ -3,10 +3,10 @@ import {isAffirmative} from 'app/common/gutil'; import {getTagManagerSnippet} from 'app/common/tagManager'; import {Document} from 'app/common/UserAPI'; import {SUPPORT_EMAIL} from 'app/gen-server/lib/HomeDBManager'; -import {appSettings} from 'app/server/lib/AppSettings'; import {isAnonymousUser, isSingleUserMode, RequestWithLogin} from 'app/server/lib/Authorizer'; import {RequestWithOrg} from 'app/server/lib/extractOrg'; import {GristServer} from 'app/server/lib/GristServer'; +import {getTemplateOrg} from 'app/server/lib/gristSettings'; import {getSupportedEngineChoices} from 'app/server/lib/serverUtils'; import {readLoadedLngs, readLoadedNamespaces} from 'app/server/localization'; import * as express from 'express'; @@ -154,18 +154,6 @@ export function makeSendAppPage(opts: { }; } -export function getTemplateOrg() { - let org = appSettings.section('templates').flag('org').readString({ - envVar: 'GRIST_TEMPLATE_ORG', - }); - if (!org) { return null; } - - if (process.env.GRIST_ID_PREFIX) { - org += `-${process.env.GRIST_ID_PREFIX}`; - } - return org; -} - function shouldSupportAnon() { // Enable UI for anonymous access if a flag is explicitly set in the environment return process.env.GRIST_SUPPORT_ANON === "true"; diff --git a/test/nbrowser/CellColor.ts b/test/nbrowser/CellColor.ts index 3df31e6cc7..00f4880206 100644 --- a/test/nbrowser/CellColor.ts +++ b/test/nbrowser/CellColor.ts @@ -306,6 +306,7 @@ describe('CellColor', function() { // Empty cell to clear error from converting toggle to date await cell.click(); await driver.sendKeys(Key.DELETE); + await gu.waitAppFocus(true); const clip = cell.find('.field_clip'); diff --git a/test/nbrowser/SelectByRefList.ts b/test/nbrowser/SelectByRefList.ts index 9c96d5a49e..6d3d3b562c 100644 --- a/test/nbrowser/SelectByRefList.ts +++ b/test/nbrowser/SelectByRefList.ts @@ -232,7 +232,7 @@ async function checkSelectingRecords(selectBy: string, sourceData: string[][], n for (let rowNum = 1; rowNum <= 3; rowNum++) { // Click an anchor link const anchorCell = gu.getCell({section: "Anchors", rowNum, col: 1}); - await anchorCell.find('.test-tb-link').click(); + await driver.withActions(a => a.click(anchorCell.find('.test-tb-link'))); // Check that navigation to the link target worked assert.equal(await gu.getActiveSectionTitle(), "LINKTARGET"); diff --git a/test/nbrowser/SelectBySummary.ts b/test/nbrowser/SelectBySummary.ts index 8b7cbdea6e..30f8141590 100644 --- a/test/nbrowser/SelectBySummary.ts +++ b/test/nbrowser/SelectBySummary.ts @@ -1,25 +1,26 @@ import * as _ from 'lodash'; -import {addToRepl, assert, driver} from 'mocha-webdriver'; +import {assert, driver} from 'mocha-webdriver'; import {enterRulePart, findDefaultRuleSet} from 'test/nbrowser/aclTestUtils'; import * as gu from 'test/nbrowser/gristUtils'; -import {server, setupTestSuite} from 'test/nbrowser/testUtils'; +import {setupTestSuite} from 'test/nbrowser/testUtils'; describe('SelectBySummary', function() { this.timeout(50000); - setupTestSuite(); - addToRepl('gu2', gu); + const cleanup = setupTestSuite(); + let headers: Record; gu.bigScreen(); before(async function() { - await server.simulateLogin("Chimpy", "chimpy@getgrist.com", 'nasa'); - const doc = await gu.importFixturesDoc('chimpy', 'nasa', 'Horizon', - 'SelectBySummary.grist', false); - await driver.get(`${server.getHost()}/o/nasa/doc/${doc.id}`); - await gu.waitForDocToLoad(); + const session = await gu.session().teamSite.login(); + await session.tempDoc(cleanup, 'SelectBySummary.grist'); + headers = { + Authorization: `Bearer ${session.getApiKey()}` + }; }); - it('should filter a source table selected by a summary table', async function() { + it('should filter a source table selected by a summary table (first option)', async function() { await checkSelectingRecords( + headers, ['onetwo'], [ '1', '16', @@ -40,8 +41,11 @@ describe('SelectBySummary', function() { ], ], ); + }); + it('should filter a source table selected by a summary table (second option)', async function() { await checkSelectingRecords( + headers, ['choices'], [ 'a', '14', @@ -67,9 +71,11 @@ describe('SelectBySummary', function() { ], ], ); + }); - + it('should filter a source table selected by a summary table (both options)', async function() { await checkSelectingRecords( + headers, ['onetwo', 'choices'], [ '1', 'a', '6', @@ -104,7 +110,6 @@ describe('SelectBySummary', function() { ], ], ); - }); it('should create new rows in the source table (link target) with correct default values', @@ -153,6 +158,7 @@ describe('SelectBySummary', function() { // selecting by the two less detailed summaries. // There was a bug previously that this would not work while the summary source table (Table1) was hidden. await checkSelectingRecords( + headers, ['onetwo'], [ '1', '16', @@ -175,6 +181,7 @@ describe('SelectBySummary', function() { ); await checkSelectingRecords( + headers, ['choices'], [ 'a', '14', @@ -208,6 +215,7 @@ describe('SelectBySummary', function() { * to the corresponding subarray of `targetData`. */ async function checkSelectingRecords( + headers: Record, groubyColumns: string[], summaryData: string[], targetData: string[][], @@ -243,7 +251,7 @@ async function checkSelectingRecords( ); if (targetSection === 'TABLE1') { assert.equal(await countCell.getText(), numTargetRows.toString()); - const csvCells = await gu.downloadSectionCsvGridCells(targetSection); + const csvCells = await gu.downloadSectionCsvGridCells(targetSection, headers); // visible cells text uses newlines to separate list items, CSV export uses commas const expectedCsvCells = targetGroup.map(s => s.replace("\n", ", ")); assert.deepEqual(csvCells, expectedCsvCells); @@ -259,7 +267,7 @@ async function checkSelectingRecords( for (let rowNum = 1; rowNum <= 8; rowNum++) { // Click an anchor link const anchorCell = gu.getCell({section: "Anchors", rowNum, col: 1}); - await anchorCell.find('.test-tb-link').click(); + await driver.withActions(a => a.click(anchorCell.find('.test-tb-link'))); // Check that navigation to the link target worked assert.equal(await gu.getActiveSectionTitle(), "TABLE1"); diff --git a/test/nbrowser/gristUtils.ts b/test/nbrowser/gristUtils.ts index 4535c19fc1..f4b81e3e9c 100644 --- a/test/nbrowser/gristUtils.ts +++ b/test/nbrowser/gristUtils.ts @@ -61,6 +61,7 @@ export const uploadFixtureDoc = homeUtil.uploadFixtureDoc.bind(homeUtil); export const getWorkspaceId = homeUtil.getWorkspaceId.bind(homeUtil); export const listDocs = homeUtil.listDocs.bind(homeUtil); export const createHomeApi = homeUtil.createHomeApi.bind(homeUtil); +export const getApiKey = homeUtil.getApiKey.bind(homeUtil); export const simulateLogin = homeUtil.simulateLogin.bind(homeUtil); export const removeLogin = homeUtil.removeLogin.bind(homeUtil); export const enableTips = homeUtil.enableTips.bind(homeUtil); @@ -2047,6 +2048,13 @@ export class Session { return createHomeApi(this.settings.name, this.settings.orgDomain, this.settings.email); } + public getApiKey(): string|null { + if (this.settings.email === 'anon@getgrist.com') { + return getApiKey(null); + } + return getApiKey(this.settings.name, this.settings.email); + } + // Get the id of this user. public async getUserId(): Promise { await this.login(); diff --git a/test/nbrowser/homeUtil.ts b/test/nbrowser/homeUtil.ts index e90826ad09..6dea6c493b 100644 --- a/test/nbrowser/homeUtil.ts +++ b/test/nbrowser/homeUtil.ts @@ -316,9 +316,14 @@ export class HomeUtil { // A helper to create a UserAPI instance for a given useranme and org, that targets the home server // Username can be null for anonymous access. public createHomeApi(username: string|null, org: string, email?: string): UserAPIImpl { + const apiKey = this.getApiKey(username, email); + return this._createHomeApiUsingApiKey(apiKey, org); + } + + public getApiKey(username: string|null, email?: string): string | null { const name = (username || '').toLowerCase(); const apiKey = username && ((email && this._apiKey.get(email)) || `api_key_for_${name}`); - return this._createHomeApiUsingApiKey(apiKey, org); + return apiKey; } /** diff --git a/test/test_under_docker.sh b/test/test_under_docker.sh index ba7af76b1b..e5d573ba0a 100755 --- a/test/test_under_docker.sh +++ b/test/test_under_docker.sh @@ -41,6 +41,7 @@ docker run --name $DOCKER_CONTAINER --rm \ --env GRIST_LOG_LEVEL=$GRIST_LOG_LEVEL \ --env GRIST_LOG_SKIP_HTTP=${DEBUG:-false} \ --env TEST_SUPPORT_API_KEY=api_key_for_support \ + --env GRIST_TEMPLATE_ORG=templates \ ${TEST_IMAGE:-gristlabs/grist} & DOCKER_PID="$!"