Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@brightdata/cli",
"version": "0.3.6",
"version": "0.3.7",
"description": "Command-line interface for Bright Data. Scrape, search, extract structured data, and automate browsers directly from your terminal.",
"main": "dist/index.js",
"bin": {
Expand Down Expand Up @@ -58,6 +58,8 @@
"@clack/prompts": "^1.1.0",
"@inquirer/prompts": "^8.2.1",
"commander": "^14.0.2",
"csv-parse": "^7.0.2",
"csv-stringify": "^6.8.3",
"open": "^11.0.0",
"picocolors": "^1.1.1",
"playwright-core": "^1.58.2",
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 63 additions & 0 deletions src/__tests__/commands/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {describe, it, expect, vi, beforeEach} from 'vitest';

const mocks = vi.hoisted(()=>({
set_config: vi.fn(),
fail: vi.fn(),
success: vi.fn(),
}));

vi.mock('../../utils/config', ()=>({
load: vi.fn(),
get: vi.fn(),
set: mocks.set_config,
}));

vi.mock('../../utils/output', ()=>({
print: vi.fn(),
fail: mocks.fail,
success: mocks.success,
}));

import {handle_set_config} from '../../commands/config';

describe('commands/config', ()=>{
beforeEach(()=>{
vi.clearAllMocks();
});

it('stores sanitize_csv false as boolean false', ()=>{
handle_set_config('sanitize_csv', 'false');

expect(mocks.set_config).toHaveBeenCalledWith(
'sanitize_csv',
false
);
expect(mocks.fail).not.toHaveBeenCalled();
expect(mocks.success).toHaveBeenCalledWith(
'Config updated: sanitize_csv=false'
);
});

it('stores sanitize_csv true as boolean true', ()=>{
handle_set_config('sanitize_csv', 'true');

expect(mocks.set_config).toHaveBeenCalledWith(
'sanitize_csv',
true
);
expect(mocks.fail).not.toHaveBeenCalled();
expect(mocks.success).toHaveBeenCalledWith(
'Config updated: sanitize_csv=true'
);
});

it('rejects invalid sanitize_csv value without modifying config', ()=>{
handle_set_config('sanitize_csv', 'maybe');

expect(mocks.fail).toHaveBeenCalledWith(
'sanitize_csv must be true or false'
);
expect(mocks.set_config).not.toHaveBeenCalled();
expect(mocks.success).not.toHaveBeenCalled();
});
});
88 changes: 85 additions & 3 deletions src/__tests__/commands/dataset.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,52 @@
import {describe, it, expect, vi, afterEach} from 'vitest';
import {describe, it, expect, vi, afterEach, beforeEach} from 'vitest';
import fs from 'fs';
import path from 'path';
import os from 'os';

const mocks = vi.hoisted(()=>({
post: vi.fn(),
poll_until: vi.fn(),
ensure_authenticated: vi.fn(),
spinner_stop: vi.fn(),
get_config: vi.fn(),
}));
vi.mock('../../utils/auth', ()=>({
ensure_authenticated: mocks.ensure_authenticated,
}));
vi.mock('../../utils/client', ()=>({
post: mocks.post,
get: vi.fn(),
}));
vi.mock('../../utils/polling', async import_original=>{
const actual = await import_original<typeof import('../../utils/polling')>();
return {
...actual,
poll_until: mocks.poll_until,
};
});
vi.mock('../../utils/spinner', ()=>({
start: ()=>({
stop: mocks.spinner_stop,
}),
}));
vi.mock('../../utils/config', ()=>({
get: mocks.get_config,
}));

import {handle_pipelines} from '../../commands/dataset';

describe('commands/pipelines list', ()=>{
beforeEach(()=>{
mocks.ensure_authenticated.mockReturnValue('test-api-key');
mocks.get_config.mockReturnValue(true);
mocks.post.mockResolvedValue({
snapshot_id: 'snapshot-1',
});
});
afterEach(()=>{
vi.restoreAllMocks();
vi.clearAllMocks();
});

it('prints available pipeline dataset types', async()=>{
let output = '';
const write = vi.spyOn(process.stdout, 'write').mockImplementation(
Expand All @@ -20,4 +61,45 @@ describe('commands/pipelines list', ()=>{
expect(output.includes('linkedin_person_profile')).toBe(true);
expect(output.includes('youtube_comments')).toBe(true);
});
});
it('sanitizes pipeline CSV written to stdout', async()=>{
mocks.poll_until.mockResolvedValue({
result: 'name,value\nfoo,"=SUM(1,2)"\n',
attempts: 1,
});
let output = '';
vi.spyOn(process.stdout, 'write').mockImplementation(text=>{
output += String(text);
return true;
});
vi.spyOn(console, 'error').mockImplementation(()=>{});
await handle_pipelines(
'amazon_product',
['https://example.com/product'],
{format: 'csv'}
);
expect(output).toContain("'=SUM(1,2)");
});
it('sanitizes pipeline CSV written to a file', async()=>{
mocks.poll_until.mockResolvedValue({
result: 'name,value\nfoo,"=SUM(1,2)"\n',
attempts: 1,
});
vi.spyOn(console, 'error').mockImplementation(()=>{});
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'brightdata-'));
const output_path = path.join(dir, 'result.csv');
try {
await handle_pipelines(
'amazon_product',
['https://example.com/product'],
{
format: 'csv',
output: output_path,
}
);
const output = fs.readFileSync(output_path, 'utf8');
expect(output).toContain("'=SUM(1,2)");
} finally {
fs.rmSync(dir, {recursive: true, force: true});
}
});
});
44 changes: 44 additions & 0 deletions src/__tests__/utils/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
resolve_api_key,
DEFAULTS,
} from '../../utils/config';
import {get_config_dir} from '../../utils/credentials';

