diff --git a/package.json b/package.json index 31e96143d3..592e60bb1b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "openapi-workspaces", "license": "MIT", "private": true, - "version": "0.55.1", + "version": "1.0.0", "workspaces": [ "projects/json-pointer-helpers", "projects/openapi-io", diff --git a/projects/fastify-capture/package.json b/projects/fastify-capture/package.json index 014cccae63..0be1c0ddfa 100644 --- a/projects/fastify-capture/package.json +++ b/projects/fastify-capture/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/fastify-capture", "license": "MIT", "packageManager": "yarn@4.1.1", - "version": "0.55.1", + "version": "1.0.0", "main": "build/index.js", "types": "build/index.d.ts", "files": [ diff --git a/projects/json-pointer-helpers/package.json b/projects/json-pointer-helpers/package.json index 7b41293006..7e8c358ea2 100644 --- a/projects/json-pointer-helpers/package.json +++ b/projects/json-pointer-helpers/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/json-pointer-helpers", "license": "MIT", "packageManager": "yarn@4.1.1", - "version": "0.55.1", + "version": "1.0.0", "main": "build/index.js", "types": "build/index.d.ts", "files": [ diff --git a/projects/openapi-io/package.json b/projects/openapi-io/package.json index baf23f7410..a00c14cb26 100644 --- a/projects/openapi-io/package.json +++ b/projects/openapi-io/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/openapi-io", "license": "MIT", "packageManager": "yarn@4.1.1", - "version": "0.55.1", + "version": "1.0.0", "main": "build/index.js", "types": "build/index.d.ts", "files": [ diff --git a/projects/openapi-utilities/package.json b/projects/openapi-utilities/package.json index 8e14008ffe..2202f28ea1 100644 --- a/projects/openapi-utilities/package.json +++ b/projects/openapi-utilities/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/openapi-utilities", "license": "MIT", "packageManager": "yarn@4.1.1", - "version": "0.55.1", + "version": "1.0.0", "main": "build/index.js", "types": "build/index.d.ts", "files": [ diff --git a/projects/optic/package.json b/projects/optic/package.json index 3c2c60fa01..0873307d20 100644 --- a/projects/optic/package.json +++ b/projects/optic/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/optic", "license": "MIT", "packageManager": "yarn@4.1.1", - "version": "0.55.1", + "version": "1.0.0", "main": "build/index.js", "types": "build/index.d.ts", "files": [ diff --git a/projects/optic/src/__tests__/config.test.ts b/projects/optic/src/__tests__/config.test.ts index a1c0a1ce2e..5be168b20f 100644 --- a/projects/optic/src/__tests__/config.test.ts +++ b/projects/optic/src/__tests__/config.test.ts @@ -109,31 +109,4 @@ describe('initializeRules', () => { initializeRules(config, createOpticClient('')) ).rejects.toThrow(UserError); }); - - test('extends ruleset from cloud', async () => { - const mockClient = { - getStandard: jest.fn(), - }; - mockClient.getStandard.mockResolvedValue({ - config: { - ruleset: [ - { name: 'from-cloud-ruleset', config: {} }, - { name: 'should-be-overwritten', config: { hello: true } }, - ], - }, - }); - (createOpticClient as jest.MockedFunction).mockImplementation( - () => mockClient - ); - - const config: ProjectYmlConfig = { - ruleset: [{ 'should-be-overwritten': { goodbye: false } }], - extends: '@orgslug/rulesetconfigid', - }; - await initializeRules(config, createOpticClient('')); - expect(config.ruleset).toEqual([ - { name: 'from-cloud-ruleset', config: {} }, - { name: 'should-be-overwritten', config: { goodbye: false } }, - ]); - }); }); diff --git a/projects/optic/src/__tests__/integration/__snapshots__/api-add.test.ts.snap b/projects/optic/src/__tests__/integration/__snapshots__/api-add.test.ts.snap deleted file mode 100644 index b7c950b7a5..0000000000 --- a/projects/optic/src/__tests__/integration/__snapshots__/api-add.test.ts.snap +++ /dev/null @@ -1,155 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`optic api add git - no cli interaction discover all files in a folder 1`] = ` -"Looking for OpenAPI specs in directory $workspace$/nested -βœ” a spec is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id - -Hint: add \`--history-depth=0\` to backfill the entire history of your API - - - -Setup CI checks by running "optic ci setup" -" -`; - -exports[`optic api add git - no cli interaction discover all files in repo 1`] = ` -"Looking for OpenAPI specs in directory $workspace$ -βœ” Empty is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id -βœ” a spec is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id -βœ” a spec is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id - -Hint: add \`--history-depth=0\` to backfill the entire history of your API - - - -Setup CI checks by running "optic ci setup" -" -`; - -exports[`optic api add git - no cli interaction discover all files in repo using --all 1`] = ` -"Looking for OpenAPI specs in directory $workspace$ -βœ” Empty is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id -βœ” a spec is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id -βœ” a spec is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id - -Hint: add \`--history-depth=0\` to backfill the entire history of your API - - - -Setup CI checks by running "optic ci setup" -" -`; - -exports[`optic api add git - no cli interaction discover one file with history depth 1`] = ` -"Adding API $workspace$/spec.yml -βœ” a spec is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id - -Hint: add \`--history-depth=0\` to backfill the entire history of your API - - - -Setup CI checks by running "optic ci setup" -" -`; - -exports[`optic api add git - no cli interaction discover one file with history depth 2`] = ` -"openapi: 3.0.3 -info: - title: a spec - description: The API - version: 0.1.0 -paths: - /filler_route: - post: - operationId: create - responses: - "201": - description: Created successfully - content: - application/json: - schema: - type: object - properties: - id: - type: string - format: uuid - example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 -" -`; - -exports[`optic api add no vcs - no cli interaction discover all files in a folder 1`] = ` -"Looking for OpenAPI specs in directory $workspace$/nested -βœ” a spec is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id - -Setup CI checks by running "optic ci setup" -" -`; - -exports[`optic api add no vcs - no cli interaction discover all files in repo 1`] = ` -"Looking for OpenAPI specs in directory $workspace$ -βœ” Empty is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id -βœ” a spec is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id -βœ” a spec is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id - -Setup CI checks by running "optic ci setup" -" -`; - -exports[`optic api add no vcs - no cli interaction discover all files in repo using --all 1`] = ` -"Looking for OpenAPI specs in directory $workspace$ -βœ” Empty is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id -βœ” a spec is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id -βœ” a spec is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id - -Setup CI checks by running "optic ci setup" -" -`; - -exports[`optic api add no vcs - no cli interaction discover one file with history depth 1`] = ` -"Adding API $workspace$/spec.yml -βœ” a spec is now being tracked. - View: https://app.useoptic.com/organizations/org-id/apis/api-id - -Setup CI checks by running "optic ci setup" -" -`; - -exports[`optic api add no vcs - no cli interaction discover one file with history depth 2`] = ` -"openapi: 3.0.3 -info: - title: a spec - description: The API - version: 0.1.0 -paths: - /filler_route: - post: - operationId: create - responses: - "201": - description: Created successfully - content: - application/json: - schema: - type: object - properties: - id: - type: string - format: uuid - example: d5b640e5-d88c-4c17-9bf0-93597b7a1ce2 -" -`; diff --git a/projects/optic/src/__tests__/integration/__snapshots__/api-list.test.ts.snap b/projects/optic/src/__tests__/integration/__snapshots__/api-list.test.ts.snap deleted file mode 100644 index 968cba913b..0000000000 --- a/projects/optic/src/__tests__/integration/__snapshots__/api-list.test.ts.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`optic api list lists all files in repo 1`] = ` -"Looking for OpenAPI specs in directory $workspace$ -example-api-v0.json  (untracked) -spec.yml  (untracked) -nested/speccopy2.yml  (untracked) - -Run optic api add to add untracked apis -" -`; diff --git a/projects/optic/src/__tests__/integration/__snapshots__/diff-all.test.ts.snap b/projects/optic/src/__tests__/integration/__snapshots__/diff-all.test.ts.snap index 912231097f..6d3bee8c48 100644 --- a/projects/optic/src/__tests__/integration/__snapshots__/diff-all.test.ts.snap +++ b/projects/optic/src/__tests__/integration/__snapshots__/diff-all.test.ts.snap @@ -1,67 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`diff-all diff all against a cloud tag 1`] = ` -"Diffing cloud:generated-api@main to spec-no-url.json - -x Empty spec-no-url.json -Preview docs:  https://useoptic.com/docs/cloud-get-started -Operations: 4 operations added, 1 removed -x  Checks: 0/1 passed - -specification details: -- /openapi changed - -x GET /api/users: removed - - x [prevent operation removal] cannot remove an operation. This is a breaking change. - at https://github.com/User/UserRepo/tree/COMMIT-HASH/empty.json#L1 - -βœ” GET /example: added - -βœ” POST /example: added - -βœ” PUT /example: added - -βœ” PATCH /example: added - -Diffing cloud:api-id@main to spec.json - -x Empty spec.json -Preview docs:  https://useoptic.com/docs/cloud-get-started -Operations: 4 operations added, 1 removed -x  Checks: 0/1 passed - -specification details: -- /x-optic-url added -- /openapi changed - -x GET /api/users: removed - - x [prevent operation removal] cannot remove an operation. This is a breaking change. - at https://github.com/User/UserRepo/tree/COMMIT-HASH/empty.json#L1 - -βœ” GET /example: added - -βœ” POST /example: added - -βœ” PUT /example: added - -βœ” PATCH /example: added - - - - Finish setting up Optic by adding your OPTIC_TOKEN. Create one here: https://app.useoptic.com/ - β†’ Add API Review Tools to your Pull Requests - Preview Docs | Visual Diffs | Notify Consumers | Sharable links | API Changelogs | Stats -" -`; - exports[`diff-all diff all not in the root of a repo 1`] = ` "Diffing HEAD~1:folder/in-folder.yml to in-folder.yml -Uploading diff... +upload diff is no longer supported x a spec in-folder.yml -Preview docs:  http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded +Preview docs:  https://useoptic.com/docs/cloud-get-started Operations: No operations changed x  Checks: 0/2 passed @@ -75,7 +19,7 @@ Uploading diff... Diffing HEAD~1:mvspec.yml to empty spec -Uploading diff... +upload diff is no longer supported x Empty HEAD~1:mvspec.yml Preview docs:  https://useoptic.com/docs/cloud-get-started Operations: 2 operations removed @@ -116,9 +60,9 @@ specification details: Diffing HEAD~1:specwithkey.json to ../specwithkey.json -Uploading diff... +upload diff is no longer supported βœ” API 1 ../specwithkey.json -Preview docs:  http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded +Preview docs:  https://useoptic.com/docs/cloud-get-started Operations: No operations changed βœ”  Checks: 4/4 passed @@ -128,9 +72,9 @@ Uploading diff... βœ” PUT /example: Diffing HEAD~1:specwithkey.yml to ../specwithkey.yml -Uploading diff... +upload diff is no longer supported x a spec ../specwithkey.yml -Preview docs:  http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded +Preview docs:  https://useoptic.com/docs/cloud-get-started Operations: No operations changed x  Checks: 0/1 passed @@ -138,35 +82,11 @@ Uploading diff... x [operation path component naming check] filler_route is not camelCase at https://github.com/User/UserRepo/tree/COMMIT-HASH/specwithkey.yml#L9 -Diffing HEAD~1:specwithoutkey.json to ../specwithoutkey.json - -Uploading diff... -βœ” API 2 ../specwithoutkey.json -Preview docs:  http://localhost:3001/organizations/org-id/apis/generated-api?specId=already-uploaded -Operations: No operations changed -βœ”  Checks: 4/4 passed - -βœ” GET /example: -βœ” PATCH /example: -βœ” POST /example: -βœ” PUT /example: -Diffing HEAD~1:specwithoutkey.yml to ../specwithoutkey.yml - -Uploading diff... -x a spec ../specwithoutkey.yml -Preview docs:  http://localhost:3001/organizations/org-id/apis/generated-api?specId=already-uploaded -Operations: No operations changed -x  Checks: 0/1 passed - -x POST /filler_route: - x [operation path component naming check] filler_route is not camelCase - at https://github.com/User/UserRepo/tree/COMMIT-HASH/specwithoutkey.yml#L8 - Diffing empty spec to ../movedspec.yml -Uploading diff... +upload diff is no longer supported x a spec ../movedspec.yml -Preview docs:  http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded +Preview docs:  https://useoptic.com/docs/cloud-get-started Operations: 2 operations added x  Checks: 0/2 passed @@ -187,6 +107,8 @@ specification details: Run the \`optic api add\` command to add these specs to optic ../spec-with-invalid-url.yml (untracked) +../specwithoutkey.json (untracked) +../specwithoutkey.yml (untracked) " `; @@ -194,7 +116,7 @@ Run the \`optic api add\` command to add these specs to optic exports[`diff-all diff all with glob and ignores 1`] = ` "Diffing HEAD~1:folder-to-run/should-run.yml to empty spec -Uploading diff... +upload diff is no longer supported x Empty HEAD~1:folder-to-run/should-run.yml Preview docs:  https://useoptic.com/docs/cloud-get-started Operations: 1 operation removed @@ -220,9 +142,9 @@ specification details: Diffing empty spec to folder-to-run/should-run-mved.yml -Uploading diff... +upload diff is no longer supported βœ” a spec folder-to-run/should-run-mved.yml -Preview docs:  http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded +Preview docs:  https://useoptic.com/docs/cloud-get-started Operations: 1 operation added specification details: @@ -233,1204 +155,6 @@ specification details: " `; -exports[`diff-all diffs all files in a workspace with x-optic-url keys with --upload 1`] = ` -"Diffing HEAD~1:folder/in-folder.yml to folder/in-folder.yml - -Uploading diff... -x a spec folder/in-folder.yml -Preview docs:  http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded -Operations: No operations changed -x  Checks: 0/2 passed - -x POST /filler_route: - x [operation path component naming check] filler_route is not camelCase - at https://github.com/User/UserRepo/tree/COMMIT-HASH/folder/in-folder.yml#L9 - -x POST /api/filler-route: - x [operation path component naming check] filler-route is not camelCase - at https://github.com/User/UserRepo/tree/COMMIT-HASH/folder/in-folder.yml#L24 - -Diffing HEAD~1:mvspec.yml to empty spec - -Uploading diff... -x Empty HEAD~1:mvspec.yml -Preview docs:  https://useoptic.com/docs/cloud-get-started -Operations: 2 operations removed -x  Checks: 0/6 passed - -specification details: -- /openapi, /info, /x-optic-url removed - -x POST /filler_route: removed - - x [prevent operation removal] cannot remove an operation. This is a breaking change. - at https://github.com/User/UserRepo/tree/COMMIT-HASH/mvspec.yml#L9 - - - response 201: - x [prevent response status code removal] must not remove response status code 201. This is a breaking change. - at https://github.com/User/UserRepo/tree/COMMIT-HASH/mvspec.yml#L12 - - - body application/json: - - property /schema/properties/id: - - x [prevent removing response property] cannot remove response property 'id'. This is a breaking change. - at https://github.com/User/UserRepo/tree/COMMIT-HASH/mvspec.yml#L19 - -x POST /api/filler-route: removed - - x [prevent operation removal] cannot remove an operation. This is a breaking change. - at https://github.com/User/UserRepo/tree/COMMIT-HASH/mvspec.yml#L24 - - - response 201: - x [prevent response status code removal] must not remove response status code 201. This is a breaking change. - at https://github.com/User/UserRepo/tree/COMMIT-HASH/mvspec.yml#L27 - - - body application/json: - - property /schema/properties/id: - - x [prevent removing response property] cannot remove response property 'id'. This is a breaking change. - at https://github.com/User/UserRepo/tree/COMMIT-HASH/mvspec.yml#L34 - -Diffing HEAD~1:specwithkey.json to specwithkey.json - -Uploading diff... -βœ” API 1 specwithkey.json -Preview docs:  http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded -Operations: No operations changed -βœ”  Checks: 4/4 passed - -βœ” GET /example: -βœ” PATCH /example: -βœ” POST /example: -βœ” PUT /example: -Diffing HEAD~1:specwithkey.yml to specwithkey.yml - -Uploading diff... -x a spec specwithkey.yml -Preview docs:  http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded -Operations: No operations changed -x  Checks: 0/1 passed - -x POST /filler_route: - x [operation path component naming check] filler_route is not camelCase - at https://github.com/User/UserRepo/tree/COMMIT-HASH/specwithkey.yml#L9 - -Diffing HEAD~1:specwithoutkey.json to specwithoutkey.json - -Uploading diff... -βœ” API 2 specwithoutkey.json -Preview docs:  http://localhost:3001/organizations/org-id/apis/generated-api?specId=already-uploaded -Operations: No operations changed -βœ”  Checks: 4/4 passed - -βœ” GET /example: -βœ” PATCH /example: -βœ” POST /example: -βœ” PUT /example: -Diffing HEAD~1:specwithoutkey.yml to specwithoutkey.yml - -Uploading diff... -x a spec specwithoutkey.yml -Preview docs:  http://localhost:3001/organizations/org-id/apis/generated-api?specId=already-uploaded -Operations: No operations changed -x  Checks: 0/1 passed - -x POST /filler_route: - x [operation path component naming check] filler_route is not camelCase - at https://github.com/User/UserRepo/tree/COMMIT-HASH/specwithoutkey.yml#L8 - -Diffing empty spec to movedspec.yml - -Uploading diff... -x a spec movedspec.yml -Preview docs:  http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded -Operations: 2 operations added -x  Checks: 0/2 passed - -specification details: -- /openapi, /info, /x-optic-url added - -x POST /filler_route: added - - x [operation path component naming check] filler_route is not camelCase - at https://github.com/User/UserRepo/tree/COMMIT-HASH/movedspec.yml#L9 - -x POST /api/filler-route: added - - x [operation path component naming check] filler-route is not camelCase - at https://github.com/User/UserRepo/tree/COMMIT-HASH/movedspec.yml#L24 - -Warning - the following OpenAPI specs were detected but did not have valid x-optic-url keys. 'optic diff-all --upload' can only runs on specs that have been added to optic - -Run the \`optic api add\` command to add these specs to optic -spec-with-invalid-url.yml (untracked) - -" -`; - -exports[`diff-all diffs all files in a workspace with x-optic-url keys with --upload 2`] = ` -{ - "completed": [ - { - "apiName": "folder/in-folder.yml", - "comparison": { - "groupedDiffs": { - "endpoints": { - "POST-~_~-/api/filler-route": { - "cookieParameters": {}, - "diffs": [], - "headerParameters": {}, - "method": "POST", - "path": "/api/filler-route", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "error": "filler-route is not camelCase", - "exempted": false, - "location": { - "jsonPath": "/paths/~1api~1filler-route/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": false, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "POST /api/filler-route", - }, - ], - }, - "POST-~_~-/filler_route": { - "cookieParameters": {}, - "diffs": [], - "headerParameters": {}, - "method": "POST", - "path": "/filler_route", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "error": "filler_route is not camelCase", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": false, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "POST /filler_route", - }, - ], - }, - }, - "specification": { - "diffs": [], - "rules": [], - }, - "unmatched": { - "diffs": [], - "rules": [], - }, - }, - "results": [ - { - "error": "filler_route is not camelCase", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": false, - "severity": 2, - "type": "requirement", - "where": "POST /filler_route", - }, - { - "error": "filler-route is not camelCase", - "exempted": false, - "location": { - "jsonPath": "/paths/~1api~1filler-route/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": false, - "severity": 2, - "type": "requirement", - "where": "POST /api/filler-route", - }, - ], - }, - "opticWebUrl": "http://localhost:3001/organizations/org-id/apis/api-id/runs/run-id", - "specUrl": "http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded", - "warnings": [], - }, - { - "apiName": "HEAD~1:mvspec.yml", - "comparison": { - "groupedDiffs": { - "endpoints": { - "POST-~_~-/api/filler-route": { - "cookieParameters": {}, - "diffs": [ - { - "before": "/paths/~1api~1filler-route/post", - "change": "removed", - "trail": "", - }, - ], - "headerParameters": {}, - "method": "POST", - "path": "/api/filler-route", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": { - "201": { - "contents": { - "application/json": { - "examples": { - "diffs": [], - "rules": [], - }, - "fields": { - "/schema/properties/id": { - "diffs": [], - "rules": [ - { - "error": "cannot remove response property 'id'. This is a breaking change.", - "exempted": false, - "location": { - "jsonPath": "/paths/~1api~1filler-route/post/responses/201/content/application~1json/schema/properties/id", - "spec": "before", - }, - "name": "prevent removing response property", - "passed": false, - "severity": 2, - "trail": "", - "type": "removed", - "where": "POST /api/filler-route response 201 response body: application/json property id", - }, - ], - }, - }, - }, - }, - "diffs": [], - "headers": {}, - "rules": [ - { - "error": "must not remove response status code 201. This is a breaking change.", - "exempted": false, - "location": { - "jsonPath": "/paths/~1api~1filler-route/post/responses/201", - "spec": "before", - }, - "name": "prevent response status code removal", - "passed": false, - "severity": 2, - "trail": "", - "type": "removed", - "where": "POST /api/filler-route response 201", - }, - ], - }, - }, - "rules": [ - { - "error": "cannot remove an operation. This is a breaking change.", - "exempted": false, - "location": { - "jsonPath": "/paths/~1api~1filler-route/post", - "spec": "before", - }, - "name": "prevent operation removal", - "passed": false, - "severity": 2, - "trail": "", - "type": "removed", - "where": "POST /api/filler-route", - }, - ], - }, - "POST-~_~-/filler_route": { - "cookieParameters": {}, - "diffs": [ - { - "before": "/paths/~1filler_route/post", - "change": "removed", - "trail": "", - }, - ], - "headerParameters": {}, - "method": "POST", - "path": "/filler_route", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": { - "201": { - "contents": { - "application/json": { - "examples": { - "diffs": [], - "rules": [], - }, - "fields": { - "/schema/properties/id": { - "diffs": [], - "rules": [ - { - "error": "cannot remove response property 'id'. This is a breaking change.", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post/responses/201/content/application~1json/schema/properties/id", - "spec": "before", - }, - "name": "prevent removing response property", - "passed": false, - "severity": 2, - "trail": "", - "type": "removed", - "where": "POST /filler_route response 201 response body: application/json property id", - }, - ], - }, - }, - }, - }, - "diffs": [], - "headers": {}, - "rules": [ - { - "error": "must not remove response status code 201. This is a breaking change.", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post/responses/201", - "spec": "before", - }, - "name": "prevent response status code removal", - "passed": false, - "severity": 2, - "trail": "", - "type": "removed", - "where": "POST /filler_route response 201", - }, - ], - }, - }, - "rules": [ - { - "error": "cannot remove an operation. This is a breaking change.", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post", - "spec": "before", - }, - "name": "prevent operation removal", - "passed": false, - "severity": 2, - "trail": "", - "type": "removed", - "where": "POST /filler_route", - }, - ], - }, - }, - "specification": { - "diffs": [ - { - "before": "/openapi", - "change": "removed", - "pathReconciliation": [], - "trail": "/openapi", - }, - { - "before": "/info", - "change": "removed", - "pathReconciliation": [], - "trail": "/info", - }, - { - "before": "/x-optic-url", - "change": "removed", - "pathReconciliation": [], - "trail": "/x-optic-url", - }, - ], - "rules": [], - }, - "unmatched": { - "diffs": [], - "rules": [], - }, - }, - "results": [ - { - "error": "cannot remove an operation. This is a breaking change.", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post", - "spec": "before", - }, - "name": "prevent operation removal", - "passed": false, - "severity": 2, - "type": "removed", - "where": "POST /filler_route", - }, - { - "error": "cannot remove response property 'id'. This is a breaking change.", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post/responses/201/content/application~1json/schema/properties/id", - "spec": "before", - }, - "name": "prevent removing response property", - "passed": false, - "severity": 2, - "type": "removed", - "where": "POST /filler_route response 201 response body: application/json property id", - }, - { - "error": "must not remove response status code 201. This is a breaking change.", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post/responses/201", - "spec": "before", - }, - "name": "prevent response status code removal", - "passed": false, - "severity": 2, - "type": "removed", - "where": "POST /filler_route response 201", - }, - { - "error": "cannot remove an operation. This is a breaking change.", - "exempted": false, - "location": { - "jsonPath": "/paths/~1api~1filler-route/post", - "spec": "before", - }, - "name": "prevent operation removal", - "passed": false, - "severity": 2, - "type": "removed", - "where": "POST /api/filler-route", - }, - { - "error": "cannot remove response property 'id'. This is a breaking change.", - "exempted": false, - "location": { - "jsonPath": "/paths/~1api~1filler-route/post/responses/201/content/application~1json/schema/properties/id", - "spec": "before", - }, - "name": "prevent removing response property", - "passed": false, - "severity": 2, - "type": "removed", - "where": "POST /api/filler-route response 201 response body: application/json property id", - }, - { - "error": "must not remove response status code 201. This is a breaking change.", - "exempted": false, - "location": { - "jsonPath": "/paths/~1api~1filler-route/post/responses/201", - "spec": "before", - }, - "name": "prevent response status code removal", - "passed": false, - "severity": 2, - "type": "removed", - "where": "POST /api/filler-route response 201", - }, - ], - }, - "opticWebUrl": "http://localhost:3001/organizations/org-id/apis/api-id/runs/run-id", - "specUrl": null, - "warnings": [], - }, - { - "apiName": "specwithkey.json", - "comparison": { - "groupedDiffs": { - "endpoints": { - "GET-~_~-/example": { - "cookieParameters": {}, - "diffs": [], - "headerParameters": {}, - "method": "GET", - "path": "/example", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/get", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "GET /example", - }, - ], - }, - "PATCH-~_~-/example": { - "cookieParameters": {}, - "diffs": [], - "headerParameters": {}, - "method": "PATCH", - "path": "/example", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/patch", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "PATCH /example", - }, - ], - }, - "POST-~_~-/example": { - "cookieParameters": {}, - "diffs": [], - "headerParameters": {}, - "method": "POST", - "path": "/example", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "POST /example", - }, - ], - }, - "PUT-~_~-/example": { - "cookieParameters": {}, - "diffs": [], - "headerParameters": {}, - "method": "PUT", - "path": "/example", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/put", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "PUT /example", - }, - ], - }, - }, - "specification": { - "diffs": [], - "rules": [], - }, - "unmatched": { - "diffs": [], - "rules": [], - }, - }, - "results": [ - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/get", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "type": "requirement", - "where": "GET /example", - }, - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/patch", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "type": "requirement", - "where": "PATCH /example", - }, - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "type": "requirement", - "where": "POST /example", - }, - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/put", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "type": "requirement", - "where": "PUT /example", - }, - ], - }, - "opticWebUrl": "http://localhost:3001/organizations/org-id/apis/api-id/runs/run-id", - "specUrl": "http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded", - "warnings": [], - }, - { - "apiName": "specwithkey.yml", - "comparison": { - "groupedDiffs": { - "endpoints": { - "POST-~_~-/filler_route": { - "cookieParameters": {}, - "diffs": [], - "headerParameters": {}, - "method": "POST", - "path": "/filler_route", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "error": "filler_route is not camelCase", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": false, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "POST /filler_route", - }, - ], - }, - }, - "specification": { - "diffs": [], - "rules": [], - }, - "unmatched": { - "diffs": [], - "rules": [], - }, - }, - "results": [ - { - "error": "filler_route is not camelCase", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": false, - "severity": 2, - "type": "requirement", - "where": "POST /filler_route", - }, - ], - }, - "opticWebUrl": "http://localhost:3001/organizations/org-id/apis/api-id/runs/run-id", - "specUrl": "http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded", - "warnings": [], - }, - { - "apiName": "specwithoutkey.json", - "comparison": { - "groupedDiffs": { - "endpoints": { - "GET-~_~-/example": { - "cookieParameters": {}, - "diffs": [], - "headerParameters": {}, - "method": "GET", - "path": "/example", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/get", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "GET /example", - }, - ], - }, - "PATCH-~_~-/example": { - "cookieParameters": {}, - "diffs": [], - "headerParameters": {}, - "method": "PATCH", - "path": "/example", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/patch", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "PATCH /example", - }, - ], - }, - "POST-~_~-/example": { - "cookieParameters": {}, - "diffs": [], - "headerParameters": {}, - "method": "POST", - "path": "/example", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "POST /example", - }, - ], - }, - "PUT-~_~-/example": { - "cookieParameters": {}, - "diffs": [], - "headerParameters": {}, - "method": "PUT", - "path": "/example", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/put", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "PUT /example", - }, - ], - }, - }, - "specification": { - "diffs": [], - "rules": [], - }, - "unmatched": { - "diffs": [], - "rules": [], - }, - }, - "results": [ - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/get", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "type": "requirement", - "where": "GET /example", - }, - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/patch", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "type": "requirement", - "where": "PATCH /example", - }, - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "type": "requirement", - "where": "POST /example", - }, - { - "exempted": false, - "location": { - "jsonPath": "/paths/~1example/put", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": true, - "severity": 2, - "type": "requirement", - "where": "PUT /example", - }, - ], - }, - "opticWebUrl": "http://localhost:3001/organizations/org-id/apis/generated-api/runs/run-id", - "specUrl": "http://localhost:3001/organizations/org-id/apis/generated-api?specId=already-uploaded", - "warnings": [], - }, - { - "apiName": "specwithoutkey.yml", - "comparison": { - "groupedDiffs": { - "endpoints": { - "POST-~_~-/filler_route": { - "cookieParameters": {}, - "diffs": [], - "headerParameters": {}, - "method": "POST", - "path": "/filler_route", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "error": "filler_route is not camelCase", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": false, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "POST /filler_route", - }, - ], - }, - }, - "specification": { - "diffs": [], - "rules": [], - }, - "unmatched": { - "diffs": [], - "rules": [], - }, - }, - "results": [ - { - "error": "filler_route is not camelCase", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": false, - "severity": 2, - "type": "requirement", - "where": "POST /filler_route", - }, - ], - }, - "opticWebUrl": "http://localhost:3001/organizations/org-id/apis/generated-api/runs/run-id", - "specUrl": "http://localhost:3001/organizations/org-id/apis/generated-api?specId=already-uploaded", - "warnings": [], - }, - { - "apiName": "movedspec.yml", - "comparison": { - "groupedDiffs": { - "endpoints": { - "POST-~_~-/api/filler-route": { - "cookieParameters": {}, - "diffs": [ - { - "after": "/paths/~1api~1filler-route/post", - "change": "added", - "trail": "", - }, - ], - "headerParameters": {}, - "method": "POST", - "path": "/api/filler-route", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "error": "filler-route is not camelCase", - "exempted": false, - "location": { - "jsonPath": "/paths/~1api~1filler-route/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": false, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "POST /api/filler-route", - }, - ], - }, - "POST-~_~-/filler_route": { - "cookieParameters": {}, - "diffs": [ - { - "after": "/paths/~1filler_route/post", - "change": "added", - "trail": "", - }, - ], - "headerParameters": {}, - "method": "POST", - "path": "/filler_route", - "pathParameters": {}, - "queryParameters": {}, - "request": { - "contents": {}, - "diffs": [], - "rules": [], - }, - "responses": {}, - "rules": [ - { - "error": "filler_route is not camelCase", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": false, - "severity": 2, - "trail": "", - "type": "requirement", - "where": "POST /filler_route", - }, - ], - }, - }, - "specification": { - "diffs": [ - { - "after": "/openapi", - "change": "added", - "trail": "/openapi", - }, - { - "after": "/info", - "change": "added", - "trail": "/info", - }, - { - "after": "/x-optic-url", - "change": "added", - "trail": "/x-optic-url", - }, - ], - "rules": [], - }, - "unmatched": { - "diffs": [], - "rules": [], - }, - }, - "results": [ - { - "error": "filler_route is not camelCase", - "exempted": false, - "location": { - "jsonPath": "/paths/~1filler_route/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": false, - "severity": 2, - "type": "requirement", - "where": "POST /filler_route", - }, - { - "error": "filler-route is not camelCase", - "exempted": false, - "location": { - "jsonPath": "/paths/~1api~1filler-route/post", - "spec": "after", - }, - "name": "operation path component naming check", - "passed": false, - "severity": 2, - "type": "requirement", - "where": "POST /api/filler-route", - }, - ], - }, - "opticWebUrl": "http://localhost:3001/organizations/org-id/apis/api-id/runs/run-id", - "specUrl": "http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded", - "warnings": [], - }, - ], - "failed": [], - "noop": [], - "severity": 2, -} -`; - exports[`diff-all diffs all files in a workspace without --upload 1`] = ` "Diffing HEAD~1:mvspec.yml to empty spec @@ -1494,7 +218,12 @@ specification details: `; exports[`diff-all diffs all files with --json 1`] = ` -"{"results":{"folder/in-folder.yml":{"operations":[]},"mvspec.yml":{"operations":[{"name":"POST /filler_route","change":"removed","attributes":[{"key":"","before":{"operationId":"create","responses":{"201":{"description":"Created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"d5b640e5-d88c-4c17-9bf0-93597b7a1ce2"}}}}},"headers":{}}},"parameters":[]},"change":"removed"}],"parameters":[],"responses":[]},{"name":"POST /api/filler-route","change":"removed","attributes":[{"key":"","before":{"operationId":"create","responses":{"201":{"description":"Created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"d5b640e5-d88c-4c17-9bf0-93597b7a1ce2"}}}}},"headers":{}}},"parameters":[]},"change":"removed"}],"parameters":[],"responses":[]}]},"specwithkey.json":{"operations":[]},"specwithkey.yml":{"operations":[]},"specwithoutkey.json":{"operations":[]},"specwithoutkey.yml":{"operations":[]},"movedspec.yml":{"operations":[{"name":"POST /filler_route","change":"added","attributes":[{"key":"","after":{"operationId":"create","responses":{"201":{"description":"Created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"d5b640e5-d88c-4c17-9bf0-93597b7a1ce2"}}}}},"headers":{}}},"parameters":[]},"change":"added"}],"parameters":[],"responses":[]},{"name":"POST /api/filler-route","change":"added","attributes":[{"key":"","after":{"operationId":"create","responses":{"201":{"description":"Created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"d5b640e5-d88c-4c17-9bf0-93597b7a1ce2"}}}}},"headers":{}}},"parameters":[]},"change":"added"}],"parameters":[],"responses":[]}]}},"warnings":{"missingOpticUrl":[{"path":"spec-with-invalid-url.yml"}],"unparseableFromSpec":[],"unparseableToSpec":[]}} +"upload diff is no longer supported +upload diff is no longer supported +upload diff is no longer supported +upload diff is no longer supported +upload diff is no longer supported +{"results":{"folder/in-folder.yml":{"operations":[]},"mvspec.yml":{"operations":[{"name":"POST /filler_route","change":"removed","attributes":[{"key":"","before":{"operationId":"create","responses":{"201":{"description":"Created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"d5b640e5-d88c-4c17-9bf0-93597b7a1ce2"}}}}},"headers":{}}},"parameters":[]},"change":"removed"}],"parameters":[],"responses":[]},{"name":"POST /api/filler-route","change":"removed","attributes":[{"key":"","before":{"operationId":"create","responses":{"201":{"description":"Created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"d5b640e5-d88c-4c17-9bf0-93597b7a1ce2"}}}}},"headers":{}}},"parameters":[]},"change":"removed"}],"parameters":[],"responses":[]}]},"specwithkey.json":{"operations":[]},"specwithkey.yml":{"operations":[]},"movedspec.yml":{"operations":[{"name":"POST /filler_route","change":"added","attributes":[{"key":"","after":{"operationId":"create","responses":{"201":{"description":"Created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"d5b640e5-d88c-4c17-9bf0-93597b7a1ce2"}}}}},"headers":{}}},"parameters":[]},"change":"added"}],"parameters":[],"responses":[]},{"name":"POST /api/filler-route","change":"added","attributes":[{"key":"","after":{"operationId":"create","responses":{"201":{"description":"Created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"d5b640e5-d88c-4c17-9bf0-93597b7a1ce2"}}}}},"headers":{}}},"parameters":[]},"change":"added"}],"parameters":[],"responses":[]}]}},"warnings":{"missingOpticUrl":[{"path":"spec-with-invalid-url.yml"},{"path":"specwithoutkey.json"},{"path":"specwithoutkey.yml"}],"unparseableFromSpec":[],"unparseableToSpec":[]}} " `; diff --git a/projects/optic/src/__tests__/integration/__snapshots__/diff.test.ts.snap b/projects/optic/src/__tests__/integration/__snapshots__/diff.test.ts.snap index 13438fbc14..c3c27e5595 100644 --- a/projects/optic/src/__tests__/integration/__snapshots__/diff.test.ts.snap +++ b/projects/optic/src/__tests__/integration/__snapshots__/diff.test.ts.snap @@ -502,167 +502,3 @@ exports[`diff with --standard arg 1`] = ` Rerun this command with the --web flag to view the detailed changes in your browser " `; - -exports[`diff with mock server custom rules 1`] = ` -"x Empty example-api-v0.json -Operations: 1 operation added, 3 changed, 1 removed -x  Checks: 5/7 passed - -x GET /invalid_name: added - - x [operation path component naming check] invalid_name is not camelCase - at example-api-v1.json:31:632 - -x POST /example: removed - - x [prevent operation removal] cannot remove an operation. This is a breaking change. - at example-api-v0.json:9:150 - -βœ” PATCH /example: - - /operationId changed - - - response 200: added -βœ” PUT /example: - - /operationId changed - -βœ” GET /example: - - /operationId changed - -Rerun this command with the --web flag to view the detailed changes in your browser -" -`; - -exports[`diff with mock server extends 1`] = ` -"x Empty example-api-v0.json -Operations: 1 operation added, 3 changed, 1 removed -x  Checks: 3/5 passed - -x GET /invalid_name: added - - x [operation path component naming check] invalid_name is not camelCase - at example-api-v1.json:31:632 - -x POST /example: removed - - x [prevent operation removal] cannot remove an operation. This is a breaking change. - at example-api-v0.json:9:150 - -βœ” PATCH /example: - - /operationId changed - - - response 200: added -βœ” PUT /example: - - /operationId changed - -βœ” GET /example: - - /operationId changed - -Rerun this command with the --web flag to view the detailed changes in your browser -" -`; - -exports[`diff with mock server ruleset key on api spec 1`] = ` -"x Empty example-api-v0.json -Operations: 1 operation added, 3 changed, 1 removed -x  Checks: 0/1 passed - -specification details: -- /x-optic-standard added - -βœ” GET /invalid_name: added - -x POST /example: removed - - x [prevent operation removal] cannot remove an operation. This is a breaking change. - at example-api-v0.json:9:150 - -βœ” PATCH /example: - - /operationId changed - - - response 200: added -βœ” PUT /example: - - /operationId changed - -βœ” GET /example: - - /operationId changed - -Rerun this command with the --web flag to view the detailed changes in your browser -" -`; - -exports[`diff with mock server uploads specs if authenticated and --upload 1`] = ` -"Uploading diff... -x Empty spec.json -Preview docs:  http://localhost:3001/organizations/org-id/apis/api-id?specId=already-uploaded -Operations: 1 operation changed -x  Checks: 1/2 passed - -x PATCH /example: - - response 200: - - body application/json: - - property : changed - - x [prevent response property type changes] expected response body application/json root shape not to be expanded. This is a breaking change. - at https://github.com/User/UserRepo/tree/COMMIT-HASH/spec.json#L24 - -" -`; - -exports[`diff with mock server with --base cloud:tag 1`] = ` -"x Empty spec.json -Operations: 4 operations added, 1 removed -x  Checks: 0/1 passed - -specification details: -- /x-optic-url added -- /openapi changed - -x GET /api/users: removed - - x [prevent operation removal] cannot remove an operation. This is a breaking change. - at empty.json:1:45 - -βœ” GET /example: added - -βœ” POST /example: added - -βœ” PUT /example: added - -βœ” PATCH /example: added - -Rerun this command with the --web flag to view the detailed changes in your browser -" -`; - -exports[`diff with mock server with cloud tag ref 1`] = ` -"βœ” Empty null: -Operations: 1 operation added - -specification details: -- /openapi, /info added - -βœ” GET /api/users: added - -Rerun this command with the --web flag to view the detailed changes in your browser -" -`; - -exports[`diff with mock server with web url and header 1`] = ` -"x Empty http://localhost/my-spec.yml -Operations: 1 operation removed -x  Checks: 0/2 passed - -specification details: -- /openapi, /info removed - -x GET /api/users: removed - - x [prevent operation removal] cannot remove an operation. This is a breaking change. - at http://localhost/my-spec.yml line 8 - - x [prevent response status code removal] must not remove response status code 200. This is a breaking change. - at http://localhost/my-spec.yml line 10 - -Rerun this command with the --web flag to view the detailed changes in your browser -" -`; diff --git a/projects/optic/src/__tests__/integration/__snapshots__/ruleset-publish.test.ts.snap b/projects/optic/src/__tests__/integration/__snapshots__/ruleset-publish.test.ts.snap deleted file mode 100644 index a0b45888c4..0000000000 --- a/projects/optic/src/__tests__/integration/__snapshots__/ruleset-publish.test.ts.snap +++ /dev/null @@ -1,32 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`optic ruleset upload can publish a ruleset 1`] = ` -"Successfully uploaded the ruleset @orgslug/ruleset-name -You can start using this ruleset by adding the ruleset @orgslug/ruleset-name in your optic.dev.yml or standards file. -" -`; - -exports[`optic ruleset upload exits if ruleset file does not have rulesConstructor 1`] = ` -"Rules file does not export a rulesetConstructor that is a function -Rules file does not match expected format. Expected ruleset file to have a default export with the shape -{ - name: string; - description: string; - configSchema?: any; - rulesetConstructor: (config: ConfigSchema) => Ruleset; -} -" -`; - -exports[`optic ruleset upload exits if ruleset file shape is not valid 1`] = ` -"Rule file is invalid: -data/default must have required property 'description' -Rules file does not match expected format. Expected ruleset file to have a default export with the shape -{ - name: string; - description: string; - configSchema?: any; - rulesetConstructor: (config: ConfigSchema) => Ruleset; -} -" -`; diff --git a/projects/optic/src/__tests__/integration/__snapshots__/run.test.ts.snap b/projects/optic/src/__tests__/integration/__snapshots__/run.test.ts.snap deleted file mode 100644 index 29a13e77ce..0000000000 --- a/projects/optic/src/__tests__/integration/__snapshots__/run.test.ts.snap +++ /dev/null @@ -1,102 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`run can include files in gitignore with -I and file paths 1`] = ` -"Optic matched 1 OpenAPI specification file: -openapi-local.yml - --------------------------------------------------------------------------------------------------- - - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ [1] Optic Cloud [2] β”‚ - β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”˜ - β”‚Compare Updateβ”‚ - β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β” - β”‚ Local specs β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - replaced line - replaced line - --------------------------------------------------------------------------------------------------- -Pass a GITHUB_TOKEN or OPTIC_GITLAB_TOKEN environment variable with write permission to let Optic post comment with API change summaries to your pull requests. - - -Uploading diff... -| a spec [openapi-local.yml] -| Report: πŸ‘οΈ http://localhost:3001/organizations/org-id/apis/generated-api/runs/run-id -| Changes: 2 operations added, 1 removed -| Rules: βœ… 5/5 passed -| Tests: Set up API contract testing for this spec: https://www.useoptic.com/docs/verify-openapi - - -πŸ’¬ Configure commenting on PR/MR: https://www.useoptic.com/docs/setup-ci#configure-commenting-on-pull-requests-optional -" -`; - -exports[`run ignores files in gitignore 1`] = ` -"Optic matched 1 OpenAPI specification file: -openapi.yml - --------------------------------------------------------------------------------------------------- - - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ [1] Optic Cloud [2] β”‚ - β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”˜ - β”‚Compare Updateβ”‚ - β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β” - β”‚ Local specs β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - replaced line - replaced line - --------------------------------------------------------------------------------------------------- -Pass a GITHUB_TOKEN or OPTIC_GITLAB_TOKEN environment variable with write permission to let Optic post comment with API change summaries to your pull requests. - - -Uploading diff... -| a spec [openapi.yml] -| Report: πŸ‘οΈ http://localhost:3001/organizations/org-id/apis/generated-api/runs/run-id -| Changes: 2 operations added, 1 removed -| Rules: βœ… 5/5 passed -| Tests: Set up API contract testing for this spec: https://www.useoptic.com/docs/verify-openapi - - -πŸ’¬ Configure commenting on PR/MR: https://www.useoptic.com/docs/setup-ci#configure-commenting-on-pull-requests-optional -" -`; - -exports[`run runs and diffs against APIs and runs capture 1`] = ` -"Optic matched 1 OpenAPI specification file: -openapi.yml - --------------------------------------------------------------------------------------------------- - - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ [1] Optic Cloud [2] β”‚ - β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”˜ - β”‚Compare Updateβ”‚ - β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β” - β”‚ Local specs β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - replaced line - replaced line - --------------------------------------------------------------------------------------------------- -Pass a GITHUB_TOKEN or OPTIC_GITLAB_TOKEN environment variable with write permission to let Optic post comment with API change summaries to your pull requests. - - -Uploading diff... -| a spec [openapi.yml] -| Report: πŸ‘οΈ http://localhost:3001/organizations/org-id/apis/generated-api/runs/run-id -| Changes: 2 operations added, 1 removed -| Rules: ⚠️  1/3 failed -| Tests: πŸ†• 5 undocumented paths ⚠️ 1 mismatch - - -Errors were found: exiting with code 1. Disable this behaviour with the \`--severity none\` option. - -πŸ’¬ Configure commenting on PR/MR: https://www.useoptic.com/docs/setup-ci#configure-commenting-on-pull-requests-optional -" -`; diff --git a/projects/optic/src/__tests__/integration/__snapshots__/spec-push.test.ts.snap b/projects/optic/src/__tests__/integration/__snapshots__/spec-push.test.ts.snap deleted file mode 100644 index 8535647bcf..0000000000 --- a/projects/optic/src/__tests__/integration/__snapshots__/spec-push.test.ts.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`optic spec push can push a spec to a repo 1`] = ` -"Uploading spec for api at http://localhost:3001/organizations/org-id/apis/api-id with tags env:production, the-favorite-api, git:COMMIT-HASH, gitbranch:master -Succesfully uploaded spec to Optic. View the spec here http://localhost:3001/organizations/org-id/apis/api-id?specId=spec-id -" -`; diff --git a/projects/optic/src/__tests__/integration/__snapshots__/update.test.ts.snap b/projects/optic/src/__tests__/integration/__snapshots__/update.test.ts.snap deleted file mode 100644 index 0d7374947b..0000000000 --- a/projects/optic/src/__tests__/integration/__snapshots__/update.test.ts.snap +++ /dev/null @@ -1,964 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`update update an existing spec with prefixed server 1`] = ` -"optic update is deprecated. Start using the new capture flow by running optic capture openapi.yml --update (get started by running optic capture init openapi.yml) -Β» Documenting new operations... - added post /books - added get /books - added post /authors - added get /some/example - added get /books/{book} - added get /authors/{author} -Β» Updating operations... - - -Share a link to documentation with your team (optic api add openapi.yml) -" -`; - -exports[`update update an existing spec with prefixed server 2`] = ` -{ - "components": { - "schemas": { - "GetBooksBook200ResponseBody": { - "properties": { - "author_id": { - "type": "string", - }, - "created_at": { - "type": "string", - }, - "id": { - "type": "string", - }, - "name": { - "type": "string", - }, - "updated_at": { - "type": "string", - }, - }, - "required": [ - "id", - "name", - "author_id", - "created_at", - "updated_at", - ], - "type": "object", - }, - "GetSomeExample200ResponseBody": { - "properties": { - "something": { - "items": { - "properties": { - "another": { - "items": { - "properties": { - "max": { - "type": "number", - }, - "min": { - "type": "number", - }, - "path": { - "type": "string", - }, - }, - "required": [ - "path", - "max", - "min", - ], - "type": "object", - }, - "type": [ - "array", - "null", - ], - }, - "name": { - "type": "string", - }, - }, - "required": [ - "name", - "another", - ], - "type": "object", - }, - "type": "array", - }, - }, - "required": [ - "something", - ], - "type": "object", - }, - "PostAuthors201ResponseBody": { - "properties": { - "id": { - "type": "string", - }, - "name": { - "type": "string", - }, - }, - "required": [ - "id", - "name", - ], - "type": "object", - }, - "PostAuthorsRequestBody": { - "properties": { - "name": { - "type": "string", - }, - }, - "required": [ - "name", - ], - "type": "object", - }, - "PostBooks201ResponseBody": { - "properties": { - "author_id": { - "type": "string", - }, - "id": { - "type": "string", - }, - "name": { - "type": "string", - }, - }, - "required": [ - "id", - "name", - "author_id", - ], - "type": "object", - }, - "PostBooksRequestBody": { - "properties": { - "author_id": { - "type": "string", - }, - "name": { - "type": "string", - }, - }, - "required": [ - "name", - "author_id", - ], - "type": "object", - }, - }, - }, - "info": { - "description": "The API", - "title": "a spec", - "version": "0.1.0", - }, - "openapi": "3.1.0", - "paths": { - "/authors": { - "post": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PostAuthorsRequestBody", - }, - }, - }, - }, - "responses": { - "201": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/PostAuthors201ResponseBody", - }, - }, - }, - "description": "201 response", - }, - }, - }, - }, - "/authors/{author}": { - "get": { - "responses": { - "200": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/GetBooksBook200ResponseBody", - }, - }, - }, - "description": "200 response", - }, - }, - }, - "parameters": [ - { - "in": "path", - "name": "author", - "required": true, - "schema": { - "type": "string", - }, - }, - ], - }, - "/books": { - "get": { - "responses": { - "200": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "properties": { - "data": { - "items": { - "$ref": "#/components/schemas/GetBooksBook200ResponseBody", - }, - "type": "array", - }, - "has_more_data": { - "type": "boolean", - }, - "next": { - "type": "null", - }, - }, - "required": [ - "data", - "next", - "has_more_data", - ], - "type": "object", - }, - }, - }, - "description": "200 response", - }, - }, - }, - "post": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PostBooksRequestBody", - }, - }, - }, - }, - "responses": { - "201": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/PostBooks201ResponseBody", - }, - }, - }, - "description": "201 response", - }, - }, - }, - }, - "/books/{book}": { - "get": { - "responses": { - "200": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/GetBooksBook200ResponseBody", - }, - }, - }, - "description": "200 response", - }, - }, - }, - "parameters": [ - { - "in": "path", - "name": "book", - "required": true, - "schema": { - "type": "string", - }, - }, - ], - }, - "/some/example": { - "get": { - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetSomeExample200ResponseBody", - }, - }, - }, - "description": "200 response", - }, - }, - }, - }, - }, - "servers": [ - { - "name": "book server", - "url": "http://localhost:3030/api", - }, - ], -} -`; - -exports[`update updates an empty spec 1`] = ` -"optic update is deprecated. Start using the new capture flow by running optic capture openapi.yml --update (get started by running optic capture init openapi.yml) -Β» Documenting new operations... - added post /books - added get /books - added post /authors - added get /some/example - added get /books/{book} - added get /authors/{author} -Β» Updating operations... - - -Share a link to documentation with your team (optic api add openapi.yml) -" -`; - -exports[`update updates an empty spec 2`] = ` -{ - "components": { - "schemas": { - "GetBooksBook200ResponseBody": { - "properties": { - "author_id": { - "type": "string", - }, - "created_at": { - "type": "string", - }, - "id": { - "type": "string", - }, - "name": { - "type": "string", - }, - "updated_at": { - "type": "string", - }, - }, - "required": [ - "id", - "name", - "author_id", - "created_at", - "updated_at", - ], - "type": "object", - }, - "GetSomeExample200ResponseBody": { - "properties": { - "something": { - "items": { - "properties": { - "another": { - "items": { - "properties": { - "max": { - "type": "number", - }, - "min": { - "type": "number", - }, - "path": { - "type": "string", - }, - }, - "required": [ - "path", - "max", - "min", - ], - "type": "object", - }, - "type": [ - "array", - "null", - ], - }, - "name": { - "type": "string", - }, - }, - "required": [ - "name", - "another", - ], - "type": "object", - }, - "type": "array", - }, - }, - "required": [ - "something", - ], - "type": "object", - }, - "PostAuthors201ResponseBody": { - "properties": { - "id": { - "type": "string", - }, - "name": { - "type": "string", - }, - }, - "required": [ - "id", - "name", - ], - "type": "object", - }, - "PostAuthorsRequestBody": { - "properties": { - "name": { - "type": "string", - }, - }, - "required": [ - "name", - ], - "type": "object", - }, - "PostBooks201ResponseBody": { - "properties": { - "author_id": { - "type": "string", - }, - "id": { - "type": "string", - }, - "name": { - "type": "string", - }, - }, - "required": [ - "id", - "name", - "author_id", - ], - "type": "object", - }, - "PostBooksRequestBody": { - "properties": { - "author_id": { - "type": "string", - }, - "name": { - "type": "string", - }, - }, - "required": [ - "name", - "author_id", - ], - "type": "object", - }, - }, - }, - "info": { - "description": "The API", - "title": "a spec", - "version": "0.1.0", - }, - "openapi": "3.1.0", - "paths": { - "/authors": { - "post": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PostAuthorsRequestBody", - }, - }, - }, - }, - "responses": { - "201": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/PostAuthors201ResponseBody", - }, - }, - }, - "description": "201 response", - }, - }, - }, - }, - "/authors/{author}": { - "get": { - "responses": { - "200": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/GetBooksBook200ResponseBody", - }, - }, - }, - "description": "200 response", - }, - }, - }, - "parameters": [ - { - "in": "path", - "name": "author", - "required": true, - "schema": { - "type": "string", - }, - }, - ], - }, - "/books": { - "get": { - "responses": { - "200": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "properties": { - "data": { - "items": { - "$ref": "#/components/schemas/GetBooksBook200ResponseBody", - }, - "type": "array", - }, - "has_more_data": { - "type": "boolean", - }, - "next": { - "type": "null", - }, - }, - "required": [ - "data", - "next", - "has_more_data", - ], - "type": "object", - }, - }, - }, - "description": "200 response", - }, - }, - }, - "post": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PostBooksRequestBody", - }, - }, - }, - }, - "responses": { - "201": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/PostBooks201ResponseBody", - }, - }, - }, - "description": "201 response", - }, - }, - }, - }, - "/books/{book}": { - "get": { - "responses": { - "200": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/GetBooksBook200ResponseBody", - }, - }, - }, - "description": "200 response", - }, - }, - }, - "parameters": [ - { - "in": "path", - "name": "book", - "required": true, - "schema": { - "type": "string", - }, - }, - ], - }, - "/some/example": { - "get": { - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetSomeExample200ResponseBody", - }, - }, - }, - "description": "200 response", - }, - }, - }, - }, - }, -} -`; - -exports[`update updates an existing spec 1`] = ` -"optic update is deprecated. Start using the new capture flow by running optic capture openapi.yml --update (get started by running optic capture init openapi.yml) -Β» Documenting new operations... - added post /books - added get /books - added post /authors - added get /some/example - added get /books/{book} - added get /authors/{author} -Β» Updating operations... - - -Share a link to documentation with your team (optic api add openapi.yml) -" -`; - -exports[`update updates an existing spec 2`] = ` -{ - "components": { - "schemas": { - "GetBooksBook200ResponseBody": { - "properties": { - "author_id": { - "type": "string", - }, - "created_at": { - "type": "string", - }, - "id": { - "type": "string", - }, - "name": { - "type": "string", - }, - "updated_at": { - "type": "string", - }, - }, - "required": [ - "id", - "name", - "author_id", - "created_at", - "updated_at", - ], - "type": "object", - }, - "GetSomeExample200ResponseBody": { - "properties": { - "something": { - "items": { - "properties": { - "another": { - "items": { - "properties": { - "max": { - "type": "number", - }, - "min": { - "type": "number", - }, - "path": { - "type": "string", - }, - }, - "required": [ - "path", - "max", - "min", - ], - "type": "object", - }, - "type": [ - "array", - "null", - ], - }, - "name": { - "type": "string", - }, - }, - "required": [ - "name", - "another", - ], - "type": "object", - }, - "type": "array", - }, - }, - "required": [ - "something", - ], - "type": "object", - }, - "PostAuthors201ResponseBody": { - "properties": { - "id": { - "type": "string", - }, - "name": { - "type": "string", - }, - }, - "required": [ - "id", - "name", - ], - "type": "object", - }, - "PostAuthorsRequestBody": { - "properties": { - "name": { - "type": "string", - }, - }, - "required": [ - "name", - ], - "type": "object", - }, - "PostBooks201ResponseBody": { - "properties": { - "author_id": { - "type": "string", - }, - "id": { - "type": "string", - }, - "name": { - "type": "string", - }, - }, - "required": [ - "id", - "name", - "author_id", - ], - "type": "object", - }, - "PostBooksRequestBody": { - "properties": { - "author_id": { - "type": "string", - }, - "name": { - "type": "string", - }, - }, - "required": [ - "name", - "author_id", - ], - "type": "object", - }, - }, - }, - "info": { - "description": "The API", - "title": "a spec", - "version": "0.1.0", - }, - "openapi": "3.1.0", - "paths": { - "/authors": { - "post": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PostAuthorsRequestBody", - }, - }, - }, - }, - "responses": { - "201": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/PostAuthors201ResponseBody", - }, - }, - }, - "description": "201 response", - }, - }, - }, - }, - "/authors/{author}": { - "get": { - "responses": { - "200": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/GetBooksBook200ResponseBody", - }, - }, - }, - "description": "200 response", - }, - }, - }, - "parameters": [ - { - "in": "path", - "name": "author", - "required": true, - "schema": { - "type": "string", - }, - }, - ], - }, - "/books": { - "get": { - "responses": { - "200": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "properties": { - "data": { - "items": { - "$ref": "#/components/schemas/GetBooksBook200ResponseBody", - }, - "type": "array", - }, - "has_more_data": { - "type": "boolean", - }, - "next": { - "type": "null", - }, - }, - "required": [ - "data", - "next", - "has_more_data", - ], - "type": "object", - }, - }, - }, - "description": "200 response", - }, - }, - }, - "post": { - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PostBooksRequestBody", - }, - }, - }, - }, - "responses": { - "201": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/PostBooks201ResponseBody", - }, - }, - }, - "description": "201 response", - }, - }, - }, - }, - "/books/{book}": { - "get": { - "responses": { - "200": { - "content": { - "application/json; charset=utf-8": { - "schema": { - "$ref": "#/components/schemas/GetBooksBook200ResponseBody", - }, - }, - }, - "description": "200 response", - }, - }, - }, - "parameters": [ - { - "in": "path", - "name": "book", - "required": true, - "schema": { - "type": "string", - }, - }, - ], - }, - "/some/example": { - "get": { - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetSomeExample200ResponseBody", - }, - }, - }, - "description": "200 response", - }, - }, - }, - }, - }, -} -`; diff --git a/projects/optic/src/__tests__/integration/api-add.test.ts b/projects/optic/src/__tests__/integration/api-add.test.ts deleted file mode 100644 index c87f7b93de..0000000000 --- a/projects/optic/src/__tests__/integration/api-add.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { - test, - expect, - describe, - jest, - beforeEach, - afterEach, -} from '@jest/globals'; -import { - runOptic, - setupWorkspace, - normalizeWorkspace, - setupTestServer, - run, -} from './integration'; -jest.setTimeout(30000); - -function sanitizeOutput(out: string) { - return out.replace(/[a-zA-Z0-9]{8}:/g, 'COMMIT-HASH:'); -} - -setupTestServer(({ url, method }) => { - if (method === 'POST' && /\/api\/specs\/prepare$/.test(url)) { - return JSON.stringify({ - upload_id: '123', - spec_url: `${process.env.BWTS_HOST_OVERRIDE}/special-s3-route/spec`, - sourcemap_url: `${process.env.BWTS_HOST_OVERRIDE}/special-s3-route/sourcemap`, - }); - } else if (method === 'POST' && /\/api\/specs$/.test(url)) { - return JSON.stringify({ - id: 'spec-id', - }); - } else if (method === 'GET' && /\/api\/token\/orgs/.test(url)) { - return JSON.stringify({ - organizations: [{ id: 'org-id', name: 'org-blah' }], - }); - } else if (method === 'GET' && /\/api\/ruleset-configs/.test(url)) { - // a return value means it exists - return JSON.stringify({}); - } else if (method === 'POST' && /\/api\/api/.test(url)) { - return JSON.stringify({ id: 'api-id' }); - } - - return JSON.stringify({}); -}); - -describe('optic api add', () => { - let oldEnv: any; - beforeEach(() => { - oldEnv = { ...process.env }; - process.env.LOG_LEVEL = 'info'; - process.env.OPTIC_ENV = 'test'; - }); - - afterEach(() => { - process.env = { ...oldEnv }; - }); - - describe.each([ - [ - 'git', - { - repo: true, - commit: true, - }, - ], - [ - 'no vcs', - { - repo: true, // To force a no vcs - we need to init an empty git repo (otherwise it uses the optic git repo) - commit: false, - }, - ], - ])('%s - no cli interaction', (vcs, setupOptions) => { - test('discover one file with history depth', async () => { - process.env.OPTIC_TOKEN = 'something'; - const workspace = await setupWorkspace('api-add/one-file', setupOptions); - if (vcs === 'git') { - // add another commit - await run( - `touch ./hello.yml && git add . && git commit -m 'another one'`, - false, - workspace - ); - } - const { combined, code } = await runOptic( - workspace, - 'api add ./spec.yml' - ); - - expect(code).toBe(0); - expect( - normalizeWorkspace(workspace, sanitizeOutput(combined)) - ).toMatchSnapshot(); - // expect spec to have added yml files - const { code: specCode, combined: specCombined } = await run( - 'cat ./spec.yml', - false, - workspace - ); - expect(specCode).toBe(0); - expect(normalizeWorkspace(workspace, specCombined)).toMatchSnapshot(); - }); - - test('discover all files in repo', async () => { - process.env.OPTIC_TOKEN = 'something'; - - const workspace = await setupWorkspace( - 'api-add/many-files', - setupOptions - ); - - const { combined, code } = await runOptic(workspace, 'api add .'); - - expect(code).toBe(0); - expect( - normalizeWorkspace(workspace, sanitizeOutput(combined)) - ).toMatchSnapshot(); - }); - - test('discover all files in repo using --all', async () => { - process.env.OPTIC_TOKEN = 'something'; - - const workspace = await setupWorkspace( - 'api-add/many-files', - setupOptions - ); - - const { combined, code } = await runOptic(workspace, 'api add --all'); - - expect(code).toBe(0); - expect( - normalizeWorkspace(workspace, sanitizeOutput(combined)) - ).toMatchSnapshot(); - }); - - test('discover all files in a folder', async () => { - process.env.OPTIC_TOKEN = 'something'; - - const workspace = await setupWorkspace( - 'api-add/many-files', - setupOptions - ); - - const { combined, code } = await runOptic(workspace, 'api add ./nested'); - - expect(code).toBe(0); - expect( - normalizeWorkspace(workspace, sanitizeOutput(combined)) - ).toMatchSnapshot(); - }); - }); -}); diff --git a/projects/optic/src/__tests__/integration/api-list.test.ts b/projects/optic/src/__tests__/integration/api-list.test.ts deleted file mode 100644 index 2d1f6349e3..0000000000 --- a/projects/optic/src/__tests__/integration/api-list.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - test, - expect, - describe, - jest, - beforeEach, - afterEach, -} from '@jest/globals'; -import { runOptic, setupWorkspace, normalizeWorkspace } from './integration'; -jest.setTimeout(30000); - -describe('optic api list', () => { - let oldEnv: any; - beforeEach(() => { - oldEnv = { ...process.env }; - process.env.LOG_LEVEL = 'info'; - process.env.OPTIC_ENV = 'test'; - }); - - afterEach(() => { - process.env = { ...oldEnv }; - }); - - test('lists all files in repo', async () => { - process.env.OPTIC_TOKEN = 'something'; - - const workspace = await setupWorkspace('api-list/many-files'); - - const { combined, code } = await runOptic(workspace, 'api list'); - - expect(code).toBe(0); - expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); - }); -}); diff --git a/projects/optic/src/__tests__/integration/diff-all.test.ts b/projects/optic/src/__tests__/integration/diff-all.test.ts index a07ed63481..6dc86d40e3 100644 --- a/projects/optic/src/__tests__/integration/diff-all.test.ts +++ b/projects/optic/src/__tests__/integration/diff-all.test.ts @@ -10,54 +10,12 @@ import { runOptic, setupWorkspace, normalizeWorkspace, - setupTestServer, run, } from './integration'; import path from 'node:path'; -import fs from 'node:fs/promises'; jest.setTimeout(30000); -setupTestServer(({ url, method }) => { - if (method === 'POST' && /\/api\/specs\/prepare$/.test(url)) { - return JSON.stringify({ - spec_id: 'already-uploaded', - }); - } else if (method === 'POST' && /\/api\/runs\/prepare$/.test(url)) { - return JSON.stringify({ - check_results_url: `${process.env.BWTS_HOST_OVERRIDE}/special-s3-route`, - upload_id: '123', - }); - } else if (method === 'POST' && /\/api\/runs2$/.test(url)) { - return JSON.stringify({ - id: 'run-id', - }); - } else if (method === 'GET' && /spec$/.test(url)) { - return `{"openapi":"3.1.0","paths":{ "/api/users": { "get": { "responses":{} }}},"info":{"version":"0.0.0","title":"Empty"}}`; - } else if (method === 'GET' && /sourcemap$/.test(url)) { - return `{"rootFilePath":"empty.json","files":[{"path":"empty.json","sha256":"815b8e5491a1f491765084f236c741d5073e10fcece23436f2db84a8c788db09","contents":"{'openapi':'3.1.0','paths':{ '/api/users': { 'get': { 'responses':{} }}},'info':{'version':'0.0.0','title':'Empty'}}","index":0}],"refMappings":{}}`; - } else if (method === 'GET' && /api\/apis\/.*\/specs\/.*$/.test(url)) { - return JSON.stringify({ - id: 'run-id', - specUrl: `${process.env.BWTS_HOST_OVERRIDE}/spec`, - sourcemapUrl: `${process.env.BWTS_HOST_OVERRIDE}/sourcemap`, - }); - } else if (method === 'GET' && /\/api\/apis/.test(url)) { - return JSON.stringify({ - apis: [null], - }); - } else if (method === 'POST' && /\/api\/api$/.test(url)) { - return JSON.stringify({ - id: 'generated-api', - }); - } else if (method === 'GET' && /\/api\/token\/orgs/.test(url)) { - return JSON.stringify({ - organizations: [{ id: 'org-id', name: 'org-blah' }], - }); - } - return JSON.stringify({}); -}); - function sanitizeOutput(out: string) { return out.replace(/tree\/[a-zA-Z0-9]{40}/g, 'tree/COMMIT-HASH'); } @@ -113,35 +71,6 @@ describe('diff-all', () => { expect(code).toBe(1); }); - test('diffs all files in a workspace with x-optic-url keys with --upload', async () => { - const workspace = await setupWorkspace('diff-all/repo', { - repo: true, - commit: true, - }); - - await run( - `mv ./mvspec.yml ./movedspec.yml && git add . && git commit -m 'move spec'`, - false, - workspace - ); - process.env.OPTIC_TOKEN = '123'; - - const { combined, code } = await runOptic( - workspace, - 'diff-all --check --upload' - ); - - expect( - normalizeWorkspace(workspace, sanitizeOutput(combined)) - ).toMatchSnapshot(); - expect( - JSON.parse( - await fs.readFile(path.join(workspace, 'ci-run-details.json'), 'utf-8') - ) - ).toMatchSnapshot(); - expect(code).toBe(1); - }); - test('diffs all files with --json', async () => { const workspace = await setupWorkspace('diff-all/repo', { repo: true, @@ -213,22 +142,4 @@ describe('diff-all', () => { ).toMatchSnapshot(); expect(code).toBe(1); }); - - test('diff all against a cloud tag', async () => { - const workspace = await setupWorkspace('diff-all/cloud-diff', { - repo: true, - commit: true, - }); - process.env.OPTIC_TOKEN = '123'; - - const { combined, code } = await runOptic( - workspace, - 'diff-all --compare-from cloud:main --check' - ); - - expect(code).toBe(1); - expect( - normalizeWorkspace(workspace, sanitizeOutput(combined)) - ).toMatchSnapshot(); - }); }); diff --git a/projects/optic/src/__tests__/integration/diff.test.ts b/projects/optic/src/__tests__/integration/diff.test.ts index ceb9dcc6d6..7e74a83afa 100644 --- a/projects/optic/src/__tests__/integration/diff.test.ts +++ b/projects/optic/src/__tests__/integration/diff.test.ts @@ -12,7 +12,6 @@ import { runOptic, setupWorkspace, normalizeWorkspace, - setupTestServer, run, } from './integration'; @@ -150,213 +149,4 @@ describe('diff', () => { expect(code).toBe(1); expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); }); - - describe('with mock server', () => { - setupTestServer(async ({ url, method, headers }) => { - if (method === 'GET' && /\/api\/token\/orgs/.test(url)) { - return JSON.stringify({ - organizations: [ - { - id: 'abc', - name: 'def', - }, - ], - }); - } else if (method === 'GET' && /\/api\/rulesets/.test(url)) { - return JSON.stringify({ - rulesets: [ - { - name: '@org/custom-ruleset', - url: `${process.env.BWTS_HOST_OVERRIDE}/download-url`, - uploaded_at: '2022-11-02T17:55:48.078Z', - }, - ], - }); - } else if (method === 'GET' && /download-url/.test(url)) { - return fs.readFile( - path.resolve( - __dirname, - './workspaces/diff/custom-rules/rules/cloud-mock.js' - ) - ); - } else if (method === 'GET' && /\/api\/ruleset-configs\//.test(url)) { - return JSON.stringify({ - organization_id: 'abc', - config: { - ruleset: [{ name: 'breaking-changes', config: {} }], - }, - ruleset_id: 'abc', - created_at: '2022-11-02T17:55:48.078Z', - updated_at: '2022-11-02T17:55:48.078Z', - }); - } else if (method === 'POST' && /\/api\/specs\/prepare$/.test(url)) { - return JSON.stringify({ - spec_id: 'already-uploaded', - }); - } else if (method === 'POST' && /\/api\/runs\/prepare$/.test(url)) { - return JSON.stringify({ - check_results_url: `${process.env.BWTS_HOST_OVERRIDE}/special-s3-route`, - upload_id: '123', - }); - } else if (method === 'POST' && /\/api\/runs2$/.test(url)) { - return JSON.stringify({ - id: 'run-id', - }); - } else if ( - method === 'GET' && - /my-spec\.yml$/.test(url) && - headers.custom_header === 'hello' - ) { - return `openapi: 3.0.3 -info: - title: a spec - description: The API - version: 0.1.0 -paths: - /api/users: - get: - responses: - '200': - description: hello - content: - application/json: - schema: - $ref: ${process.env.BWTS_HOST_OVERRIDE}/shared-schema.yml -`; - } else if ( - method === 'GET' && - /shared-schema\.yml$/.test(url) && - headers.custom_header === 'hello' - ) { - return `type: string`; - } else if (method === 'GET' && /spec$/.test(url)) { - return `{"openapi":"3.1.0","paths":{ "/api/users": { "get": { "responses":{} }}},"info":{"version":"0.0.0","title":"Empty"}}`; - } else if (method === 'GET' && /sourcemap$/.test(url)) { - return `{"rootFilePath":"empty.json","files":[{"path":"empty.json","sha256":"815b8e5491a1f491765084f236c741d5073e10fcece23436f2db84a8c788db09","contents":"{'openapi':'3.1.0','paths':{ '/api/users': { 'get': { 'responses':{} }}},'info':{'version':'0.0.0','title':'Empty'}}","index":0}],"refMappings":{}}`; - } else if (method === 'GET' && /api\/apis\/.*\/specs\/.*$/.test(url)) { - return JSON.stringify({ - id: 'run-id', - specUrl: `${process.env.BWTS_HOST_OVERRIDE}/spec`, - sourcemapUrl: `${process.env.BWTS_HOST_OVERRIDE}/sourcemap`, - }); - } else if (method === 'GET' && /\/api\/apis/.test(url)) { - return JSON.stringify({ - apis: [], - }); - } - return JSON.stringify({}); - }); - - test('uploads specs if authenticated and --upload', async () => { - const workspace = await setupWorkspace('diff/upload', { - repo: true, - commit: true, - }); - - await run( - `sed -i.bak 's/string/number/' spec.json spec.json && git add . && git commit -m 'update spec'`, - false, - workspace - ); - - process.env.OPTIC_TOKEN = '123'; - process.env.CI = 'true'; - - const { combined, code } = await runOptic( - workspace, - 'diff spec.json --base HEAD~1 --check --upload' - ); - - expect(code).toBe(1); - expect( - normalizeWorkspace(workspace, sanitizeOutput(combined)) - ).toMatchSnapshot(); - }); - - test('ruleset key on api spec', async () => { - const workspace = await setupWorkspace('diff/with-x-optic-standard', { - repo: true, - commit: true, - }); - const { combined, code } = await runOptic( - workspace, - 'diff example-api-v0.json example-api-v1.json --check' - ); - - expect(code).toBe(1); - expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); - }); - - test('custom rules', async () => { - const workspace = await setupWorkspace('diff/custom-rules', { - repo: true, - commit: true, - }); - const { combined, code } = await runOptic( - workspace, - 'diff example-api-v0.json example-api-v1.json --check' - ); - - expect(code).toBe(1); - expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); - }); - - test('extends', async () => { - const workspace = await setupWorkspace('diff/extends', { - repo: true, - commit: true, - }); - const { combined, code } = await runOptic( - workspace, - 'diff example-api-v0.json example-api-v1.json --check' - ); - - expect(code).toBe(1); - expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); - }); - - test('with web url and header', async () => { - const workspace = await setupWorkspace('diff/ref-resolve-headers', { - repo: false, - }); - const { combined, code } = await runOptic( - workspace, - `diff ${process.env.BWTS_HOST_OVERRIDE}/my-spec.yml null: --check` - ); - - expect(code).toBe(1); - expect( - normalizeWorkspace(workspace, combined).replaceAll( - process.env.BWTS_HOST_OVERRIDE!, - 'http://localhost' - ) - ).toMatchSnapshot(); - }); - - test('with cloud tag ref', async () => { - const workspace = await setupWorkspace('diff/files-no-repo', { - repo: false, - }); - const { combined, code } = await runOptic( - workspace, - `diff null: cloud:api-id@main --check` - ); - - expect(code).toBe(0); - expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); - }); - - test('with --base cloud:tag', async () => { - const workspace = await setupWorkspace('diff/upload', { - repo: false, - }); - const { combined, code } = await runOptic( - workspace, - `diff spec.json --base cloud:main --check` - ); - - expect(code).toBe(1); - expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); - }); - }); }); diff --git a/projects/optic/src/__tests__/integration/ruleset-publish.test.ts b/projects/optic/src/__tests__/integration/ruleset-publish.test.ts deleted file mode 100644 index de4a4d1c7f..0000000000 --- a/projects/optic/src/__tests__/integration/ruleset-publish.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - test, - expect, - describe, - jest, - beforeEach, - afterEach, -} from '@jest/globals'; -import { - runOptic, - setupWorkspace, - normalizeWorkspace, - setupTestServer, -} from './integration'; -jest.setTimeout(30000); - -setupTestServer(({ url, method }) => { - if (method === 'POST' && /\/api\/organizations\/.*\/rulesets$/.test(url)) { - return JSON.stringify({ - id: '123', - slug: '@orgslug/ruleset-name', - upload_url: `${process.env.BWTS_HOST_OVERRIDE}/upload-url`, - ruleset_url: 'http://app.useoptic.com/ruleset_url', - }); - } - return JSON.stringify({}); -}); - -describe('optic ruleset upload', () => { - let oldEnv: any; - beforeEach(() => { - oldEnv = { ...process.env }; - process.env.LOG_LEVEL = 'info'; - }); - - afterEach(() => { - process.env = { ...oldEnv }; - }); - - test('can publish a ruleset', async () => { - const workspace = await setupWorkspace('ruleset-publish/valid-js-file'); - process.env.OPTIC_TOKEN = '123'; - const { combined, code } = await runOptic( - workspace, - 'ruleset upload ./rules.js --organization-id 123' - ); - expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); - expect(code).toBe(0); - }); - - test('exits if ruleset file does not have rulesConstructor', async () => { - const workspace = await setupWorkspace( - 'ruleset-publish/no-rulesConstructor' - ); - process.env.OPTIC_TOKEN = '123'; - const { combined, code } = await runOptic( - workspace, - 'ruleset upload ./rules.js --organization-id 123' - ); - expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); - expect(code).toBe(1); - }); - - test('exits if ruleset file shape is not valid', async () => { - const workspace = await setupWorkspace('ruleset-publish/invalid-js-file'); - process.env.OPTIC_TOKEN = '123'; - const { combined, code } = await runOptic( - workspace, - 'ruleset upload ./rules.js --organization-id 123' - ); - expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); - expect(code).toBe(1); - }); -}); diff --git a/projects/optic/src/__tests__/integration/run.test.ts b/projects/optic/src/__tests__/integration/run.test.ts deleted file mode 100644 index 3ed110e2f4..0000000000 --- a/projects/optic/src/__tests__/integration/run.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { - test, - expect, - describe, - jest, - beforeEach, - afterEach, -} from '@jest/globals'; -import { - runOptic, - setupWorkspace, - normalizeWorkspace, - run, - setupTestServer, -} from './integration'; -import portfinder from 'portfinder'; - -jest.setTimeout(30000); - -let oldEnv: any; -beforeEach(async () => { - oldEnv = { ...process.env }; - process.env.LOG_LEVEL = 'info'; - process.env.OPTIC_ENV = 'local'; - process.env.OPTIC_TOKEN = '123'; - process.env.GITHUB_BASE_REF = 'main'; - process.env.CI = 'true'; - delete process.env.GITHUB_TOKEN; -}); - -afterEach(() => { - process.env = { ...oldEnv }; -}); - -async function setPortInFile(workspace: string, file: string, port: string) { - // Set the port in the optic yml for an available port - await run(`sed -i.bak 's/%PORT/${port}/' ${file} ${file}`, false, workspace); -} - -function sanitizeOutput(out: string) { - return out.replace(/\[[1|2]\]: `.+` tag\n/g, 'replaced line\n'); -} - -setupTestServer(({ url, method }) => { - if (method === 'POST' && /\/api\/specs\/prepare$/.test(url)) { - return JSON.stringify({ - spec_id: 'already-uploaded', - }); - } else if (method === 'POST' && /\/api\/runs\/prepare$/.test(url)) { - return JSON.stringify({ - check_results_url: `${process.env.BWTS_HOST_OVERRIDE}/special-s3-route`, - upload_id: '123', - }); - } else if (method === 'POST' && /\/api\/verifications\/prepare$/.test(url)) { - return JSON.stringify({ - url: `${process.env.BWTS_HOST_OVERRIDE}/special-s3-route`, - upload_id: '123', - }); - } else if (method === 'POST' && /\api\/verifications$/.test(url)) { - return JSON.stringify({ - id: 'verification-id', - }); - } else if (method === 'POST' && /\/api\/runs2$/.test(url)) { - return JSON.stringify({ - id: 'run-id', - }); - } else if (method === 'GET' && /spec$/.test(url)) { - return `{"openapi":"3.1.0","paths":{ "/api/users": { "get": { "responses":{} }}},"info":{"version":"0.0.0","title":"Empty"}}`; - } else if (method === 'GET' && /sourcemap$/.test(url)) { - return `{"rootFilePath":"empty.json","files":[{"path":"empty.json","sha256":"815b8e5491a1f491765084f236c741d5073e10fcece23436f2db84a8c788db09","contents":"{'openapi':'3.1.0','paths':{ '/api/users': { 'get': { 'responses':{} }}},'info':{'version':'0.0.0','title':'Empty'}}","index":0}],"refMappings":{}}`; - } else if (method === 'GET' && /api\/apis\/.*\/specs\/.*$/.test(url)) { - return JSON.stringify({ - id: 'run-id', - specUrl: `${process.env.BWTS_HOST_OVERRIDE}/spec`, - sourcemapUrl: `${process.env.BWTS_HOST_OVERRIDE}/sourcemap`, - }); - } else if (method === 'GET' && /\/api\/apis/.test(url)) { - return JSON.stringify({ - apis: [null], - }); - } else if (method === 'POST' && /\/api\/api$/.test(url)) { - return JSON.stringify({ - id: 'generated-api', - }); - } else if (method === 'GET' && /\/api\/token\/orgs/.test(url)) { - return JSON.stringify({ - organizations: [{ id: 'org-id', name: 'org-blah' }], - }); - } - return JSON.stringify({}); -}); - -describe('run', () => { - test('runs and diffs against APIs and runs capture', async () => { - const workspace = await setupWorkspace('run/multi-spec', { - repo: true, - commit: true, - }); - const port = String( - await portfinder.getPortPromise({ - port: 9000, - stopPort: 10000, - }) - ); - process.env.PORT = port; - await setPortInFile(workspace, 'optic.yml', port); - const { combined, code } = await runOptic(workspace, 'run'); - expect( - sanitizeOutput(normalizeWorkspace(workspace, combined)) - ).toMatchSnapshot(); - expect(code).toBe(1); - }); - - test('ignores files in gitignore', async () => { - const workspace = await setupWorkspace('run/gitignore', { - repo: true, - commit: true, - }); - - await run( - `cp openapi.yml openapi-local.yml && echo openapi-local.yml > .gitignore`, - false, - workspace - ); - - const { combined, code } = await runOptic(workspace, 'run'); - expect( - sanitizeOutput(normalizeWorkspace(workspace, combined)) - ).toMatchSnapshot(); - expect(code).toBe(0); - }); - - test('can include files in gitignore with -I and file paths', async () => { - const workspace = await setupWorkspace('run/gitignore', { - repo: true, - commit: true, - }); - await run( - `cp openapi.yml openapi-local.yml && echo openapi-local.yml > .gitignore`, - false, - workspace - ); - const { combined, code } = await runOptic( - workspace, - 'run openapi-local.yml -I' - ); - expect( - sanitizeOutput(normalizeWorkspace(workspace, combined)) - ).toMatchSnapshot(); - expect(code).toBe(0); - }); -}); diff --git a/projects/optic/src/__tests__/integration/spec-push.test.ts b/projects/optic/src/__tests__/integration/spec-push.test.ts deleted file mode 100644 index 42a397982a..0000000000 --- a/projects/optic/src/__tests__/integration/spec-push.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - test, - expect, - describe, - jest, - beforeEach, - afterEach, -} from '@jest/globals'; -import { - runOptic, - setupWorkspace, - normalizeWorkspace, - setupTestServer, - run, -} from './integration'; -jest.setTimeout(30000); - -setupTestServer(({ url, method }) => { - if (method === 'POST' && /\/api\/specs\/prepare$/.test(url)) { - return JSON.stringify({ - upload_id: '123', - spec_url: `${process.env.BWTS_HOST_OVERRIDE}/special-s3-route/spec`, - sourcemap_url: `${process.env.BWTS_HOST_OVERRIDE}/special-s3-route/sourcemap`, - }); - } else if (method === 'POST' && /\/api\/specs$/.test(url)) { - return JSON.stringify({ - id: 'spec-id', - }); - } - - return JSON.stringify({}); -}); - -function sanitizeOutput(out: string) { - return out.replace(/git:[a-zA-Z0-9]{40}/g, 'git:COMMIT-HASH'); -} - -describe('optic spec push', () => { - let oldEnv: any; - beforeEach(() => { - oldEnv = { ...process.env }; - process.env.LOG_LEVEL = 'info'; - process.env.OPTIC_ENV = 'local'; - }); - - afterEach(() => { - process.env = { ...oldEnv }; - }); - - test('can push a spec to a repo', async () => { - const workspace = await setupWorkspace('spec-push/simple', { - repo: true, - commit: true, - }); - process.env.OPTIC_TOKEN = '123'; - const { combined, code } = await runOptic( - workspace, - 'spec push ./spec.yml --tag env:production,the-favorite-api' - ); - expect(code).toBe(0); - expect( - normalizeWorkspace(workspace, sanitizeOutput(combined)) - ).toMatchSnapshot(); - }); -}); diff --git a/projects/optic/src/__tests__/integration/update.test.ts b/projects/optic/src/__tests__/integration/update.test.ts deleted file mode 100644 index f10706b328..0000000000 --- a/projects/optic/src/__tests__/integration/update.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - test, - expect, - describe, - jest, - beforeEach, - afterEach, -} from '@jest/globals'; -import { runOptic, setupWorkspace, normalizeWorkspace } from './integration'; -import path from 'node:path'; -import fs from 'node:fs/promises'; -import yaml from 'js-yaml'; - -jest.setTimeout(30000); - -let oldEnv: any; -beforeEach(() => { - oldEnv = { ...process.env }; - process.env.LOG_LEVEL = 'info'; - process.env.OPTIC_ENV = 'local'; - process.env.CI = 'false'; -}); - -afterEach(() => { - process.env = { ...oldEnv }; -}); - -describe('update', () => { - test('updates an empty spec', async () => { - const workspace = await setupWorkspace('update/empty-spec'); - const { combined, code } = await runOptic( - workspace, - 'update openapi.yml --har har.har --all' - ); - expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); - expect(code).toBe(0); - - expect( - yaml.load(await fs.readFile(path.join(workspace, 'openapi.yml'), 'utf-8')) - ).toMatchSnapshot(); - }); - - test('updates an existing spec', async () => { - const workspace = await setupWorkspace('update/existing-spec'); - const { combined, code } = await runOptic( - workspace, - 'update openapi.yml --har har.har --all' - ); - expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); - expect(code).toBe(0); - - expect( - yaml.load(await fs.readFile(path.join(workspace, 'openapi.yml'), 'utf-8')) - ).toMatchSnapshot(); - }); - - test('update an existing spec with prefixed server', async () => { - const workspace = await setupWorkspace('update/prefix-server-spec'); - const { combined, code } = await runOptic( - workspace, - 'update openapi.yml --har har.har --all' - ); - expect(normalizeWorkspace(workspace, combined)).toMatchSnapshot(); - expect(code).toBe(0); - - expect( - yaml.load(await fs.readFile(path.join(workspace, 'openapi.yml'), 'utf-8')) - ).toMatchSnapshot(); - }); -}); diff --git a/projects/optic/src/client/optic-backend.ts b/projects/optic/src/client/optic-backend.ts index ebe436c59b..7d4f49f5ff 100644 --- a/projects/optic/src/client/optic-backend.ts +++ b/projects/optic/src/client/optic-backend.ts @@ -1,6 +1,5 @@ import fetch from 'node-fetch'; import { JsonHttpClient } from './JsonHttpClient'; -import * as Types from './optic-backend-types'; export class OpticBackendClient extends JsonHttpClient { constructor( @@ -32,255 +31,6 @@ export class OpticBackendClient extends JsonHttpClient { ? 'http://localhost:3001' : 'https://app.useoptic.com'; } - - public getTokenOrgs(): Promise<{ - organizations: { id: string; name: string }[]; - }> { - return this.getJson(`/api/token/orgs`); - } - - public async createCapture( - data: - | { - run_id: string; - organization_id: string; - success: true; - unmatched_interactions: number; - total_interactions: number; - percent_covered: number; - endpoints_added: number; - endpoints_matched: number; - endpoints_unmatched: number; - endpoints_total: number; - has_any_diffs: boolean; - mismatched_endpoints: number; - } - | { - success: false; - } - ): Promise<{}> { - return this.postJson(`/api/captures`, data); - } - - public async createRuleset( - organizationId: string, - name: string, - description: string, - config_schema: any - ): Promise<{ - id: string; - slug: string; - upload_url: string; - ruleset_url: string; - }> { - return this.postJson(`/api/organizations/${organizationId}/rulesets`, { - name, - description, - config_schema, - }); - } - - public async patchRuleset( - organizationId: string, - rulesetId: string, - uploaded: boolean - ): Promise { - return this.patchJson( - `/api/organizations/${organizationId}/rulesets/${rulesetId}`, - { - uploaded, - } - ); - } - - public async getManyRulesetsByName(rulesets: string[]): Promise<{ - rulesets: ({ - name: string; - url: string; - uploaded_at: string; - } | null)[]; - }> { - const encodedRulesets = rulesets - .map((r) => encodeURIComponent(r)) - .join(','); - return this.getJson(`/api/rulesets?rulesets=${encodedRulesets}`); - } - - public async getStandard( - rulesetConfigIdentifier: string - ): Promise { - const encodedIdentifier = encodeURIComponent(rulesetConfigIdentifier); - return this.getJson(`/api/ruleset-configs/${encodedIdentifier}`); - } - - public async createOrgStandard( - organizationId: string, - standard: Types.StandardConfig - ): Promise<{ id: string; slug: string }> { - return this.postJson<{ id: string; slug: string }>( - `/api/organizations/${organizationId}/standards`, - { - config: { ruleset: standard }, - } - ); - } - - public async getOrgStandards( - organizationId: string - ): Promise { - const response = await this.getJson<{ - data: Types.Standard[]; - }>(`/api/organizations/${organizationId}/standards`); - - return response.data; - } - - public async prepareSpecUpload(body: { - api_id: string; - spec_checksum: string; - sourcemap_checksum: string; - }): Promise< - | { - upload_id: string; - spec_url: string; - sourcemap_url: string; - } - | { - spec_id: string; - } - > { - return this.postJson(`/api/specs/prepare`, body); - } - - public async getSpec( - apiId: string, - tag: string - ): Promise<{ - id: string; - specUrl: string | null; - sourcemapUrl: string | null; - }> { - return this.getJson(`/api/apis/${apiId}/specs/tag:${tag}`); - } - - public async createSpec(spec: { - tags: string[]; - openapi_version: '2.x.x' | '3.0.x' | '3.1.x'; - upload_id: string; - api_id: string; - effective_at?: Date; - git_name?: string; - git_email?: string; - commit_message?: string; - forward_effective_at_to_tags?: boolean; - }): Promise<{ id: string }> { - return this.postJson(`/api/specs`, spec); - } - - public async tagSpec(specId: string, tags: string[], effective_at?: Date) { - return this.patchJson(`/api/specs/${specId}/tags`, { - tags, - effective_at, - }); - } - - public async prepareRunUpload(body: { - checksum: string; - api_id: string; - }): Promise<{ upload_id: string; check_results_url: string }> { - return this.postJson(`/api/runs/prepare`, body); - } - - public async createRun(run: { - upload_id: string; - api_id: string; - from_spec_id: string; - to_spec_id: string; - ruleset: Types.StandardConfig; - ci?: boolean; - }): Promise<{ id: string }> { - return this.postJson(`/api/runs2`, run); - } - - public async getApis( - paths: string[], - web_url: string - ): Promise<{ apis: (Types.Api | null)[] }> { - return this.getJson<{ apis: (Types.Api | null)[] }>( - `/api/apis?paths=${paths - .map((p) => encodeURIComponent(p)) - .join(',')}&web_url=${encodeURIComponent(web_url)}` - ); - } - - public async createApi( - organizationId: string, - opts: { - name: string; - path?: string; - web_url?: string; - default_branch: string; - default_tag?: string; - } - ): Promise<{ id: string }> { - return this.postJson(`/api/api`, { - ...opts, - organization_id: organizationId, - }); - } - - public async prepareVerification(specId: string, checksum: string) { - return this.postJson<{ - upload_id: string; - url: string; - }>(`/api/verifications/prepare`, { - spec_id: specId, - checksum, - }); - } - - public async createVerification(opts: { - spec_id: string; - upload_id: string; - message?: string; - run_id?: string; - }) { - return this.postJson<{ id: string }>(`/api/verifications`, { - ...opts, - }); - } - - public async verifyToken(): Promise<{ - user?: { email: string; userId: string }; - organization?: { organizationId: string }; - }> { - return this.getJson(`/api/token/verify`); - } - - public async getLintgptPreps(rule_checksums: string[]) { - return this.postJson(`/api/lintgpt-preps/list`, { rule_checksums }); - } - - public async requestLintgptPreps(rules: string[]) { - return this.postJson(`/api/lintgpt-preps/create`, { rules }); - } - - public async getLintgptEvals( - evals: { rule_checksum: string; node_checksum: string }[] - ) { - return this.postJson(`/api/lintgpt-evals/list`, { evals }); - } - - public async requestLintgptEvals( - eval_requests: { - node: string; - node_before?: string; - location_context: string; - rule_checksum: string; - }[] - ) { - return this.postJson(`/api/lintgpt-evals/create`, { eval_requests }); - } } export const createOpticClient = (opticToken: string) => { diff --git a/projects/optic/src/commands/api/add.ts b/projects/optic/src/commands/api/add.ts index b7a9e4fe86..2a7d36e273 100644 --- a/projects/optic/src/commands/api/add.ts +++ b/projects/optic/src/commands/api/add.ts @@ -1,27 +1,7 @@ import { Command } from 'commander'; -import prompts from 'prompts'; -import open from 'open'; -import path from 'path'; -import fs from 'node:fs/promises'; -import { OpticCliConfig, VCS } from '../../config'; -import { loadSpec, ParseResult } from '../../utils/spec-loaders'; -import { logger } from '../../logger'; -import { OPTIC_URL_KEY } from '../../constants'; -import chalk from 'chalk'; -import * as GitCandidates from './git-get-file-candidates'; -import * as FsCandidates from './get-file-candidates'; -import { uploadSpec } from '../../utils/cloud-specs'; -import * as Git from '../../utils/git-utils'; +import { OpticCliConfig } from '../../config'; -import { getApiUrl, getOpticUrlDetails } from '../../utils/cloud-urls'; -import { flushEvents, trackEvent } from '../../segment'; import { errorHandler } from '../../error-handler'; -import { getOrganizationFromToken } from '../../utils/organization'; -import { sanitizeGitTag } from '@useoptic/openapi-utilities'; -import stableStringify from 'json-stable-stringify'; -import { computeChecksumForAws } from '../../utils/checksum'; -import { getSpinner } from '../../utils/spinner'; -import { getUniqueTags } from '../../utils/tags'; function short(sha: string) { return sha.slice(0, 8); @@ -76,465 +56,9 @@ type ApiAddActionOptions = { startCommit?: string; }; -async function initializeApi( - orgId: string, - file_path: string, - config: OpticCliConfig, - options: { - path_to_spec: string | undefined; - web?: boolean; - default_branch: string; - default_tag?: string | undefined; - web_url?: string; - } -) { - const pathRelativeToRoot = path.relative(config.root, file_path); - let parseResult: ParseResult; - try { - parseResult = await loadSpec(file_path, config, { - strict: false, - denormalize: true, - }); - } catch (e) { - if (file_path === options.path_to_spec) { - logger.info( - chalk.red( - `File ${options.path_to_spec} is not a valid OpenAPI file. Optic currently supports OpenAPI 3 and 3.1` - ) - ); - logger.info(e); - } else { - logger.debug(`Disregarding candidate ${pathRelativeToRoot}`); - logger.debug(e); - } - return; - } - if (parseResult.isEmptySpec) { - logger.info( - chalk.red( - `File ${pathRelativeToRoot} does not exist in working directory` - ) - ); - return; - } - const specName = parseResult.jsonLike.info.title || 'Untitled spec'; - - const existingOpticUrl: string | undefined = - parseResult.jsonLike[OPTIC_URL_KEY]; - - const opticUrlDetails = await getOpticUrlDetails(config, { - filePath: options.path_to_spec - ? path.relative(config.root, path.resolve(options.path_to_spec)) - : undefined, - opticUrl: existingOpticUrl, - webUrl: options.web_url, - orgId, - }); - - let alreadyTracked = false; - let tagsToAdd: string[] = []; - - let api: { id: string; url: string }; - if (opticUrlDetails) { - alreadyTracked = true; - api = { - id: opticUrlDetails.apiId, - url: - existingOpticUrl ?? - getApiUrl( - config.client.getWebBase(), - opticUrlDetails.orgId, - opticUrlDetails.apiId - ), - }; - } else { - if (config.vcs?.type === VCS.Git) { - const sha = config.vcs.sha; - tagsToAdd.push(`git:${sha}`); - - const branch = await Git.getCurrentBranchName(); - if (branch !== 'HEAD') { - tagsToAdd.push(sanitizeGitTag(`gitbranch:${branch}`)); - } - } - - const name = parseResult.jsonLike?.info?.title ?? pathRelativeToRoot; - const { id } = await config.client.createApi(orgId, { - name, - path: path.relative( - config.root, - path.resolve(options.path_to_spec ?? '') - ), - default_branch: options.default_branch, - default_tag: options.default_tag, - web_url: options.web_url, - }); - api = { - id, - url: getApiUrl(config.client.getWebBase(), orgId, id), - }; - } - await uploadSpec(api.id, { - spec: parseResult, - tags: getUniqueTags(tagsToAdd), - client: config.client, - orgId, - }); - - if (!opticUrlDetails) { - logger.debug(`Added spec ${pathRelativeToRoot} to ${api.url}`); - - trackEvent('api.added', { - apiId: api.id, - orgId: orgId, - url: api.url, - }); - - if (options.web) { - await open(api.url, { wait: false }); - } - } else { - logger.debug( - `Spec ${pathRelativeToRoot} has already been added at ${api.url}` - ); - } - - logger.info( - `${chalk.bold.green('βœ”')} ${chalk.bold.blue(specName)} ${ - alreadyTracked ? 'is already being tracked' : 'is now being tracked' - }.\n ${chalk.bold(`View: ${chalk.underline(api.url)}`)}` - ); - - return { - specName, - api, - file_path, - alreadyTracked, - }; -} - -async function backfillHistory( - orgId: string, - shas: string[], - addedApis: NonNullable>>[], - config: OpticCliConfig, - options: { - path_to_spec: string | undefined; - web?: boolean; - default_branch: string; - default_tag?: string | undefined; - web_url?: string; - startCommit?: string; - } -) { - const specsStatus = new Map< - string, - { completed: boolean; uploadedChecksums: Set } - >(); - - logger.info(''); - const spinner = getSpinner(``); - spinner?.start(); - if (spinner) spinner.color = 'blue'; - - if (config.vcs?.type === VCS.Git) { - const currentBranch = await Git.getCurrentBranchName(); - let mergeBaseSha: string | null = null; - let shouldTag = true; - let shouldStart = options.startCommit === undefined; - let branchToUseForTag = currentBranch; - - if (options.default_branch !== '') { - mergeBaseSha = await Git.getMergeBase( - currentBranch, - options.default_branch - ); - shouldTag = false; - branchToUseForTag = options.default_branch; - } - - for await (const sha of shas) { - if (mergeBaseSha === sha) { - shouldTag = true; - } - if (options.startCommit && !shouldStart) { - const adjustedSha = sha.slice(0, options.startCommit.length); - if (adjustedSha === options.startCommit) { - shouldStart = true; - } else { - logger.debug( - `Skipping ${adjustedSha}, waiting for ${options.startCommit}` - ); - continue; - } - } - const baseText = `${chalk.bold.blue( - 'Backfilling' - )} version ${sha.substring(0, 6)}`; - if (spinner) spinner.text = baseText; - - for (const { api, file_path } of addedApis) { - const pathRelativeToRoot = path.relative(config.root, file_path); - const status = specsStatus.get(pathRelativeToRoot) ?? { - completed: false, - uploadedChecksums: new Set(), - }; - - if (status.completed) { - continue; - } - - if (spinner) spinner.text = `${baseText} file ${pathRelativeToRoot}`; - - let parseResult: ParseResult; - try { - parseResult = await loadSpec(`${sha}:${pathRelativeToRoot}`, config, { - strict: false, - denormalize: true, - }); - } catch (e) { - logger.debug( - `${short( - sha - )}:${pathRelativeToRoot} is not a valid OpenAPI file, skipping sha version`, - e - ); - continue; - } - if (parseResult.isEmptySpec) { - logger.debug( - `File ${pathRelativeToRoot} does not exist in sha ${short( - sha - )}, stopping here` - ); - specsStatus.set(pathRelativeToRoot, { - ...status, - completed: true, - }); - continue; - } - - const stableSpecString = stableStringify(parseResult.jsonLike); - const checksum = computeChecksumForAws(stableSpecString); - if (status.uploadedChecksums.has(checksum)) { - continue; - } - - const specId = await uploadSpec(api.id, { - spec: parseResult, - tags: [`git:${sha}`], - client: config.client, - orgId, - forward_effective_at_to_tags: true, - precomputed: { - specString: stableSpecString, - specChecksum: checksum, - }, - }); - status.uploadedChecksums.add(checksum); - const effective_at = - parseResult.context?.vcs === 'git' - ? parseResult.context.effective_at - : undefined; - - if (shouldTag) { - const tags = [sanitizeGitTag(`gitbranch:${branchToUseForTag}`)]; - await config.client.tagSpec(specId, tags, effective_at); - } - - specsStatus.set(pathRelativeToRoot, status); - } - } - } - spinner?.succeed(`Successfully backfilled history`); -} - export const getApiAddAction = (config: OpticCliConfig) => async (path_to_spec: string | undefined, options: ApiAddActionOptions) => { - if (isNaN(Number(options.historyDepth))) { - logger.error( - chalk.red( - '--history-depth is not a number. history-depth must be a number' - ) - ); - process.exitCode = 1; - return; - } else if (!config.isAuthenticated) { - logger.error( - chalk.red( - 'You must be logged in to add APIs to Optic Cloud. Please run "optic login"' - ) - ); - process.exitCode = 1; - return; - } - - let file: { - path: string; - isDir: boolean; - }; - if (path_to_spec) { - try { - const isDir = (await fs.lstat(path_to_spec)).isDirectory(); - file = { - path: path.resolve(path_to_spec), - isDir, - }; - } catch (e) { - logger.error(chalk.red(`${path} is not a file or directory`)); - - process.exitCode = 1; - return; - } - } else if (options.all) { - file = { - path: path.resolve(config.root), - isDir: true, - }; - } else { - logger.error( - chalk.red( - 'Invalid argument combination, must specify either a `path` or `--all`' - ) - ); - process.exitCode = 1; - return; - } - - const orgRes = await getOrganizationFromToken( - config.client, - 'Select the organization you want to add APIs to' - ); - if (!orgRes.ok) { - logger.error(orgRes.error); - process.exitCode = 1; - return; - } - - let default_branch: string = ''; - let default_tag: string | undefined = undefined; - let web_url: string | undefined = undefined; - - if (config.vcs && config.vcs?.type === VCS.Git) { - const maybeDefaultBranch = await Git.getDefaultBranchName(); - if (maybeDefaultBranch) { - default_branch = maybeDefaultBranch; - default_tag = `gitbranch:${default_branch}`; - } - const maybeOrigin = await Git.guessRemoteOrigin(); - if (maybeOrigin) { - web_url = maybeOrigin.web_url; - } else { - logger.info( - chalk.red( - 'Could not parse git origin details for where this repository lives.' - ) - ); - const results = await prompts( - [ - { - message: - 'Do you want to enter the origin details manually? This will help optic link your specs back to your git hosting provider', - type: 'confirm', - - name: 'add', - initial: true, - }, - { - type: (prev) => (prev ? 'text' : null), - message: - 'Enter the web url where this API is uploaded (example: https://github.com/opticdev/optic)', - name: 'webUrl', - }, - ], - { onCancel: () => process.exit(1) } - ); - if (results.webUrl) { - web_url = results.webUrl; - } - logger.info(''); - } - } - - logger.info( - chalk.bold.gray( - file.isDir - ? `Looking for OpenAPI specs in directory ${file.path}` - : `Adding API ${file.path}` - ) - ); - - let candidates: { shas: string[]; paths: string[] }; - - if (config.vcs?.type === VCS.Git) { - candidates = !file.isDir - ? await GitCandidates.getShasCandidatesForPath( - file.path, - options.historyDepth - ) - : await GitCandidates.getPathCandidatesForSha(config.vcs.sha, { - startsWith: file.path, - depth: options.historyDepth, - }); - } else { - const files = !file.isDir - ? [path.resolve(file.path)] - : await FsCandidates.getFileCandidates({ - startsWith: file.path, - }); - - candidates = { shas: [], paths: files }; - } - const addedApis: NonNullable>>[] = - []; - for await (const file_path of candidates.paths) { - const api = await initializeApi(orgRes.org.id, file_path, config, { - path_to_spec: file?.path, - web: options.web, - default_branch, - default_tag, - web_url, - }); - - if (api) { - addedApis.push(api); - } - } - - const tracked = await Promise.all( - candidates.paths.map((p) => Git.isTracked(p)) - ); - - const someTracked = tracked.some((p) => !!p); - - if (addedApis.length > 0 && candidates.shas.length > 0 && someTracked) { - logger.info(``); - if (candidates.shas.length === 1) { - logger.info( - chalk.yellow( - 'Hint: add `--history-depth=0` to backfill the entire history of your API' - ) - ); - } else { - logger.info( - chalk.blue.bold( - 'Backfilling API history, you can exit at any time (`ctrl + c`) and finish this later.' - ) - ); - } - await backfillHistory(orgRes.org.id, candidates.shas, addedApis, config, { - path_to_spec: file?.path, - web: options.web, - default_branch, - default_tag, - web_url, - startCommit: options.startCommit, - }); - } - - logger.info(''); - logger.info(chalk.blue.bold(`Setup CI checks by running "optic ci setup"`)); - - await flushEvents(); + console.error('Api add is not supported'); + return; }; diff --git a/projects/optic/src/commands/api/create.ts b/projects/optic/src/commands/api/create.ts index fe8b6918d2..b573012036 100644 --- a/projects/optic/src/commands/api/create.ts +++ b/projects/optic/src/commands/api/create.ts @@ -1,13 +1,6 @@ import { Command } from 'commander'; import { errorHandler } from '../../error-handler'; import { OpticCliConfig, VCS } from '../../config'; -import { OPTIC_URL_KEY } from '../../constants'; -import chalk from 'chalk'; -import { logger } from '../../logger'; -import { getOrganizationFromToken } from '../../utils/organization'; -import * as Git from '../../utils/git-utils'; -import prompts from 'prompts'; -import { getApiUrl } from '../../utils/cloud-urls'; const usage = () => ` optic api create `; @@ -33,85 +26,6 @@ export const registerApiCreate = (cli: Command, config: OpticCliConfig) => { }; const getApiCreateAction = (config: OpticCliConfig) => async (name: string) => { - if (!config.isAuthenticated) { - logger.error( - chalk.red( - 'You must be logged in to create an API in Optic Cloud. Please run "optic login"' - ) - ); - process.exitCode = 1; - return; - } - const orgRes = await getOrganizationFromToken( - config.client, - 'Select the organization you want to create this API in' - ); - if (!orgRes.ok) { - logger.error(orgRes.error); - process.exitCode = 1; - return; - } - logger.info(''); - let default_branch: string = ''; - let default_tag: string | undefined = undefined; - let web_url: string | undefined = undefined; - - logger.info(''); - - if (config.vcs && config.vcs?.type === VCS.Git) { - const maybeDefaultBranch = await Git.getDefaultBranchName(); - if (maybeDefaultBranch) { - default_branch = maybeDefaultBranch; - default_tag = `gitbranch:${default_branch}`; - } - const maybeOrigin = await Git.guessRemoteOrigin(); - if (maybeOrigin) { - web_url = maybeOrigin.web_url; - } else { - logger.info( - chalk.red( - 'Could not parse git origin details for where this repository lives.' - ) - ); - const results = await prompts( - [ - { - message: - 'Do you want to enter the origin details manually? This will help optic link your specs back to your git hosting provider', - type: 'confirm', - - name: 'add', - initial: true, - }, - { - type: (prev) => (prev ? 'text' : null), - message: - 'Enter the web url where this API is uploaded (example: https://github.com/opticdev/optic)', - name: 'webUrl', - }, - ], - { onCancel: () => process.exit(1) } - ); - if (results.webUrl) { - web_url = results.webUrl; - } - logger.info(''); - } - } - - const { id } = await config.client.createApi(orgRes.org.id, { - name, - default_branch, - default_tag, - web_url, - }); - const url = getApiUrl(config.client.getWebBase(), orgRes.org.id, id); - logger.info(`API created at ${url}`); - logger.info(''); - - logger.info( - chalk.green( - `Add this url to your spec under the '${OPTIC_URL_KEY}' key or run "optic spec add-api-url ${url}"` - ) - ); + console.error('api create is not supported'); + return; }; diff --git a/projects/optic/src/commands/capture/actions/upload-coverage.ts b/projects/optic/src/commands/capture/actions/upload-coverage.ts deleted file mode 100644 index 525401c4e5..0000000000 --- a/projects/optic/src/commands/capture/actions/upload-coverage.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { sanitizeGitTag } from '@useoptic/openapi-utilities'; -import { OpticCliConfig, VCS } from '../../../config'; -import { uploadSpec, uploadSpecVerification } from '../../../utils/cloud-specs'; -import * as Git from '../../../utils/git-utils'; -import { ParseResult } from '../../../utils/spec-loaders'; -import { ApiCoverageCounter } from '../coverage/api-coverage'; -import { getSpecUrl } from '../../../utils/cloud-urls'; - -export async function uploadCoverage( - spec: ParseResult, - coverage: ApiCoverageCounter, - { orgId, apiId }: { orgId: string; apiId: string }, - config: OpticCliConfig, - opts?: { runId?: string } -) { - const tags: string[] = []; - let branchTag: string | undefined = undefined; - if (config.vcs?.type === VCS.Git) { - tags.push(`git:${config.vcs.sha}`); - const currentBranch = await Git.getCurrentBranchName(); - branchTag = sanitizeGitTag(`gitbranch:${currentBranch}`); - tags.push(branchTag); - } - const specId = await uploadSpec(apiId, { - spec: spec, - client: config.client, - tags, - orgId, - }); - - await uploadSpecVerification(specId, { - client: config.client, - verificationData: coverage.coverage, - runId: opts?.runId, - }); - - const specUrl = getSpecUrl(config.client.getWebBase(), orgId, apiId, specId); - - return { specUrl, branchTag }; -} diff --git a/projects/optic/src/commands/capture/capture.ts b/projects/optic/src/commands/capture/capture.ts index 3370fc07ae..4e273c0a4c 100644 --- a/projects/optic/src/commands/capture/capture.ts +++ b/projects/optic/src/commands/capture/capture.ts @@ -30,13 +30,10 @@ import { PostmanCollectionEntries } from './sources/postman'; import { CapturedInteractions } from './sources/captured-interactions'; import * as AT from '../oas/lib/async-tools'; import { GroupedCaptures } from './interactions/grouped-interactions'; -import { OPTIC_URL_KEY } from '../../constants'; -import { uploadCoverage } from './actions/upload-coverage'; import { resolveRelativePath } from '../../utils/capture'; import { PathInference } from './operations/path-inference'; import { getSpinner } from '../../utils/spinner'; import { flushEvents, trackEvent } from '../../segment'; -import { getOpticUrlDetails } from '../../utils/cloud-urls'; import sortBy from 'lodash.sortby'; import * as Git from '../../utils/git-utils'; @@ -307,42 +304,7 @@ const getCaptureAction = process.exitCode = 1; return; } - // We need to load the spec as is with denormalize=true so that the endpoint shas match - spec = denormalize(spec, spec.version); - - const opticUrlDetails = await getOpticUrlDetails(config, { - filePath: path.relative(config.root, path.resolve(filePath)), - opticUrl: spec.jsonLike[OPTIC_URL_KEY], - }); - - if (config.vcs?.type !== VCS.Git) { - logger.error( - 'optic capture --upload can only be run in a git repository.' - ); - process.exitCode = 1; - return; - } - - if (!opticUrlDetails) { - logger.error( - `File ${filePath} could not be associated with an Optic API. Files must be added to Optic before verification data can be uploaded.` - ); - logger.error(`${chalk.yellow('Hint: ')} Run optic api add ${filePath}`); - process.exitCode = 1; - return; - } - - const { specUrl, branchTag } = await uploadCoverage( - spec, - coverage, - opticUrlDetails, - config - ); - logger.info( - `Successfully uploaded verification data ${ - branchTag ? `for tag '${branchTag}'` : '' - }. View your spec at ${specUrl}` - ); + logger.error(`Coverage upload is no longer supported`); } if (hasAnyDiffs) { process.exitCode = 1; diff --git a/projects/optic/src/commands/ci/setup.ts b/projects/optic/src/commands/ci/setup.ts index 48e5035382..246a94f0f7 100644 --- a/projects/optic/src/commands/ci/setup.ts +++ b/projects/optic/src/commands/ci/setup.ts @@ -1,14 +1,8 @@ import { Command, Option } from 'commander'; import { OpticCliConfig } from '../../config'; -import prompts from 'prompts'; -import fs from 'fs/promises'; import path from 'path'; -import chalk from 'chalk'; -import { guessRemoteOrigin } from '../../utils/git-utils'; import { errorHandler } from '../../error-handler'; -const configsPath = path.join(__dirname, '..', '..', '..', 'ci', 'configs'); - const usage = () => ` optic ci setup `; @@ -42,141 +36,6 @@ export const registerCiSetup = (cli: Command, config: OpticCliConfig) => { const getCiSetupAction = (config: OpticCliConfig) => async (options: CISetupOptions) => { - let maybeProvider = await guessRemoteOrigin(); - - const provider: Provider = options.provider - ? options.provider - : ( - await prompts( - [ - { - type: 'select', - name: 'provider', - message: 'What CI provider would you like to configure?', - choices: [ - { title: 'GitHub Actions', value: 'github' }, - { title: 'GitLab CI/CD', value: 'gitlab' }, - ], - - initial: maybeProvider?.provider === 'gitlab' ? 1 : undefined, - }, - ], - { onCancel: () => process.exit(1) } - ) - ).provider; - - if (provider === 'github') { - await setupGitHub(config, options); - } else if (provider === 'gitlab') { - await setupGitLab(config, options); - } - }; - -async function setupGitHub(config: OpticCliConfig, options: CISetupOptions) { - const target = '.github/workflows/optic.yml'; - const targetPath = path.join(config.root, target); - const targetDir = path.dirname(targetPath); - const shouldContinue = await verifyPath(config.root, target); - - if (!shouldContinue) { + console.error('Ci setup is not supported'); return; - } - - await fs.mkdir(targetDir, { recursive: true }); - - const fromConfig = path.join(configsPath, 'github.yml'); - - let configContent = await fs.readFile(fromConfig, 'utf-8'); - - if (options.stdout) { - console.log(configContent); - } else { - await fs.writeFile(path.join(config.root, target), configContent); - console.log(`${chalk.green('βœ”')} Wrote CI configuration to ${target}`); - - console.log(''); - console.log('Next:'); - console.log(`- Edit and commit ${target}`); - console.log( - '- Configure your standards and authorize Optic to comment on your PRs: https://useoptic.com/docs/setup-ci, then make a change to your OpenAPI files and submit a PR to see Optic in action!' - ); - console.log( - '- Check Optic cloud to get hosted preview documentation, visual changelogs and API history: https://www.useoptic.com/docs/cloud-get-started' - ); - } -} - -async function setupGitLab(config: OpticCliConfig, options: CISetupOptions) { - const target = '.gitlab-ci.yml'; - const targetPath = path.join(config.root, target); - const targetDir = path.dirname(targetPath); - - let exists = false; - try { - await fs.access(targetPath); - exists = true; - } catch (e) { - exists = false; - } - - const fromConfig = path.join(configsPath, 'gitlab.yml'); - let configContent = await fs.readFile(fromConfig, 'utf-8'); - - if (options.stdout) { - console.log(configContent); - } else { - if (!exists) { - await fs.mkdir(targetDir, { recursive: true }); - - await fs.writeFile(targetPath, configContent); - - console.log(`${chalk.green('βœ”')} Wrote CI configuration to ${target}`); - } else { - console.log(); - console.log( - chalk.green( - "Since you already have a .gitlab-ci.yml file, here's an example of what you'll need to add:" - ) - ); - console.log(); - console.log('-- .gitlab-ci.yml -----'); - console.log(configContent.toString()); - console.log('-----------------------'); - } - - console.log(''); - console.log('Next:'); - console.log(`- Edit and commit ${target}`); - console.log( - '- Configure your standards and authorize Optic to comment on your MRs: https://useoptic.com/docs/setup-ci, then make a change to your OpenAPI files and submit a MR to see Optic in action!' - ); - console.log( - '- Check Optic cloud to get hosted preview documentation, visual changelogs and API history: https://www.useoptic.com/docs/cloud-get-started' - ); - } -} - -async function verifyPath(_root: string, target: string): Promise { - let exists = false; - try { - await fs.access(target); - exists = true; - } catch (e) { - exists = false; - } - - if (exists) { - const answer = await prompts( - { - type: 'confirm', - name: 'continue', - message: `Continuing will overwrite the file at ${target}. Continue?`, - }, - { onCancel: () => process.exit(1) } - ); - - return answer.continue; - } - - return true; -} + }; diff --git a/projects/optic/src/commands/diff/diff-all.ts b/projects/optic/src/commands/diff/diff-all.ts index 6f49fae757..f786fe7665 100644 --- a/projects/optic/src/commands/diff/diff-all.ts +++ b/projects/optic/src/commands/diff/diff-all.ts @@ -12,17 +12,13 @@ import chalk from 'chalk'; import { flushEvents, trackEvent } from '../../segment'; import { compressDataV2 } from './compressResults'; import { GetSourcemapOptions, textToSev } from '@useoptic/openapi-utilities'; -import { uploadDiff } from './upload-diff'; import { getApiFromOpticUrl } from '../../utils/cloud-urls'; import { writeDataForCi } from '../../utils/ci-data'; import { errorHandler } from '../../error-handler'; import { checkOpenAPIVersion } from '@useoptic/openapi-io'; import path from 'path'; -import { getApiUrl } from '../../utils/cloud-urls'; -import { getDetailsForGeneration } from '../../utils/generated'; import { terminalChangelog } from './changelog-renderers/terminal-changelog'; import { jsonChangelog } from './changelog-renderers/json-changelog'; -import * as Types from '../../client/optic-backend-types'; import { openUrl } from '../../utils/open-url'; import { renderCloudSetup } from '../../utils/render-cloud'; import { CustomUploadFn } from '../../types'; @@ -49,7 +45,7 @@ export const registerDiffAll = ( options: { customUpload?: CustomUploadFn } ) => { cli - .command('diff-all', { hidden: true }) + .command('diff-all') .configureHelp({ commandUsage: usage, }) @@ -278,52 +274,6 @@ async function computeAll( }); } - const generatedDetails = await getDetailsForGeneration(config); - if (generatedDetails) { - const { web_url, organization_id, default_branch, default_tag } = - generatedDetails; - - const pathToUrl: Record = {}; - const pathToName: Record = {}; - for (const [p, comparison] of comparisons.entries()) { - if (!comparison.opticUrl) { - pathToUrl[p] = null; - } - pathToName[p] = comparison.name ?? null; - } - let apis: (Types.Api | null)[] = []; - const chunks = chunk(Object.keys(pathToUrl), 20); - for (const chunk of chunks) { - const { apis: apiChunk } = await config.client.getApis(chunk, web_url); - apis.push(...apiChunk); - } - - for (const api of apis) { - if (api) { - pathToUrl[api.path] = getApiUrl( - config.client.getWebBase(), - api.organization_id, - api.api_id - ); - } - } - - for (let [path, url] of Object.entries(pathToUrl)) { - if (!url) { - const api = await config.client.createApi(organization_id, { - name: pathToName[path] ?? path, - path, - web_url: web_url, - default_branch, - default_tag, - }); - url = getApiUrl(config.client.getWebBase(), organization_id, api.id); - } - const comparison = comparisons.get(path); - if (comparison) comparison.opticUrl = url; - } - } - for (let { from, to, opticUrl } of comparisons.values()) { const cloudTag: string | null = !!from && /^cloud:/.test(from) ? from.replace(/^cloud:/, '') : null; @@ -409,21 +359,9 @@ async function computeAll( if (customOptions.customUpload) { await customOptions.customUpload(toParseResults); } else { - const uploadResults = await uploadDiff( - { - from: fromParseResults, - to: toParseResults, - }, - specResults, - config, - specDetails, - { - headTag: options.headTag, - standard, - } - ); - specUrl = uploadResults?.headSpecUrl ?? null; - changelogUrl = uploadResults?.changelogUrl ?? null; + console.log('upload diff is no longer supported'); + specUrl = null; + changelogUrl = null; } } diff --git a/projects/optic/src/commands/diff/diff.ts b/projects/optic/src/commands/diff/diff.ts index c18f21750f..2ea2452faa 100644 --- a/projects/optic/src/commands/diff/diff.ts +++ b/projects/optic/src/commands/diff/diff.ts @@ -9,7 +9,6 @@ import { parseFilesFromRef, ParseResult, loadSpec, - parseFilesFromCloud, } from '../../utils/spec-loaders'; import { ConfigRuleset, OpticCliConfig, VCS } from '../../config'; import chalk from 'chalk'; @@ -18,13 +17,11 @@ import { terminalChangelog } from './changelog-renderers/terminal-changelog'; import { jsonChangelog } from './changelog-renderers/json-changelog'; import { compute } from './compute'; import { compressDataV2 } from './compressResults'; -import { uploadDiff } from './upload-diff'; import { writeDataForCi } from '../../utils/ci-data'; import { logger } from '../../logger'; import { errorHandler } from '../../error-handler'; import path from 'path'; import { OPTIC_URL_KEY } from '../../constants'; -import { getApiFromOpticUrl, getOpticUrlDetails } from '../../utils/cloud-urls'; import * as Git from '../../utils/git-utils'; import * as GitCandidates from '../api/git-get-file-candidates'; import stableStringify from 'json-stable-stringify'; @@ -172,15 +169,7 @@ const getHeadAndLastChanged = async ( } } - const opticUrl = - headFile.jsonLike[OPTIC_URL_KEY] ?? - baseFile.jsonLike[OPTIC_URL_KEY] ?? - undefined; - - const specDetails = await getOpticUrlDetails(config, { - filePath: path.relative(config.root, path.resolve(file)), - opticUrl, - }); + const specDetails = null; return { specs: [baseFile, headFile, specDetails], @@ -210,11 +199,7 @@ const getBaseAndHeadFromFiles = async ( denormalize: true, }), ]); - const opticUrl: string | null = - headFile.jsonLike[OPTIC_URL_KEY] ?? - baseFile.jsonLike[OPTIC_URL_KEY] ?? - null; - const specDetails = opticUrl ? getApiFromOpticUrl(opticUrl) : null; + const specDetails = null; return [baseFile, headFile, specDetails]; } catch (e) { throw new UserError({ @@ -232,16 +217,7 @@ const getBaseAndHeadFromFileAndBase = async ( ): Promise<[ParseResult, ParseResult, SpecDetails]> => { try { if (/^cloud:/.test(base)) { - const { baseFile, headFile, specDetails } = await parseFilesFromCloud( - file1, - base.replace(/^cloud:/, ''), - config, - { - denormalize: true, - headStrict: options.validation === 'strict', - } - ); - return [baseFile, headFile, specDetails]; + throw new Error('cloud refs are not supported'); } else { const { baseFile, headFile } = await parseFilesFromRef( file1, @@ -253,11 +229,7 @@ const getBaseAndHeadFromFileAndBase = async ( headStrict: options.validation === 'strict', } ); - const opticUrl: string | null = - headFile.jsonLike[OPTIC_URL_KEY] ?? - baseFile.jsonLike[OPTIC_URL_KEY] ?? - null; - const specDetails = opticUrl ? getApiFromOpticUrl(opticUrl) : null; + const specDetails = null; return [baseFile, headFile, specDetails]; } } catch (e) { @@ -401,21 +373,9 @@ const getDiffAction = if (customOptions.customUpload) { await customOptions.customUpload(headParseResult); } else { - const uploadResults = await uploadDiff( - { - from: baseParseResult, - to: headParseResult, - }, - diffResult.specResults, - config, - specDetails, - { - headTag: options.headTag, - standard: diffResult.standard, - } - ); - specUrl = uploadResults?.headSpecUrl ?? null; - maybeChangelogUrl = uploadResults?.changelogUrl ?? null; + console.log('upload diff is no longer supported'); + specUrl = null; + maybeChangelogUrl = null; } } if (options.json) { @@ -465,8 +425,6 @@ const getDiffAction = } } - if (config.isInCi && !options.upload) renderCloudSetup(); - if (options.web) { if ( diffResult.specResults.diffs.length === 0 && diff --git a/projects/optic/src/commands/diff/generate-rule-runner.ts b/projects/optic/src/commands/diff/generate-rule-runner.ts index 9590ffdbd8..a7e6b39d3f 100644 --- a/projects/optic/src/commands/diff/generate-rule-runner.ts +++ b/projects/optic/src/commands/diff/generate-rule-runner.ts @@ -47,10 +47,8 @@ const getStandardToUse = async (options: { } else if (options.specRuleset) { if (options.specRuleset.startsWith('@')) { try { - const ruleset = await options.config.client.getStandard( - options.specRuleset - ); - return ruleset.config.ruleset; + logger.error('Cloud rulesets are not supported'); + return []; } catch (e) { logger.warn( `${chalk.red('Warning:')} Could not download standard ${ @@ -134,18 +132,9 @@ export const generateRuleRunner = async ( rulesToFetch.push(rule.name); } } - const response = - rulesToFetch.length > 0 - ? await options.config.client.getManyRulesetsByName(rulesToFetch) - : { rulesets: [] }; - for (const hostedRuleset of response.rulesets) { - if (hostedRuleset) { - hostedRulesets[hostedRuleset.name] = { - uploaded_at: hostedRuleset.uploaded_at, - url: hostedRuleset.url, - should_decompress: true, - }; - } + + if (rulesToFetch.length) { + logger.error('Hosted rules are not supported'); } logger.debug({ diff --git a/projects/optic/src/commands/diff/upload-diff.ts b/projects/optic/src/commands/diff/upload-diff.ts deleted file mode 100644 index f4214f47b1..0000000000 --- a/projects/optic/src/commands/diff/upload-diff.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { getRunUrl, getSpecUrl } from '../../utils/cloud-urls'; -import { ConfigRuleset, OpticCliConfig, VCS } from '../../config'; -import { ParseResult } from '../../utils/spec-loaders'; -import { EMPTY_SPEC_ID, uploadRun, uploadSpec } from '../../utils/cloud-specs'; -import * as Git from '../../utils/git-utils'; -import { logger } from '../../logger'; -import { sanitizeGitTag } from '@useoptic/openapi-utilities'; -import { getTagsFromOptions, getUniqueTags } from '../../utils/tags'; -import { getSpinner } from '../../utils/spinner'; - -export async function uploadDiff( - specs: { from: ParseResult; to: ParseResult }, - specResults: Parameters['1']['specResults'], - config: OpticCliConfig, - specDetails: { - apiId: string; - orgId: string; - } | null, - options: { - headTag?: string; - standard: ConfigRuleset[]; - silent?: boolean; - currentBranch?: string; - } -): Promise<{ - runId: string; - baseSpecUrl: string | null; - headSpecUrl: string | null; - changelogUrl: string; -} | null> { - const spinner = getSpinner({ text: `Uploading diff...`, color: 'blue' }); - - // We upload a spec if it is unchanged in git and there is an API id on the spec - let baseSpecId: string | null = null; - let headSpecId: string | null = null; - if (specs.from.context && specDetails) { - if (specs.from.context.vcs === VCS.Git) { - const tags = [`git:${specs.from.context.sha}`]; - baseSpecId = await uploadSpec(specDetails.apiId, { - spec: specs.from, - client: config.client, - tags, - orgId: specDetails.orgId, - }); - } else if (specs.from.context.vcs === VCS.Cloud) { - baseSpecId = specs.from.context.specId; - } - } else if (specs.from.isEmptySpec) { - baseSpecId = EMPTY_SPEC_ID; - } - - if (specs.to.context && specDetails) { - if (specs.to.context.vcs === VCS.Git) { - let tags: string[] = []; - const tagsFromOptions = getTagsFromOptions(options.headTag); - tags.push(...tagsFromOptions); - tags.push(`git:${specs.to.context.sha}`); - // If no gitbranch is set, try to add own git branch - if (!tagsFromOptions.some((tag) => /^gitbranch\:/.test(tag))) { - const currentBranch = - options.currentBranch ?? (await Git.getCurrentBranchName()); - if (currentBranch !== 'HEAD') { - tags.push(sanitizeGitTag(`gitbranch:${currentBranch}`)); - } else { - logger.warn( - `Warning: current branch was detected as 'HEAD'. This usually means the git is running against a detached HEAD and Optic will not be able to add gitbranch tags.` - ); - } - } - - tags = getUniqueTags(tags); - headSpecId = await uploadSpec(specDetails.apiId, { - spec: specs.to, - client: config.client, - tags, - orgId: specDetails.orgId, - }); - } else if (specs.to.context.vcs === VCS.Cloud) { - headSpecId = specs.to.context.specId; - } - } else if (specs.to.isEmptySpec) { - headSpecId = EMPTY_SPEC_ID; - } - - if (baseSpecId && headSpecId && specDetails) { - const run = await uploadRun(specDetails.apiId, { - fromSpecId: baseSpecId, - toSpecId: headSpecId, - client: config.client, - specResults, - standard: options.standard, - orgId: specDetails.orgId, - ci: config.isInCi, - }); - - const changelogUrl = getRunUrl( - config.client.getWebBase(), - specDetails.orgId, - specDetails.apiId, - run.id - ); - if (!options.silent) - spinner?.succeed(`Uploaded results of diff to ${changelogUrl}`); - - return { - runId: run.id, - changelogUrl, - headSpecUrl: - headSpecId === EMPTY_SPEC_ID - ? null - : getSpecUrl( - config.client.getWebBase(), - specDetails.orgId, - specDetails.apiId, - headSpecId - ), - baseSpecUrl: - baseSpecId === EMPTY_SPEC_ID - ? null - : getSpecUrl( - config.client.getWebBase(), - specDetails.orgId, - specDetails.apiId, - baseSpecId - ), - }; - } else if (!options.silent) { - const reason = !specDetails - ? 'no x-optic-url was set on the spec file' - : config.vcs?.type === VCS.Git - ? 'there are uncommitted changes in your working directory' - : 'the current working directory is not a git repo'; - spinner?.warn(`Not uploading diff results because ${reason}`); - } - return null; -} diff --git a/projects/optic/src/commands/identify-apis.ts b/projects/optic/src/commands/identify-apis.ts deleted file mode 100644 index 024e4d4ea5..0000000000 --- a/projects/optic/src/commands/identify-apis.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { OpticCliConfig } from '../config'; -import { logger } from '../logger'; -import { loadRaw } from '../utils/spec-loaders'; -import { OPTIC_URL_KEY } from '../constants'; -import { checkOpenAPIVersion } from '@useoptic/openapi-io'; -import { getDetailsForGeneration } from '../utils/generated'; -import path from 'path'; -import { OpenAPIV3 } from '@useoptic/openapi-utilities'; -import * as Types from '../client/optic-backend-types'; -import chunk from 'lodash.chunk'; -import { getApiUrl } from '../utils/cloud-urls'; - -export async function identifyOrCreateApis( - config: OpticCliConfig, - localSpecPaths: string[], - generatedDetails: Exclude< - Awaited>, - null - >, - pathsToName: Record -) { - const { web_url, organization_id, default_branch, default_tag } = - generatedDetails; - - const pathUrls = new Map(); - - for await (const specPath of localSpecPaths) { - let rawSpec: any; - - try { - rawSpec = await loadRaw(specPath, config); - } catch (e) { - logger.error('Error loading raw spec', e); - continue; // TODO: handle failures - } - - try { - checkOpenAPIVersion(rawSpec); - } catch (e) { - logger.error('Error checking OpenAPI version', e); - continue; // TODO: handle failures - } - - const opticUrl = rawSpec[OPTIC_URL_KEY]; - const relativePath = path.relative(config.root, path.resolve(specPath)); - pathUrls.set(relativePath, opticUrl); - } - - let apis: (Types.Api | null)[] = []; - const chunks = chunk([...pathUrls.keys()], 20); - for (const chunk of chunks) { - const { apis: apiChunk } = await config.client.getApis(chunk, web_url); - apis.push(...apiChunk); - } - - for (const api of apis) { - if (api) { - pathUrls.set( - api.path, - getApiUrl(config.client.getWebBase(), api.organization_id, api.api_id) - ); - } - } - - for (let [path, url] of pathUrls.entries()) { - if (!url) { - const api = await config.client.createApi(organization_id, { - name: pathsToName[path] ?? path, - path, - web_url: web_url, - default_branch, - default_tag, - }); - const url = getApiUrl( - config.client.getWebBase(), - organization_id, - api.id - ); - pathUrls.set(path, url); - } - } - - return pathUrls; -} diff --git a/projects/optic/src/commands/login/login.ts b/projects/optic/src/commands/login/login.ts index 0eacc1f101..dda5568cd2 100644 --- a/projects/optic/src/commands/login/login.ts +++ b/projects/optic/src/commands/login/login.ts @@ -1,17 +1,6 @@ import { Command } from 'commander'; -import prompts from 'prompts'; -import path from 'path'; -import fs from 'node:fs/promises'; -import open from 'open'; - -import { OpticCliConfig, USER_CONFIG_PATH, readUserConfig } from '../../config'; -import { logger } from '../../logger'; -import chalk from 'chalk'; -import { getNewTokenUrl } from '../../utils/cloud-urls'; +import { OpticCliConfig } from '../../config'; import { errorHandler } from '../../error-handler'; -import { createOpticClient } from '../../client'; -import { flushEvents, identify, alias, trackEvent } from '../../segment'; -import { anonymizeOrgToken } from '../../client/optic-backend'; export const registerLogin = (cli: Command, config: OpticCliConfig) => { cli .command('login') @@ -19,98 +8,7 @@ export const registerLogin = (cli: Command, config: OpticCliConfig) => { .action(errorHandler(getLoginAction(config), { command: 'login' })); }; -export async function identifyLoginFromToken(token: string) { - const newClient = createOpticClient(token); - const result = await newClient.verifyToken(); - - if (result.user) { - alias(result.user.userId); - identify(result.user.email); - trackEvent('cli.login'); - await flushEvents(); - } else { - const id = anonymizeOrgToken(token); - if (id) alias(id); - } -} - -export const handleTokenInput = async (token: string, silent?: boolean) => { - const userConfig = await readUserConfig(); - try { - await identifyLoginFromToken(token); - } catch (e) { - if (!silent) { - console.log(e); - logger.error(chalk.red(`An error occurred while verifying your token.`)); - } - process.exitCode = 1; - return false; - } - - const base64Token = Buffer.from(token).toString('base64'); - - const newConfig = userConfig - ? { - ...userConfig, - token: base64Token, - } - : { - token: base64Token, - }; - await fs.mkdir(path.dirname(USER_CONFIG_PATH), { recursive: true }); - await fs.writeFile(USER_CONFIG_PATH, JSON.stringify(newConfig), 'utf-8'); - - logger.info( - chalk.green( - `Successfully saved your personal access token to ${USER_CONFIG_PATH}` - ) - ); - return true; -}; - const getLoginAction = (config: OpticCliConfig) => async () => { - const userConfig = await readUserConfig(); - if (userConfig?.token) { - const { overwrite } = await prompts( - { - type: 'confirm', - name: 'overwrite', - message: - 'It appears you are already logged in, would you like to continue? Continuing will overwrite the old login credentials', - }, - { onCancel: () => process.exit(1) } - ); - if (!overwrite) { - return; - } - } - - const tokenUrl = getNewTokenUrl(config.client.getWebBase()); - - logger.info(`${chalk.blue('Generate a token below')} - -Create an account and generate a personal access token at ${chalk.underline.blue( - tokenUrl - )} - -`); - - // prompt breaks if we steal focus as its starting. - setTimeout(() => open(tokenUrl), 100); - - const response = await prompts( - { - type: 'password', - name: 'token', - message: 'Enter your token here:', - }, - { onCancel: () => process.exit(1) } - ); - - if (!response.token) { - throw new Error('Expected token'); - } - - logger.info(chalk.green(`\nVerifying your token`)); - await handleTokenInput(response.token); + console.error('login is not supported'); + return; }; diff --git a/projects/optic/src/commands/oas/verify.ts b/projects/optic/src/commands/oas/verify.ts index 4822178d02..e3155e1ad4 100644 --- a/projects/optic/src/commands/oas/verify.ts +++ b/projects/optic/src/commands/oas/verify.ts @@ -16,7 +16,6 @@ import { specToPaths } from '../capture/operations/queries'; import { OpticCliConfig, VCS } from '../../config'; import { OPTIC_URL_KEY } from '../../constants'; import { getApiFromOpticUrl, getSpecUrl } from '../../utils/cloud-urls'; -import { uploadSpec, uploadSpecVerification } from '../../utils/cloud-specs'; import { loadSpec, ParseResult } from '../../utils/spec-loaders'; import * as Git from '../../utils/git-utils'; import { @@ -192,41 +191,7 @@ export async function runVerify( process.exitCode = 1; return; } - - const { orgId, apiId } = opticUrlDetails; - const tags: string[] = []; - let branchTag: string | undefined = undefined; - if (config.vcs?.type === VCS.Git) { - tags.push(`git:${config.vcs.sha}`); - const currentBranch = await Git.getCurrentBranchName(); - branchTag = sanitizeGitTag(`gitbranch:${currentBranch}`); - tags.push(branchTag); - } - const specId = await uploadSpec(apiId, { - spec: parseResult, - client: config.client, - tags, - orgId, - }); - - await uploadSpecVerification(specId, { - client: config.client, - verificationData: coverage.coverage, - message: options.message, - }); - - const specUrl = getSpecUrl( - config.client.getWebBase(), - orgId, - apiId, - specId - ); - - console.log( - `Successfully uploaded verification data ${ - branchTag ? `for tag '${branchTag}'` : '' - }. View your spec at ${specUrl}` - ); + console.error('Coverage upload is not supported'); } analytics.forEach((event) => trackEvent(event.event, event.properties)); diff --git a/projects/optic/src/commands/ruleset/init.ts b/projects/optic/src/commands/ruleset/init.ts index 9c7d0f885f..4550e9297f 100644 --- a/projects/optic/src/commands/ruleset/init.ts +++ b/projects/optic/src/commands/ruleset/init.ts @@ -1,19 +1,7 @@ -import fs from 'node:fs/promises'; import { Command } from 'commander'; -import fetch from 'node-fetch'; import { OpticCliConfig } from '../../config'; -import tar from 'tar'; -import path from 'path'; -import chalk from 'chalk'; -import latestVersion from 'latest-version'; import { errorHandler } from '../../error-handler'; -const DEFAULT_INIT_FOLDER = 'optic-ruleset'; -const owner = 'opticdev'; -const repo = 'optic-custom-rules-starter'; -const ref = 'main'; -const NAME_PLACEHOLDER = 'name-of-custom-rules-package'; - export const registerRulesetInit = (cli: Command, config: OpticCliConfig) => { cli .command('init') @@ -23,81 +11,7 @@ export const registerRulesetInit = (cli: Command, config: OpticCliConfig) => { }; const getInitAction = () => async (name?: string) => { - const projectName = name ?? DEFAULT_INIT_FOLDER; - console.log( - chalk.bold.blue( - `Initializing ${ - name ? `project ${name}` : 'a new optic ruleset project' - }...` - ) - ); - const folderToCreate = path.join(process.cwd(), projectName); - try { - await fs.access(folderToCreate); - console.error( - chalk.red( - `Cannot create a project named ${projectName} because a folder ${folderToCreate} already exists.` - ) - ); - return process.exit(1); - } catch (e) {} - - await fs.mkdir(folderToCreate); - - console.log(); - console.log(`Downloading template...`); - console.log(); - - const templateReadStream = await fetch( - `https://api.github.com/repos/${owner}/${repo}/tarball/${ref}` - ).then((res) => res.body); - const tarConfig = folderToCreate - ? { - strip: 1, - cwd: folderToCreate, - } - : {}; - const tarWriteStream = tar.extract(tarConfig); - - await new Promise((resolve) => { - templateReadStream.pipe(tarWriteStream); - tarWriteStream.on('finish', resolve); - }); - - // update file names and dependencies - const rulesFilePath = path.join(folderToCreate, 'src', 'main.ts'); - const packageJsonFilePath = path.join(folderToCreate, 'package.json'); - const dependencies = [ - '@useoptic/rulesets-base', - '@useoptic/openapi-utilities', - ]; - const version = await latestVersion(dependencies[0]); - - await fs - .readFile(rulesFilePath, 'utf-8') - .then((file) => file.replaceAll(NAME_PLACEHOLDER, projectName)) - .then((file) => fs.writeFile(rulesFilePath, file)); - await fs - .readFile(packageJsonFilePath, 'utf-8') - .then((file) => file.replaceAll(NAME_PLACEHOLDER, projectName)) - .then((file) => - dependencies.reduce( - (f, dep) => f.replace(`"${dep}": "*"`, `"${dep}": "^${version}"`), - file - ) - ) - .then((file) => fs.writeFile(packageJsonFilePath, file)); + console.log('Ruleset upload is no longer supported'); - console.log( - chalk.green.bold(`Successfully initialized your optic ruleset project!`), - folderToCreate - ); - console.log(); - console.log( - `Your newly created project has been created at:`, - folderToCreate - ); - console.log( - `Get started by reading the README at: ${folderToCreate}/README.md and installing the project dependencies (npm install or yarn install)` - ); + return; }; diff --git a/projects/optic/src/commands/ruleset/upload.ts b/projects/optic/src/commands/ruleset/upload.ts index 946a3fa38f..e0cd992ecb 100644 --- a/projects/optic/src/commands/ruleset/upload.ts +++ b/projects/optic/src/commands/ruleset/upload.ts @@ -1,25 +1,7 @@ -import zlib from 'node:zlib'; -import fs from 'node:fs/promises'; -import path from 'path'; import { Command } from 'commander'; -import Ajv from 'ajv'; - -import { UserError } from '@useoptic/openapi-utilities'; import { OpticCliConfig } from '../../config'; -import { uploadFileToS3 } from '../../utils/s3'; import { errorHandler } from '../../error-handler'; -import { getOrganizationFromToken } from '../../utils/organization'; -import { logger } from '../../logger'; -import { loadRuleset } from '@useoptic/rulesets-base'; - -const expectedFileShape = `Expected ruleset file to have a default export with the shape -{ - name: string; - description: string; - configSchema?: any; - rulesetConstructor: (config: ConfigSchema) => Ruleset; -}`; export const registerRulesetUpload = (cli: Command, config: OpticCliConfig) => { cli @@ -54,120 +36,6 @@ type UploadActionOptions = { const getUploadAction = (config: OpticCliConfig) => async (filePath: string, options: UploadActionOptions) => { - if (!config.isAuthenticated) { - throw new UserError({ - message: - 'No optic token was provided (set the environment variable `OPTIC_TOKEN` with your optic token). Generate an optic token at https://app.useoptic.com.', - }); - } - - let organizationId: string; - if (options.organizationId) { - organizationId = options.organizationId; - } else { - const orgRes = await getOrganizationFromToken( - config.client, - 'Select the organization you want to upload to' - ); - if (!orgRes.ok) { - logger.error(orgRes.error); - process.exitCode = 1; - return; - } - organizationId = orgRes.org.id; - } - - const absolutePath = path.join(process.cwd(), filePath); - let userRuleFile: any; - try { - userRuleFile = loadRuleset(absolutePath); - } catch (e) { - console.error(e); - throw new UserError(); - } - - if (!fileIsValid(userRuleFile)) { - throw new UserError({ - message: `Rules file does not match expected format. ${expectedFileShape}`, - }); - } - - const name = userRuleFile.default.name; - const configSchema = userRuleFile.default.configSchema ?? {}; - const description = userRuleFile.default.description; - - const fileBuffer = await fs.readFile(absolutePath); - const compressed = zlib.brotliCompressSync(fileBuffer); - - const compressedFileBuffer = Buffer.from(compressed); - const ruleset = await config.client.createRuleset( - organizationId, - name, - description, - configSchema - ); - await uploadFileToS3(ruleset.upload_url, compressedFileBuffer); - await config.client.patchRuleset(organizationId, ruleset.id, true); - - console.log(`Successfully uploaded the ruleset ${ruleset.slug}`); - console.log( - `You can start using this ruleset by adding the ruleset ${ruleset.slug} in your optic.dev.yml or standards file.` - ); + console.log('Ruleset upload is no longer supported'); + return; }; - -const ajv = new Ajv(); -const configSchema = { - type: 'object', - properties: { - default: { - type: 'object', - properties: { - name: { - type: 'string', - pattern: '^[a-zA-Z-]+$', - }, - description: { - type: 'string', - maxLength: 1024, - }, - configSchema: { - type: 'object', - }, - }, - required: ['description', 'name'], - }, - }, - required: ['default'], -}; -const validateRulesFile = ajv.compile(configSchema); - -const fileIsValid = ( - file: any -): file is { - default: { - name: string; - description: string; - configSchema?: any; - rulesetConstructor: () => any; - }; -} => { - const result = validateRulesFile(file); - - if (!result) { - console.error( - `Rule file is invalid:\n${ajv.errorsText(validateRulesFile.errors)}` - ); - return false; - } - - // manually validate that rulesetConstructor is a function - const rulesetConstructor = (file.default as any)?.rulesetConstructor; - if (typeof rulesetConstructor !== 'function') { - console.error( - 'Rules file does not export a rulesetConstructor that is a function' - ); - return false; - } - - return true; -}; diff --git a/projects/optic/src/commands/run.ts b/projects/optic/src/commands/run.ts index bfb29bf685..b9ef1249b6 100644 --- a/projects/optic/src/commands/run.ts +++ b/projects/optic/src/commands/run.ts @@ -1,56 +1,18 @@ import { Command, Option } from 'commander'; -import prompts from 'prompts'; -import path from 'path'; -import { ConfigRuleset, OpticCliConfig, readUserConfig, VCS } from '../config'; +import { OpticCliConfig } from '../config'; import { errorHandler } from '../error-handler'; -import { logger } from '../logger'; -import { getApiFromOpticUrl, getNewTokenUrl } from '../utils/cloud-urls'; -import open from 'open'; -import { handleTokenInput, identifyLoginFromToken } from './login/login'; -import { matchSpecCandidates } from './diff/diff-all'; -import { - checkIgnore, - getCurrentBranchName, - guessRemoteOrigin, -} from '../utils/git-utils'; -import { loadSpec, loadRaw } from '../utils/spec-loaders'; -import type { ParseResult } from '../utils/spec-loaders'; -import { - CompareSpecResults, - getOperationsChangedLabel, - RuleResult, - Severity, -} from '@useoptic/openapi-utilities'; -import { compute } from './diff/compute'; -import { uploadDiff } from './diff/upload-diff'; -import chalk from 'chalk'; -import { flushEvents, trackEvent, identify } from '../segment'; -import { - anonymizeOrgToken, - anonymizeUserToken, - createOpticClient, -} from '../client/optic-backend'; + import fs from 'fs'; import { CommentApi, GithubCommenter, GitlabCommenter, } from './ci/comment/comment-api'; -import { CaptureForCI, CiRunDetails, getDataForCi } from '../utils/ci-data'; +import { CiRunDetails } from '../utils/ci-data'; import { COMPARE_SUMMARY_IDENTIFIER, generateCompareSummaryMarkdown, } from './ci/comment/common'; -import { GroupedDiffs } from '@useoptic/openapi-utilities/build/openapi3/group-diff'; -import { identifyOrCreateApis } from './identify-apis'; -import { getDetailsForGeneration } from '../utils/generated'; -import { checkOpenAPIVersion } from '@useoptic/openapi-io'; -import { resolveRelativePath } from '../utils/capture'; -import { GroupedCaptures } from './capture/interactions/grouped-interactions'; -import { getCaptureStorage } from './capture/storage'; -import { captureRequestsFromProxy } from './capture/actions/captureRequests'; -import { processCaptures } from './capture/capture'; -import { uploadCoverage } from './capture/actions/upload-coverage'; import { CustomUploadFn } from '../types'; const usage = () => ` @@ -125,7 +87,7 @@ export function registerRunCommand( options: { customUpload?: CustomUploadFn } ) { cli - .command('run') + .command('run', { hidden: true }) .description( 'CI workflow command that tests each OpenAPI specification in your repo and summarizes the results as a pull (or merge) request comment' ) @@ -160,809 +122,11 @@ type RunActionOptions = { includeGitIgnored: boolean; }; -async function authenticateInteractive(config: OpticCliConfig) { - const userConfig = await readUserConfig(); - if (userConfig?.token) { - try { - await identifyLoginFromToken(userConfig.token); - } catch (e) {} - return true; - } - - const { token } = await prompts( - [ - { - type: 'select', - name: 'login', - message: `This command requires a valid Optic token`, - choices: [ - { - title: 'Get a token from app.useoptic.com', - value: 'open-web', - }, - { - title: 'Paste a token', - value: 'paste', - }, - ], - }, - { - type: 'password', - name: 'token', - message: 'Enter your token here:', - }, - ], - { - onCancel: () => process.exit(1), - onSubmit: (_, answer) => { - if (answer === 'open-web') - open(getNewTokenUrl(config.client.getWebBase())); - }, - } - ); - - if (!token) { - return false; - } - - let validToken = false; - try { - validToken = await handleTokenInput(token, true); - } catch (e) { - logger.debug("Can't validate token", e); - return false; - } - - if (!validToken) return false; - - config.client = createOpticClient(token); - config.authenticationType = 'user'; - config.isAuthenticated = true; - config.userId = token.startsWith('opat') - ? anonymizeUserToken(token) - : anonymizeOrgToken(token); - - trackEvent('cli.login'); - await flushEvents(); - - return true; -} - -async function authenticateCI(config: OpticCliConfig) { - if (config.userId) identify(config.userId); - return config.isAuthenticated; -} - -type DiffResult = { - warnings: string[]; - groupedDiffs: GroupedDiffs; - results: RuleResult[]; - name: string; - specUrl: string; - changelogUrl: string; -}; - -type SpecReport = { - path: string; - title?: string; - error?: string; - diff?: - | { success: false; error: string } - | { - runId?: string; - diffResult?: DiffResult; - checks?: Awaited>['checks']; - success: true; - changelogLink?: string; - diffs?: number; - endpoints?: { - added: number; - removed: number; - changed: number; - }; - }; - capture?: Awaited>; -}; - -function reportDiff(report: SpecReport) { - const diffReport = report.diff; - if (!diffReport) return; - - if (!diffReport.success) { - logger.info(`| Optic run failed to diff:`); - logger.info(`| ${diffReport.error}`); - logger.info('|'); - return; - } - - const results = diffReport.diffResult?.results ?? []; - const failingResults = results.filter((r) => !r.passed && !r.exempted); - - if (diffReport.diffResult?.groupedDiffs) { - logger.info( - `| Changes: ${ - getOperationsChangedLabel(diffReport.diffResult?.groupedDiffs) || - 'No operation changed' - }` - ); - } - - const rulesReport = failingResults.length - ? `⚠️  ${failingResults.length}/${results?.length} failed` - : `βœ… ${results.length}/${results.length} passed`; - - logger.info(`| Rules: ${rulesReport}`); -} - -function reportCapture(report: SpecReport) { - const capture = report.capture; - - if (!capture) { - logger.info( - `| Tests: Set up API contract testing for this spec: https://www.useoptic.com/docs/verify-openapi` - ); - return; - } - - if (!capture.success) { - logger.info(`| Tests: ❌ Failed to run`); - logger.debug(capture.bufferedOutput); - return; - } - - const { hasAnyDiffs, unmatchedInteractions, coverage } = capture; - const passed = !hasAnyDiffs && !unmatchedInteractions; - - if (passed) { - logger.info( - `| Tests: βœ… ${Math.round( - Number(coverage.calculateCoverage().percent) - )}% coverage` - ); - } else { - const { unmatchedInteractions, mismatchedEndpoints } = capture; - - const undocumentedChunk = unmatchedInteractions - ? `πŸ†• ${unmatchedInteractions} undocumented path${ - unmatchedInteractions > 1 ? 's' : '' - }` - : ''; - - const mismatchedChunk = mismatchedEndpoints - ? `⚠️ ${mismatchedEndpoints} mismatch${ - mismatchedEndpoints > 1 ? 'es' : '' - }` - : ''; - - logger.info( - `| Tests: ${[undocumentedChunk, mismatchedChunk] - .filter(Boolean) - .join(' ')}` - ); - } -} - -function reportSpec(report: SpecReport) { - logger.info(`| ${chalk.bold(report.title)} [${report.path}]`); - - if (report.error) { - logger.info(`| Diff failed:`); - logger.info(`| ${report.error}`); - return; - } - - if (report.diff?.success && report.diff.changelogLink) { - logger.info(`| Report: πŸ‘οΈ ${report.diff.changelogLink}`); - } - - reportDiff(report); - reportCapture(report); - logger.info(''); -} - -const optionsForAnalytics = (options: RunActionOptions) => ({ - ignore: !!options.ignore, - severity: !!options.severity, -}); - -const getGithubBranchName = () => { - const headRef = process.env.GITHUB_HEAD_REF; - if (headRef) return headRef; - const ref = process.env.GITHUB_REF; - if (!ref || ref.indexOf('/heads/') < 0) return undefined; - return ref.split('/heads/')[1]; -}; - -const runDiffs = async ({ - specPath, - cloudTag, - config, - localSpec, - currentBranch, - specDetails, - customUpload, -}: { - specPath: string; - cloudTag: string; - config: OpticCliConfig; - localSpec: ParseResult; - currentBranch: string; - specDetails: Exclude, null>; - customUpload?: CustomUploadFn; -}) => { - let specResults: CompareSpecResults, - warnings: string[], - checks: any, - standard: ConfigRuleset[], - changelogData: GroupedDiffs, - changelogUrl: string | undefined, - specUrl: string | undefined; - - let cloudSpec: ParseResult | undefined = undefined; - try { - cloudSpec = await loadSpec( - `cloud:${specDetails.apiId}@${cloudTag}`, - config, - { - strict: false, - denormalize: true, - } - ); - } catch (e) { - return { - success: false, - error: `Failed to load cloud specification: ${e}`, - } as const; - } - if (localSpec.version === '2.x.x') { - return { - success: false, - error: `Local spec for ${specPath} is swagger 2 - not supported`, - } as const; - } - if (cloudSpec.version === '2.x.x') { - return { - success: false, - error: `Cloud spec for ${specPath} is swagger 2 - not supported`, - } as const; - } - - if (cloudSpec.jsonLike['x-optic-ci-empty-spec']) { - cloudSpec.jsonLike.info.title = localSpec.jsonLike.info.title; - cloudSpec.jsonLike.info.version = localSpec.jsonLike.info.version; - cloudSpec.jsonLike.openapi = localSpec.jsonLike.openapi; - } - - let computeResults: Awaited>; - - try { - computeResults = await compute([cloudSpec, localSpec], config, { - check: true, - path: specPath, - }); - } catch (e) { - return { - success: false, - error: `Run failed to compute diffs: ${e}`, - } as const; - } - - ({ specResults, standard, checks, changelogData, warnings } = computeResults); - - let upload: Awaited>; - try { - if (customUpload) { - await customUpload(cloudSpec); - return; - } else { - upload = await uploadDiff( - { - from: cloudSpec, - to: localSpec, - }, - specResults, - config, - specDetails, - { - standard, - silent: true, - currentBranch, - } - ); - specUrl = upload?.headSpecUrl ?? undefined; - changelogUrl = upload?.changelogUrl ?? undefined; - } - } catch (e) { - return { - success: false, - error: `Failed to upload run to Optic: ${e}`, - } as const; - } - const diffReport: SpecReport['diff'] = { - success: true, - diffs: specResults.diffs.length, - changelogLink: upload?.changelogUrl, - runId: upload?.runId, - checks, - }; - - diffReport.diffResult = { - groupedDiffs: changelogData, - warnings, - name: specPath, - specUrl: specUrl ?? '', - changelogUrl: changelogUrl ?? '', - results: specResults.results, - }; - - return diffReport; -}; - -const runCapture = async ({ - specPath, - config, - localSpec, - specDetails, - runId, - organizationId, -}: { - specPath: string; - config: OpticCliConfig; - localSpec: ParseResult; - specDetails: Exclude, null>; - runId?: string; - organizationId?: string; -}) => { - if (localSpec.version === '2.x.x') { - logger.error( - `${specPath} is Swagger 2 - capture does not support swagger 2` - ); - return; - } - const pathFromRoot = resolveRelativePath(config.root, specPath); - const captureConfig = config.capture?.[pathFromRoot]; - - if (captureConfig) { - const trafficDirectory = await getCaptureStorage(path.resolve(specPath)); - const captures = new GroupedCaptures(trafficDirectory, localSpec.jsonLike, { - baseServerUrl: captureConfig.server.url, - }); - - let harEntries: any; - try { - harEntries = await captureRequestsFromProxy(config, captureConfig, { - serverUrl: captureConfig.server.url, - disableSpinner: true, - }); - } catch (e) { - return { - success: false, - bufferedOutput: [`Run failed to capture: ${e}`], - } as const; - } - - if (!harEntries) { - return { - success: false, - bufferedOutput: [`Run failed: no har entries were captured`], - } as const; - } - - for await (const har of harEntries) { - captures.addHar(har); - logger.debug( - `Captured ${har.request.method.toUpperCase()} ${har.request.url}` - ); - } - - const captureResults = await processCaptures( - { - captureConfig, - cliConfig: config, - captures, - spec: localSpec, - filePath: specPath, - }, - { - bufferLogs: true, - verbose: false, - } - ); - if (runId && organizationId) { - try { - const captureData = captureResults.success - ? ({ - run_id: runId, - organization_id: organizationId, - unmatched_interactions: captureResults.unmatchedInteractions, - total_interactions: captureResults.totalInteractions, - percent_covered: Math.round( - Number(captureResults.coverage.calculateCoverage().percent) - ), - endpoints_added: captureResults.endpointsAdded, - endpoints_matched: captureResults.endpointCounts.matched, - endpoints_unmatched: captureResults.endpointCounts.unmatched, - endpoints_total: captureResults.endpointCounts.total, - has_any_diffs: captureResults.hasAnyDiffs, - mismatched_endpoints: captureResults.mismatchedEndpoints, - success: true, - } as const) - : ({ success: false } as const); - await config.client.createCapture(captureData); - } catch (e) { - logger.debug(e); - } - } - - if (captureResults.success) { - try { - await uploadCoverage( - localSpec, - captureResults.coverage, - specDetails, - config, - { runId } - ); - } catch (e) { - return { - success: false, - bufferedOutput: [`Run failed to upload coverage: ${e}`], - } as const; - } - } - - return captureResults; - } -}; - export const getRunAction = (config: OpticCliConfig, customOptions: { customUpload?: CustomUploadFn }) => async (matchArg: string | undefined, options: RunActionOptions) => { - const commentToken = - process.env.GITHUB_TOKEN ?? process.env.OPTIC_GITLAB_TOKEN; - - const baseBranch = - process.env.GITHUB_BASE_REF ?? - process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME; - - const isPR = config.isInCi && !!baseBranch; - - const maybeOrigin = - config.vcs?.type === VCS.Git ? await guessRemoteOrigin() : null; - - trackEvent( - 'optic.run.init', - { - isInCi: config.isInCi, - isAuthenticated: !!config.isAuthenticated, - commentToken: !!commentToken, - isPR, - webUrl: maybeOrigin?.web_url, - ...optionsForAnalytics(options), - }, - config.userId - ); - - if (config.vcs?.type !== VCS.Git) { - const error = `Error: optic must be called from a git repository.`; - logger.error(chalk.red(error)); - trackEvent( - 'optic.run.error', - { - isInCi: config.isInCi, - error, - }, - config.userId - ); - process.exitCode = 1; - return; - } - - const currentBranch = - getGithubBranchName() ?? - process.env.CI_COMMIT_REF_NAME ?? - (await getCurrentBranchName()); - - const currentBranchCloudTag = `gitbranch:${currentBranch}`; - - const cloudTag = baseBranch - ? `gitbranch:${baseBranch}` - : currentBranchCloudTag; - - const authentified = config.isInCi - ? await authenticateCI(config) - : await authenticateInteractive(config); - - if (!authentified) { - logger.error( - chalk.red('A valid Optic token is required to run this command.') - ); - process.exitCode = 1; - return; - } - - const match = matchArg ?? `**/*.(json|yml|yaml)`; - let localSpecPathsUnchecked = await matchSpecCandidates( - match, - options.ignore - ); - - if (!options.includeGitIgnored) - try { - localSpecPathsUnchecked = await excludeGitIgnored( - localSpecPathsUnchecked - ); - } catch (e) {} - - const localSpecPaths: string[] = []; - const pathsToName: Record = {}; - for (const path of localSpecPathsUnchecked) { - try { - const spec = await loadRaw(path, config); - if (checkOpenAPIVersion(spec)) { - localSpecPaths.push(path); - pathsToName[path] = spec.info?.title ?? null; - } - } catch (e) { - continue; - } - } - - if (!localSpecPaths.length) { - logger.info( - `Optic couldn't find any OpenAPI specification in your repository${ - matchArg || options.ignore ? ` that matches your filters` : '' - }.` - ); - logger.info(''); - logger.info( - `ℹ️ No OpenAPI file yet? Get one: https://www.useoptic.com/docs/how-to-generate-openapi` - ); - return; - } - - logger.info( - `Optic matched ${localSpecPaths.length} OpenAPI specification file${ - localSpecPaths.length > 1 ? 's' : '' - }:` + console.error( + 'Run is not supported - use `optic diff` or `optic diff-all` instead' ); - logger.info(`${localSpecPaths.join(', ')}\n`); - - const generatedDetails = await getDetailsForGeneration(config); - if (!generatedDetails) return; - - logger.info(`-------------------------------------------------------------------------------------------------- - - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ [1] Optic Cloud [2] β”‚ - β””β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–²β”€β”€β”€β”˜ - β”‚Compare Updateβ”‚ - β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β” - β”‚ Local specs β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - - [1]: \`${cloudTag}\` tag - [2]: \`${currentBranchCloudTag}\` tag - ---------------------------------------------------------------------------------------------------`); - if (!commentToken && isPR) { - logger.info( - `Pass a GITHUB_TOKEN or OPTIC_GITLAB_TOKEN environment variable with write permission to let Optic post comment with API change summaries to your pull requests.\n` - ); - } - logger.info(''); - - const specReports: SpecReport[] = []; - let pathUrls: Map; - try { - pathUrls = await identifyOrCreateApis( - config, - localSpecPaths, - generatedDetails, - pathsToName - ); - } catch (e) { - logger.error(`${e}`); - return; - } - - for (const [specPath, opticUrl] of pathUrls.entries()) { - let localSpec: ParseResult; - try { - localSpec = await loadSpec(specPath, config, { - strict: true, - denormalize: true, - }); - } catch (e) { - const specReport = { - error: `Invalid specification: ${e}`, - path: specPath, - }; - specReports.push(specReport); - reportSpec(specReport); - continue; - } - - let specDetails: ReturnType; - try { - specDetails = getApiFromOpticUrl(opticUrl); - } catch (e) { - const specReport = { - error: `Failed to load specification from Optic: ${e}`, - path: specPath, - }; - specReports.push(specReport); - reportSpec(specReport); - continue; - } - if (!specDetails) { - const specReport = { - error: `Could not load specification from Optic ${opticUrl}`, - path: specPath, - }; - specReports.push(specReport); - reportSpec(specReport); - continue; - } - - const diffsReport = await runDiffs({ - cloudTag, - config, - currentBranch, - localSpec, - specDetails, - specPath, - customUpload: customOptions.customUpload, - }); - - const captureReport = await runCapture({ - config, - localSpec, - specPath, - specDetails, - runId: diffsReport?.success ? diffsReport.runId : undefined, - organizationId: generatedDetails.organization_id, - }); - - const specReport = { - path: specPath, - title: localSpec.jsonLike.info.title, - diff: diffsReport, - capture: captureReport, - }; - - specReports.push(specReport); - reportSpec(specReport); - } - - if (commentToken && isPR) { - const data = specReports - .map((report) => { - const result = report?.diff?.success - ? report.diff.diffResult - : undefined; - if (!result) return null; - - const captureData: CaptureForCI | undefined = report.capture - ? report.capture?.success - ? { - success: true, - unmatchedInteractions: report.capture.unmatchedInteractions, - percentCovered: Math.round( - Number(report.capture.coverage.calculateCoverage().percent) - ), - mismatchedEndpoints: report.capture.mismatchedEndpoints, - } - : { success: false } - : undefined; - - return { - warnings: result.warnings, - groupedDiffs: result.groupedDiffs, - results: result.results, - name: result.name ?? 'Unknown comparison', - specUrl: result.specUrl, - changelogUrl: result.changelogUrl, - capture: captureData, - }; - }) - .filter( - ( - r: DiffResult | null - ): r is DiffResult & { capture: CaptureForCI | undefined } => !!r - ); - - switch (getProvider()) { - case 'github': { - const commenter = await getGithubCommenter(); - const sha = process.env.GITHUB_SHA!; - - await comment( - await getDataForCi(data, { severity: Severity.Error }), - commenter, - sha - ); - - break; - } - case 'gitlab': { - const commenter = await getGitlabCommenter(); - const sha = process.env.CI_COMMIT_SHA!; - - await comment( - await getDataForCi(data, { severity: Severity.Error }), - commenter, - sha - ); - - break; - } - } - } - - const hasFailures = - specReports.some((r) => r.error) || - specReports.some( - (r) => !r.diff?.success || (r.diff.checks?.failed.error ?? 0) > 0 - ) || - specReports.some((r) => { - if (!r.capture) return false; - const captureFailedToRun = !r.capture.success; - const captureHasMismatch = - r.capture.success && - (r.capture.unmatchedInteractions > 0 || r.capture.hasAnyDiffs); - return captureFailedToRun || captureHasMismatch; - }); - - const exit1 = options.severity === 'error' && hasFailures; - - if (exit1) { - logger.info(''); - logger.info( - 'Errors were found: exiting with code 1. Disable this behaviour with the `--severity none` option.' - ); - process.exitCode = 1; - } - - logger.info(''); - - if (!config.isInCi) { - logger.info( - `πŸ€– Add Optic to your CI flow: https://www.useoptic.com/docs/setup-ci` - ); - } - - if (config.isInCi && !commentToken) { - logger.info( - `πŸ’¬ Configure commenting on PR/MR: https://www.useoptic.com/docs/setup-ci#configure-commenting-on-pull-requests-optional` - ); - } - - if (config.isDefaultConfig) { - logger.info( - `πŸ”§ Customize your governance rules: https://www.useoptic.com/docs/lint-openapi` - ); - } - - trackEvent( - 'optic.run.complete', - { - isInCi: config.isInCi, - specs: localSpecPaths.length, - failed_specs: specReports.filter( - (s) => !s.diff?.success || s.capture?.success === false - ).length, - exit1, - webUrl: maybeOrigin?.web_url, - ...optionsForAnalytics(options), - }, - config.userId - ); - - await flushEvents(); + return; }; - -async function excludeGitIgnored(filepaths: string[]): Promise { - const ignored = new Set(await checkIgnore(filepaths)); - return filepaths.filter((p) => !ignored.has(p)); -} diff --git a/projects/optic/src/commands/spec/push.ts b/projects/optic/src/commands/spec/push.ts index b425b472c2..75992d0be0 100644 --- a/projects/optic/src/commands/spec/push.ts +++ b/projects/optic/src/commands/spec/push.ts @@ -1,22 +1,6 @@ import { Command } from 'commander'; -import open from 'open'; -import { sanitizeGitTag } from '@useoptic/openapi-utilities'; -import path from 'path'; - import { OpticCliConfig, VCS } from '../../config'; -import { loadSpec, ParseResult } from '../../utils/spec-loaders'; -import { logger } from '../../logger'; -import { OPTIC_URL_KEY } from '../../constants'; -import * as Git from '../../utils/git-utils'; -import chalk from 'chalk'; -import { uploadSpec } from '../../utils/cloud-specs'; -import { - getApiUrl, - getOpticUrlDetails, - getSpecUrl, -} from '../../utils/cloud-urls'; import { errorHandler } from '../../error-handler'; -import { getTagsFromOptions, getUniqueTags } from '../../utils/tags'; const usage = () => ` optic spec push @@ -56,96 +40,6 @@ type SpecPushActionOptions = { const getSpecPushAction = (config: OpticCliConfig) => async (spec_path: string | undefined, options: SpecPushActionOptions) => { - if (!config.isAuthenticated) { - logger.error( - 'Must be logged in to push specs. Log in with `optic login`' - ); - process.exitCode = 1; - return; - } - - let parseResult: ParseResult; - try { - parseResult = await loadSpec(spec_path, config, { - strict: false, - denormalize: true, - }); - } catch (e) { - logger.error( - `File ${spec_path} is not a valid OpenAPI file. Optic currently supports OpenAPI 3 and 3.1` - ); - logger.error(e); - process.exitCode = 1; - return; - } - - if (parseResult.isEmptySpec) { - logger.error( - `File ${spec_path} could not be found in the current working directory` - ); - process.exitCode = 1; - return; - } - let tagsToAdd: string[] = getTagsFromOptions(options.tag); - - if (config.vcs?.type === VCS.Git) { - const sha = config.vcs.sha; - tagsToAdd.push(`git:${sha}`); - - const branch = await Git.getCurrentBranchName(); - - if (branch !== 'HEAD') { - tagsToAdd.push(sanitizeGitTag(`gitbranch:${branch}`)); - } - } - - tagsToAdd = getUniqueTags(tagsToAdd); - - const specDetails = await getOpticUrlDetails(config, { - filePath: spec_path - ? path.relative(config.root, path.resolve(spec_path)) - : undefined, - opticUrl: parseResult.jsonLike[OPTIC_URL_KEY], - }); - - if (!specDetails) { - logger.error( - `File ${spec_path} could not be associated to an Optic API. Files must be added to Optic before specs can be pushed up to Optic.` - ); - logger.error(`${chalk.yellow('Hint: ')} Run optic api add ${spec_path}`); - process.exitCode = 1; - return; - } - - const opticUrl = getApiUrl( - config.client.getWebBase(), - specDetails.orgId, - specDetails.apiId - ); - - logger.info( - `Uploading spec for api at ${opticUrl} ${ - tagsToAdd.length > 0 ? `with tags ${tagsToAdd.join(', ')}` : '' - }` - ); - const specId = await uploadSpec(specDetails.apiId, { - spec: parseResult, - client: config.client, - tags: tagsToAdd, - orgId: specDetails.orgId, - }); - const url = getSpecUrl( - config.client.getWebBase(), - specDetails.orgId, - specDetails.apiId, - specId - ); - - logger.info( - `Succesfully uploaded spec to Optic. View the spec here ${url}` - ); - - if (options.web) { - await open(url, { wait: false }); - } + console.error('Spec push is not supported'); + return; }; diff --git a/projects/optic/src/config.ts b/projects/optic/src/config.ts index 34bbdf448c..198902a0e3 100644 --- a/projects/optic/src/config.ts +++ b/projects/optic/src/config.ts @@ -224,10 +224,7 @@ export const initializeRules = async ( try { if (config.extends.startsWith('@')) { - const response = await client.getStandard(config.extends); - rulesetMap = new Map( - response.config.ruleset.map((conf) => [conf.name, conf]) - ); + logger.error('Cloud rulesets are not supported'); } else { // Assumption is that we're fetching a yaml file const response = await fetch(config.extends).then((response) => { diff --git a/projects/optic/src/init.ts b/projects/optic/src/init.ts index 71ff8857e6..aa6dcccd84 100644 --- a/projects/optic/src/init.ts +++ b/projects/optic/src/init.ts @@ -12,10 +12,6 @@ import { registerApiAdd } from './commands/api/add'; import { registerApiCreate } from './commands/api/create'; import { registerCaptureCommand } from './commands/capture/capture'; import { registerConfigCommand } from './commands/config'; -import { captureCommand as captureV1Command } from './commands/oas/capture'; -import { newCommand } from './commands/oas/new'; -import { setupTlsCommand } from './commands/oas/setup-tls'; -import { verifyCommand } from './commands/oas/verify'; import { registerDiffAll } from './commands/diff/diff-all'; import { registerSpecPush } from './commands/spec/push'; import { registerSpecAddApiUrl } from './commands/spec/add-api-url'; @@ -27,11 +23,9 @@ import { registerDereference } from './commands/dereference/dereference'; import { registerCiSetup } from './commands/ci/setup'; import { registerLint } from './commands/lint/lint'; import { registerBundle } from './commands/bundle/bundle'; -import { updateCommand } from './commands/oas/update'; import { registerApiList } from './commands/api/list'; import { registerHistory } from './commands/history'; import { registerRunCommand } from './commands/run'; -import path from 'path'; import { CustomUploadFn } from './types'; const packageJson = require('../package.json'); @@ -145,24 +139,6 @@ export const initCli = async ( registerCaptureCommand(cli, cliConfig); registerConfigCommand(cli, cliConfig); - //@todo by 2023/5/10 - const oas = new Command('oas').description( - '[Deprecated] capture/verify/update are now top-level commands' - ); - oas.addCommand(await captureV1Command(cliConfig)); - oas.addCommand(await newCommand()); - oas.addCommand(await setupTlsCommand()); - oas.addCommand(verifyCommand(cliConfig)); - oas.addCommand(updateCommand()); - - cli.addCommand(oas, { hidden: true }); - - // commands for tracking changes with openapi - cli.addCommand(await newCommand(), { hidden: true }); - cli.addCommand(await setupTlsCommand(), { hidden: true }); - cli.addCommand(verifyCommand(cliConfig), { hidden: true }); - cli.addCommand(updateCommand(), { hidden: true }); - registerLint(cli, cliConfig); registerDiffAll(cli, cliConfig, options); registerLogin(cli, cliConfig); diff --git a/projects/optic/src/utils/cloud-specs.ts b/projects/optic/src/utils/cloud-specs.ts deleted file mode 100644 index 5826ea3be8..0000000000 --- a/projects/optic/src/utils/cloud-specs.ts +++ /dev/null @@ -1,239 +0,0 @@ -import stableStringify from 'json-stable-stringify'; -import { - CompareSpecResults, - UserError, - ApiCoverage, -} from '@useoptic/openapi-utilities'; -import { OpticBackendClient } from '../client'; -import { computeChecksumForAws } from './checksum'; -import { downloadFileFromS3, uploadFileToS3 } from './s3'; -import { ParseResult } from './spec-loaders'; -import { trackEvent } from '../segment'; -import { logger } from '../logger'; -import { NotFoundError } from '../client/errors'; -import chalk from 'chalk'; -import { createNullSpec, createNullSpecSourcemap } from './specs'; -import { JsonSchemaSourcemap } from '@useoptic/openapi-io'; -import { ConfigRuleset } from '../config'; - -export const EMPTY_SPEC_ID = 'EMPTY'; - -export async function downloadSpec( - spec: { apiId: string; tag: string }, - opts: { client: OpticBackendClient } -): Promise<{ - jsonLike: ParseResult['jsonLike']; - sourcemap: ParseResult['sourcemap']; - spec: { - id: string; - }; -}> { - const response = await opts.client - .getSpec(spec.apiId, spec.tag) - .catch((e) => { - if (e instanceof Error && /spec does not exist/i.test(e.message)) { - return { id: EMPTY_SPEC_ID, specUrl: null, sourcemapUrl: null }; - } - throw e; - }); - - if (response.id === EMPTY_SPEC_ID) { - const spec = createNullSpec(); - const sourcemap = createNullSpecSourcemap(spec); - - return { - jsonLike: spec, - sourcemap, - spec: { - id: response.id, - }, - }; - } else { - // fetch from cloud - const [specStr, sourcemapStr] = await Promise.all([ - downloadFileFromS3(response.specUrl!), - downloadFileFromS3(response.sourcemapUrl!), - ]); - return { - jsonLike: JSON.parse(specStr), - sourcemap: JsonSchemaSourcemap.fromSerializedSourcemap( - JSON.parse(sourcemapStr) - ), - spec: { - id: response.id, - }, - }; - } -} - -export async function uploadSpec( - apiId: string, - opts: { - spec: ParseResult; - client: OpticBackendClient; - tags: string[]; - orgId: string; - // Sets spec_tag.effective_at to spec.effective_at instead of current date - forward_effective_at_to_tags?: boolean; - precomputed?: { - specString?: string; - specChecksum?: string; - sourcemapString?: string; - sourcemapChecksum?: string; - }; - } -): Promise { - const stableSpecString = - opts.precomputed?.specString ?? stableStringify(opts.spec.jsonLike); - const stableSourcemapString = - opts.precomputed?.sourcemapString ?? stableStringify(opts.spec.sourcemap); - const spec_checksum = - opts.precomputed?.specChecksum ?? computeChecksumForAws(stableSpecString); - const sourcemap_checksum = - opts.precomputed?.sourcemapChecksum ?? - computeChecksumForAws(stableSourcemapString); - let result: Awaited>; - const tags = opts.tags.filter((tag, ndx) => opts.tags.indexOf(tag) === ndx); - try { - result = await opts.client.prepareSpecUpload({ - api_id: apiId, - spec_checksum, - sourcemap_checksum, - }); - } catch (e) { - if (e instanceof NotFoundError && e.source === 'optic') { - logger.debug(e); - logger.error(chalk.red.bold('Error uploading spec to Optic')); - logger.error( - chalk.red( - `This may be because your login credentials do not have access to the api specified in the x-optic-url. Check the x-optic-url in your spec, or try regenerate your credentials by running optic login.` - ) - ); - throw new UserError(); - } else { - throw e; - } - } - - if ('upload_id' in result) { - await Promise.all([ - uploadFileToS3(result.spec_url, stableSpecString, { - 'x-amz-checksum-sha256': spec_checksum, - }), - uploadFileToS3(result.sourcemap_url, stableSourcemapString, { - 'x-amz-checksum-sha256': sourcemap_checksum, - }), - ]); - - let effective_at: Date | undefined = undefined; - let git_name: string | undefined = undefined; - let git_email: string | undefined = undefined; - let commit_message: string | undefined = undefined; - - if (opts.spec.context?.vcs === 'git') { - ({ - effective_at, - name: git_name, - email: git_email, - message: commit_message, - } = opts.spec.context); - } - - const { id } = await opts.client.createSpec({ - upload_id: result.upload_id, - openapi_version: opts.spec.version, - api_id: apiId, - tags: tags, - effective_at, - git_name, - git_email, - commit_message, - forward_effective_at_to_tags: opts.forward_effective_at_to_tags, - }); - trackEvent('spec.added', { - apiId, - orgId: opts.orgId, - specId: id, - }); - return id; - } else { - await opts.client.tagSpec(result.spec_id, tags); - - return result.spec_id; - } -} - -export async function uploadRun( - apiId: string, - opts: { - fromSpecId: string; - toSpecId: string; - orgId: string; - client: OpticBackendClient; - specResults: CompareSpecResults; - standard: ConfigRuleset[]; - ci: boolean; - } -) { - const stableResultsString = stableStringify(opts.specResults); - const checksum = computeChecksumForAws(stableResultsString); - const result = await opts.client.prepareRunUpload({ - api_id: apiId, - checksum, - }); - - await uploadFileToS3( - result.check_results_url, - Buffer.from(stableResultsString), - { - 'x-amz-checksum-sha256': checksum, - } - ); - - const run = await opts.client.createRun({ - upload_id: result.upload_id, - api_id: apiId, - from_spec_id: opts.fromSpecId, - to_spec_id: opts.toSpecId, - ruleset: opts.standard, - ci: opts.ci, - }); - trackEvent('run.added', { - apiId, - orgId: opts.orgId, - runId: run.id, - }); - - return run; -} - -export async function uploadSpecVerification( - specId: string, - opts: { - client: OpticBackendClient; - verificationData: ApiCoverage; - message?: string; - runId?: string; - } -) { - const stableResultsString = stableStringify(opts.verificationData); - const checksum = computeChecksumForAws(stableResultsString); - - const { upload_id, url } = await opts.client.prepareVerification( - specId, - checksum - ); - - await uploadFileToS3(url, Buffer.from(stableResultsString), { - 'x-amz-checksum-sha256': checksum, - }); - - const { id } = await opts.client.createVerification({ - spec_id: specId, - upload_id, - message: opts.message, - run_id: opts.runId, - }); - - return id; -} diff --git a/projects/optic/src/utils/cloud-urls.ts b/projects/optic/src/utils/cloud-urls.ts index 43975003e3..cbaa650de4 100644 --- a/projects/optic/src/utils/cloud-urls.ts +++ b/projects/optic/src/utils/cloud-urls.ts @@ -1,7 +1,5 @@ import { URL } from 'node:url'; import urljoin from 'url-join'; -import { OpticCliConfig } from '../config'; -import { getDetailsForGeneration } from './generated'; // expected format: app.useoptic.com/organizations/:orgId/apis/:apiId const PATH_NAME_REGEXP = @@ -87,42 +85,3 @@ export function getCiSetupUrl( return url.toString(); } - -type OpticUrlDetails = { - orgId: string; - apiId: string; -}; - -export async function getOpticUrlDetails( - config: OpticCliConfig, - { - filePath, - opticUrl, - webUrl, - orgId, - }: { - filePath?: string; - opticUrl?: string; - webUrl?: string; - orgId?: string; - } -): Promise { - if (opticUrl) return getApiFromOpticUrl(opticUrl); - else if (filePath) { - let organization_id = orgId; - let web_url = webUrl; - if (!organization_id || !webUrl) { - const generatedDetails = await getDetailsForGeneration(config); - web_url = generatedDetails?.web_url; - organization_id = generatedDetails?.organization_id; - } - if (web_url && organization_id) { - const res = await config.client.getApis([filePath], web_url); - const api = res?.apis?.[0]; - if (api) { - return { apiId: api.api_id, orgId: organization_id }; - } - } - } - return null; -} diff --git a/projects/optic/src/utils/generated.ts b/projects/optic/src/utils/generated.ts deleted file mode 100644 index 1a7b57b204..0000000000 --- a/projects/optic/src/utils/generated.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as Git from './git-utils'; -import { getOrganizationFromToken } from './organization'; -import { OpticCliConfig } from '../config'; -import { logger } from '../logger'; -import chalk from 'chalk'; - -export async function getDetailsForGeneration(config: OpticCliConfig): Promise<{ - default_branch: string; - default_tag: string; - web_url: string; - organization_id: string; -} | null> { - let default_branch: string = 'main'; - let default_tag: string = 'gitbranch:main'; - const maybeOrigin = await Git.guessRemoteOrigin(); - - const message = `Select the organization that your APIs belong to. Use an organization token to disambiguate in non interactive environments.`; - if (!config.isAuthenticated) return null; - const orgRes = await getOrganizationFromToken(config.client, message); - - const maybeDefaultBranch = await Git.getDefaultBranchName(); - if (maybeDefaultBranch) { - default_branch = maybeDefaultBranch; - default_tag = `gitbranch:${default_branch}`; - } - - if (maybeOrigin && orgRes.ok) { - return { - default_branch, - default_tag, - web_url: maybeOrigin.web_url, - organization_id: orgRes.org.id, - }; - } else if (!maybeOrigin) { - logger.warn(chalk.red('Could not identify your APIs with Optic cloud')); - logger.warn( - "Optic identifies your APIs by their path in the repository and the repository's Git remote origin, but the latter could not be determined." - ); - logger.warn( - "Either set your repository's Git remote and run the command again, or add an identifier to your specification: run `optic api new ` and follow the instructions.\n" - ); - return null; - } else if (!orgRes.ok) { - logger.error( - `Optic encountered an error trying to connect APIs to Optic cloud: ${orgRes.error}` - ); - return null; - } - return null; -} diff --git a/projects/optic/src/utils/organization.ts b/projects/optic/src/utils/organization.ts deleted file mode 100644 index 06d425135a..0000000000 --- a/projects/optic/src/utils/organization.ts +++ /dev/null @@ -1,63 +0,0 @@ -import prompts from 'prompts'; -import { OpticBackendClient } from '../client'; - -function isInteractive() { - return ( - process.stdout?.isTTY && - process.env.TERM !== 'dumb' && - !('CI' in process.env) - ); -} - -export async function getOrganizationFromToken( - client: OpticBackendClient, - message: string -): Promise< - | { - ok: true; - org: { id: string; name: string }; - } - | { - ok: false; - error: string; - } -> { - let org: { id: string; name: string }; - - const { organizations } = await client.getTokenOrgs(); - if (organizations.length > 1) { - if (isInteractive()) { - const response = await prompts( - { - type: 'select', - name: 'orgId', - message, - choices: organizations.map((org) => ({ - title: org.name, - value: org.id, - })), - }, - { onCancel: () => process.exit(1) } - ); - org = organizations.find((o) => o.id === response.orgId)!; - console.log(''); - } else { - return { - ok: false, - error: - "Authenticated personal access token can access multiple organizations and Optic didn't know which one was the right one. Use an organization token to disambiguate in non TTY environments.", - }; - } - } else if (organizations.length === 0) { - process.exitCode = 1; - return { - ok: false, - error: - 'Authenticated token was not associated with any organizations. Generate a new token at https://app.useoptic.com', - }; - } else { - org = organizations[0]; - } - - return { ok: true, org }; -} diff --git a/projects/optic/src/utils/spec-loaders.ts b/projects/optic/src/utils/spec-loaders.ts index b8a9581d98..2cdaf989c2 100644 --- a/projects/optic/src/utils/spec-loaders.ts +++ b/projects/optic/src/utils/spec-loaders.ts @@ -24,12 +24,7 @@ import { import { OpticCliConfig, VCS } from '../config'; import * as Git from './git-utils'; import { createNullSpec, createNullSpecSourcemap } from './specs'; -import { downloadSpec } from './cloud-specs'; import { OpticBackendClient } from '../client'; -import { getApiFromOpticUrl, getApiUrl } from './cloud-urls'; -import { OPTIC_URL_KEY } from '../constants'; -import chalk from 'chalk'; -import { getDetailsForGeneration } from './generated'; import { logger } from '../logger'; const exec = promisify(callbackExec); @@ -162,11 +157,7 @@ export async function loadRaw( rawString = await fetch(input.url).then((res) => res.text()); format = 'unknown'; } else if (input.from === 'cloud') { - const spec = await downloadSpec( - { apiId: input.apiId, tag: input.tag }, - config - ); - return spec.jsonLike as any; + throw new Error('cloud refs are not supported'); } else { return createNullSpec(); } @@ -221,24 +212,7 @@ async function parseSpecAndDereference( }; } case 'cloud': { - // try fetch from cloud, if 404 return an error - // todo handle empty spec case - const { jsonLike, sourcemap, spec } = await downloadSpec( - { apiId: input.apiId, tag: input.tag }, - config - ); - return { - jsonLike, - fileContext: input, - sourcemap, - version: checkOpenAPIVersion(jsonLike), - from: 'cloud', - isEmptySpec: false, - context: { - vcs: 'cloud', - specId: spec.id, - }, - } as ParseResult; + throw new Error('cloud refs are not supported'); } case 'git': { if (config.vcs?.type !== VCS.Git) { @@ -406,73 +380,3 @@ export const parseFilesFromRef = async ( pathFromGitRoot: gitFileName, }; }; - -export const parseFilesFromCloud = async ( - filePath: string, - cloudTag: string, - config: OpticCliConfig, - options: { - denormalize: boolean; - headStrict: boolean; - } -) => { - const headFile = await loadSpec(filePath, config, { - denormalize: options.denormalize, - strict: options.headStrict, - }); - - let specDetails = getApiFromOpticUrl(headFile.jsonLike[OPTIC_URL_KEY]); - - const relativePath = path.relative(config.root, path.resolve(filePath)); - const generatedDetails = await getDetailsForGeneration(config); - if (generatedDetails) { - const { web_url, organization_id, default_branch, default_tag } = - generatedDetails; - - const { apis } = await config.client.getApis([relativePath], web_url); - let url: string; - if (!apis[0]) { - const api = await config.client.createApi(organization_id, { - name: relativePath, - path: relativePath, - web_url: web_url, - default_branch, - default_tag, - }); - url = getApiUrl(config.client.getWebBase(), organization_id, api.id); - } else { - url = getApiUrl( - config.client.getWebBase(), - organization_id, - apis[0].api_id - ); - } - specDetails = getApiFromOpticUrl(url); - } - - if (!specDetails) { - throw new Error( - `${chalk.bold.red( - "Must have an 'x-optic-url' in your OpenAPI spec file to be able to compare against a cloud base." - )}. - -${chalk.gray(`Get started by running 'optic api add ${filePath}'`)}` - ); - } - const baseFile = validateAndDenormalize( - await parseSpecAndDereference( - `cloud:${specDetails.apiId}@${cloudTag}`, - config - ), - { - denormalize: options.denormalize, - strict: false, - } - ); - - return { - baseFile, - headFile, - specDetails, - }; -}; diff --git a/projects/rulesets-base/package.json b/projects/rulesets-base/package.json index f13e7a63c0..2b2b1285e0 100644 --- a/projects/rulesets-base/package.json +++ b/projects/rulesets-base/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/rulesets-base", "license": "MIT", "packageManager": "yarn@4.1.1", - "version": "0.55.1", + "version": "1.0.0", "main": "build/index.js", "types": "build/index.d.ts", "files": [ diff --git a/projects/standard-rulesets/package.json b/projects/standard-rulesets/package.json index cd71d310d3..a89a4cd66d 100644 --- a/projects/standard-rulesets/package.json +++ b/projects/standard-rulesets/package.json @@ -2,7 +2,7 @@ "name": "@useoptic/standard-rulesets", "license": "MIT", "packageManager": "yarn@4.1.1", - "version": "0.55.1", + "version": "1.0.0", "main": "build/index.js", "types": "build/index.d.ts", "files": [ diff --git a/projects/standard-rulesets/src/lintgpt/index.ts b/projects/standard-rulesets/src/lintgpt/index.ts index cd11d0476d..9c2b3b83ec 100644 --- a/projects/standard-rulesets/src/lintgpt/index.ts +++ b/projects/standard-rulesets/src/lintgpt/index.ts @@ -11,7 +11,6 @@ import { import Ajv from 'ajv'; import { appliesWhen } from './constants'; import { - LintGptClient, LintgptEval, LintgptRulesHelper, PreparedRule, @@ -63,59 +62,15 @@ const configSchema = { const validateConfigSchema = ajv.compile(configSchema); export class LintGpt extends ExternalRuleBase { - static async fromOpticConfig( - config: unknown, - { client }: { client: LintGptClient } - ): Promise { - const result = validateConfigSchema(config); - - if (!result) { - return `- ${ajv.errorsText(validateConfigSchema.errors, { - separator: '\n- ', - dataVar: 'ruleset/lintgpt', - })}`; - } - - const validatedConfig = config as LintGptConfig; - const requirementRules: PreparedRule[] = []; - const addedRules: PreparedRule[] = []; - const lintgptRulesHelper = new LintgptRulesHelper(client); - - const rules: string[] = []; - - for (const [, config] of Object.entries(validatedConfig)) { - for (const rule of config.rules) { - rules.push(rule); - } - } - - const results = await lintgptRulesHelper.getRulePreps(rules); - - for (const [, config] of Object.entries(validatedConfig)) { - for (const rule of config.rules) { - const rule_checksum = computeRuleChecksum(rule); - const result = results.get(rule_checksum); - if (result?.prep?.prep_result) { - if ( - config.required_on === 'added' || - config.required_on === 'addedOrChanged' - ) { - addedRules.push(result.prep.prep_result); - } else { - requirementRules.push(result.prep.prep_result); - } - } - } - } - - return new LintGpt(validatedConfig, requirementRules, addedRules, client); + static async fromOpticConfig(config: unknown): Promise { + return 'Error: LintGPT is no longer supported'; } constructor( private config: LintGptConfig, private requirementRules: PreparedRule[], private addedRules: PreparedRule[], - private lintgptClient: LintGptClient + private lintgptClient: any ) { super(); } @@ -131,7 +86,7 @@ export class LintGpt extends ExternalRuleBase { const responsesToRun: AIRuleRunInputs[] = []; const propertiesToRun: AIRuleRunInputs[] = []; - const lintgptRulesHelper = new LintgptRulesHelper(this.lintgptClient); + const lintgptRulesHelper = new LintgptRulesHelper(); inputs.groupedFacts.endpoints.forEach((endpoint) => { const { path, method } = endpoint; diff --git a/projects/standard-rulesets/src/lintgpt/rules-helper.ts b/projects/standard-rulesets/src/lintgpt/rules-helper.ts index 7e348017af..b634453863 100644 --- a/projects/standard-rulesets/src/lintgpt/rules-helper.ts +++ b/projects/standard-rulesets/src/lintgpt/rules-helper.ts @@ -39,24 +39,6 @@ export type LintgptEval = { eval_error?: string | null; // Rule did not pass }; -export type LintGptClient = { - getLintgptPreps: ( - rule_checksums: string[] - ) => Promise<{ lintgpt_preps: CachedRulePrep[] }>; - requestLintgptPreps: (rules: string[]) => Promise; - getLintgptEvals: ( - evals: { rule_checksum: string; node_checksum: string }[] - ) => Promise<{ lintgpt_evals: LintgptEval[] }>; - requestLintgptEvals: ( - evals: { - node: string; - node_before?: string; - location_context: string; - rule_checksum: string; - }[] - ) => Promise; -}; - export type EvalRequest = { rule_checksum: string; location_context: string; @@ -65,7 +47,7 @@ export type EvalRequest = { }; export class LintgptRulesHelper { - constructor(private client: LintGptClient) {} + constructor() {} private getPrepSpinnerText = ({ total, @@ -89,84 +71,11 @@ export class LintgptRulesHelper { }); spinner.start(); - - try { - for (const rule of rules) { - const rule_checksum = computeRuleChecksum(rule); - preparedRulesMap.set(rule_checksum, { rule, rule_checksum }); - } - - const getRulesWithoutPrep = () => - [...preparedRulesMap.values()].filter( - ({ prep }) => !prep || prep.status === 'requested' - ); - - const maxTime = Date.now() + 3 * 60 * 1000; - - let rulesWithoutPreps = getRulesWithoutPrep(); - let firstRun = true; - const pollInterval = 2000; - - while (rulesWithoutPreps.length && maxTime > Date.now()) { - if (!firstRun) - await new Promise((resolve) => setTimeout(resolve, pollInterval)); - - const results = await this.client.getLintgptPreps( - rulesWithoutPreps.map((r) => r.rule_checksum) - ); - - for (const result of results.lintgpt_preps) { - preparedRulesMap.set(result.rule_checksum, { - ...preparedRulesMap.get(result.rule_checksum)!, - prep: result, - }); - } - - rulesWithoutPreps = getRulesWithoutPrep(); - - if (firstRun && rulesWithoutPreps.length) { - const rulesToPrep = rulesWithoutPreps.map((r) => r.rule); - await this.client.requestLintgptPreps(rulesToPrep); - } - - spinner.text = this.getPrepSpinnerText({ - total: rules.length, - evaluated: rules.length - rulesWithoutPreps.length, - }); - - firstRun = false; - } - - if (rulesWithoutPreps.length) { - spinner.warn( - `LintGPT: ${rulesWithoutPreps.length}/${rules.length} rules timed out` - ); - } else { - spinner.succeed( - this.getPrepSpinnerText({ - total: rules.length, - evaluated: rules.length - rulesWithoutPreps.length, - }) - ); - } - - return preparedRulesMap; - } catch (e) { - spinner.fail(`LintGPT: an error occured while preparing rules`); - throw e; - } + spinner.fail(`LintGPT: no longer supported`); } - private getEvalSpinnerText = ({ - total, - evaluated, - }: { - total: number; - evaluated: number; - }) => `LintGPT: ${evaluated}/${total} checks run`; - public async getRuleEvals(eval_requests: EvalRequest[]) { - const evalsMap = new Map< + return new Map< string, { rule_checksum: string; @@ -175,115 +84,6 @@ export class LintgptRulesHelper { rule_eval?: LintgptEval; } >(); - - let spinner = ora({ - text: this.getEvalSpinnerText({ - total: eval_requests.length, - evaluated: 0, - }), - }); - - spinner.start(); - - try { - for (const eval_request of eval_requests) { - const rule_checksum = eval_request.rule_checksum; - const node_checksum = computeNodeChecksum(eval_request); - const key = getEvalKey(rule_checksum, node_checksum); - evalsMap.set(key, { - eval_request, - rule_checksum, - node_checksum, - }); - } - - const getRequestsWithoutEvals = () => - [...evalsMap.values()].filter( - ({ rule_eval }) => !rule_eval || rule_eval.status === 'requested' - ); - - const maxTime = Date.now() + 5 * 60 * 1000; - - let requestsWithoutEvals = getRequestsWithoutEvals(); - let firstRun = true; - let pollInterval0 = 0; - let pollInterval1 = 1000; - - while (requestsWithoutEvals.length && maxTime > Date.now()) { - if (!firstRun) { - pollInterval1 = pollInterval0 + pollInterval1; - pollInterval0 = pollInterval1 - pollInterval0; - await new Promise((resolve) => setTimeout(resolve, pollInterval1)); - } - - const queryChunks = chunk( - requestsWithoutEvals.map((r) => ({ - rule_checksum: r.rule_checksum, - node_checksum: r.node_checksum, - })), - 20 - ); - - const resultChunks = await Promise.all( - queryChunks.map((c) => this.client.getLintgptEvals(c)) - ); - - const results = resultChunks.reduce( - (acc, val) => ({ - lintgpt_evals: [...acc.lintgpt_evals, ...val.lintgpt_evals], - }), - { lintgpt_evals: [] } - ); - - for (const result of results.lintgpt_evals) { - const key = getEvalKey(result.rule_checksum, result.node_checksum); - if (!evalsMap.has(key)) { - continue; - } - evalsMap.set(key, { - ...evalsMap.get(key)!, - rule_eval: result, - }); - } - - requestsWithoutEvals = getRequestsWithoutEvals(); - - spinner.text = this.getEvalSpinnerText({ - total: eval_requests.length, - evaluated: eval_requests.length - requestsWithoutEvals.length, - }); - - if (firstRun && requestsWithoutEvals.length) { - const evalsToRequest = requestsWithoutEvals.map( - (r) => r.eval_request - ); - const evalsToRequestChunks = chunk(evalsToRequest, 20); - for (const chunk of evalsToRequestChunks) { - await this.client.requestLintgptEvals(chunk); - await new Promise((resolve) => setTimeout(resolve, 20)); - } - } - firstRun = false; - } - - if (requestsWithoutEvals.length) { - spinner.warn( - `LintGPT: ${requestsWithoutEvals.length}/${eval_requests.length} checks timed out` - ); - } else { - spinner.succeed( - this.getEvalSpinnerText({ - total: eval_requests.length, - evaluated: eval_requests.length - requestsWithoutEvals.length, - }) - ); - } - - return evalsMap; - } catch (e) { - spinner.fail('LintGPT: an error occured while running evals'); - throw e; - } } }