diff --git a/test/e2e/commands/actor/calculate-memory.test.ts b/test/e2e/commands/actor/calculate-memory.test.ts new file mode 100644 index 000000000..1e197d1bf --- /dev/null +++ b/test/e2e/commands/actor/calculate-memory.test.ts @@ -0,0 +1,31 @@ +import { runCli } from '../../__helpers__/run-cli.js'; +import { createTestActor, removeTestActor, type TestActor } from '../../__helpers__/test-actor.js'; + +describe('[e2e] actor calculate-memory', () => { + let actor: TestActor; + + beforeAll(async () => { + actor = await createTestActor(); + }); + + afterAll(async () => { + if (actor) await removeTestActor(actor); + }); + + it('calculates memory with --default-memory-mbytes flag', async () => { + const result = await runCli('apify', ['actor', 'calculate-memory', '--default-memory-mbytes', '256'], { + cwd: actor.dir, + }); + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('Calculated memory'); + expect(result.stdout).toContain('256'); + }); + + it('errors when no memory expression is found', async () => { + const result = await runCli('apify', ['actor', 'calculate-memory'], { + cwd: actor.dir, + }); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain('No memory-calculation expression found'); + }); +}); diff --git a/test/e2e/commands/actor/get-value.test.ts b/test/e2e/commands/actor/get-value.test.ts new file mode 100644 index 000000000..ebce30b1f --- /dev/null +++ b/test/e2e/commands/actor/get-value.test.ts @@ -0,0 +1,34 @@ +import { runCli } from '../../__helpers__/run-cli.js'; +import { createTestActor, removeTestActor, type TestActor } from '../../__helpers__/test-actor.js'; + +describe('[e2e] actor get-value', () => { + let actor: TestActor; + + beforeAll(async () => { + actor = await createTestActor(); + + // Run the actor once so storage is initialized + const runResult = await runCli('apify', ['run'], { cwd: actor.dir }); + if (runResult.exitCode !== 0) { + throw new Error(`Test actor failed to run:\n${runResult.stderr}`); + } + }); + + afterAll(async () => { + if (actor) await removeTestActor(actor); + }); + + it('gets the value back from the default key-value store', async () => { + // First make sure the value is set + await runCli('apify', ['actor', 'set-value', 'MY_KEY', '{"hello":"world"}'], { + cwd: actor.dir, + }); + + const result = await runCli('apify', ['actor', 'get-value', 'MY_KEY'], { + cwd: actor.dir, + }); + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stdout).toContain('hello'); + expect(result.stdout).toContain('world'); + }); +}); diff --git a/test/e2e/commands/actor/push-data.test.ts b/test/e2e/commands/actor/push-data.test.ts new file mode 100644 index 000000000..e02714f1d --- /dev/null +++ b/test/e2e/commands/actor/push-data.test.ts @@ -0,0 +1,38 @@ +import { mkdir, readdir } from 'node:fs/promises'; +import path from 'node:path'; + +import { runCli } from '../../__helpers__/run-cli.js'; +import { createTestActor, removeTestActor, type TestActor } from '../../__helpers__/test-actor.js'; + +describe('[e2e] actor push-data', () => { + let actor: TestActor; + + beforeAll(async () => { + actor = await createTestActor(); + + // Run the actor once so storage is initialized + const runResult = await runCli('apify', ['run'], { cwd: actor.dir }); + if (runResult.exitCode !== 0) { + throw new Error(`Test actor failed to run:\n${runResult.stderr}`); + } + + // Ensure the default dataset directory exists for push-data tests + await mkdir(path.join(actor.dir, 'storage', 'datasets', 'default'), { recursive: true }); + }); + + afterAll(async () => { + if (actor) await removeTestActor(actor); + }); + + it('pushes data to the default dataset', async () => { + const result = await runCli('apify', ['actor', 'push-data', '{"item":"test"}'], { + cwd: actor.dir, + }); + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + + // Verify a file exists in the dataset directory + const datasetDir = path.join(actor.dir, 'storage', 'datasets', 'default'); + const files = await readdir(datasetDir); + expect(files.length).toBeGreaterThan(0); + }); +}); diff --git a/test/e2e/actor-run-input.test.ts b/test/e2e/commands/actor/run-input.test.ts similarity index 97% rename from test/e2e/actor-run-input.test.ts rename to test/e2e/commands/actor/run-input.test.ts index e41418fa9..9a270d4f0 100644 --- a/test/e2e/actor-run-input.test.ts +++ b/test/e2e/commands/actor/run-input.test.ts @@ -1,14 +1,14 @@ import { writeFile } from 'node:fs/promises'; import path from 'node:path'; -import { runCli } from './__helpers__/run-cli.js'; +import { runCli } from '../../__helpers__/run-cli.js'; import { cleanRunResults, createTestActor, getRunResults, removeTestActor, type TestActor, -} from './__helpers__/test-actor.js'; +} from '../../__helpers__/test-actor.js'; describe('[e2e] actor run input', () => { let actor: TestActor; diff --git a/test/e2e/commands/actor/set-value.test.ts b/test/e2e/commands/actor/set-value.test.ts new file mode 100644 index 000000000..45d1edad3 --- /dev/null +++ b/test/e2e/commands/actor/set-value.test.ts @@ -0,0 +1,27 @@ +import { runCli } from '../../__helpers__/run-cli.js'; +import { createTestActor, removeTestActor, type TestActor } from '../../__helpers__/test-actor.js'; + +describe('[e2e] actor set-value', () => { + let actor: TestActor; + + beforeAll(async () => { + actor = await createTestActor(); + + // Run the actor once so storage is initialized + const runResult = await runCli('apify', ['run'], { cwd: actor.dir }); + if (runResult.exitCode !== 0) { + throw new Error(`Test actor failed to run:\n${runResult.stderr}`); + } + }); + + afterAll(async () => { + if (actor) await removeTestActor(actor); + }); + + it('sets a value in the default key-value store', async () => { + const result = await runCli('apify', ['actor', 'set-value', 'MY_KEY', '{"hello":"world"}'], { + cwd: actor.dir, + }); + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + }); +}); diff --git a/test/e2e/builds.test.ts b/test/e2e/commands/builds/create-info-ls.test.ts similarity index 96% rename from test/e2e/builds.test.ts rename to test/e2e/commands/builds/create-info-ls.test.ts index 8d901752c..47cd739b2 100644 --- a/test/e2e/builds.test.ts +++ b/test/e2e/commands/builds/create-info-ls.test.ts @@ -2,9 +2,9 @@ import { randomBytes } from 'node:crypto'; import { ApifyClient } from 'apify-client'; -import { getApifyClientOptions } from '../../src/lib/utils.js'; -import { runCli } from './__helpers__/run-cli.js'; -import { createTestActor, removeTestActor, type TestActor } from './__helpers__/test-actor.js'; +import { getApifyClientOptions } from '../../../../src/lib/utils.js'; +import { runCli } from '../../__helpers__/run-cli.js'; +import { createTestActor, removeTestActor, type TestActor } from '../../__helpers__/test-actor.js'; describe('[e2e][api] builds namespace', () => { let actor: TestActor; diff --git a/test/e2e/commands/create.test.ts b/test/e2e/commands/create.test.ts new file mode 100644 index 000000000..765c62c9a --- /dev/null +++ b/test/e2e/commands/create.test.ts @@ -0,0 +1,74 @@ +import { randomBytes } from 'node:crypto'; +import { access, mkdir, rm } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { runCli } from '../__helpers__/run-cli.js'; + +const TestTmpRoot = fileURLToPath(new URL('../../../tmp/', import.meta.url)); + +describe('[e2e] apify create', () => { + let tmpDir: string; + const createdDirs: string[] = []; + + beforeAll(async () => { + const dirName = `e2e-create-${randomBytes(6).toString('hex')}`; + tmpDir = path.join(TestTmpRoot, dirName); + await mkdir(tmpDir, { recursive: true }); + }); + + afterAll(async () => { + for (const dir of createdDirs) { + await rm(dir, { recursive: true, force: true }); + } + await rm(tmpDir, { recursive: true, force: true }); + }); + + it('creates an actor project with --template project_empty --skip-dependency-install', async () => { + const actorName = `test-actor-${randomBytes(4).toString('hex')}`; + const actorDir = path.join(tmpDir, actorName); + createdDirs.push(actorDir); + + const result = await runCli( + 'apify', + ['create', actorName, '--template', 'project_empty', '--skip-dependency-install'], + { + cwd: tmpDir, + }, + ); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stderr).toContain('created successfully'); + + // Verify directory structure + await expect(access(actorDir)).resolves.toBeUndefined(); + await expect(access(path.join(actorDir, '.actor', 'actor.json'))).resolves.toBeUndefined(); + await expect(access(path.join(actorDir, 'src'))).resolves.toBeUndefined(); + }); + + it('fails when creating in a directory that already exists with content', async () => { + const actorName = `test-actor-${randomBytes(4).toString('hex')}`; + const actorDir = path.join(tmpDir, actorName); + createdDirs.push(actorDir); + + // First create succeeds + const first = await runCli( + 'apify', + ['create', actorName, '--template', 'project_empty', '--skip-dependency-install'], + { + cwd: tmpDir, + }, + ); + expect(first.exitCode, `stderr: ${first.stderr}`).toBe(0); + + // Second create in same dir should fail + const second = await runCli( + 'apify', + ['create', actorName, '--template', 'project_empty', '--skip-dependency-install'], + { + cwd: tmpDir, + }, + ); + expect(second.exitCode).not.toBe(0); + }); +}); diff --git a/test/e2e/help.test.ts b/test/e2e/commands/help.test.ts similarity index 97% rename from test/e2e/help.test.ts rename to test/e2e/commands/help.test.ts index 6a517af25..4b4893665 100644 --- a/test/e2e/help.test.ts +++ b/test/e2e/commands/help.test.ts @@ -1,4 +1,4 @@ -import { runCli } from './__helpers__/run-cli.js'; +import { runCli } from '../__helpers__/run-cli.js'; describe.concurrent('[e2e] help command', () => { it('apify help prints the full help message', async () => { diff --git a/test/e2e/commands/init.test.ts b/test/e2e/commands/init.test.ts new file mode 100644 index 000000000..3bc28ba49 --- /dev/null +++ b/test/e2e/commands/init.test.ts @@ -0,0 +1,32 @@ +import { randomBytes } from 'node:crypto'; +import { access, mkdir, rm } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { runCli } from '../__helpers__/run-cli.js'; + +const TestTmpRoot = fileURLToPath(new URL('../../../tmp/', import.meta.url)); + +describe('[e2e] apify init', () => { + let tmpDir: string; + + beforeAll(async () => { + const dirName = `e2e-init-${randomBytes(6).toString('hex')}`; + tmpDir = path.join(TestTmpRoot, dirName); + await mkdir(tmpDir, { recursive: true }); + }); + + afterAll(async () => { + await rm(tmpDir, { recursive: true, force: true }); + }); + + it('initializes an actor project in an empty directory with --yes', async () => { + const result = await runCli('apify', ['init', 'my-test-actor', '--yes'], { cwd: tmpDir }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stderr).toContain('The Actor has been initialized in the current directory.'); + + // Verify .actor/actor.json exists + await expect(access(path.join(tmpDir, '.actor', 'actor.json'))).resolves.toBeUndefined(); + }); +}); diff --git a/test/e2e/invalid-config.test.ts b/test/e2e/commands/invalid-config.test.ts similarity index 87% rename from test/e2e/invalid-config.test.ts rename to test/e2e/commands/invalid-config.test.ts index ff01fb619..d7ca63250 100644 --- a/test/e2e/invalid-config.test.ts +++ b/test/e2e/commands/invalid-config.test.ts @@ -1,5 +1,5 @@ -import { runCli } from './__helpers__/run-cli.js'; -import { corruptActorJson, createTestActor, removeTestActor, type TestActor } from './__helpers__/test-actor.js'; +import { runCli } from '../__helpers__/run-cli.js'; +import { corruptActorJson, createTestActor, removeTestActor, type TestActor } from '../__helpers__/test-actor.js'; describe('[e2e] invalid actor.json', () => { let actor: TestActor; diff --git a/test/e2e/commands/logout.test.ts b/test/e2e/commands/logout.test.ts new file mode 100644 index 000000000..96631f40e --- /dev/null +++ b/test/e2e/commands/logout.test.ts @@ -0,0 +1,14 @@ +import { randomBytes } from 'node:crypto'; + +import { runCli } from '../__helpers__/run-cli.js'; + +describe('[e2e] apify logout', () => { + const authEnv = { __APIFY_INTERNAL_TEST_AUTH_PATH__: `e2e-logout-${randomBytes(6).toString('hex')}` }; + + it('logs out successfully even when not logged in', async () => { + const result = await runCli('apify', ['logout'], { env: authEnv }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stderr).toContain('You are logged out from your Apify account'); + }); +}); diff --git a/test/e2e/commands/secrets/lifecycle.test.ts b/test/e2e/commands/secrets/lifecycle.test.ts new file mode 100644 index 000000000..aa178798b --- /dev/null +++ b/test/e2e/commands/secrets/lifecycle.test.ts @@ -0,0 +1,45 @@ +import { randomBytes } from 'node:crypto'; + +import { runCli } from '../../__helpers__/run-cli.js'; + +describe('[e2e] apify secrets add/ls/rm', () => { + const authEnv = { __APIFY_INTERNAL_TEST_AUTH_PATH__: `e2e-secrets-${randomBytes(6).toString('hex')}` }; + + it('full lifecycle: add, ls, rm', async () => { + const secretName = `MY_SECRET_${randomBytes(4).toString('hex')}`; + + // Add a secret + const addResult = await runCli('apify', ['secrets', 'add', secretName, 'my-value'], { env: authEnv }); + expect(addResult.exitCode, `stderr: ${addResult.stderr}`).toBe(0); + + // List secrets — should contain the secret name + const lsResult = await runCli('apify', ['secrets', 'ls'], { env: authEnv }); + expect(lsResult.exitCode, `stderr: ${lsResult.stderr}`).toBe(0); + expect(lsResult.stdout).toContain(secretName); + + // Remove the secret + const rmResult = await runCli('apify', ['secrets', 'rm', secretName], { env: authEnv }); + expect(rmResult.exitCode, `stderr: ${rmResult.stderr}`).toBe(0); + + // List again — should NOT contain the secret name + const lsAfterRm = await runCli('apify', ['secrets', 'ls'], { env: authEnv }); + expect(lsAfterRm.exitCode, `stderr: ${lsAfterRm.stderr}`).toBe(0); + expect(lsAfterRm.stdout).not.toContain(secretName); + }); + + it('fails when adding a secret that already exists', async () => { + const secretName = `DUP_SECRET_${randomBytes(4).toString('hex')}`; + + // Add first time + const addResult = await runCli('apify', ['secrets', 'add', secretName, 'value1'], { env: authEnv }); + expect(addResult.exitCode, `stderr: ${addResult.stderr}`).toBe(0); + + // Add same secret again — should fail + const dupResult = await runCli('apify', ['secrets', 'add', secretName, 'value2'], { env: authEnv }); + expect(dupResult.exitCode).not.toBe(0); + expect(dupResult.stderr).toContain('already exists'); + + // Cleanup + await runCli('apify', ['secrets', 'rm', secretName], { env: authEnv }); + }); +}); diff --git a/test/e2e/commands/telemetry/enable-disable.test.ts b/test/e2e/commands/telemetry/enable-disable.test.ts new file mode 100644 index 000000000..8896027ed --- /dev/null +++ b/test/e2e/commands/telemetry/enable-disable.test.ts @@ -0,0 +1,21 @@ +import { randomBytes } from 'node:crypto'; + +import { runCli } from '../../__helpers__/run-cli.js'; + +describe('[e2e] apify telemetry enable/disable', () => { + const authEnv = { __APIFY_INTERNAL_TEST_AUTH_PATH__: `e2e-telemetry-${randomBytes(6).toString('hex')}` }; + + it('disables telemetry', async () => { + const result = await runCli('apify', ['telemetry', 'disable'], { env: authEnv }); + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + // Output goes to stderr and contains either "Telemetry disabled" or "already disabled" + expect(result.stderr).toMatch(/Telemetry disabled|already disabled/i); + }); + + it('enables telemetry', async () => { + const result = await runCli('apify', ['telemetry', 'enable'], { env: authEnv }); + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + // Output goes to stderr and contains either "Telemetry enabled" or "already enabled" + expect(result.stderr).toMatch(/Telemetry enabled|already enabled/i); + }); +}); diff --git a/test/e2e/commands/validate-schema.test.ts b/test/e2e/commands/validate-schema.test.ts new file mode 100644 index 000000000..6bb96181d --- /dev/null +++ b/test/e2e/commands/validate-schema.test.ts @@ -0,0 +1,43 @@ +import { writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { runCli } from '../__helpers__/run-cli.js'; +import { createTestActor, removeTestActor, type TestActor } from '../__helpers__/test-actor.js'; + +describe('[e2e] apify validate-schema', () => { + let actor: TestActor; + + beforeAll(async () => { + actor = await createTestActor(); + + // The project_empty template doesn't include an input schema, + // so we create a valid one for testing. + const inputSchema = { + title: 'Test input schema', + description: 'A test input schema', + type: 'object', + schemaVersion: 1, + properties: { + testField: { + title: 'Test Field', + type: 'string', + description: 'A test field', + editor: 'textfield', + }, + }, + }; + + await writeFile(path.join(actor.dir, '.actor', 'INPUT_SCHEMA.json'), JSON.stringify(inputSchema, null, 2)); + }); + + afterAll(async () => { + if (actor) await removeTestActor(actor); + }); + + it('validates a valid input schema', async () => { + const result = await runCli('apify', ['validate-schema'], { cwd: actor.dir }); + + expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); + expect(result.stderr).toContain('Input schema is valid'); + }); +});