Skip to content

Commit

Permalink
feat: support imports using the user editor (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinechalifour authored Sep 13, 2019
1 parent 4966e0f commit c7abbf4
Show file tree
Hide file tree
Showing 17 changed files with 514 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_modules
.rts2_cache_umd
dist
.mementorc
.mementorc.json
.memento-cache
coverage
.vscode/
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,25 @@ A common use case is ignoring Google Analytics cookies : `_ga`, `_gid`, `_gat`,

Launching Memento will start the interactive Memento CLI, where you can type commands to modify the cached requests and responses. The documentation can be found by typing `help` in the command prompt.


### Command: import

You may import cURL commands into Memento. This feature is intended to be used as such:

1. develop your app like the API is ready
2. open the "network" tab in the Chrome devtools
3. right click the failed request and select "copy as cURL"

![How to copy curl](./doc/how-to-copy-curl.png)

4. use the "import" command in memento and paste the cURL command.

![How to import curl](./doc/how-to-import-curl.png)

5. you can now edit the response in your editor to stub the API call 🎉

**Note: Memento will open the editor defined in your EDITOR environment variable. Non-terminal editors are not supported.**

## Cache location

By default, memento will create a `.memento-cache` directory in the current directory where each response will be mapped to a directory containing:
Expand Down
Binary file added doc/how-to-copy-curl.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/how-to-import-curl.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,13 @@
"chalk": "2.4.2",
"content-type": "^1.0.4",
"cosmiconfig": "5.2.1",
"env-editor": "^0.4.0",
"fs-extra": "8.1.0",
"koa": "2.8.1",
"koa-bodyparser": "4.2.1",
"minimatch": "^3.0.4",
"object-hash": "1.3.1",
"parse-curl": "0.2.6",
"text-table": "0.2.0",
"vorpal": "1.12.0"
}
Expand Down
104 changes: 104 additions & 0 deletions src/application/cli/import/import.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { readFile } from 'fs-extra';
import { defaultEditor } from 'env-editor';
import { spawnSync } from 'child_process';
import chalk from 'chalk';

import { ImportCurl } from '../../../domain/usecase';
import { getTestConfiguration } from '../../../test-utils/config';
import { MementoConfiguration } from '../../../configuration';
import { Logger } from '../types';
import { CliImport } from '.';
import { Request } from '../../../domain/entity';

jest.mock('child_process');
jest.mock('fs-extra');
jest.mock('env-editor');

const CACHE_DIRECTORY = 'some directory';
const TARGET_URL = 'some url';

function getTestDependencies(): {
logger: Logger;
importCurlUseCase: ImportCurl;
config: MementoConfiguration;
} {
return {
logger: jest.fn(),
// @ts-ignore
importCurlUseCase: {
execute: jest.fn().mockResolvedValue(null),
},
config: getTestConfiguration({
cacheDirectory: CACHE_DIRECTORY,
targetUrl: TARGET_URL,
}),
};
}

describe('import', () => {
beforeEach(() => {
// @ts-ignore
(spawnSync as jest.Mock).mockReset();
(readFile as jest.Mock).mockReset();
(defaultEditor as jest.Mock).mockReturnValue({ binary: 'neovim' });
(readFile as jest.Mock).mockResolvedValue('# Comment\ncurl command here');
});

it('should import the curl command', async () => {
// Given
const dependencies = getTestDependencies();
const cliImport = new CliImport(dependencies);

(dependencies.importCurlUseCase.execute as jest.Mock).mockResolvedValue(
new Request('GET', '/', {}, '')
);

// When
await cliImport.import();

//Then
expect(dependencies.importCurlUseCase.execute).toHaveBeenCalledTimes(1);
expect(dependencies.importCurlUseCase.execute).toHaveBeenCalledWith(
'curl command here'
);
});

it('should use the user editor', async () => {
// Given
const dependencies = getTestDependencies();
const cliImport = new CliImport(dependencies);

(dependencies.importCurlUseCase.execute as jest.Mock).mockResolvedValue(
new Request('GET', '/', {}, '')
);

// When
await cliImport.import();

//Then
expect(spawnSync).toHaveBeenCalledTimes(1);
expect(spawnSync).toHaveBeenCalledWith('neovim', [expect.any(String)], {
stdio: 'inherit',
});
});

it('should log an error message when an error is thrown', async () => {
// Given
const dependencies = getTestDependencies();
const cliImport = new CliImport(dependencies);

(dependencies.importCurlUseCase.execute as jest.Mock).mockRejectedValue(
new Error('Something')
);

// When
await cliImport.import();

//Then
expect(dependencies.logger).toHaveBeenCalledWith(
chalk.red(
'Invalid curl command provided. Please refer to the documentation for more instructions.'
)
);
});
});
67 changes: 67 additions & 0 deletions src/application/cli/import/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { spawnSync } from 'child_process';
import { tmpdir } from 'os';
import { writeFile, readFile } from 'fs-extra';
import chalk from 'chalk';
import { defaultEditor } from 'env-editor';

import { ImportCurl } from '../../../domain/usecase';
import { getRequestDirectory } from '../../../utils/path';
import { MementoConfiguration } from '../../../configuration';
import { Logger } from '../types';

const EDITOR_OUPUT_FILE = `${tmpdir}/memento-editor`;

interface Dependencies {
config: MementoConfiguration;
importCurlUseCase: ImportCurl;
logger: Logger;
}

