From b59063e1fe11cafc9133e9e37338863d616389a7 Mon Sep 17 00:00:00 2001 From: Niv Lipetz Date: Sat, 12 Oct 2019 13:08:26 +0300 Subject: [PATCH 01/17] docs(image): fix outdated image in docs (#214) --- docs/devguide/docs/images/dsltestinui.png | Bin 37298 -> 64513 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/devguide/docs/images/dsltestinui.png b/docs/devguide/docs/images/dsltestinui.png index b85614f442606b5d0d8a9be3f05c92abab41f4c1..476270e7876849334fd92b35931982e356754fb5 100644 GIT binary patch literal 64513 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 E02+Q$djJ3c literal 37298 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 From 203bf4d87504f16b230563ad8dfb475927ef7d49 Mon Sep 17 00:00:00 2001 From: Niv Lipetz Date: Sun, 20 Oct 2019 13:52:12 +0300 Subject: [PATCH 02/17] chore(ci/cd): remove circleci docker_layer_caching (#218) --- .circleci/config.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2de5bfda6..a74ca2321 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ jobs: steps: - checkout - setup_remote_docker: - docker_layer_caching: true + docker_layer_caching: false - restore_cache: key: dependency-cache-{{ checksum "package.json" }} - run: npm install @@ -22,7 +22,7 @@ jobs: steps: - checkout - setup_remote_docker: - docker_layer_caching: true + docker_layer_caching: false - restore_cache: key: dependency-cache-{{ checksum "package.json" }} - run: npm audit @@ -49,7 +49,7 @@ jobs: steps: - checkout - setup_remote_docker: - docker_layer_caching: true + docker_layer_caching: false - restore_cache: key: dependency-cache-{{ checksum "package.json" }} - run: @@ -88,7 +88,7 @@ jobs: steps: - checkout - setup_remote_docker: - docker_layer_caching: true + docker_layer_caching: false - run: .circleci/dockerBuild.sh end-to-end-tests: docker: @@ -96,7 +96,7 @@ jobs: steps: - checkout - setup_remote_docker: - docker_layer_caching: true + docker_layer_caching: false - restore_cache: key: dependency-cache-{{ checksum "package.json" }} - run: .circleci/dockerRun.sh @@ -107,7 +107,7 @@ jobs: steps: - checkout - setup_remote_docker: - docker_layer_caching: true + docker_layer_caching: false - restore_cache: key: dependency-cache-{{ checksum "package.json" }} - add_ssh_keys: @@ -120,7 +120,7 @@ jobs: steps: - checkout - setup_remote_docker: - docker_layer_caching: true + docker_layer_caching: false - restore_cache: key: dependency-cache-{{ checksum "package.json" }} - add_ssh_keys: @@ -133,7 +133,7 @@ jobs: steps: - checkout - setup_remote_docker: - docker_layer_caching: true + docker_layer_caching: false - restore_cache: key: dependency-cache-{{ checksum "package.json" }} - add_ssh_keys: From 01f288a59bda4899f268a6c5edcdd8d671fa2094 Mon Sep 17 00:00:00 2001 From: enudler Date: Sat, 17 Aug 2019 23:26:51 +0300 Subject: [PATCH 03/17] feat(api): add api spec for processors resource (#200) * feat(api): add api spec for processors resource --- docs/openapi3.yaml | 798 ++++++++++++++++++++++++++++++--------------- 1 file changed, 529 insertions(+), 269 deletions(-) diff --git a/docs/openapi3.yaml b/docs/openapi3.yaml index 6635f8e8a..8f2717849 100644 --- a/docs/openapi3.yaml +++ b/docs/openapi3.yaml @@ -1,6 +1,6 @@ openapi: 3.0.0 info: - version: 1.0.0 + version: 1.2.0 title: Predator x-logo: url: favicon.png @@ -19,39 +19,43 @@ info: For an introduction to Predator and its concepts, see the Predator Documentation. tags: - - name: DSL Definitions - description: | - Predator's Domain Specific Language (DSL) allows you to generate request templates, which you can then reuse in the same test and in other tests, reducing replication. - - name: Tests - description: | - Tests include end-to-end scenarios that are executed at pre-configured intervals to provide in-depth performance metrics of your API. - - name: Jobs - description: | - Predator executes tests through so-called **jobs**. Depending on your configuration, the job will either execute immediately or at scheduled intervals. - - name: Reports - description: | - Reports give you insight into the performance of your API. Predator generates a report for each test that is executed. - - name: Configuration - description: | - This resource allows you to configure Predator programmatically. +- name: DSL Definitions + description: | + Predator's Domain Specific Language (DSL) allows you to generate request templates, which you can then reuse in the same test and in other tests, reducing replication. +- name: Tests + description: | + Tests include end-to-end scenarios that are executed at pre-configured intervals to provide in-depth performance metrics of your API. +- name: Processors + description: | + Processor files allow Predator to execute custom javascript code during test flows. +- name: Jobs + description: | + Predator executes tests through so-called **jobs**. Depending on your configuration, the job will either execute immediately or at scheduled intervals. +- name: Reports + description: | + Reports give you insight into the performance of your API. Predator generates a report for each test that is executed. +- name: Configuration + description: | + This resource allows you to configure Predator programmatically. x-tagGroups: - - name: Reference - tags: - - DSL Definitions - - Tests - - Jobs - - Reports - - Configuration +- name: Reference + tags: + - DSL Definitions + - Tests + - Processors + - Jobs + - Reports + - Configuration paths: #DSL Definitions '/v1/dsl/{dsl_name}/definitions': parameters: - - $ref: '#/components/parameters/dsl_name' + - $ref: '#/components/parameters/dsl_name' post: operationId: create-a-dsl-definition tags: - - DSL Definitions + - DSL Definitions summary: Create a DSL Definition description: Create a new DSL definition to generate a request template. responses: @@ -89,7 +93,7 @@ paths: get: operationId: retrieve-all-dsl-definitions tags: - - DSL Definitions + - DSL Definitions summary: Retrieve all DSL Definitions description: Retrieve all DSL definitions for the specified DSL group name. responses: @@ -119,12 +123,12 @@ paths: $ref: '#/components/schemas/error_response' '/v1/dsl/{dsl_name}/definitions/{definition_name}': parameters: - - $ref: '#/components/parameters/dsl_name' - - $ref: '#/components/parameters/definition_name' + - $ref: '#/components/parameters/dsl_name' + - $ref: '#/components/parameters/definition_name' get: operationId: retrieve-a-dsl-definition tags: - - DSL Definitions + - DSL Definitions summary: Retrieve a DSL Definition description: Retrieve a specific DSL definition. responses: @@ -155,7 +159,7 @@ paths: put: operationId: update-a-dsl-definition tags: - - DSL Definitions + - DSL Definitions summary: Update a DSL Definition description: Update a specific DSL definition. responses: @@ -193,7 +197,7 @@ paths: delete: operationId: Delete-a-dsl-definition tags: - - DSL Definitions + - DSL Definitions summary: Delete a DSL Definition description: Delete a specific DSL definition. responses: @@ -216,7 +220,7 @@ paths: post: operationId: create-a-test tags: - - Tests + - Tests summary: Create a Test description: Create a new test. responses: @@ -254,7 +258,7 @@ paths: get: operationId: retrieve-all-tests tags: - - Tests + - Tests summary: Retrieve all Tests description: Retrieve all available tests. responses: @@ -282,23 +286,23 @@ paths: get: operationId: retrieve-a-file tags: - - Tests + - Tests summary: Retrieve a file encoding to base 64 description: Retrieve a specific f. parameters: - - in: path - name: file_id - description: The file id. - required: true - schema: - type: string - format: uuid - example: 4bf5d7ab-f310-4a64-8ec2-d65c06188ec1 - - in: query - name: format - description: Should return file content in utf8 or base64 (default) - schema: - enum: ['base64','utf8'] + - in: path + name: file_id + description: The file id. + required: true + schema: + type: string + format: uuid + example: 4bf5d7ab-f310-4a64-8ec2-d65c06188ec1 + - in: query + name: format + description: Should return file content in utf8 or base64 (default) + schema: + enum: ['base64','utf8'] responses: '200': description: Success @@ -324,18 +328,18 @@ paths: get: operationId: retrieve-a-test tags: - - Tests + - Tests summary: Retrieve a Test description: Retrieve a specific test. parameters: - - in: path - name: test_id - description: The test id. - required: true - schema: - type: string - format: uuid - example: 4bf5d7ab-f310-4a64-8ec2-d65c06188ec1 + - in: path + name: test_id + description: The test id. + required: true + schema: + type: string + format: uuid + example: 4bf5d7ab-f310-4a64-8ec2-d65c06188ec1 responses: '200': description: Success @@ -364,17 +368,17 @@ paths: put: operationId: update-a-test tags: - - Tests + - Tests summary: Update a Test description: Update a specific test. parameters: - - in: path - name: test_id - description: The test id. - required: true - schema: - type: string - example: 4bf5d7ab-f310-4a64-8ec2-d65c06188ec1 + - in: path + name: test_id + description: The test id. + required: true + schema: + type: string + example: 4bf5d7ab-f310-4a64-8ec2-d65c06188ec1 responses: '200': description: Success @@ -410,17 +414,17 @@ paths: delete: operationId: delete-a-test tags: - - Tests + - Tests summary: Delete a Test description: Delete a specific test. parameters: - - in: path - name: test_id - description: The test id. - required: true - schema: - type: string - example: 4bf5d7ab-f310-4a64-8ec2-d65c06188ec1 + - in: path + name: test_id + description: The test id. + required: true + schema: + type: string + example: 4bf5d7ab-f310-4a64-8ec2-d65c06188ec1 responses: '200': description: Success @@ -440,18 +444,18 @@ paths: get: operationId: retrieve-all-test-revisions tags: - - Tests + - Tests summary: Retrieve Test Revisions description: Retrieve the revisions of the specified test. parameters: - - in: path - name: test_id - description: The test id. - required: true - schema: - type: string - format: uuid - example: 4bf5d7ab-f310-4a64-8ec2-d65c06188ec1 + - in: path + name: test_id + description: The test id. + required: true + schema: + type: string + format: uuid + example: 4bf5d7ab-f310-4a64-8ec2-d65c06188ec1 responses: '200': description: Success @@ -479,12 +483,13 @@ paths: application/json: schema: $ref: '#/components/schemas/error_response' + # Jobs /v1/jobs: post: operationId: create-a-job tags: - - Jobs + - Jobs summary: Create a Job description: Create a new job that will trigger a test run. responses: @@ -522,15 +527,15 @@ paths: get: operationId: retrieve-all-jobs tags: - - Jobs + - Jobs summary: Retrieve all Jobs description: Retrieve the details of all available jobs. parameters: - - in: query - name: one_time - description: Should return the one time tests (no cron expression) - schema: - type: boolean + - in: query + name: one_time + description: Should return the one time tests (no cron expression) + schema: + type: boolean responses: '200': description: Success @@ -552,21 +557,20 @@ paths: application/json: schema: $ref: '#/components/schemas/error_response' - '/v1/jobs/{job_id}': get: operationId: retrieve-a-job tags: - - Jobs + - Jobs summary: Retrieve a Job description: Retrieve a specific job. parameters: - - in: path - name: job_id - description: The id of the job to retrieve. - required: true - schema: - type: string + - in: path + name: job_id + description: The id of the job to retrieve. + required: true + schema: + type: string responses: '200': description: Success @@ -595,16 +599,16 @@ paths: put: operationId: update-a-job tags: - - Jobs + - Jobs summary: Update a Job description: Update a specific job. parameters: - - in: path - name: job_id - description: The id of the job to update. - required: true - schema: - type: string + - in: path + name: job_id + description: The id of the job to update. + required: true + schema: + type: string responses: '200': description: Success @@ -640,15 +644,15 @@ paths: delete: operationId: delete-a-job tags: - - Jobs + - Jobs summary: Delete a Job description: Delete a specific job. parameters: - - in: path - name: job_id - required: true - schema: - type: string + - in: path + name: job_id + required: true + schema: + type: string responses: '204': description: Success @@ -668,22 +672,22 @@ paths: post: operationId: terminate-a-job tags: - - Jobs + - Jobs summary: Terminate a Job description: Terminate the execution of a running job. parameters: - - in: path - description: The ID of the job. - name: job_id - required: true - schema: - type: string - - in: path - description: The id of a specific run in metronome which is also the report id. - name: run_id - required: true - schema: - type: string + - in: path + description: The ID of the job. + name: job_id + required: true + schema: + type: string + - in: path + description: The id of a specific run in metronome which is also the report id. + name: run_id + required: true + schema: + type: string responses: '204': description: Success @@ -702,21 +706,21 @@ paths: /v1/jobs/{job_id}/runs/{run_id}/logs: get: tags: - - jobs + - Jobs description: get logs of specific job. parameters: - - in: path - description: The ID of the job. - name: job_id - required: true - schema: - type: string - - in: path - description: The id of a specific run in metronome which is also the report id. - name: run_id - required: true - schema: - type: string + - in: path + description: The ID of the job. + name: job_id + required: true + schema: + type: string + - in: path + description: The id of a specific run in metronome which is also the report id. + name: run_id + required: true + schema: + type: string responses: "200": description: Success @@ -738,21 +742,22 @@ paths: application/json: schema: $ref: "#/components/schemas/error_response" + # Reports '/v1/tests/{test_id}/reports': get: operationId: retrieve-all-reports tags: - - Reports + - Reports summary: Retrieve all Reports description: Retrieve all reports for the specified test. parameters: - - in: path - name: test_id - description: The id of the test for which to retrieve the reports. - required: true - schema: - type: string + - in: path + name: test_id + description: The id of the test for which to retrieve the reports. + required: true + schema: + type: string responses: '200': description: Success @@ -777,15 +782,15 @@ paths: post: operationId: create-a-report tags: - - Reports + - Reports summary: Create a Report description: Create a new report for the specified test. parameters: - - in: path - name: test_id - required: true - schema: - type: string + - in: path + name: test_id + required: true + schema: + type: string responses: '201': description: Success @@ -818,23 +823,23 @@ paths: get: operationId: retrieve-a-report tags: - - Reports + - Reports summary: Retrieve a Report description: Retrieve a specific report for the specified test. parameters: - - in: path - name: test_id - required: true - description: The id of the test for which to retrieve the report. - schema: - type: string - example: 81a27853-0db5-4e57-ad63-4b637528398a - - in: path - name: report_id - description: The id of the report to retrieve. - required: true - schema: - type: string + - in: path + name: test_id + required: true + description: The id of the test for which to retrieve the report. + schema: + type: string + example: 81a27853-0db5-4e57-ad63-4b637528398a + - in: path + name: report_id + description: The id of the report to retrieve. + required: true + schema: + type: string responses: '200': description: Success @@ -864,17 +869,17 @@ paths: get: operationId: retrieve-recent-reports tags: - - Reports + - Reports summary: Retrieve Recent Reports description: Retrieve the most recent reports. parameters: - - in: query - name: limit - description: The number of most recent reports to retrieve. - required: true - minimum: 1 - maximum: 250 - type: integer + - in: query + name: limit + description: The number of most recent reports to retrieve. + required: true + minimum: 1 + maximum: 250 + type: integer responses: '200': description: Success @@ -899,20 +904,20 @@ paths: '/v1/tests/{test_id}/reports/{report_id}/stats': post: tags: - - Reports + - Reports summary: Insert a Stats Object description: Insert a new stats object for a specific report parameters: - - in: path - name: test_id - required: true - schema: - type: string - - in: path - name: report_id - required: true - schema: - type: string + - in: path + name: test_id + required: true + schema: + type: string + - in: path + name: report_id + required: true + schema: + type: string responses: '204': description: Success @@ -941,12 +946,13 @@ paths: $ref: '#/components/schemas/stats' description: stats data required: true + # Config /v1/config: get: operationId: retrieve-configurations tags: - - Configuration + - Configuration summary: Retrieve Configuration Settings description: Retrieve all Predator configuration settings. responses: @@ -961,7 +967,7 @@ paths: put: operationId: update-configurations tags: - - Configuration + - Configuration summary: Update Configuration Settings description: Update existing configuration settings. This will override environment variables. requestBody: @@ -988,18 +994,18 @@ paths: delete: operationId: delete-a-configuration tags: - - Configuration + - Configuration summary: Delete a Specific Configuration Value description: | Delete a specific configuration value. This will restore the original environment variable. parameters: - - in: path - name: config_key - required: true - description: They key to delete. - schema: - type: string + - in: path + name: config_key + required: true + description: They key to delete. + schema: + type: string responses: "204": description: Success @@ -1010,6 +1016,196 @@ paths: schema: $ref: "#/components/schemas/error_response" + # Processors + /v1/processors: + post: + operationId: create-processor-file + tags: + - Processors + summary: Create processor file + description: Create a new processor file which will have javascript code that can be executed by tests. + responses: + '201': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/processor' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/processor' + description: The processor file to create + required: true + get: + operationId: retrieve-all-processor-files + tags: + - Processors + summary: Retrieve all processors files + description: Retrieve the details of all processor files. + parameters: + - in: query + name: from + description: From which result to start retrieve data + required: false + schema: + type: integer + minimum: 0 + default: 0 + - in: query + name: limit + description: Max results to return from this query ( Limit to 100 ) + required: false + schema: + type: integer + maximum: 100 + default: 100 + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/processor' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '/v1/processors/{processor_id}': + get: + operationId: retrieve-processor-file + tags: + - Processors + summary: Retrieve processor file + description: Retrieve a specific job. + parameters: + - in: path + name: processor_id + description: The id of the processor file to retrieve. + required: true + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/processor' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + put: + operationId: update-processor-file + tags: + - Processors + summary: Update a processor file + description: Update a specific processor file. + parameters: + - in: path + name: processor_id + description: The id of the processor file to update. + required: true + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/processor' + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/processor' + description: The processor to update + required: true + delete: + operationId: delete-processor-file + tags: + - Processors + summary: Delete processor file + description: Delete a specific processor file. + parameters: + - in: path + name: processor_id + required: true + schema: + type: string + responses: + '204': + description: Success + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + components: parameters: dsl_name: @@ -1032,8 +1228,8 @@ components: definition: type: object required: - - name - - request + - name + - request properties: name: description: The name of the DSL definition. @@ -1067,7 +1263,7 @@ components: } update_definition: required: - - request + - request properties: request: type: object @@ -1083,12 +1279,12 @@ components: discriminator: propertyName: type oneOf: - - $ref: '#/components/schemas/basic' - - $ref: '#/components/schemas/dsl' + - $ref: '#/components/schemas/basic' + - $ref: '#/components/schemas/dsl' required: - - name - - description - - type + - name + - description + - type properties: name: type: string @@ -1099,7 +1295,6 @@ components: format: uri example: http://www.example.com/file.txt description: Url to text file location , will be used by the processor - description: type: string description: A description of the test. @@ -1120,7 +1315,7 @@ components: dsl: description: A test that is made of scenarios base on domain specific language required: - - scenarios + - scenarios properties: scenarios: description: A scenario is a sequence of HTTP requests aimed to test the performance of specific functionality. @@ -1159,13 +1354,13 @@ components: basic: description: A test that is made of artillery json required: - - artillery_test + - artillery_test properties: artillery_test: type: object test_id: required: - - id + - id properties: id: type: string @@ -1173,9 +1368,9 @@ components: description: The id of the test. create_test_response: allOf: - - $ref: '#/components/schemas/test_id' + - $ref: '#/components/schemas/test_id' required: - - revision_id + - revision_id properties: revision_id: type: string @@ -1192,10 +1387,10 @@ components: dsl: '#/components/schemas/dsl_test_response' basic: '#/components/schemas/basic_test_response' required: - - type - - name - - description - - updated_at + - type + - name + - description + - updated_at properties: type: $ref: '#/components/schemas/test_type' @@ -1216,7 +1411,7 @@ components: type: object description: A test that is made of scenarios base on domain specific language allOf: - - $ref: '#/components/schemas/create_test_response' + - $ref: '#/components/schemas/create_test_response' properties: scenarios: description: A scenario is a sequence of HTTP requests aimed to test the performance of specific functionality. @@ -1318,10 +1513,10 @@ components: type: object description: bla allOf: - - $ref: '#/components/schemas/create_test_response' - - type: object - required: - - artillery_test + - $ref: '#/components/schemas/create_test_response' + - type: object + required: + - artillery_test properties: artillery_test: type: object @@ -1349,14 +1544,14 @@ components: } error_response: required: - - message + - message properties: message: type: string scenario: required: - - scenario_name - - steps + - scenario_name + - steps properties: scenario_name: type: string @@ -1368,7 +1563,7 @@ components: items: type: object required: - - action + - action properties: action: type: string @@ -1401,12 +1596,12 @@ components: example: 20 job: required: - - test_id - - arrival_rate - - duration + - test_id + - arrival_rate + - duration allOf: - - $ref: '#/components/schemas/job_update' - - type: object + - $ref: '#/components/schemas/job_update' + - type: object properties: run_immediately: type: boolean @@ -1477,32 +1672,75 @@ components: type: string description: set to * in order to print all requests and responeses of the runner example: test + force_download: + type: boolean + description: If job has a processor file of type file_download, then user has the ability to force download it or not upon each job start. + default: false job_response: allOf: - - $ref: '#/components/schemas/job' - - properties: - id: - type: string - pattern: >- - ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ - description: The job id. + - $ref: '#/components/schemas/job' + - properties: + id: + type: string + pattern: >- + ^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ + description: The job id. create_job_response: allOf: - - $ref: '#/components/schemas/job_response' - - properties: - run_id: - type: string - description: The run id. Only shown if the job is ran immediately. + - $ref: '#/components/schemas/job_response' + - properties: + run_id: + type: string + description: The run id. Only shown if the job is ran immediately. + processor: + discriminator: + propertyName: type + oneOf: + - $ref: '#/components/schemas/file_download' + - $ref: '#/components/schemas/raw_javascript' + required: + - name + - description + - type + properties: + name: + type: string + description: The name of the processor file. + example: Custom javascript for logging + description: + type: string + description: A description of the processor file. + example: logs every error (5xx). + type: + $ref: '#/components/schemas/processor_type' + processor_id: + description: Unique processor identifier + type: string + readOnly: true + updated_at: + type: string + format: date-time + description: The date and time that the procesor file was updated. + readOnly: true + created_at: + type: string + format: date-time + description: The date and time that the procesor file was created. + readOnly: true + javascript: + type: string + description: Raw javascript that will be used by the processor resource. + readOnly: true post_report: required: - - report_id - - job_id - - revision_id - - test_type - - test_name - - test_description - - start_time - - runner_id + - report_id + - job_id + - revision_id + - test_type + - test_name + - test_description + - start_time + - runner_id properties: report_id: type: string @@ -1536,16 +1774,16 @@ components: description: the id of the runner that created the report post_report_response: required: - - report_id + - report_id properties: report_id: type: string description: The id of the report/same as run_id stats: required: - - stats_time - - phase_status - - data + - stats_time + - phase_status + - data properties: stats_time: type: string @@ -1564,16 +1802,16 @@ components: description: Error thrown while running test report_response: required: - - test_id - - revision_id - - report_id - - job_id - - test_type - - status - - start_time - - end_time - - duration_seconds - - arrival_rate + - test_id + - revision_id + - report_id + - job_id + - test_type + - status + - start_time + - end_time + - duration_seconds + - arrival_rate properties: test_id: type: string @@ -1641,8 +1879,8 @@ components: metrics_plugin_name: type: string enum: - - influx - - prometheus + - influx + - prometheus description: Metrics to use, in case one or both metrics are configured. default_email_address: type: string @@ -1668,10 +1906,10 @@ components: type: object description: Influx configuration . required: - - host - - username - - password - - database + - host + - username + - password + - database properties: host: type: string @@ -1690,7 +1928,7 @@ components: type: object description: prometheus configuration. required: - - push_gateway_url + - push_gateway_url properties: push_gateway_url: type: string @@ -1703,12 +1941,12 @@ components: type: object description: Simple mail transfer protocol. required: - - from - - host - - port - - username - - password - - timeout + - from + - host + - port + - username + - password + - timeout properties: from: type: string @@ -1725,3 +1963,25 @@ components: timeout: description: timout to smtp server in milliseconds type: number + file_download: + description: Download processor file from the file_url and store it. + required: + - file_url + properties: + file_url: + type: string + description: url that the file can be downloaded from, should be direct download link. + raw_javascript: + description: Javascript file that will be used by the processor resource. + required: + - javascript + properties: + javascript: + type: string + description: Javascript as a string. + processor_type: + type: string + description: | + The type of the processor resource. Can be one of the following: + * `raw_javascript`: Raw javascript string that will be stored and persisted as it is. + * `file_download`: Predator will download the custom javascript by file_url and persist it. \ No newline at end of file From c58b7a1748636b3b9fe7cda720ce2c89db4230ea Mon Sep 17 00:00:00 2001 From: Eli Nudler Date: Fri, 9 Aug 2019 19:57:32 +0300 Subject: [PATCH 04/17] feat(api): add api spec for processors resource closes #185 fix #185 --- docs/openapi3.yaml | 47 +++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/docs/openapi3.yaml b/docs/openapi3.yaml index 8f2717849..ad8cc73dc 100644 --- a/docs/openapi3.yaml +++ b/docs/openapi3.yaml @@ -27,7 +27,7 @@ tags: Tests include end-to-end scenarios that are executed at pre-configured intervals to provide in-depth performance metrics of your API. - name: Processors description: | - Processor files allow Predator to execute custom javascript code during test flows. + Processor file allows Preadtor execute custom javascript code. - name: Jobs description: | Predator executes tests through so-called **jobs**. Depending on your configuration, the job will either execute immediately or at scheduled intervals. @@ -877,9 +877,10 @@ paths: name: limit description: The number of most recent reports to retrieve. required: true - minimum: 1 - maximum: 250 - type: integer + schema: + minimum: 1 + maximum: 250 + type: integer responses: '200': description: Success @@ -1059,12 +1060,11 @@ paths: parameters: - in: query name: from - description: From which result to start retrieve data + description: From which result to start retrieve data ( default is zero ) required: false schema: type: integer minimum: 0 - default: 0 - in: query name: limit description: Max results to return from this query ( Limit to 100 ) @@ -1072,7 +1072,6 @@ paths: schema: type: integer maximum: 100 - default: 100 responses: '200': description: Success @@ -1295,6 +1294,7 @@ components: format: uri example: http://www.example.com/file.txt description: Url to text file location , will be used by the processor + description: type: string description: A description of the test. @@ -1672,10 +1672,6 @@ components: type: string description: set to * in order to print all requests and responeses of the runner example: test - force_download: - type: boolean - description: If job has a processor file of type file_download, then user has the ability to force download it or not upon each job start. - default: false job_response: allOf: - $ref: '#/components/schemas/job' @@ -1696,8 +1692,8 @@ components: discriminator: propertyName: type oneOf: - - $ref: '#/components/schemas/file_download' - - $ref: '#/components/schemas/raw_javascript' + - $ref: '#/components/schemas/download' + - $ref: '#/components/schemas/raw' required: - name - description @@ -1719,17 +1715,17 @@ components: readOnly: true updated_at: type: string - format: date-time + format: date_time description: The date and time that the procesor file was updated. readOnly: true created_at: type: string - format: date-time + format: date_time description: The date and time that the procesor file was created. readOnly: true javascript: type: string - description: Raw javascript that will be used by the processor resource. + description: Javascript raw that will be used by the processor resource. readOnly: true post_report: required: @@ -1873,9 +1869,7 @@ components: runner_memory: type: number minimum: 128 - allow_insecure_tls: - type: boolean - description: If true, don't fail requests on unverified server certificate errors. + description: Max memory to use by each runner. metrics_plugin_name: type: string enum: @@ -1963,7 +1957,7 @@ components: timeout: description: timout to smtp server in milliseconds type: number - file_download: + download: description: Download processor file from the file_url and store it. required: - file_url @@ -1971,7 +1965,10 @@ components: file_url: type: string description: url that the file can be downloaded from, should be direct download link. - raw_javascript: + force_download: + type: boolean + description: wether to try download the file_url on every invoke by a test. + raw: description: Javascript file that will be used by the processor resource. required: - javascript @@ -1983,5 +1980,9 @@ components: type: string description: | The type of the processor resource. Can be one of the following: - * `raw_javascript`: Raw javascript string that will be stored and persisted as it is. - * `file_download`: Predator will download the custom javascript by file_url and persist it. \ No newline at end of file + * `raw`: Raw javascript string that will be stored and persisted as it is. + * `download`: Predator will download the custom javascript by file_url and persist it. + enum: + - dsl + - basic + example: dsl \ No newline at end of file From b2b21cd7fb41550285d2b6a91e4c08008188aa08 Mon Sep 17 00:00:00 2001 From: Eli Nudler Date: Fri, 9 Aug 2019 20:22:22 +0300 Subject: [PATCH 05/17] fix(typo): typo date_time to date-time --- docs/openapi3.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/openapi3.yaml b/docs/openapi3.yaml index ad8cc73dc..973862890 100644 --- a/docs/openapi3.yaml +++ b/docs/openapi3.yaml @@ -1715,12 +1715,12 @@ components: readOnly: true updated_at: type: string - format: date_time + format: date-time description: The date and time that the procesor file was updated. readOnly: true created_at: type: string - format: date_time + format: date-time description: The date and time that the procesor file was created. readOnly: true javascript: From 5079c5b4617cf42842f56b4c22502003eaed0158 Mon Sep 17 00:00:00 2001 From: Eli Nudler Date: Fri, 9 Aug 2019 21:17:55 +0300 Subject: [PATCH 06/17] fix(typo): typo date_time to date-time --- docs/openapi3.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/openapi3.yaml b/docs/openapi3.yaml index 973862890..15a5b606e 100644 --- a/docs/openapi3.yaml +++ b/docs/openapi3.yaml @@ -877,10 +877,9 @@ paths: name: limit description: The number of most recent reports to retrieve. required: true - schema: - minimum: 1 - maximum: 250 - type: integer + minimum: 1 + maximum: 250 + type: integer responses: '200': description: Success From 5c0491a549aa106bc0674321ef54cd77171b9f1e Mon Sep 17 00:00:00 2001 From: Eli Nudler Date: Sat, 17 Aug 2019 10:31:09 +0300 Subject: [PATCH 07/17] chore(openapi3): code review comments addressed --- docs/openapi3.yaml | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/docs/openapi3.yaml b/docs/openapi3.yaml index 15a5b606e..2072f9367 100644 --- a/docs/openapi3.yaml +++ b/docs/openapi3.yaml @@ -27,7 +27,7 @@ tags: Tests include end-to-end scenarios that are executed at pre-configured intervals to provide in-depth performance metrics of your API. - name: Processors description: | - Processor file allows Preadtor execute custom javascript code. + Processor files allow Predator to execute custom javascript code during test flows. - name: Jobs description: | Predator executes tests through so-called **jobs**. Depending on your configuration, the job will either execute immediately or at scheduled intervals. @@ -1059,11 +1059,12 @@ paths: parameters: - in: query name: from - description: From which result to start retrieve data ( default is zero ) + description: From which result to start retrieve data required: false schema: type: integer minimum: 0 + default: 0 - in: query name: limit description: Max results to return from this query ( Limit to 100 ) @@ -1071,6 +1072,7 @@ paths: schema: type: integer maximum: 100 + default: 100 responses: '200': description: Success @@ -1293,7 +1295,6 @@ components: format: uri example: http://www.example.com/file.txt description: Url to text file location , will be used by the processor - description: type: string description: A description of the test. @@ -1671,6 +1672,10 @@ components: type: string description: set to * in order to print all requests and responeses of the runner example: test + force_download: + type: boolean + description: If job has a processor file of type file_download, then user has the ability to force download it or not upon each job start. + default: false job_response: allOf: - $ref: '#/components/schemas/job' @@ -1691,8 +1696,8 @@ components: discriminator: propertyName: type oneOf: - - $ref: '#/components/schemas/download' - - $ref: '#/components/schemas/raw' + - $ref: '#/components/schemas/file_download' + - $ref: '#/components/schemas/raw_javascript' required: - name - description @@ -1724,7 +1729,7 @@ components: readOnly: true javascript: type: string - description: Javascript raw that will be used by the processor resource. + description: Raw javascript that will be used by the processor resource. readOnly: true post_report: required: @@ -1956,7 +1961,7 @@ components: timeout: description: timout to smtp server in milliseconds type: number - download: + file_download: description: Download processor file from the file_url and store it. required: - file_url @@ -1964,10 +1969,7 @@ components: file_url: type: string description: url that the file can be downloaded from, should be direct download link. - force_download: - type: boolean - description: wether to try download the file_url on every invoke by a test. - raw: + raw_javascript: description: Javascript file that will be used by the processor resource. required: - javascript @@ -1979,9 +1981,5 @@ components: type: string description: | The type of the processor resource. Can be one of the following: - * `raw`: Raw javascript string that will be stored and persisted as it is. - * `download`: Predator will download the custom javascript by file_url and persist it. - enum: - - dsl - - basic - example: dsl \ No newline at end of file + * `raw_javascript`: Raw javascript string that will be stored and persisted as it is. + * `file_download`: Predator will download the custom javascript by file_url and persist it. \ No newline at end of file From 122cd04ba1f2ac3d30fc208c6d985c3ccb3cf3f8 Mon Sep 17 00:00:00 2001 From: NivLipetz Date: Thu, 22 Aug 2019 08:32:28 +0300 Subject: [PATCH 08/17] docs(api): add download endpoint for processor files --- docs/openapi3.yaml | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/docs/openapi3.yaml b/docs/openapi3.yaml index 2072f9367..657be313d 100644 --- a/docs/openapi3.yaml +++ b/docs/openapi3.yaml @@ -1205,6 +1205,41 @@ paths: application/json: schema: $ref: '#/components/schemas/error_response' + '/v1/processors/{processor_id}/download': + post: + operationId: update-processor-file-content + tags: + - Processors + summary: Download the file and update its javascript content. + description: Download the file from the 'file_url' configured for the processor and update its javascript content. If download fails, keep the original javascript content without overriding it. + parameters: + - in: path + name: processor_id + description: The id of the processor file to download. + required: true + schema: + type: string + responses: + '204': + description: Success + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '404': + description: Not found + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/error_response' components: parameters: @@ -1672,10 +1707,6 @@ components: type: string description: set to * in order to print all requests and responeses of the runner example: test - force_download: - type: boolean - description: If job has a processor file of type file_download, then user has the ability to force download it or not upon each job start. - default: false job_response: allOf: - $ref: '#/components/schemas/job' From 577a5d7d4c7cf34cba80bb1702f931bee80978df Mon Sep 17 00:00:00 2001 From: Daniel Hermon Date: Thu, 3 Oct 2019 18:21:41 +0300 Subject: [PATCH 09/17] chore(dependencies): update cassandra-driver to v4.1.0 (#210) --- package-lock.json | 16 +++++++++++++--- package.json | 2 +- src/config/databaseConfig.js | 4 ++-- src/database/cassandra-handler/cassandra.js | 3 ++- .../cassandra-handler/cassandraMigration.js | 3 ++- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 642b152ba..f926b4f4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1396,9 +1396,9 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "cassandra-driver": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-3.6.0.tgz", - "integrity": "sha512-CkN3V+oPaF5RvakUjD3uUjEm8f6U8S0aT1+YqeQsVT3UDpPT2K8SOdNDEHA1KjamakHch6zkDgHph1xWyqBGGw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.1.0.tgz", + "integrity": "sha512-MHI8PcCCaPEa9DKsP+L6MsokrK5Twhu4UBb3reyEAezB0XFhu7DnjDcfWuDHpzmuCY3OSmgskO1pnoz1GsRIIA==", "requires": { "long": "^2.2.0" } @@ -1413,6 +1413,16 @@ "durations": "^3.4.1", "lodash": "^4.13.1", "q": "^1.4" + }, + "dependencies": { + "cassandra-driver": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-3.6.0.tgz", + "integrity": "sha512-CkN3V+oPaF5RvakUjD3uUjEm8f6U8S0aT1+YqeQsVT3UDpPT2K8SOdNDEHA1KjamakHch6zkDgHph1xWyqBGGw==", + "requires": { + "long": "^2.2.0" + } + } } }, "chai": { diff --git a/package.json b/package.json index 47528edf0..4438c3337 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "bluebird": "^3.5.5", "body-parser": "^1.19.0", "bunyan": "^1.8.12", - "cassandra-driver": "^3.5.0", + "cassandra-driver": "^4.1.0", "cassandra-migration": "^2.7.0", "copy-dir": "^0.3.0", "cron": "^1.7.1", diff --git a/src/config/databaseConfig.js b/src/config/databaseConfig.js index bd4a74202..0fdb48f0f 100644 --- a/src/config/databaseConfig.js +++ b/src/config/databaseConfig.js @@ -8,7 +8,7 @@ const config = { cassandraReplicationFactor: process.env.CASSANDRA_REPLICATION_FACTOR || 1, cassandraConsistency: getCassandraConsistencyByName(process.env.CASSANDRA_CONSISTENCY), cassandraKeyspaceStrategy: process.env.CASSANDRA_KEY_SPACE_STRATEGY || 'SimpleStrategy', - cassandraLocalDataCenter: process.env.CASSANDRA_LOCAL_DATA_CENTER, + cassandraLocalDataCenter: process.env.CASSANDRA_LOCAL_DATA_CENTER || 'datacenter1', sqliteStorage: process.env.SQLITE_STORAGE || 'predator' }; @@ -20,4 +20,4 @@ function getCassandraConsistencyByName(cassandraConsistencyName) { return consistency; } -module.exports = config; \ No newline at end of file +module.exports = config; diff --git a/src/database/cassandra-handler/cassandra.js b/src/database/cassandra-handler/cassandra.js index cf9b12aba..0a88b38df 100644 --- a/src/database/cassandra-handler/cassandra.js +++ b/src/database/cassandra-handler/cassandra.js @@ -58,7 +58,8 @@ async function createClient() { const config = { contactPoints: String(databaseConfig.address).split(','), keyspace: databaseConfig.name, - authProvider + authProvider, + localDataCenter: databaseConfig.cassandraLocalDataCenter }; let cassandraClient = new cassandra.Client(config); diff --git a/src/database/cassandra-handler/cassandraMigration.js b/src/database/cassandra-handler/cassandraMigration.js index 7a59ad298..3c90be75c 100644 --- a/src/database/cassandra-handler/cassandraMigration.js +++ b/src/database/cassandra-handler/cassandraMigration.js @@ -213,7 +213,8 @@ function buildClient(keyspace) { let authProvider = new cassandra.auth.PlainTextAuthProvider(args.cassandra_username, args.cassandra_password); let cassandraClient = { contactPoints: args.cassandra_url.split(','), - authProvider: authProvider + authProvider: authProvider, + localDataCenter: args.cassandra_local_data_center }; if (keyspace) { From d7d81e7ed88937494cdb6abdc27766805c38ef25 Mon Sep 17 00:00:00 2001 From: NivLipetz Date: Sat, 12 Oct 2019 12:09:09 +0300 Subject: [PATCH 10/17] chore: fix rebase to master --- docs/openapi3.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/openapi3.yaml b/docs/openapi3.yaml index 657be313d..2f901ff0d 100644 --- a/docs/openapi3.yaml +++ b/docs/openapi3.yaml @@ -1904,7 +1904,9 @@ components: runner_memory: type: number minimum: 128 - description: Max memory to use by each runner. + allow_insecure_tls: + type: boolean + description: If true, don't fail requests on unverified server certificate errors. metrics_plugin_name: type: string enum: From a0e31aef64b83191bdfe29e5247c65170a82b4a3 Mon Sep 17 00:00:00 2001 From: NivLipetz Date: Tue, 15 Oct 2019 08:47:48 +0300 Subject: [PATCH 11/17] feat(custom-js): implement POST /processors --- src/common/consts.js | 3 ++ src/database/cassandra-handler/cassandra.js | 2 + .../14__create_processors_table.cql | 10 ++++ src/database/sequlize-handler/sequlize.js | 2 + .../controllers/processorController.js | 12 +++++ .../database/cassandra/cassandraConnector.js | 38 ++++++++++++++ .../models/database/databaseConnector.js | 24 +++++++++ .../database/sequelize/sequelizeConnector.js | 52 +++++++++++++++++++ src/processors/models/processorsManager.js | 23 ++++++++ src/processors/routes/processorsRoute.js | 11 ++++ src/tests/models/fileManager.js | 7 +-- 11 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 src/database/cassandra-handler/init-scripts/14__create_processors_table.cql create mode 100644 src/processors/controllers/processorController.js create mode 100644 src/processors/models/database/cassandra/cassandraConnector.js create mode 100644 src/processors/models/database/databaseConnector.js create mode 100644 src/processors/models/database/sequelize/sequelizeConnector.js create mode 100644 src/processors/models/processorsManager.js create mode 100644 src/processors/routes/processorsRoute.js diff --git a/src/common/consts.js b/src/common/consts.js index 0657b1e26..5665a135e 100644 --- a/src/common/consts.js +++ b/src/common/consts.js @@ -1,6 +1,9 @@ module.exports = { TEST_TYPE_BASIC: 'basic', TEST_TYPE_DSL: 'dsl', + PROCESSOR_TYPE_FILE_DOWNLOAD: 'file_download', + PROCESSOR_TYPE_RAW_JAVASCRIPT: 'raw_javascript', + ERROR_MESSAGES: { NOT_FOUND: 'Not found', DSL_DEF_ALREADY_EXIST: 'Definition already exists' diff --git a/src/database/cassandra-handler/cassandra.js b/src/database/cassandra-handler/cassandra.js index 0a88b38df..f906784f3 100644 --- a/src/database/cassandra-handler/cassandra.js +++ b/src/database/cassandra-handler/cassandra.js @@ -4,6 +4,7 @@ const schedulerCassandraConnector = require('../../jobs/models/database/cassandr const reportsCassandraConnector = require('../../reports/models/database/cassandra/cassandraConnector'); const testsCassandraConnector = require('../../tests/models/database/cassandra/cassandraConnector'); const configCassandraConnector = require('../../configManager/models/database/cassandra/cassandraConnector'); +const processorsSequlizeConnector = require('../../processors/models/database/cassandra/cassandraConnector'); const databaseConfig = require('../../config/databaseConfig'); const cassandraMigration = require('./cassandraMigration'); const logger = require('../../common/logger'); @@ -17,6 +18,7 @@ module.exports.init = async () => { await schedulerCassandraConnector.init(cassandraClient); await testsCassandraConnector.init(cassandraClient); await configCassandraConnector.init(cassandraClient); + await processorsSequlizeConnector.init(cassandraClient); logger.info('cassandra client initialized'); }; diff --git a/src/database/cassandra-handler/init-scripts/14__create_processors_table.cql b/src/database/cassandra-handler/init-scripts/14__create_processors_table.cql new file mode 100644 index 000000000..bdf0d4c73 --- /dev/null +++ b/src/database/cassandra-handler/init-scripts/14__create_processors_table.cql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS processors +( + processor_id uuid, + name text, + description text, + type text, + file_url text, + javascript text, + PRIMARY KEY (processor_id) +); diff --git a/src/database/sequlize-handler/sequlize.js b/src/database/sequlize-handler/sequlize.js index acda53091..556da6235 100644 --- a/src/database/sequlize-handler/sequlize.js +++ b/src/database/sequlize-handler/sequlize.js @@ -6,6 +6,7 @@ const schedulerSequlizeConnector = require('../../jobs/models/database/sequelize const reportsSequlizeConnector = require('../../reports/models/database/sequelize/sequelizeConnector'); const testsSequlizeConnector = require('../../tests/models/database/sequelize/sequelizeConnector'); const configSequlizeConnector = require('../../configManager/models/database/sequelize/sequelizeConnector'); +const processorsSequlizeConnector = require('../../processors/models/database/sequelize/sequelizeConnector'); const logger = require('../../../src/common/logger'); const databaseConfig = require('../../config/databaseConfig'); const Sequelize = require('sequelize'); @@ -17,6 +18,7 @@ module.exports.init = async () => { await reportsSequlizeConnector.init(sequlizeClient); await testsSequlizeConnector.init(sequlizeClient); await configSequlizeConnector.init(sequlizeClient); + await processorsSequlizeConnector.init(sequlizeClient); await runSequlizeMigrations(); }; diff --git a/src/processors/controllers/processorController.js b/src/processors/controllers/processorController.js new file mode 100644 index 000000000..c6d72aeb4 --- /dev/null +++ b/src/processors/controllers/processorController.js @@ -0,0 +1,12 @@ +'use strict'; +let processorManager = require('../models/processorManager'); + +module.exports.createProcessor = function (req, res, next) { + return processorManager.createProcessor(req.body) + .then(function (result) { + return res.status(201).json(result); + }) + .catch(function (err) { + return next(err); + }); +}; \ No newline at end of file diff --git a/src/processors/models/database/cassandra/cassandraConnector.js b/src/processors/models/database/cassandra/cassandraConnector.js new file mode 100644 index 000000000..375b69331 --- /dev/null +++ b/src/processors/models/database/cassandra/cassandraConnector.js @@ -0,0 +1,38 @@ +let logger = require('../../../../common/logger'); +let databaseConfig = require('../../../../config/databaseConfig'); +let client; + +const INSERT_PROCESSOR = 'INSERT INTO processors(processor_id, name, description, type, file_url, javascript) values(?,?,?,?,?,?)'; + +module.exports = { + init, + insertProcessor +}; + +let queryOptions = { + consistency: databaseConfig.cassandraConsistency, + prepare: true +}; + +async function init(cassandraClient) { + client = cassandraClient; +} + +function insertProcessor(processorId, processorInfo) { + let params = [processorId, processorInfo.name, processorInfo.description, processorInfo.type, processorInfo.file_url, processorInfo.javascript]; + return executeQuery(INSERT_PROCESSOR, params, queryOptions); +} + +function executeQuery(query, params, queryOptions) { + return client.execute(query, params, { prepare: true }, queryOptions).then((result) => { + logger.trace('Query result', { + query: query, + params: params, + rows_returned: result.rowLength + }); + return Promise.resolve(result.rows ? result.rows : []); + }).catch((exception) => { + logger.error(`Cassandra query failed \n ${JSON.stringify({ query, params, queryOptions })}`, exception); + return Promise.reject(new Error('Error occurred in communication with cassandra')); + }); +} \ No newline at end of file diff --git a/src/processors/models/database/databaseConnector.js b/src/processors/models/database/databaseConnector.js new file mode 100644 index 000000000..254f56ef3 --- /dev/null +++ b/src/processors/models/database/databaseConnector.js @@ -0,0 +1,24 @@ +'use strict'; + +let databaseConfig = require('../../../config/databaseConfig'); +let cassandraConnector = require('./cassandra/cassandraConnector'); +let sequelizeConnector = require('./sequelize/sequelizeConnector'); +let databaseConnector = databaseConfig.type.toLowerCase() === 'cassandra' ? cassandraConnector : sequelizeConnector; + +module.exports = { + init, + closeConnection, + insertProcessor +}; + +async function insertProcessor(jobId, jobInfo) { + return databaseConnector.insertProcessor(jobId, jobInfo); +} + +async function init() { + return databaseConnector.init(); +} + +function closeConnection() { + return databaseConnector.closeConnection(); +} \ No newline at end of file diff --git a/src/processors/models/database/sequelize/sequelizeConnector.js b/src/processors/models/database/sequelize/sequelizeConnector.js new file mode 100644 index 000000000..49beec10a --- /dev/null +++ b/src/processors/models/database/sequelize/sequelizeConnector.js @@ -0,0 +1,52 @@ +'use strict'; + +const Sequelize = require('sequelize'); +let client; + +module.exports = { + init, + insertProcessor +}; + +async function init(sequelizeClient) { + client = sequelizeClient; + await initSchemas(); +} + +async function insertProcessor(processorId, processorInfo) { + const processor = client.model('processor'); + let params = { + processor_id: processorId, + name: processorInfo.name, + description: processorInfo.description, + type: processorInfo.type, + file_url: processorInfo.file_url, + javascript: processorInfo.javascript + }; + return processor.create(params); +} + +async function initSchemas() { + const processorsFiles = client.define('processors', { + processor_id: { + type: Sequelize.DataTypes.UUID, + primaryKey: true + }, + name: { + type: Sequelize.DataTypes.TEXT('medium') + }, + description: { + type: Sequelize.DataTypes.TEXT('long') + }, + type: { + type: Sequelize.DataTypes.TEXT('medium') + }, + file_url: { + type: Sequelize.DataTypes.TEXT('long') + }, + javascript: { + type: Sequelize.DataTypes.TEXT('long') + } + }); + await processorsFiles.sync(); +} diff --git a/src/processors/models/processorsManager.js b/src/processors/models/processorsManager.js new file mode 100644 index 000000000..a9072cd5e --- /dev/null +++ b/src/processors/models/processorsManager.js @@ -0,0 +1,23 @@ +'use strict'; + +const uuid = require('uuid'); + +const logger = require('../../common/logger'), + databaseConnector = require('./database/databaseConnector'), + common = require('../../common/consts.js'), + fileManager = require('../../tests/models/fileManager.js'); + +module.exports.createProcessor = async function (processor) { + let processorId = uuid.v4(); + try { + if (processor.type === common.PROCESSOR_TYPE_FILE_DOWNLOAD) { + const file = await fileManager.downloadFile(processor.file_url); + processor.javascript = file.body; // TODO check dis + } + await databaseConnector.insertProcessor(processorId, processor); + logger.info('Processor saved successfully to database'); + } catch (error) { + logger.error(error, 'Error occurred trying to create new processor'); + return Promise.reject(error); + } +}; \ No newline at end of file diff --git a/src/processors/routes/processorsRoute.js b/src/processors/routes/processorsRoute.js new file mode 100644 index 000000000..111cec906 --- /dev/null +++ b/src/processors/routes/processorsRoute.js @@ -0,0 +1,11 @@ +'use strict'; + +let swaggerValidator = require('express-ajv-swagger-validation'); +let express = require('express'); +let router = express.Router(); + +let processors = require('../controllers/processorController'); + +router.post('/', swaggerValidator.validate, processors.createProcessor); + +module.exports = router; \ No newline at end of file diff --git a/src/tests/models/fileManager.js b/src/tests/models/fileManager.js index 97af0dd88..b6d03e9d4 100644 --- a/src/tests/models/fileManager.js +++ b/src/tests/models/fileManager.js @@ -6,6 +6,7 @@ const database = require('./database'), module.exports = { createFileFromUrl, + downloadFile, getFile }; async function createFileFromUrl(testRawData) { @@ -21,8 +22,7 @@ async function downloadFile(fileUrl) { }; try { const response = await request.get(options); - const base64Value = Buffer.from(response).toString('base64'); - return base64Value; + return response; } catch (err) { const errMsg = 'Error to read file, throw exception: ' + err; const error = new Error(errMsg); @@ -45,6 +45,7 @@ async function getFile(fileId) { async function saveFile(fileUrl) { const id = uuid(); const fileToSave = await downloadFile(fileUrl); - await database.saveFile(id, fileToSave); + const fileBase64Value = Buffer.from(fileToSave).toString('base64'); + await database.saveFile(id, fileBase64Value); return id; } From f0223ed0a4074932a29be8254aaae672c2f21454 Mon Sep 17 00:00:00 2001 From: Niv Lipetz Date: Tue, 15 Oct 2019 23:37:00 +0300 Subject: [PATCH 12/17] test(processors): add tests for POST /processors (#217) * test(processors): add tests for POST /processors --- src/app.js | 2 + .../controllers/processorController.js | 2 +- .../database/sequelize/sequelizeConnector.js | 2 +- src/processors/models/processorsManager.js | 3 +- .../processors/helpers/requestCreator.js | 25 ++++ .../processors/processors-test.js | 108 ++++++++++++++++++ 6 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 tests/integration-tests/processors/helpers/requestCreator.js create mode 100644 tests/integration-tests/processors/processors-test.js diff --git a/src/app.js b/src/app.js index 02efb6164..17db4fe54 100644 --- a/src/app.js +++ b/src/app.js @@ -8,6 +8,7 @@ let reportsRouter = require('./reports/routes/reportsRoute.js'); let configRouter = require('./configManager/routes/configRoute.js'); let dslRouter = require('./tests/routes/dslRoute.js'); let testsRouter = require('./tests/routes/testsRoute.js'); +let processorssRouter = require('./processors/routes/processorsRoute.js'); let swaggerValidator = require('express-ajv-swagger-validation'); let audit = require('express-requests-logger'); @@ -56,6 +57,7 @@ module.exports = () => { app.use('/v1/dsl', dslRouter); app.use('/v1/tests', reportsRouter); app.use('/v1/tests', testsRouter); + app.use('/v1/processors', processorssRouter); app.use('/', function (req, res, next) { res.redirect('/ui'); diff --git a/src/processors/controllers/processorController.js b/src/processors/controllers/processorController.js index c6d72aeb4..78a598241 100644 --- a/src/processors/controllers/processorController.js +++ b/src/processors/controllers/processorController.js @@ -1,5 +1,5 @@ 'use strict'; -let processorManager = require('../models/processorManager'); +let processorManager = require('../models/processorsManager'); module.exports.createProcessor = function (req, res, next) { return processorManager.createProcessor(req.body) diff --git a/src/processors/models/database/sequelize/sequelizeConnector.js b/src/processors/models/database/sequelize/sequelizeConnector.js index 49beec10a..ca0621559 100644 --- a/src/processors/models/database/sequelize/sequelizeConnector.js +++ b/src/processors/models/database/sequelize/sequelizeConnector.js @@ -27,7 +27,7 @@ async function insertProcessor(processorId, processorInfo) { } async function initSchemas() { - const processorsFiles = client.define('processors', { + const processorsFiles = client.define('processor', { processor_id: { type: Sequelize.DataTypes.UUID, primaryKey: true diff --git a/src/processors/models/processorsManager.js b/src/processors/models/processorsManager.js index a9072cd5e..9d3b10fdd 100644 --- a/src/processors/models/processorsManager.js +++ b/src/processors/models/processorsManager.js @@ -12,10 +12,11 @@ module.exports.createProcessor = async function (processor) { try { if (processor.type === common.PROCESSOR_TYPE_FILE_DOWNLOAD) { const file = await fileManager.downloadFile(processor.file_url); - processor.javascript = file.body; // TODO check dis + processor.javascript = file; } await databaseConnector.insertProcessor(processorId, processor); logger.info('Processor saved successfully to database'); + return processor; } catch (error) { logger.error(error, 'Error occurred trying to create new processor'); return Promise.reject(error); diff --git a/tests/integration-tests/processors/helpers/requestCreator.js b/tests/integration-tests/processors/helpers/requestCreator.js new file mode 100644 index 000000000..ae9de1011 --- /dev/null +++ b/tests/integration-tests/processors/helpers/requestCreator.js @@ -0,0 +1,25 @@ + +const request = require('supertest'), + expressApp = require('../../../../src/app'); +let app; +module.exports = { + init, + createProcessor +}; +async function init() { + try { + app = await expressApp(); + } catch (err){ + console.log(err); + process.exit(1); + } +} + +function createProcessor(body, headers) { + return request(app).post('/v1/processors') + .send(body) + .set(headers) + .expect(function(res){ + return res; + }); +} \ No newline at end of file diff --git a/tests/integration-tests/processors/processors-test.js b/tests/integration-tests/processors/processors-test.js new file mode 100644 index 000000000..bd538c9be --- /dev/null +++ b/tests/integration-tests/processors/processors-test.js @@ -0,0 +1,108 @@ +const should = require('should'), + nock = require('nock'); + +let validHeaders = { 'Content-Type': 'application/json' }; +const requestSender = require('./helpers/requestCreator'); +describe('Processors api', function() { + this.timeout(5000000); + before(async function () { + await requestSender.init(); + }); + + describe('Good requests', function() { + it('Create processor with type file_download', async () => { + nock('https://authentication.predator.dev').get('/?dl=1').reply(200, + `{ + const uuid = require('uuid/v4'); + module.exports = { + createAuthToken + }; + + function createAuthToken(userContext, events, done) { + userContext.vars.token = uuid(); + return done(); + } + }` + ); + + const requestBody = { + name: 'authentication', + description: 'Creates authorization token and saves it in the context', + type: 'file_download', + file_url: 'https://authentication.predator.dev/?dl=1' + }; + let createProcessorResponse = await requestSender.createProcessor(requestBody, validHeaders); + createProcessorResponse.statusCode.should.eql(201); + }); + + it('Create processor with type raw_javascript', async () => { + const requestBody = { + name: 'authentication', + description: 'Creates authorization token and saves it in the context', + type: 'raw_javascript', + javascript: + `{ + const uuid = require('uuid/v4'); + module.exports = { + createAuthToken + }; + + function createAuthToken(userContext, events, done) { + userContext.vars.token = uuid(); + return done(); + } + }` + }; + let createProcessorResponse = await requestSender.createProcessor(requestBody, validHeaders); + createProcessorResponse.statusCode.should.eql(201); + }); + }); + + describe('Bad requests', function () { + it('Create processor with unknown type', async () => { + const requestBody = { + name: 'bad-processor', + description: 'Processor with unknown type', + type: 'unknown' + }; + let createProcessorResponse = await requestSender.createProcessor(requestBody, validHeaders); + createProcessorResponse.statusCode.should.eql(400); + }); + + it('Create processor with type file_download and no url', async () => { + const requestBody = { + name: 'download-me', + description: 'Processor with no file url', + type: 'file_download' + }; + let createProcessorResponse = await requestSender.createProcessor(requestBody, validHeaders); + createProcessorResponse.statusCode.should.eql(400); + }); + + it('Create processor with type raw_javascript and no js', async () => { + const requestBody = { + name: 'javascript-me', + description: 'Processor with no js', + type: 'raw_javascript', + file_url: 'bad' + }; + let createProcessorResponse = await requestSender.createProcessor(requestBody, validHeaders); + createProcessorResponse.statusCode.should.eql(400); + }); + }); + + describe('Sad requests', function () { + it('Create processor with type file_download and invalid file_url', async () => { + nock('https://authentication.predator.dev').get('/?dl=1').replyWithError('error downloading file'); + + const requestBody = { + name: 'authentication', + description: 'Creates authorization token and saves it in the context', + type: 'file_download', + file_url: 'https://authentication.predator.dev/?dl=1' + }; + let createProcessorResponse = await requestSender.createProcessor(requestBody, validHeaders); + createProcessorResponse.statusCode.should.eql(422); + }); + }); +}); \ No newline at end of file From f7a41e6b38e2b326ea7bbe8d6de3887b8a380c6b Mon Sep 17 00:00:00 2001 From: NivLipetz Date: Tue, 15 Oct 2019 23:41:59 +0300 Subject: [PATCH 13/17] style(processors): fix processors cassandra connector name --- src/database/cassandra-handler/cassandra.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/cassandra-handler/cassandra.js b/src/database/cassandra-handler/cassandra.js index f906784f3..a0b4c2059 100644 --- a/src/database/cassandra-handler/cassandra.js +++ b/src/database/cassandra-handler/cassandra.js @@ -4,7 +4,7 @@ const schedulerCassandraConnector = require('../../jobs/models/database/cassandr const reportsCassandraConnector = require('../../reports/models/database/cassandra/cassandraConnector'); const testsCassandraConnector = require('../../tests/models/database/cassandra/cassandraConnector'); const configCassandraConnector = require('../../configManager/models/database/cassandra/cassandraConnector'); -const processorsSequlizeConnector = require('../../processors/models/database/cassandra/cassandraConnector'); +const processorsCassandraConnector = require('../../processors/models/database/cassandra/cassandraConnector'); const databaseConfig = require('../../config/databaseConfig'); const cassandraMigration = require('./cassandraMigration'); const logger = require('../../common/logger'); @@ -18,7 +18,7 @@ module.exports.init = async () => { await schedulerCassandraConnector.init(cassandraClient); await testsCassandraConnector.init(cassandraClient); await configCassandraConnector.init(cassandraClient); - await processorsSequlizeConnector.init(cassandraClient); + await processorsCassandraConnector.init(cassandraClient); logger.info('cassandra client initialized'); }; From e62f7d28811b32d0e62d4e961f175fe850bd3f9a Mon Sep 17 00:00:00 2001 From: NivLipetz Date: Sat, 19 Oct 2019 18:29:27 +0300 Subject: [PATCH 14/17] feat(processors): add js validation on POST /processors --- package-lock.json | 13 +++++-- package.json | 1 + src/processors/models/processorsManager.js | 4 +- src/tests/models/fileManager.js | 39 +++++++++++++++++-- .../processors/processors-test.js | 26 +++++++++++++ 5 files changed, 74 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index f926b4f4b..38667149b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4333,6 +4333,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.3.tgz", "integrity": "sha512-B0W4A2U1ww3q7VVthTKfh+epHx+q4mCt6iK+zEAzbMBpWQAwxCeKxEGpj/1oQTpzPXDNSOG7hmG14TsISH50yw==", + "dev": true, "requires": { "neo-async": "^2.6.0", "optimist": "^0.6.1", @@ -6190,7 +6191,8 @@ "neo-async": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true }, "nested-error-stacks": { "version": "2.1.0", @@ -6582,6 +6584,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, "requires": { "minimist": "~0.0.1", "wordwrap": "~0.0.2" @@ -6590,12 +6593,14 @@ "minimist": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true } } }, @@ -9321,6 +9326,7 @@ "version": "3.6.1", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.1.tgz", "integrity": "sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ==", + "dev": true, "optional": true, "requires": { "commander": "2.20.0", @@ -9331,6 +9337,7 @@ "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true, "optional": true } } diff --git a/package.json b/package.json index 4438c3337..d874f0bb8 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "copy-dir": "^0.3.0", "cron": "^1.7.1", "dockerode": "^2.5.8", + "esprima": "^4.0.1", "express": "^4.17.1", "express-ajv-swagger-validation": "^0.9.0", "express-easy-zip": "^1.1.4", diff --git a/src/processors/models/processorsManager.js b/src/processors/models/processorsManager.js index 9d3b10fdd..30a92ae6f 100644 --- a/src/processors/models/processorsManager.js +++ b/src/processors/models/processorsManager.js @@ -11,9 +11,9 @@ module.exports.createProcessor = async function (processor) { let processorId = uuid.v4(); try { if (processor.type === common.PROCESSOR_TYPE_FILE_DOWNLOAD) { - const file = await fileManager.downloadFile(processor.file_url); - processor.javascript = file; + processor.javascript = await fileManager.downloadFile(processor.file_url); } + fileManager.validateJavascriptContent(processor.javascript); await databaseConnector.insertProcessor(processorId, processor); logger.info('Processor saved successfully to database'); return processor; diff --git a/src/tests/models/fileManager.js b/src/tests/models/fileManager.js index b6d03e9d4..f418591ac 100644 --- a/src/tests/models/fileManager.js +++ b/src/tests/models/fileManager.js @@ -1,13 +1,16 @@ 'use strict'; -const database = require('./database'), - uuid = require('uuid'), +const uuid = require('uuid'), request = require('request-promise-native'), + esprima = require('esprima'); + +const database = require('./database'), { ERROR_MESSAGES } = require('../../common/consts'); module.exports = { createFileFromUrl, downloadFile, - getFile + getFile, + validateJavascriptContent }; async function createFileFromUrl(testRawData) { if (testRawData['processor_file_url']) { @@ -16,6 +19,7 @@ async function createFileFromUrl(testRawData) { } return undefined; } + async function downloadFile(fileUrl) { const options = { url: fileUrl @@ -24,7 +28,7 @@ async function downloadFile(fileUrl) { const response = await request.get(options); return response; } catch (err) { - const errMsg = 'Error to read file, throw exception: ' + err; + const errMsg = 'Error to download file: ' + err; const error = new Error(errMsg); error.statusCode = 422; throw error; @@ -49,3 +53,30 @@ async function saveFile(fileUrl) { await database.saveFile(id, fileBase64Value); return id; } + +function validateJavascriptContent (javascriptFileContent) { + let error, errorMessage; + try { + const syntax = esprima.parseScript(javascriptFileContent, { tolerant: true }); + const errors = syntax.errors; + if (errors.length > 0) { + let errorsString = ''; + for (let i = 0; i < errors.length; i++) { + errorsString += errors[i].description + ', '; + } + errorsString = errorsString.substring(0, errorsString.length - 2); + + errorMessage = 'js syntax validation failed with error: ' + errorsString; + error = new Error(errorMessage); + error.statusCode = 422; + } + } catch (err) { + errorMessage = err.description; + error = new Error(errorMessage); + error.statusCode = 422; + } + + if (error) { + throw error; + } +} \ No newline at end of file diff --git a/tests/integration-tests/processors/processors-test.js b/tests/integration-tests/processors/processors-test.js index bd538c9be..c90c9c001 100644 --- a/tests/integration-tests/processors/processors-test.js +++ b/tests/integration-tests/processors/processors-test.js @@ -104,5 +104,31 @@ describe('Processors api', function() { let createProcessorResponse = await requestSender.createProcessor(requestBody, validHeaders); createProcessorResponse.statusCode.should.eql(422); }); + + it('Create processor with type file_download and invalid js syntax', async () => { + nock('https://authentication.predator.dev').get('/?dl=1').reply(200, + `{ + const uuid = require('uuid/v4'); + module.exports = { + createAuthToken + }; + + function createAuthToken(userContext, events, done) { + userContext.vars.token = uuid(); + return done(); + } + + this is not valid javascript + }` + ); + const requestBody = { + name: 'authentication', + description: 'Creates authorization token and saves it in the context', + type: 'file_download', + file_url: 'https://authentication.predator.dev/?dl=1' + }; + let createProcessorResponse = await requestSender.createProcessor(requestBody, validHeaders); + createProcessorResponse.statusCode.should.eql(422); + }); }); }); \ No newline at end of file From 5a18c56f6ac831b9a93ad3b26e3c7266dd674c5c Mon Sep 17 00:00:00 2001 From: NivLipetz Date: Sun, 20 Oct 2019 14:13:30 +0300 Subject: [PATCH 15/17] fix(processors): add updated_at/created_at to processors resource --- docs/openapi3.yaml | 3 +++ .../init-scripts/14__create_processors_table.cql | 2 ++ .../models/database/cassandra/cassandraConnector.js | 4 ++-- .../models/database/sequelize/sequelizeConnector.js | 10 +++++++++- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/openapi3.yaml b/docs/openapi3.yaml index 2f901ff0d..1dbc5b936 100644 --- a/docs/openapi3.yaml +++ b/docs/openapi3.yaml @@ -2012,6 +2012,9 @@ components: description: Javascript as a string. processor_type: type: string + enum: + - raw_javascript + - file_download description: | The type of the processor resource. Can be one of the following: * `raw_javascript`: Raw javascript string that will be stored and persisted as it is. diff --git a/src/database/cassandra-handler/init-scripts/14__create_processors_table.cql b/src/database/cassandra-handler/init-scripts/14__create_processors_table.cql index bdf0d4c73..1808df32e 100644 --- a/src/database/cassandra-handler/init-scripts/14__create_processors_table.cql +++ b/src/database/cassandra-handler/init-scripts/14__create_processors_table.cql @@ -6,5 +6,7 @@ CREATE TABLE IF NOT EXISTS processors type text, file_url text, javascript text, + created_at timestamp, + updated_at timestamp, PRIMARY KEY (processor_id) ); diff --git a/src/processors/models/database/cassandra/cassandraConnector.js b/src/processors/models/database/cassandra/cassandraConnector.js index 375b69331..87b645bfd 100644 --- a/src/processors/models/database/cassandra/cassandraConnector.js +++ b/src/processors/models/database/cassandra/cassandraConnector.js @@ -2,7 +2,7 @@ let logger = require('../../../../common/logger'); let databaseConfig = require('../../../../config/databaseConfig'); let client; -const INSERT_PROCESSOR = 'INSERT INTO processors(processor_id, name, description, type, file_url, javascript) values(?,?,?,?,?,?)'; +const INSERT_PROCESSOR = 'INSERT INTO processors(processor_id, name, description, type, file_url, javascript, created_at, updated_at) values(?,?,?,?,?,?,?,?)'; module.exports = { init, @@ -19,7 +19,7 @@ async function init(cassandraClient) { } function insertProcessor(processorId, processorInfo) { - let params = [processorId, processorInfo.name, processorInfo.description, processorInfo.type, processorInfo.file_url, processorInfo.javascript]; + let params = [processorId, processorInfo.name, processorInfo.description, processorInfo.type, processorInfo.file_url, processorInfo.javascript, Date.now(), Date.now()]; return executeQuery(INSERT_PROCESSOR, params, queryOptions); } diff --git a/src/processors/models/database/sequelize/sequelizeConnector.js b/src/processors/models/database/sequelize/sequelizeConnector.js index ca0621559..3afca9b97 100644 --- a/src/processors/models/database/sequelize/sequelizeConnector.js +++ b/src/processors/models/database/sequelize/sequelizeConnector.js @@ -21,7 +21,9 @@ async function insertProcessor(processorId, processorInfo) { description: processorInfo.description, type: processorInfo.type, file_url: processorInfo.file_url, - javascript: processorInfo.javascript + javascript: processorInfo.javascript, + created_at: Date.now(), + updated_at: Date.now() }; return processor.create(params); } @@ -46,6 +48,12 @@ async function initSchemas() { }, javascript: { type: Sequelize.DataTypes.TEXT('long') + }, + created_at: { + type: Sequelize.DataTypes.DATE + }, + updated_at: { + type: Sequelize.DataTypes.DATE } }); await processorsFiles.sync(); From aba9557e8aafdfe09602bf55decd536c207b064b Mon Sep 17 00:00:00 2001 From: NivLipetz Date: Sun, 20 Oct 2019 14:42:16 +0300 Subject: [PATCH 16/17] test(processors): add validateJavascriptContent unit-tests --- src/tests/models/fileManager.js | 19 ++-------- .../tests/models/fileManager-test.js | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 17 deletions(-) create mode 100644 tests/unit-tests/tests/models/fileManager-test.js diff --git a/src/tests/models/fileManager.js b/src/tests/models/fileManager.js index f418591ac..9b4d9befc 100644 --- a/src/tests/models/fileManager.js +++ b/src/tests/models/fileManager.js @@ -57,26 +57,11 @@ async function saveFile(fileUrl) { function validateJavascriptContent (javascriptFileContent) { let error, errorMessage; try { - const syntax = esprima.parseScript(javascriptFileContent, { tolerant: true }); - const errors = syntax.errors; - if (errors.length > 0) { - let errorsString = ''; - for (let i = 0; i < errors.length; i++) { - errorsString += errors[i].description + ', '; - } - errorsString = errorsString.substring(0, errorsString.length - 2); - - errorMessage = 'js syntax validation failed with error: ' + errorsString; - error = new Error(errorMessage); - error.statusCode = 422; - } + esprima.parseScript(javascriptFileContent); } catch (err) { errorMessage = err.description; - error = new Error(errorMessage); + error = new Error('javascript syntax validation failed with error: ' + errorMessage); error.statusCode = 422; - } - - if (error) { throw error; } } \ No newline at end of file diff --git a/tests/unit-tests/tests/models/fileManager-test.js b/tests/unit-tests/tests/models/fileManager-test.js new file mode 100644 index 000000000..d4ce87598 --- /dev/null +++ b/tests/unit-tests/tests/models/fileManager-test.js @@ -0,0 +1,36 @@ +'use strict'; +const should = require('should'); + +const fileManager = require('../../../../src/tests/models/fileManager'); + +describe('Javascript validation', function () { + it('Should pass javascript validation', function () { + fileManager.validateJavascriptContent(` + { + let i = 10; + i++; + console.log(i); + } + `) + }); + + it('Should fail javascript validation with error thrown', function () { + let error; + try { + fileManager.validateJavascriptContent(` + { + return 10; + + function xyz() { + console.log('xyz') + } + } + `) + } catch(e) { + error = e; + } + + should(error.statusCode).eql(422); + should(error.message).containDeep('javascript syntax validation failed with error: Illegal return statement'); + }); +}); From ca36ed872097c916739f773f69eb435244cafe76 Mon Sep 17 00:00:00 2001 From: NivLipetz Date: Sun, 20 Oct 2019 14:47:35 +0300 Subject: [PATCH 17/17] test(create-test): fix processor file download error message in test --- tests/integration-tests/tests/tests-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration-tests/tests/tests-test.js b/tests/integration-tests/tests/tests-test.js index f5eac52d6..589780e55 100644 --- a/tests/integration-tests/tests/tests-test.js +++ b/tests/integration-tests/tests/tests-test.js @@ -39,7 +39,7 @@ describe('the tests api', function() { let requestBody = Object.assign({ processor_file_url: 'https://www.notRealUrl.com' }, simpleTest.test); const res = await requestSender.createTest(requestBody, validHeaders); res.statusCode.should.eql(422); - res.body.message.should.eql('Error to read file, throw exception: RequestError: Error: getaddrinfo ENOTFOUND www.notrealurl.com www.notrealurl.com:443'); + res.body.message.should.eql('Error to download file: RequestError: Error: getaddrinfo ENOTFOUND www.notrealurl.com www.notrealurl.com:443'); }); let badBodyScenarios = ['Body_with_illegal_artillery', 'Body_with_no_artillery_schema', 'Body_with_no_test_type', 'Body_with_no_description', 'Body_with_no_name', 'Body_with_no_scenarios', 'Body_with_no_step_action', 'Body_with_no_steps'];