diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index 3d9b1f854a..ec7b321940 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -75,6 +75,21 @@ function makeConfig(username: string): AxiosRequestConfig { }; } +async function makeUserApi( + org: string, + username: string, + options?: { + headers?: Record, + baseUrl?: string + } +) { + return new UserAPIImpl(`${options?.baseUrl ?? homeUrl}/o/${org}`, { + headers: options?.headers ?? makeConfig(username).headers as Record, + fetch: fetch as unknown as typeof globalThis.fetch, + newFormData: () => new FormData() as any, + }); +} + describe('DocApi', function () { this.timeout(30000); testUtils.setTmpLogLevel('error'); @@ -118,7 +133,7 @@ describe('DocApi', function () { GRIST_DATA_DIR: dataDir }; home = docs = await TestServer.startServer('home,docs', tmpDir, suitename, additionalEnvConfiguration); - homeUrl = serverUrl = home.serverUrl; + homeUrl = serverUrl = await home.getServerUrl(); hasHomeApi = true; }); testDocApi(); @@ -132,7 +147,7 @@ describe('DocApi', function () { GRIST_ANON_PLAYGROUND: 'false' }; home = docs = await TestServer.startServer('home,docs', tmpDir, suitename, additionalEnvConfiguration); - homeUrl = serverUrl = home.serverUrl; + homeUrl = serverUrl = await home.getServerUrl(); hasHomeApi = true; }); @@ -153,8 +168,8 @@ describe('DocApi', function () { }; home = await TestServer.startServer('home', tmpDir, suitename, additionalEnvConfiguration); - docs = await TestServer.startServer('docs', tmpDir, suitename, additionalEnvConfiguration, home.serverUrl); - homeUrl = serverUrl = home.serverUrl; + homeUrl = serverUrl = await home.getServerUrl(); + docs = await TestServer.startServer('docs', tmpDir, suitename, additionalEnvConfiguration, homeUrl); hasHomeApi = true; }); testDocApi(); @@ -171,10 +186,16 @@ describe('DocApi', function () { GRIST_SINGLE_PORT: '0', ...overrideEnvConf }; - const home = await TestServer.startServer('home', tmpDir, suitename, additionalEnvConfiguration); - const docs = await TestServer.startServer( - 'docs', tmpDir, suitename, additionalEnvConfiguration, home.serverUrl - ); + + const home = new TestServer('home', tmpDir, suitename); + await home.start(await home.getServerUrl(), additionalEnvConfiguration); + const docs = new TestServer('docs', tmpDir, suitename); + await docs.start(await home.getServerUrl(), { + ...additionalEnvConfiguration, + APP_DOC_URL: `${await proxy.getServerUrl()}/dw/dw1`, + APP_DOC_INTERNAL_URL: await docs.getServerUrl(), + }); + proxy.requireFromOutsideHeader(); await proxy.start(home, docs); @@ -210,11 +231,7 @@ describe('DocApi', function () { }); async function testCompareDocs(proxy: TestServerReverseProxy, home: TestServer) { - const chimpy = makeConfig('chimpy'); - const userApiServerUrl = await proxy.getServerUrl(); - const chimpyApi = home.makeUserApi( - ORG_NAME, 'chimpy', { serverUrl: userApiServerUrl, headers: chimpy.headers as Record } - ); + const chimpyApi = await makeUserApi(ORG_NAME, 'chimpy'); const ws1 = (await chimpyApi.getOrgWorkspaces('current'))[0].id; const docId1 = await chimpyApi.newDoc({name: 'testdoc1'}, ws1); const docId2 = await chimpyApi.newDoc({name: 'testdoc2'}, ws1); @@ -256,9 +273,9 @@ describe('DocApi', function () { GRIST_DATA_DIR: dataDir }; home = await TestServer.startServer('home', tmpDir, suitename, additionalEnvConfiguration); - docs = await TestServer.startServer('docs', tmpDir, suitename, additionalEnvConfiguration, home.serverUrl); - homeUrl = home.serverUrl; - serverUrl = docs.serverUrl; + homeUrl = await home.getServerUrl(); + docs = await TestServer.startServer('docs', tmpDir, suitename, additionalEnvConfiguration, homeUrl); + serverUrl = await docs.getServerUrl(); hasHomeApi = false; }); testDocApi(); @@ -348,7 +365,7 @@ function testDocApi() { const ws1 = (await userApi.getOrgWorkspaces('current'))[0].id; // Make sure kiwi isn't allowed here. await userApi.updateOrgPermissions(ORG_NAME, {users: {[kiwiEmail]: null}}); - const kiwiApi = home.makeUserApi(ORG_NAME, 'kiwi'); + const kiwiApi = await makeUserApi(ORG_NAME, 'kiwi'); await assert.isRejected(kiwiApi.getWorkspaceAccess(ws1), /Forbidden/); // Add kiwi as an editor for the org. await assert.isRejected(kiwiApi.getOrgAccess(ORG_NAME), /Forbidden/); @@ -368,7 +385,7 @@ function testDocApi() { const ws1 = (await userApi.getOrgWorkspaces('current'))[0].id; await userApi.updateOrgPermissions(ORG_NAME, {users: {[kiwiEmail]: null}}); // Make sure kiwi isn't allowed here. - const kiwiApi = home.makeUserApi(ORG_NAME, 'kiwi'); + const kiwiApi = await makeUserApi(ORG_NAME, 'kiwi'); await assert.isRejected(kiwiApi.getWorkspaceAccess(ws1), /Forbidden/); // Add kiwi as an editor of this workspace. await userApi.updateWorkspacePermissions(ws1, {users: {[kiwiEmail]: 'editors'}}); @@ -387,7 +404,7 @@ function testDocApi() { it("should allow only owners to remove a document", async () => { const ws1 = (await userApi.getOrgWorkspaces('current'))[0].id; const doc1 = await userApi.newDoc({name: 'testdeleteme1'}, ws1); - const kiwiApi = home.makeUserApi(ORG_NAME, 'kiwi'); + const kiwiApi = await makeUserApi(ORG_NAME, 'kiwi'); // Kiwi is editor of the document, so he can't delete it. await userApi.updateDocPermissions(doc1, {users: {'kiwi@getgrist.com': 'editors'}}); @@ -403,7 +420,7 @@ function testDocApi() { it("should allow only owners to rename a document", async () => { const ws1 = (await userApi.getOrgWorkspaces('current'))[0].id; const doc1 = await userApi.newDoc({name: 'testrenameme1'}, ws1); - const kiwiApi = home.makeUserApi(ORG_NAME, 'kiwi'); + const kiwiApi = await makeUserApi(ORG_NAME, 'kiwi'); // Kiwi is editor of the document, so he can't rename it. await userApi.updateDocPermissions(doc1, {users: {'kiwi@getgrist.com': 'editors'}}); @@ -2915,7 +2932,7 @@ function testDocApi() { }); it('POST /workspaces/{wid}/import handles empty filenames', async function () { - if (!process.env.TEST_REDIS_URL || docs.proxiedServer) { + if (!process.env.TEST_REDIS_URL) { this.skip(); } const worker1 = await userApi.getWorkerAPI('import'); @@ -2969,16 +2986,11 @@ function testDocApi() { }); it("document is protected during upload-and-import sequence", async function () { - if (!process.env.TEST_REDIS_URL || home.proxiedServer) { + if (!process.env.TEST_REDIS_URL) { this.skip(); } // Prepare an API for a different user. - const kiwiApi = new UserAPIImpl(`${homeUrl}/o/Fish`, { - headers: {Authorization: 'Bearer api_key_for_kiwi'}, - fetch: fetch as any, - newFormData: () => new FormData() as any, - }); - // upload something for Chimpy and something else for Kiwi. + const kiwiApi = await makeUserApi('Fish', 'kiwi'); // upload something for Chimpy and something else for Kiwi. const worker1 = await userApi.getWorkerAPI('import'); const fakeData1 = await testUtils.readFixtureDoc('Hello.grist'); const uploadId1 = await worker1.upload(fakeData1, 'upload.grist'); @@ -3077,12 +3089,11 @@ function testDocApi() { }); it('filters urlIds by org', async function () { - if (home.proxiedServer) { this.skip(); } // Make two documents with same urlId const ws1 = (await userApi.getOrgWorkspaces('current'))[0].id; const doc1 = await userApi.newDoc({name: 'testdoc1', urlId: 'urlid'}, ws1); const nasaApi = new UserAPIImpl(`${homeUrl}/o/nasa`, { - headers: {Authorization: 'Bearer api_key_for_chimpy'}, + headers: chimpy.headers as Record, fetch: fetch as any, newFormData: () => new FormData() as any, }); @@ -3110,11 +3121,10 @@ function testDocApi() { it('allows docId access to any document from merged org', async function () { // Make two documents - if (home.proxiedServer) { this.skip(); } const ws1 = (await userApi.getOrgWorkspaces('current'))[0].id; const doc1 = await userApi.newDoc({name: 'testdoc1'}, ws1); const nasaApi = new UserAPIImpl(`${homeUrl}/o/nasa`, { - headers: {Authorization: 'Bearer api_key_for_chimpy'}, + headers: chimpy.headers as Record, fetch: fetch as any, newFormData: () => new FormData() as any, }); @@ -3244,9 +3254,7 @@ function testDocApi() { // Pass kiwi's headers as it contains both Authorization and Origin headers // if run behind a proxy, so we can ensure that the Origin header check is not made. const userApiServerUrl = docs.proxiedServer ? serverUrl : undefined; - const chimpyApi = home.makeUserApi( - ORG_NAME, 'chimpy', { serverUrl: userApiServerUrl, headers: chimpy.headers as Record } - ); + const chimpyApi = await makeUserApi(ORG_NAME, 'chimpy', { baseUrl: userApiServerUrl }); const ws1 = (await chimpyApi.getOrgWorkspaces('current'))[0].id; const docId1 = await chimpyApi.newDoc({name: 'testdoc1'}, ws1); const docId2 = await chimpyApi.newDoc({name: 'testdoc2'}, ws1); @@ -3452,7 +3460,7 @@ function testDocApi() { if (docs.proxiedServer) { this.skip(); } - const docWorkerUrl = docs.serverUrl; + const docWorkerUrl = await docs.getServerUrl(); let resp = await axios.get(`${docWorkerUrl}/api/docs/${docIds.Timesheets}/tables/Table1/data`, chimpy); assert.equal(resp.status, 200); assert.containsAllKeys(resp.data, ['A', 'B', 'C']); @@ -3756,7 +3764,7 @@ function testDocApi() { it("limits daily API usage", async function () { // Make a new document in a test product with a low daily limit - const api = home.makeUserApi('testdailyapilimit'); + const api = await makeUserApi('testdailyapilimit', 'chimpy'); const workspaceId = await getWorkspaceId(api, 'TestDailyApiLimitWs'); const docId = await api.newDoc({name: 'TestDoc1'}, workspaceId); const max = testDailyApiLimitFeatures.baseMaxApiUnitsPerDocumentPerDay; @@ -3784,7 +3792,7 @@ function testDocApi() { it("limits daily API usage and sets the correct keys in redis", async function () { this.retries(3); // Make a new document in a free team site, currently the only real product which limits daily API usage. - const freeTeamApi = home.makeUserApi('freeteam'); + const freeTeamApi = await makeUserApi('freeteam', 'chimpy'); const workspaceId = await getWorkspaceId(freeTeamApi, 'FreeTeamWs'); const docId = await freeTeamApi.newDoc({name: 'TestDoc2'}, workspaceId); // Rather than making 5000 requests, set high counts directly for the current and next daily and hourly keys @@ -5356,7 +5364,7 @@ function setup(name: string, cb: () => Promise) { await cb(); // create TestDoc as an empty doc into Private workspace - userApi = api = home.makeUserApi(ORG_NAME); + userApi = api = await makeUserApi(ORG_NAME, 'chimpy'); const wid = await getWorkspaceId(api, 'Private'); docIds.TestDoc = await api.newDoc({name: 'TestDoc'}, wid); }); diff --git a/test/server/lib/ManyFetches.ts b/test/server/lib/ManyFetches.ts index 59db25514a..788f94f1d0 100644 --- a/test/server/lib/ManyFetches.ts +++ b/test/server/lib/ManyFetches.ts @@ -48,8 +48,8 @@ describe('ManyFetches', function() { // Without this limit, there is no pressure on node to garbage-collect, so it may use more // memory than we expect, making the test less reliable. NODE_OPTIONS: '--max-old-space-size=210', - }, home.serverUrl); - userApi = home.makeUserApi(org, userName); + }, await home.getServerUrl()); + userApi = await home.makeUserApi(org, userName); }); afterEach(async function() { @@ -222,13 +222,13 @@ describe('ManyFetches', function() { // function. async function prepareGristWSConnection(docId: string): Promise<() => GristWSConnection> { // Use cookies for access to stay as close as possible to regular operation. - const resp = await fetch(`${home.serverUrl}/test/session`); + const resp = await fetch(`${await home.getServerUrl()}/test/session`); const sid = cookie.parse(resp.headers.get('set-cookie'))[cookieName]; if (!sid) { throw new Error('no session available'); } await home.testingHooks.setLoginSessionProfile(sid, {name: userName, email}, org); // Load the document html. - const pageUrl = `${home.serverUrl}/o/docs/doc/${docId}`; + const pageUrl = `${await home.getServerUrl()}/o/docs/doc/${docId}`; const headers = {Cookie: `${cookieName}=${sid}`}; const doc = await fetch(pageUrl, {headers}); const pageBody = await doc.text(); diff --git a/test/server/lib/UnhandledErrors.ts b/test/server/lib/UnhandledErrors.ts index 8aad3fcc55..d6da424a03 100644 --- a/test/server/lib/UnhandledErrors.ts +++ b/test/server/lib/UnhandledErrors.ts @@ -40,7 +40,7 @@ describe('UnhandledErrors', function() { const server = await TestServer.startServer('home', testDir, errType, undefined, undefined, {output}); try { - assert.equal((await fetch(`${server.serverUrl}/status`)).status, 200); + assert.equal((await fetch(`${await server.getServerUrl()}/status`)).status, 200); serverLogLines.length = 0; // Trigger an unhandled error, and check that the server logged it and attempted cleanup. @@ -51,7 +51,7 @@ describe('UnhandledErrors', function() { }, 1000, 100); // We expect the server to be dead now. - await assert.isRejected(fetch(`${server.serverUrl}/status`), /failed.*ECONNREFUSED/); + await assert.isRejected(fetch(`${await server.getServerUrl()}/status`), /failed.*ECONNREFUSED/); } finally { await server.stop(); diff --git a/test/server/lib/Webhooks-Proxy.ts b/test/server/lib/Webhooks-Proxy.ts index f929c66711..19c094d604 100644 --- a/test/server/lib/Webhooks-Proxy.ts +++ b/test/server/lib/Webhooks-Proxy.ts @@ -71,7 +71,7 @@ describe('Webhooks-Proxy', function () { await cb(); // create TestDoc as an empty doc into Private workspace - userApi = api = home.makeUserApi(ORG_NAME); + userApi = api = await home.makeUserApi(ORG_NAME); const wid = await getWorkspaceId(api, 'Private'); docIds.TestDoc = await api.newDoc({name: 'TestDoc'}, wid); }); @@ -125,7 +125,7 @@ describe('Webhooks-Proxy', function () { describe("should work with a merged server", async () => { setupMockServers('merged', tmpDir, async () => { home = docs = await TestServer.startServer('home,docs', tmpDir, suitename, additionaEnvConfiguration); - serverUrl = home.serverUrl; + serverUrl = await home.getServerUrl(); }); subTestCall(); }); @@ -135,8 +135,9 @@ describe('Webhooks-Proxy', function () { describe("should work with a home server and a docworker", async () => { setupMockServers('separated', tmpDir, async () => { home = await TestServer.startServer('home', tmpDir, suitename, additionaEnvConfiguration); - docs = await TestServer.startServer('docs', tmpDir, suitename, additionaEnvConfiguration, home.serverUrl); - serverUrl = home.serverUrl; + const homeUrl = await home.getServerUrl(); + docs = await TestServer.startServer('docs', tmpDir, suitename, additionaEnvConfiguration, homeUrl); + serverUrl = homeUrl; }); subTestCall(); }); @@ -145,8 +146,9 @@ describe('Webhooks-Proxy', function () { describe("should work directly with a docworker", async () => { setupMockServers('docs', tmpDir, async () => { home = await TestServer.startServer('home', tmpDir, suitename, additionaEnvConfiguration); - docs = await TestServer.startServer('docs', tmpDir, suitename, additionaEnvConfiguration, home.serverUrl); - serverUrl = docs.serverUrl; + const homeUrl = await home.getServerUrl(); + docs = await TestServer.startServer('docs', tmpDir, suitename, additionaEnvConfiguration, homeUrl); + serverUrl = await docs.getServerUrl(); }); subTestCall(); }); diff --git a/test/server/lib/helpers/TestServer.ts b/test/server/lib/helpers/TestServer.ts index 959468598f..d66f0760e9 100644 --- a/test/server/lib/helpers/TestServer.ts +++ b/test/server/lib/helpers/TestServer.ts @@ -37,15 +37,9 @@ export class TestServer { public testingSocket: string; public testingHooks: TestingHooksClient; public stopped = false; - public get serverUrl() { - if (this._proxiedServer) { - throw new Error('Direct access to this test server is disallowed'); - } - return this._serverUrl; - } public get proxiedServer() { return this._proxiedServer; } - private _serverUrl: string; + private _serverUrl: Promise; private _server: ChildProcess; private _exitPromise: Promise; private _proxiedServer: boolean = false; @@ -65,8 +59,21 @@ export class TestServer { GRIST_MAX_QUEUE_SIZE: '10', ...process.env }; + this._serverUrl = new Promise((resolve) => { + return getAvailablePort().then((port) => { + resolve(`http://localhost:${port}`); + }); + }); } - public async start(_homeUrl?: string, customEnv?: NodeJS.ProcessEnv, options: {output?: Writable} = {}) { + + public getServerUrl() { + if (this._proxiedServer) { + throw new Error('Direct access to this test server is disallowed'); + } + return this._serverUrl; + } + + public async start(homeUrl?: string, customEnv?: NodeJS.ProcessEnv, options: {output?: Writable} = {}) { // put node logs into files with meaningful name that relate to the suite name and server type const fixedName = this._serverTypes.replace(/,/, '_'); const nodeLogPath = path.join(this.rootDir, `${this._suiteName}-${fixedName}-node.log`); @@ -79,15 +86,14 @@ export class TestServer { throw new Error(`Path of testingSocket too long: ${this.testingSocket.length} (${this.testingSocket})`); } - const port = await getAvailablePort(); - this._serverUrl = `http://localhost:${port}`; - const homeUrl = _homeUrl ?? (this._serverTypes.includes('home') ? this._serverUrl : undefined); + const serverUrl = await this.getServerUrl(); + const port = new URL(serverUrl).port; const env: NodeJS.ProcessEnv = { APP_HOME_URL: homeUrl, APP_HOME_INTERNAL_URL: homeUrl, GRIST_TESTING_SOCKET: this.testingSocket, - GRIST_PORT: String(port), + GRIST_PORT: port, ...this._defaultEnv, ...customEnv }; @@ -157,19 +163,9 @@ export class TestServer { // Returns the promise for the ChildProcess's signal or exit code. public getExitPromise(): Promise { return this._exitPromise; } - public makeUserApi( - org: string, - user: string = 'chimpy', - { - headers = {Authorization: `Bearer api_key_for_${user}`}, - serverUrl = this._serverUrl, - }: { - headers?: Record - serverUrl?: string, - } = { headers: undefined, serverUrl: undefined }, - ): UserAPIImpl { - return new UserAPIImpl(`${serverUrl}/o/${org}`, { - headers, + public async makeUserApi(org: string, user: string = 'chimpy'): Promise { + return new UserAPIImpl(`${await this.getServerUrl()}/o/${org}`, { + headers: {Authorization: `Bearer api_key_for_${user}`}, fetch: fetch as unknown as typeof globalThis.fetch, newFormData: () => new FormData() as any, }); @@ -258,8 +254,8 @@ export class TestServerReverseProxy { } public async start(homeServer: TestServer, docServer: TestServer) { - this._app.all(['/dw/dw1', '/dw/dw1/*'], (oreq, ores) => this._getRequestHandlerFor(docServer)); - this._app.all('/*', this._getRequestHandlerFor(homeServer)); + this._app.all(['/dw/dw1', '/dw/dw1/*'], await this._getRequestHandlerFor(docServer)); + this._app.all('/*', await this._getRequestHandlerFor(homeServer)); // Forbid now the use of serverUrl property, so we don't allow the tests to // call the workers directly @@ -287,8 +283,8 @@ export class TestServerReverseProxy { this._proxy.close(); } - private _getRequestHandlerFor(server: TestServer) { - const serverUrl = new URL(server.serverUrl); + private async _getRequestHandlerFor(server: TestServer) { + const serverUrl = new URL(await server.getServerUrl()); return (oreq: express.Request, ores: express.Response) => { log.debug(`[proxy] Requesting (method=${oreq.method}): ${new URL(oreq.url, serverUrl).href}`);