export class CliImport {
private config: MementoConfiguration;
private importCurl: ImportCurl;
private logger: Logger;

public constructor({ config, importCurlUseCase, logger }: Dependencies) {
this.config = config;
this.importCurl = importCurlUseCase;
this.logger = logger;
}

public async import() {
const editor = defaultEditor().binary;

// Reset the file content
await writeFile(
EDITOR_OUPUT_FILE,
'# Paste your curl commmand on the next line\n'
);

// Get the command using the user editor
spawnSync(editor, [EDITOR_OUPUT_FILE], {
stdio: 'inherit',
});

const editorContents = await readFile(EDITOR_OUPUT_FILE, 'utf-8');
const command = editorContents.split('\n')[1];

try {
const request = await this.importCurl.execute(command);

const requestDirectory = getRequestDirectory(
this.config.cacheDirectory,
this.config.targetUrl,
request
);

this.logger(chalk`Request {yellow ${request.id}} has been created.`);
this.logger(
chalk`You may edit the request information at {yellow ${requestDirectory}}`
);
} catch (err) {
this.logger(
chalk.red`Invalid curl command provided. Please refer to the documentation for more instructions.`
);
}
}
}
1 change: 1 addition & 0 deletions src/application/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export * from './info';
export * from './ls';
export * from './refresh';
export * from './responseTime';
export * from './import';
export * from './injector';
5 changes: 5 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
CliRefresh,
CliInfo,
CliResponseTime,
CliImport,
CliInjector,
} from './application/cli';

Expand Down Expand Up @@ -88,6 +89,10 @@ export function createCli({ container, reload }: CreateCliOptions) {
)
.action(injector.action(CliResponseTime, 'set'));

vorpal
.command('import', 'Imports curl commands')
.action(injector.action(CliImport, 'import'));

vorpal.log(chalk`{green
__ __ _______ __ __ _______ __ _ _______ _______
| |_| || || |_| || || | | || || |
Expand Down
53 changes: 53 additions & 0 deletions src/domain/usecase/ImportCurl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { getTestRequestRepository } from '../../test-utils/infrastructure';
import { getTestConfiguration } from '../../test-utils/config';
import { RequestRepository } from '../repository';
import { Response, Request } from '../entity';
import { ImportCurl } from './ImportCurl';

let targetUrl: string;
let useCase: ImportCurl;
let requestRepository: RequestRepository;

beforeEach(() => {
targetUrl = 'https://pokeapi.co/api/v2';
requestRepository = getTestRequestRepository();

useCase = new ImportCurl({
requestRepository,
config: getTestConfiguration({ targetUrl }),
});
});

it('should parse the curl command and save an empty response', async () => {
// Given
const curl =
'curl --header "Content-Type: application/json" --request POST --data \'{"username":"xyz","password":"xyz"}\' https://pokeapi.co/api/v2/login';

// When
const result = await useCase.execute(curl);

// Then
expect(result).toEqual(
new Request(
'POST',
'/login',
{
'content-type': 'application/json',
},
'{"username":"xyz","password":"xyz"}'
)
);

expect(requestRepository.persistResponseForRequest).toHaveBeenCalledTimes(1);
expect(requestRepository.persistResponseForRequest).toHaveBeenCalledWith(
new Request(
'POST',
'/login',
{
'content-type': 'application/json',
},
'{"username":"xyz","password":"xyz"}'
),
new Response(200, {}, Buffer.from(''), 0)
);
});
30 changes: 30 additions & 0 deletions src/domain/usecase/ImportCurl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createRequestFromCurl } from '../../utils/request';
import { MementoConfiguration } from '../../configuration';
import { Response } from '../entity';
import { RequestRepository } from '../repository';

interface Dependencies {
config: MementoConfiguration;
requestRepository: RequestRepository;
}

export class ImportCurl {
private config: MementoConfiguration;
private requestRepository: RequestRepository;

public constructor({ requestRepository, config }: Dependencies) {
this.requestRepository = requestRepository;
this.config = config;
}

public async execute(curlCommand: string) {
const request = createRequestFromCurl(curlCommand, this.config.targetUrl);

await this.requestRepository.persistResponseForRequest(
request,
new Response(200, {}, Buffer.from(''), 0)
);

return request;
}
}
1 change: 1 addition & 0 deletions src/domain/usecase/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './RefreshRequest';
export * from './ListRequests';
export * from './GetRequestDetails';
export * from './SetResponseTime';
export * from './ImportCurl';
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ListRequest,
GetRequestDetails,
SetResponseTime,
ImportCurl,
} from './domain/usecase';
import { NetworkServiceAxios } from './infrastructure/service';
import { RequestRepositoryFile } from './infrastructure/repository';
Expand Down Expand Up @@ -44,6 +45,7 @@ export function Memento({ cacheDirectory }: MementoOptions = {}) {
listRequestsUseCase: asClass(ListRequest),
getRequestDetailsUseCase: asClass(GetRequestDetails),
setResponseTimeUseCase: asClass(SetResponseTime),
importCurlUseCase: asClass(ImportCurl),

// Repositories
requestRepository: asClass(RequestRepositoryFile),
Expand Down
30 changes: 30 additions & 0 deletions src/types/parse-curl.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
declare module 'parse-curl' {
interface Headers {
[key: string]: string;
}
type Method =
| 'get'
| 'GET'
| 'post'
| 'POST'
| 'put'
| 'PUT'
| 'head'
| 'HEAD'
| 'delete'
| 'DELETE'
| 'options'
| 'OPTIONS'
| 'patch'
| 'PATCH';

interface ParsedCurl {
method: Method;
url: string;
header: Headers;
body: string;
}

function parseCurl(curl: string): ParsedCurl;
export = parseCurl;
}
Loading

0 comments on commit c7abbf4

Please sign in to comment.