repo_description: Generate automated tests for your Node.js app via LLMs without developers having to write a single line of code.
repo_topics: api-testing api-testing-framework automated-testing database-testing developer-tools integration-testing nodejs test-coverage testing
code: const MODES = require('./const/modes.json');
  const RedisInterceptor = require('./helpers/redis.js');
  const { cleanupDb } = require('./helpers/mongodb.js');
  const { makeTestRequest } = require('./helpers/testing.js');
  const { getPythagoraVersion, setUpPythagoraDirs } = require("./helpers/starting.js");
  const { logCaptureFinished, pythagoraFinishingUp } = require('./utils/cmdPrint.js');
  const { getCircularReplacer, getMetadata, getFreePortInRange } = require('./utils/common.js');
  const {
      PYTHAGORA_TESTS_DIR,
      PYTHAGORA_METADATA_DIR,
      METADATA_FILENAME,
      PYTHAGORA_DELIMITER
  } = require("@pythagora.io/js-code-processing").common;
  
  let  { BatchInterceptor } = require('@mswjs/interceptors');
  let  nodeInterceptors = require('@mswjs/interceptors/lib/presets/node.js');
  let  fs = require("fs");
  let  _ = require("lodash");
  const path = require('path');
  let  { AsyncLocalStorage } = require('node:async_hooks');
  // const duplexify = require('duplexify');
  
  global.asyncLocalStorage = new AsyncLocalStorage();
  
  class Pythagora {
  
      constructor(args) {
          if (!MODES[args.mode]) throw new Error('Invalid mode ', args.mode);
          else this.mode = args.mode;
  
          this.rerunAllFailed = args.rerun_all_failed;
          this.ignoreRedis = args.ignore_redis;
          this.testId = args.test_id;
          this.pick = args.pick;
          this.ignore = args.ignore;
          this.fullCodeCoverageReport = args.full_code_coverage_report;
          this.initCommand = args.init_command.join(' ');
          this.pythagora_root = args.pythagora_root
  
          this.idSeq = 0;
          this.requests = {};
          this.testingRequests = {};
          this.loggingEnabled = this.mode === MODES.capture;
          //todo move all global vars to tempVars
          this.tempVars = {};
  
          getPythagoraVersion(this);
  
          setUpPythagoraDirs();
          // this.setUpHttpInterceptor();
  
          this.cleanupDone = false;
          this.metadata = getMetadata();
  
          process.on('SIGINT', this.exit.bind(this));
          process.on('exit', this.exit.bind(this));
      }
  
      async exit() {
          if (this.cleanupDone) return process.exit();
          this.cleanupDone = true;
          if (this.mode === MODES.test) {
              this.saveTestRunMetadata();
              await cleanupDb(this);
          } else if (this.mode === MODES.capture) {
              pythagoraFinishingUp();
              this.mode = MODES.test;
              let savedRequests = [], failedRequests = [];
              for (const request of _.values(this.requests)) {
                  if (request.error) {
                      // TODO make a general function for comparing requests and use it here as well instead of only endpoint
                      failedRequests.push(request.endpoint);
                      continue;
                  }
                  let result = await makeTestRequest(request, true, false);
                  result = result.testResult;
                  if (!result) {
                      failedRequests.push(request.endpoint);
                      console.log(`Capture is not valid for endpoint ${request.endpoint} (${request.method}). Erasing...`)
                      let reqFileName = path.resolve(this.pythagora_root, PYTHAGORA_TESTS_DIR, `${request.endpoint.replace(/\//g, PYTHAGORA_DELIMITER)}.json`);
                      if (!fs.existsSync(reqFileName)) continue;
                      let fileContent = JSON.parse(fs.readFileSync(reqFileName));
                      if (fileContent.length === 1) {
                          fs.unlinkSync(reqFileName);
                      } else {
                          let identicalRequestIndex = fileContent.findIndex(req => {
                              return req && req.id === request.id;
                          });
  
                          if (identicalRequestIndex === -1) {
                              console.error('Could not find request to delete. This should not happen. Please report this issue to Pythagora team.');
                          } else {
                              fileContent.splice(identicalRequestIndex, 1);
                              let storeData = typeof fileContent === 'string' ? fileContent : JSON.stringify(fileContent, getCircularReplacer());
                              fs.writeFileSync(reqFileName, storeData);
                          }
                      }
                  } else savedRequests.push(request.endpoint);
              }
  
              logCaptureFinished(savedRequests.length, failedRequests.length);
          }
  
          fs.writeFileSync(path.resolve(this.pythagora_root, PYTHAGORA_METADATA_DIR, 'finishingup'), 'done');
          process.exit();
      }
  
      getTestsToExecute() {
          if (this.testId) return [this.testId];
          if (!this.metadata || !this.metadata.runs) return undefined;
          let runs = this.metadata.runs;
  
          if (this.rerunAllFailed) return runs[runs.length - 1].failed;
  
          return undefined;
      }
  
      saveTestRunMetadata() {
          let passed = _.values(this.testingRequests).filter(t => t.passed).map(t => t.id);
          let failed = _.values(this.testingRequests).filter(t => !t.passed).map(t => t.id);
          if (!passed.length && !failed.length) return;
  
          this.metadata.runs = (this.metadata.runs || []).concat([{
              date: new Date(),
              version: this.version,
              passed,
              failed
          }]);
          this.metadata.runs = this.metadata.runs.slice(-10);
          this.metadata.initCommand = this.initCommand;
          fs.writeFileSync(path.resolve(this.pythagora_root, PYTHAGORA_METADATA_DIR, METADATA_FILENAME), JSON.stringify(this.metadata, getCircularReplacer(), 2));
          console.log('Test run metadata saved.');
      }
  
      setMongoClient(client) {
          this.mongoClient = client;
      }
  
      setUpHttpInterceptor() {
          const interceptor = new BatchInterceptor({
              name: 'my-interceptor',
              interceptors: nodeInterceptors.default,
          });
  
          interceptor.apply();
  
          interceptor.on('request', (req, reqId) => this.httpRequestInterceptor(req, reqId, this));
          interceptor.on('response', (res, req) => this.httpResponseInterceptor(res, req, this));
      }
  
      httpRequestInterceptor(request, requestId, pythagora) {
          if (pythagora.mode === MODES.test) {
              let mockResponse = pythagora.getHttpMockResponse(request);
              if (!mockResponse) return console.error('No mock response found for request!');
  
              request.respondWith(
                  new Response(
                      JSON.stringify(mockResponse.responseData),
                      {
                          status: mockResponse.response.status,
                          statusText: mockResponse.response.statusText,
                          // TODO headers: mockResponse.response.headers
                      }
                  )
              )
          }
      }
  
      async httpResponseInterceptor(response, request, pythagora) {
          if (pythagora.mode !== MODES.capture) return;
          async function readStream(reader) {
              let result;
              let values = [];
              while (!result || !result.done) {
                  result = await reader.read();
                  values.push(result.value);
              }
  
              let finalResult = values.join('').toString('utf8');
              try { finalResult = JSON.parse(finalResult); } catch (e) {}
              return finalResult;
          }
  
          let reader = response.body.getReader();
          let responseBody = await readStream(reader);
          if (!pythagora.requests[pythagora.getRequestKeyByAsyncStore()]) return console.error('No TRACE found for response!');
          pythagora.requests[pythagora.getRequestKeyByAsyncStore()].intermediateData.push({
              type: 'outgoing_request',
              url: request.url,
              method: request.method,
              responseData: responseBody,
              response: {
                  status: response.status,
                  statusText: response.statusText,
                  // TODO headers: response.headers
              }
          });
      }
  
      // TODO track request order and make sure the correct ones get chosen
      getHttpMockResponse(request) {
          return this.testingRequests[global.asyncLocalStorage.getStore()].intermediateData.find(intData => {
              // TODO add more checks (body, query, params)
              return intData.type === 'outgoing_request' &&
                  intData.url === request.url &&
                  intData.method === request.method;
          });
      }
  
      getRequestKeyByAsyncStore(asyncStoreId) {
          if (asyncStoreId === undefined) asyncStoreId = global.asyncLocalStorage.getStore();
          return Object.keys(this.requests).find(key => {
              return this.requests[key].asyncStore === asyncStoreId;
          });
      }
  
      getRequestByAsyncStore(asyncStoreId) {
          if (asyncStoreId === undefined) asyncStoreId = global.asyncLocalStorage.getStore();
          return this.requests[this.getRequestKeyByAsyncStore(asyncStoreId)];
      }
  
      getTestingRequestByAsyncStore(asyncStoreId) {
          if (asyncStoreId === undefined) asyncStoreId = global.asyncLocalStorage.getStore();
          return this.testingRequests[asyncStoreId];
      }
  
      updateTrace(asyncId, triggerAsyncId) {
          for (let i = 0; i < Object.keys(this.requests).length; i++) {
              if (this.requests[Object.keys(this.requests)[i]].trace.includes(triggerAsyncId)) {
                  this.requests[Object.keys(this.requests)[i]].trace.push(asyncId);
                  break;
              }
          }
      }
  
      async getRequestMockDataById(req) {
          let reqPath = path.resolve(this.pythagora_root, PYTHAGORA_TESTS_DIR, `${req.path.replace(/\//g, PYTHAGORA_DELIMITER)}.json`);
          if (!fs.existsSync(reqPath)) return;
          let capturedRequests = JSON.parse(await fs.promises.readFile(reqPath, 'utf8'));
          return capturedRequests.find(request => request.id === req.headers['pythagora-req-id']);
      }
  
      async runRedisInterceptor(intermediateData) {
          if (this.ignoreRedis) return;
  
          let listenPort = await getFreePortInRange(16000, 17000);
          this.RedisInterceptor = new RedisInterceptor(
              this,
              listenPort,
              6379,
              intermediateData
          );
  
          await this.RedisInterceptor.init();
      }
  
      saveRedisData(request, response) {
          _.keys(this.requests).forEach(k => this.requests[k].intermediateData.push({
              type: 'redis',
              request, response
          }));
      }
  
      isMongoConnected() {
          return this.mongoClient && (this.mongoClient.connected ||
              (this.mongoClient.isConnected && this.mongoClient.isConnected()) ||
              (this.mongoClient.topology && this.mongoClient.topology.isConnected && this.mongoClient.topology.isConnected()))
      }
  
      runWhenServerReady(callback) {
          let interval = setInterval(() => {
              // todo add here all checks that server is ready (express, redis,...)
              if (this.isMongoConnected()) {
                  clearInterval(interval);
                  callback();
              }
          }, 200);
      }
  
  }
  
  module.exports = Pythagora;
  