diff --git a/test/integration/_fixtures.ts b/test/integration/_fixtures.ts index 064759e39..bf2b240f8 100644 --- a/test/integration/_fixtures.ts +++ b/test/integration/_fixtures.ts @@ -6,9 +6,9 @@ const TOKEN_ENV_VAR = 'APIFY_TEST_USER_API_TOKEN'; /** Overrides the API the suite runs against, so it can be pointed at a staging deployment. */ const API_URL_ENV_VAR = 'APIFY_INTEGRATION_TESTS_API_URL'; -/** Client authenticated as the primary test user. */ -export function makeClient(): ApifyClient { - const token = process.env[TOKEN_ENV_VAR]; +/** Client authenticated as the primary test user, or with the given `token` instead. */ +export function makeClient(token?: string): ApifyClient { + token ??= process.env[TOKEN_ENV_VAR]; if (!token) { throw new Error(`${TOKEN_ENV_VAR} environment variable is missing, cannot run tests!`); } diff --git a/test/integration/run.test.ts b/test/integration/run.test.ts index 0a1f54ad1..62203c0bf 100644 --- a/test/integration/run.test.ts +++ b/test/integration/run.test.ts @@ -240,26 +240,59 @@ test('resurrect() restarts a finished run, which then succeeds again', async () }); test('metamorph() transforms a started run into another Actor, keeping the run id', async () => { - const run = await client.actor(HELLO_WORLD_ACTOR).start(); - const runClient = client.run(run.id); + // Metamorphing the run from outside is not allowed, so we cheat a bit: the Actor stores its own run token in its key-value store, + // and we construct a new client with this token, and test that. + const actor = await client.actors().create({ + name: getRandomResourceName('actor'), + versions: [ + { + versionNumber: '0.0', + sourceType: ActorSourceType.SourceFiles, + buildTag: 'latest', + sourceFiles: [ + { + name: 'Dockerfile', + format: 'TEXT', + content: 'FROM apify/actor-node:22\nCOPY . ./\nCMD ["node", "main.js"]\n', + }, + { + name: 'main.js', + format: 'TEXT', + content: ` + const { Actor } = require('apify'); + Actor.main(async () => { + await Actor.setValue('TOKEN', process.env.APIFY_TOKEN, { contentType: 'text/plain' }); + await new Promise((resolve) => setTimeout(resolve, 60000)); + }); +`, + }, + ], + }, + ], + }); + const actorClient = client.actor(actor.id); + let runClient: RunClient | undefined; try { - await waitUntilStarted(runClient); - - try { - const metamorphedRun = await runClient.metamorph(HELLO_WORLD_ACTOR, { - message: 'Hello from metamorph!', - }); - expect(metamorphedRun.id).toBe(run.id); + await client.build((await actorClient.build('0.0')).id).waitForFinish(); + const run = await actorClient.start(); + runClient = client.run(run.id); + + // Only the run's own API token may metamorph it, so take it from the Actor's key-value store. + const record = await pollUntilCondition( + async () => client.keyValueStore(run.defaultKeyValueStoreId).getRecord('TOKEN'), + (rec) => rec !== undefined, + { timeoutSecs: 120, backoffFactor: 2 }, + ); + const runTokenClient = makeClient(record!.value as string); - await runClient.waitForFinish(); - } catch (err) { - // hello-world can finish before the metamorph lands; the API call was still exercised. - rethrowUnlessAlreadyFinished(err); - } + const metamorphedRun = await runTokenClient.run(run.id).metamorph(HELLO_WORLD_ACTOR); + expect(metamorphedRun.id).toBe(run.id); } finally { - await runClient.waitForFinish(); - await runClient.delete(); + await runClient?.abort(); + await runClient?.waitForFinish(); + await runClient?.delete(); + await actorClient.delete(); } });