@salesforce/agents
Version:
Client side APIs for working with Salesforce agents
189 lines • 7.6 kB
JavaScript
/*
* Copyright (c) 2024, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MaybeMock = void 0;
const node_path_1 = require("node:path");
const node_fs_1 = require("node:fs");
const promises_1 = require("node:fs/promises");
const core_1 = require("@salesforce/core");
const kit_1 = require("@salesforce/kit");
const nock_1 = __importDefault(require("nock"));
/**
* If the `SF_MOCK_DIR` environment variable is set, resolve to an absolute path
* and ensure the directory exits, then return the path.
*
* NOTE: THIS SHOULD BE MOVED TO SOME OTHER LIBRARY LIKE `@salesforce/kit`.
*
* @returns the absolute path to an existing directory used for mocking behavior
*/
const getMockDir = () => {
const mockDir = kit_1.env.getString('SF_MOCK_DIR');
if (mockDir) {
let mockDirStat;
try {
mockDirStat = (0, node_fs_1.statSync)((0, node_path_1.resolve)(mockDir));
}
catch (err) {
throw core_1.SfError.create({
name: 'InvalidMockDir',
message: `SF_MOCK_DIR [${mockDir}] not found`,
cause: err,
actions: [
"If you're trying to mock agent behavior you must create the mock directory and add expected mock files to it.",
],
});
}
if (!mockDirStat.isDirectory()) {
throw core_1.SfError.create({
name: 'InvalidMockDir',
message: `SF_MOCK_DIR [${mockDir}] is not a directory`,
actions: [
"If you're trying to mock agent behavior you must create the mock directory and add expected mock files to it.",
],
});
}
return mockDir;
}
};
async function readJson(path) {
return JSON.parse(await (0, promises_1.readFile)(path, 'utf-8'));
}
async function readPlainText(path) {
return (0, promises_1.readFile)(path, 'utf-8');
}
async function readDirectory(path) {
const files = await (0, promises_1.readdir)(path);
const promises = files.map((file) => {
if (file.endsWith('.json')) {
return readJson((0, node_path_1.join)(path, file));
}
else {
return readPlainText((0, node_path_1.join)(path, file));
}
});
return (await Promise.all(promises)).filter((r) => !!r);
}
async function readResponses(mockDir, url, logger) {
const mockResponseName = url.replace(/\//g, '_').replace(/:/g, '_').replace(/^_/, '').split('?')[0];
const mockResponsePath = (0, node_path_1.join)(mockDir, mockResponseName);
// Try all possibilities for the mock response file
const responses = (await Promise.all([
readJson(`${mockResponsePath}.json`)
.then((r) => {
logger.debug(`Found JSON mock file: ${mockResponsePath}.json`);
return r;
})
.catch(() => undefined),
readPlainText(mockResponsePath)
.then((r) => {
logger.debug(`Found plain text mock file: ${mockResponsePath}`);
return r;
})
.catch(() => undefined),
readDirectory(mockResponsePath)
.then((r) => {
logger.debug(`Found directory of mock files: ${mockResponsePath}`);
return r;
})
.catch(() => undefined),
]))
.filter((r) => !!r)
.flat();
if (responses.length === 0) {
throw core_1.SfError.create({
name: 'MissingMockFile',
message: `SF_MOCK_DIR [${mockDir}] must contain a spec file with name ${mockResponsePath} or ${mockResponsePath}.json`,
});
}
logger.debug(`Using responses: ${responses.map((r) => JSON.stringify(r)).join(', ')}`);
return responses;
}
/**
* A class to act as an in-between the library's request, and the orgs response
*
* if `SF_MOCK_DIR` is set it will read from the directory, resolving files as API responses with nock
*
* if it is NOT set, it will hit the endpoint and use real server responses
*/
class MaybeMock {
connection;
mockDir = getMockDir();
scopes = new Map();
logger;
constructor(connection) {
this.connection = connection;
this.logger = core_1.Logger.childFromRoot(this.constructor.name);
}
/**
* Will either use mocked responses, or the real server response, as the library/APIs become more feature complete,
* there will be fewer mocks and more real responses
*
* @param {"GET" | "POST" | "DELETE"} method
* @param {string} url
* @param {nock.RequestBodyMatcher} body
* @returns {Promise<T>}
*/
async request(method, url, body = {}, headers = {}) {
if (this.mockDir) {
this.logger.debug(`Mocking ${method} request to ${url} using ${this.mockDir}`);
const responses = await readResponses(this.mockDir, url, this.logger);
const baseUrl = this.connection.baseUrl();
const scope = this.scopes.get(baseUrl) ?? (0, nock_1.default)(baseUrl);
// Look up status code to determine if it's successful or not
// Be have to assert this is a number because AgentTester has a status that is non-numeric
const getCode = (response) => typeof response === 'object' && 'status' in response && typeof response.status === 'number'
? response.status
: 200;
// This is a hack to work with SFAP endpoints
url = url.replace('https://api.salesforce.com', '');
this.scopes.set(baseUrl, scope);
switch (method) {
case 'GET':
for (const response of responses) {
scope.get(url).reply(getCode(response), response);
}
break;
case 'POST':
for (const response of responses) {
scope.post(url, body).reply(getCode(response), response);
}
break;
case 'DELETE':
for (const response of responses) {
scope.delete(url).reply(getCode(response), response);
}
break;
}
}
this.logger.debug(`Making ${method} request to ${url}`);
switch (method) {
case 'GET':
return this.connection.requestGet(url, { retry: { maxRetries: 3 } });
case 'POST':
if (!body) {
throw core_1.SfError.create({
name: 'InvalidBody',
message: 'POST requests must include a body',
});
}
return this.connection.requestPost(url, body, { retry: { maxRetries: 3 } });
case 'DELETE':
// We use .request() rather than .requestDelete() so that we can pass in the headers
return this.connection.request({
method: 'DELETE',
url,
headers,
}, { retry: { maxRetries: 3 } });
}
}
}
exports.MaybeMock = MaybeMock;
//# sourceMappingURL=maybe-mock.js.map
;