const mk_tmp_home = ()=>{
const stamp = `${Date.now()}-${Math.random()}`;
Expand Down Expand Up @@ -49,6 +50,49 @@ describe('utils/config', ()=>{
expect(get('default_format')).toBe('json');
});

it('persists sanitize_csv false as JSON boolean', ()=>{
set('sanitize_csv', false);
const config_path = path.join(get_config_dir(), 'config.json');
const persisted = JSON.parse(fs.readFileSync(config_path, 'utf8'));
expect(persisted.sanitize_csv).toBe(false);
expect(typeof persisted.sanitize_csv).toBe('boolean');
});

it('persists sanitize_csv true as JSON boolean', ()=>{
set('sanitize_csv', true);
const config_path = path.join(get_config_dir(), 'config.json');
const persisted = JSON.parse(fs.readFileSync(config_path, 'utf8'));
expect(persisted.sanitize_csv).toBe(true);
expect(typeof persisted.sanitize_csv).toBe('boolean');
});

it('normalizes persisted sanitize_csv string false', ()=>{
set('sanitize_csv', true);
const config_path = path.join(get_config_dir(), 'config.json');
fs.writeFileSync(config_path, JSON.stringify({
sanitize_csv: 'false',
}));
expect(load().sanitize_csv).toBe(false);
});

it('normalizes persisted sanitize_csv string true', ()=>{
set('sanitize_csv', false);
const config_path = path.join(get_config_dir(), 'config.json');
fs.writeFileSync(config_path, JSON.stringify({
sanitize_csv: 'true',
}));
expect(load().sanitize_csv).toBe(true);
});

it('rejects invalid persisted sanitize_csv value', ()=>{
set('sanitize_csv', true);
const config_path = path.join(get_config_dir(), 'config.json');
fs.writeFileSync(config_path, JSON.stringify({
sanitize_csv: 'maybe',
}));
expect(()=>load()).toThrow('sanitize_csv must be true or false');
});

it('resolves value by cli then env then config', ()=>{
set('default_zone_unlocker', 'from_config');
process.env['TEST_ZONE_ENV'] = 'from_env';
Expand Down
Loading