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
3 changes: 3 additions & 0 deletions examples/collection/echo/pre-request-script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const key = 'counter';
const value = parseInt(trufos.getCollectionVariable(key) || '0');
trufos.setCollectionVariable(key, (value + 1).toString());
3 changes: 2 additions & 1 deletion examples/collection/echo/request-body.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"randomUuid": "{{$randomUuid}}"
"randomUuid": "{{$randomUuid}}",
"counter": {{counter}}
}
7 changes: 7 additions & 0 deletions src/main/event/main-event-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ vi.mock('electron', () => ({
}));

vi.mock('./stream-events', () => ({}));
vi.mock('main/network/service/http-service', () => ({
HttpService: {
instance: {
fetchAsync: vi.fn(),
},
},
}));

const TEST_STRING = 'Hello, World!';
const TEST_FILE_PATH = path.join(tmpdir(), 'test.txt');
Expand Down
14 changes: 11 additions & 3 deletions src/main/event/main-event-service.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { app, dialog, ipcMain } from 'electron';
import { EnvironmentService } from 'main/environment/service/environment-service';
import { HttpService } from 'main/network/service/http-service';
import type { IEventService, ImportStrategy } from 'shim/event-service';
import type {
Collection,
TrufosObject,
Folder,
TrufosRequest,
VariableMap,
EnvironmentMap,
} from 'shim/objects';
ScriptType,
IEventService,
ImportStrategy,
} from 'shim';
import { PersistenceService } from '../persistence/service/persistence-service';
import './stream-events';
import { ImportService } from 'main/import/service/import-service';
import { updateElectronApp } from 'update-electron-app';

// register stream events
import './stream-events';

const persistenceService = PersistenceService.instance;
const environmentService = EnvironmentService.instance;
const importService = ImportService.instance;
Expand Down Expand Up @@ -95,6 +99,10 @@ export class MainEventService implements IEventService {
return await persistenceService.saveRequest(request, textBody);
}

async saveScript(request: TrufosRequest, type: ScriptType, script: string) {
return await persistenceService.saveScript(request, type, script);
}

async copyRequest(request: TrufosRequest): Promise<TrufosRequest> {
return await persistenceService.copyRequest(request);
}
Expand Down
10 changes: 9 additions & 1 deletion src/main/event/stream-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,15 @@ ipcMain.handle(
return id;
}
stream = createReadStream(filePath, encoding);
} else if ((stream = await persistenceService.loadTextBodyOfRequest(input, encoding)) == null) {
} else if (input.type === 'script') {
stream = await persistenceService.loadScript(input.request, input.source);
} else if (input.type === 'request') {
stream = await persistenceService.loadTextBodyOfRequest(input, encoding);
} else {
logger.error('Invalid stream input:', input);
}

if (stream == null) {
setImmediate(() => sender.send('stream-end', id));
return id;
}
Expand Down
42 changes: 40 additions & 2 deletions src/main/network/service/http-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import { HttpService } from './http-service';
import { MockAgent } from 'undici';
import fs from 'node:fs';
import { TrufosRequest } from 'shim/objects/request';
import { parseUrl, TrufosURL } from 'shim/objects/url';
import { parseUrl } from 'shim/objects/url';
import { randomUUID } from 'node:crypto';
import { RequestMethod } from 'shim/objects/request-method';
import { IncomingHttpHeaders } from 'undici/types/header';
import { describe, it, expect, beforeAll, vi } from 'vitest';
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
import { AuthorizationType } from 'shim/objects';
import { EnvironmentService } from 'main/environment/service/environment-service';
import { TemplateReplaceStream } from 'template-replace-stream';
import { ResponseBodyService } from 'main/network/service/response-body-service';
import { PersistenceService } from 'main/persistence/service/persistence-service';
import { ScriptingService } from 'main/scripting/scripting-service';
import { Readable } from 'node:stream';

const mockAgent = new MockAgent({ connections: 1 });
const environmentService = EnvironmentService.instance;
Expand All @@ -22,6 +25,10 @@ describe('HttpService', () => {
mockAgent.enableCallHistory();
});

beforeEach(() => {
vi.spyOn(PersistenceService.instance, 'loadScript').mockResolvedValue(null);
});

it('fetchAsync() should make an HTTP call and return the body on read', async () => {
// Arrange
const text = 'Hello, world!';
Expand Down Expand Up @@ -264,6 +271,37 @@ describe('HttpService', () => {
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});

it('fetchAsync() should execute scripts when provided', async () => {
// Arrange
const preScript = 'console.log("pre");';
const postScript = 'console.log("post");';
vi.spyOn(PersistenceService.instance, 'loadScript')
.mockResolvedValueOnce(Readable.from(preScript) as fs.ReadStream)
.mockResolvedValueOnce(Readable.from(postScript) as fs.ReadStream);
const executeSpy = vi.spyOn(ScriptingService.instance, 'executeScript');

const url = new URL('https://example.com/api/data');
const httpService = setupMockHttpService(url, 'OK');
const request: TrufosRequest = {
id: randomUUID(),
parentId: randomUUID(),
type: 'request',
title: 'Scripted Request',
url: parseUrl(url.toString()),
method: RequestMethod.GET,
headers: [],
body: null,
};

// Act
await httpService.fetchAsync(request);

// Assert
expect(executeSpy).toHaveBeenCalledTimes(2);
expect(executeSpy.mock.calls[0]?.[0]).toEqual(preScript);
expect(executeSpy.mock.calls[1]?.[0]).toEqual(postScript);
});
});

function setupMockHttpService(
Expand Down
20 changes: 19 additions & 1 deletion src/main/network/service/http-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ import { calculateResponseSize } from 'main/util/size-calculation';
import { app } from 'electron';
import process from 'node:process';
import { ResponseBodyService } from 'main/network/service/response-body-service';
import { ScriptingService } from 'main/scripting/scripting-service';
import { ScriptType } from 'shim/scripting';
import { text } from 'node:stream/consumers';

const fileSystemService = FileSystemService.instance;
const environmentService = EnvironmentService.instance;
const persistenceService = PersistenceService.instance;
const responseBodyService = ResponseBodyService.instance;
const scriptingService = ScriptingService.instance;

declare type HttpHeaders = Record<string, string[]>;

Expand Down Expand Up @@ -65,6 +69,9 @@ export class HttpService {
this.trufosHeadersToUndiciHeaders(request.headers)
);

// execute pre-request script if it exists
await this.executeScript(request, ScriptType.PRE_REQUEST);

const { stream, size } = await this.readBody(request);

// measure duration of the request
Expand All @@ -83,7 +90,10 @@ export class HttpService {
});

const duration = getDurationFromNow(now);
logger.info(`Received response in ${duration} milliseconds`);
logger.info(`Received response in ${duration}ms`);

// execute post-response script if it exists
await this.executeScript(request, ScriptType.POST_RESPONSE);

// write the response body to a temporary file
const bodyFile = fileSystemService.temporaryFile();
Expand Down Expand Up @@ -182,4 +192,12 @@ export class HttpService {
private resolveVariablesInHeaderValues(values: string[]) {
return Promise.all(values.map((value) => environmentService.setVariablesInString(value)));
}

private async executeScript(request: TrufosRequest, type: ScriptType) {
const stream = await persistenceService.loadScript(request, type);
if (stream != null) {
const script = await text(stream);
scriptingService.executeScript(script);
}
}
}
27 changes: 27 additions & 0 deletions src/main/persistence/service/persistence-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
OrderFile,
SECRETS_FILE_NAME,
} from 'main/persistence/constants';
import { ScriptType } from 'shim';

const secretService = SecretService.instance;

Expand Down Expand Up @@ -305,6 +306,17 @@ export class PersistenceService {
return folderCopy;
}

public async saveScript(request: TrufosRequest, type: ScriptType, script: string) {
await fs.writeFile(this.getScriptFilePath(request, type), script);
}

public async loadScript(request: TrufosRequest, type: ScriptType) {
const filePath = this.getScriptFilePath(request, type);
if (await exists(filePath)) {
return createReadStream(filePath, 'utf-8');
}
}

/**
* Saves the information of a trufos object to the file system.
* @param object the object to save
Expand Down Expand Up @@ -666,6 +678,21 @@ export class PersistenceService {
return `${type}.json`;
}

private getScriptFilePath(request: TrufosRequest, type: ScriptType) {
let dirPath = this.getOrCreateDirPath(request);
if (request.draft) dirPath = this.getDraftDirPath(dirPath);
return path.join(dirPath, this.getScriptFileName(type));
}

/**
* Gets the script file name for a given script type.
* @param type the type of the script
* @returns the script file name with file extension `.js`
*/
private getScriptFileName(type: ScriptType) {
return `${type}-script.js`;
}

private isDirPathTaken(targetDirPath: string) {
return this.idToPathMap.values().some((path) => path === targetDirPath);
}
Expand Down
Loading