diff --git a/src/models/internal.ts b/src/models/internal.ts index d596e73..00bc06d 100644 --- a/src/models/internal.ts +++ b/src/models/internal.ts @@ -14,7 +14,8 @@ export enum MessageCommandType { export enum ApiMessageType { WorkspacesList = "workspaceList", EndpointsList = "endpointList", - EndpointTests = "endpointTests" + EndpointTests = "endpointTests", + Swagger = "swagger" } export interface WebViewApiMessage { diff --git a/src/providers/up9Api.ts b/src/providers/up9Api.ts index 09995fb..e151ea8 100644 --- a/src/providers/up9Api.ts +++ b/src/providers/up9Api.ts @@ -35,4 +35,10 @@ export class UP9ApiProvider { raiseForBadResponse(response); return response.data; } + + public getSwagger = async(workspaceId: string, token: string): Promise => { + const response = await axios.get(`${this._trccUrl}/models/${workspaceId}/lastResults/all/swagger`, {headers: {'Authorization': `Bearer ${token}`}}); + raiseForBadResponse(response); + return response.data; + } } \ No newline at end of file diff --git a/src/providers/webviewCommunicator.ts b/src/providers/webviewCommunicator.ts index 4e45f35..35a0aea 100644 --- a/src/providers/webviewCommunicator.ts +++ b/src/providers/webviewCommunicator.ts @@ -67,16 +67,20 @@ export class UP9WebviewCommunicator { } private notifyPanelOfAuthStateChange(authStatus: boolean): void { - if (authStatus) { - this._panel.webview.postMessage({ - command: MessageCommandType.AuthSuccess, - username: this._authProvider.getUsernameFromToken() - }); - } else { - this._panel.webview.postMessage({ - command: MessageCommandType.AuthSignOut, - }); - } + try { + if (authStatus) { + this._panel.webview.postMessage({ + command: MessageCommandType.AuthSuccess, + username: this._authProvider.getUsernameFromToken() + }); + } else { + this._panel.webview.postMessage({ + command: MessageCommandType.AuthSignOut, + }); + } + } catch (error) { + console.warn('failed to send auth state to panel', error); + } } private async sendStoredDataToPanel(): Promise { @@ -107,6 +111,10 @@ export class UP9WebviewCommunicator { const tests = await this._apiProvider.getTestsForSpan(messageData.params.workspaceId, messageData.params.spanGuid, token); this.handlePanelUP9ApiResponse(messageData, tests, null); break; + case ApiMessageType.Swagger: + const swagger = await this._apiProvider.getSwagger(messageData.params.workspaceId, token); + this.handlePanelUP9ApiResponse(messageData, swagger, null); + break; } } catch (error) { console.error("error handling api request from panel", messageData, error); diff --git a/src/tests/integration/resources/invalidTests.py b/src/tests/integration/resources/invalidTests.py index 4196eb0..be1a7a6 100644 --- a/src/tests/integration/resources/invalidTests.py +++ b/src/tests/integration/resources/invalidTests.py @@ -1,8 +1,13 @@ -deaf test_1(self): - a = 5 - b = 5 - assert a == b - -def test_2(self): - s = "hello" - assert len(s) > 3 +import unittest + + +class Tests(unittest.TestCase): + + deaf test_1(self): + a = 5 + b = 5 + assert a == b + + def test_2(self): + s = "hello" + assert len(s) > 3 diff --git a/src/tests/integration/resources/testWithError.py b/src/tests/integration/resources/testWithError.py index 3151359..a2d3a21 100644 --- a/src/tests/integration/resources/testWithError.py +++ b/src/tests/integration/resources/testWithError.py @@ -1,8 +1,13 @@ -def test_1(self): - a = 5 - b = 7 - assert a == b - -def test_2(self): - s = "hello" - assert len(s) > 3 +import unittest + + +class Tests(unittest.TestCase): + + def test_1(self): + a = 5 + b = 7 + assert a == b + + def test_2(self): + s = "hello" + assert len(s) > 3 diff --git a/src/tests/integration/resources/validTests.py b/src/tests/integration/resources/validTests.py index 211bd57..3537043 100644 --- a/src/tests/integration/resources/validTests.py +++ b/src/tests/integration/resources/validTests.py @@ -1,8 +1,12 @@ -def test_1(self): - a = 5 - b = 5 - assert a == b +import unittest -def test_2(self): - s = "hello" - assert len(s) > 3 + +class Tests(unittest.TestCase): + def test_1(self): + a = 5 + b = 5 + assert a == b + + def test_2(self): + s = "hello" + assert len(s) > 3 diff --git a/src/webview/src/components/testsBrowserComponent.tsx b/src/webview/src/components/testsBrowserComponent.tsx index 98e9945..cae86a0 100644 --- a/src/webview/src/components/testsBrowserComponent.tsx +++ b/src/webview/src/components/testsBrowserComponent.tsx @@ -3,14 +3,15 @@ import {observer} from "mobx-react"; import { up9AuthStore } from "../stores/up9AuthStore"; import {sendApiMessage, sendInfoToast, setExtensionDefaultWorkspace} from "../providers/extensionConnectionProvider"; import { ApiMessageType } from "../../../models/internal"; -import {Form, FormControl, Dropdown, Container, Row, Col, Card} from 'react-bootstrap'; -import { isHexColorDark, transformTest } from "../utils"; +import {Form, FormControl, Dropdown, Container, Row, Col, Card, Accordion} from 'react-bootstrap'; +import { getSchemaForViewForEndpointSchema, isHexColorDark, transformTest } from "../utils"; import { v4 as uuidv4 } from 'uuid'; import { copyIcon, userIcon } from "./svgs"; import { microTestsHeader } from "../../../consts"; import AceEditor from "react-ace"; import "ace-builds/src-noconflict/mode-python"; +import "ace-builds/src-noconflict/mode-json"; import "ace-builds/src-noconflict/theme-chaos"; import "ace-builds/src-noconflict/theme-chrome"; import { LoadingOverlay } from "./loadingOverlay"; @@ -26,6 +27,7 @@ const TestsBrowserComponent: React.FC<{}> = observer(() => { const [workspaces, setWorkspaces] = useState(null); const [workspaceFilterInput, setWorkspaceFilterInput] = useState(""); const [selectedWorkspace, setSelectedWorkspace] = useState(""); + const [workspaceOAS, setWorkspaceOAS] = useState(null); const [endpoints, setEndpoints] = useState(null); const [endpointFilterInput, setEndpointFilterInput] = useState(""); @@ -60,6 +62,20 @@ const TestsBrowserComponent: React.FC<{}> = observer(() => { return workspaces.filter(workspace => workspace.toLocaleLowerCase().indexOf(workspaceFilterInput.toLowerCase()) > -1); }, [workspaces, workspaceFilterInput]); + const endpointSchemaJSONString = useMemo(() => { + if (!selectedEndpoint || !workspaceOAS) { + return null; + } + + const endpointSchema = workspaceOAS?.[selectedEndpoint.service]?.paths?.[selectedEndpoint.path]?.[selectedEndpoint.method.toLowerCase()]; + if (!endpointSchema) { + console.warn("could not find schema for endpoint from OAS"); + return null; + } + console.log('endpointSchema', endpointSchema); + return getSchemaForViewForEndpointSchema(endpointSchema); + }, [selectedEndpoint]); + useEffect(() => { setIsThemeDark(isHexColorDark(editorBackgroundColor)) }, [editorBackgroundColor]); @@ -100,12 +116,21 @@ const TestsBrowserComponent: React.FC<{}> = observer(() => { setSelectedEndpoint(null); setEndpointFilterInput(""); setEndpoints(null); + setWorkspaceOAS(null); + if (selectedWorkspace) { try { const endpoints = await sendApiMessage(ApiMessageType.EndpointsList, {workspaceId: selectedWorkspace}); setEndpoints(endpoints); } catch (error) { - console.log('error loading endpoints', error); + console.error('error loading workspace endpoints', error); + } + + try { + const workspaceOAS = await sendApiMessage(ApiMessageType.Swagger, {workspaceId: selectedWorkspace}); + setWorkspaceOAS(workspaceOAS); + } catch (error) { + console.error('error loading workspace OAS', error); } } })() @@ -224,6 +249,16 @@ const TestsBrowserComponent: React.FC<{}> = observer(() => { setOptions={{showGutter: false, hScrollBarAlwaysVisible: false, highlightActiveLine: false}}/> + {endpointSchemaJSONString && + + Endpoint Schema + + + + + } } diff --git a/src/webview/src/main.css b/src/webview/src/main.css index d666f2b..5302a20 100644 --- a/src/webview/src/main.css +++ b/src/webview/src/main.css @@ -53,6 +53,24 @@ hr { color: var(--vscode-input-foreground); } +.accordion, .accordion-item, .accordion-header, .accordion-button { + background: var(--vscode-editor-background) !important; + color: var(--vscode-editor-foreground) !important; +} + +.accordion-header { + border:1px solid var(--vscode-window-inactiveBorder); +} + +.accordion-header:active, .accordion-header:hover, .accordion-header:focus { + border:1px solid var(--vscode-window-activeBorder); +} + +.accordion-button::after { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e") !important; + opacity: 0.75; +} + .select-dropdown { width: 100%; } diff --git a/src/webview/src/providers/extensionConnectionProvider.ts b/src/webview/src/providers/extensionConnectionProvider.ts index df741bb..0d8a81b 100644 --- a/src/webview/src/providers/extensionConnectionProvider.ts +++ b/src/webview/src/providers/extensionConnectionProvider.ts @@ -14,6 +14,7 @@ if (!("acquireVsCodeApi" in window)) { // for development up9AuthStore.setIsAuthConfigured(true); up9AuthStore.setUsername("testuser@gmail.com"); + up9AuthStore.setDefaultWorkspace("rb-reg"); isDebug = true; } @@ -134,8 +135,8 @@ export const getDebugReply = (apiMessageType: ApiMessageType): Promise < any > = "6dffc92f-3119-48ad-8bec-78726070bea5", "896ca1d0-26ec-4e2a-b484-a7bcf920c253" ], - "method": "get", - "path": "/", + "method": "post", + "path": "/anything", "service": "http://httpbin2.trdemo", "serviceEndpointHashes": [ "987e3bd8f985f131a825f4b21e47e698ca3cdc4e", @@ -606,6 +607,8 @@ export const getDebugReply = (apiMessageType: ApiMessageType): Promise < any > = case ApiMessageType.WorkspacesList: response = ["rb-reg", "test", "workspace-b", "super-long-name-that-doesnt-end"]; break; + case ApiMessageType.Swagger: + response = {"http://httpbin2.trdemo":{"info":{"title":"http://httpbin.trdemo - rami schema-test all","version":"0.0.3"},"openapi":"3.1.0","paths":{"/":{"get":{"operationId":"e8913aae-0fe7-404e-928a-03bbe1f15f42","parameters":[],"responses":{"200":{"content":{"text/html":{"example":"\n\n\n\n \n httpbin.org\n \n \n \n \n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n\n \n \n \n\n \n \n \n\n \n \n\n\n
\n
\n
\n
\n
\n
\n
\n

httpbin.org\n \n
0.9.2
\n
\n

\n
[ Base URL: httpbin.org/ ]
\n
\n
\n
\n

A simple HTTP Request & Response Service.\n
\n
\n Run locally: \n $ docker run -p 80:80 kennethreitz/httpbin\n

\n
\n
\n \n
\n \n
\n
\n
\n\n
\n
\n
\n
\n
\n\n\n
\n
\n
\n \n [Powered by\n Flasgger]\n
\n
\n
\n
\n
\n\n\n\n \n \n \n
\n
\n
\n
\n\n

Other Utilities

\n\n
    \n
  • \n HTML form that posts to /post /forms/post
  • \n
\n\n
\n
\n
\n
\n
\n
\n\n\n"}},"description":"OK"}},"summary":"Seen 1 requests","x-endpoints":["e8913aae-0fe7-404e-928a-03bbe1f15f42"],"x-kpis":{"avg_rt":1.0,"entries":1,"err_rate":0.0,"failures":0,"first_seen":1638437398.302,"hits_rate":35.71413731150109,"last_active":1638437398.3300002,"last_seen":1638437398.3300002,"sessions":1,"sum_duration":0.0280001163482666,"sum_rt":0.0280001163482666}},"parameters":[]},"/anything":{"parameters":[],"post":{"operationId":"b6c1f298-489d-4283-a8bc-c313bb0c5cb9","parameters":[{"examples":["b2ecf022-b748-4889-a7c0-fa926e0e2576","71f4a365-8a51-45e8-8540-a8118a31ef13","ee94009c-16ce-4e83-92d0-ef25a5d33597","1755af01-6085-42b9-a65e-f410876299e9","58ffbfc2-2d96-4ccd-ae74-0311a7a5c73f"],"in":"header","name":"postman-token","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"password":{"type":"string"},"token":{"type":"string"},"username":{"type":"string"}},"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"example":{"args":{},"data":"{\n \"token\": \"0e2bd42e-eacb-4051-8dfb-ed8fa2d15b9a\"\n}","files":{},"form":{},"headers":{"Accept":"*/*","Accept-Encoding":"gzip, deflate, br","Cache-Control":"no-cache","Connection":"keep-alive","Content-Length":"55","Content-Type":"application/json","Host":"34.140.55.41","Postman-Token":"c2083746-6970-4229-99de-892a891a604f","User-Agent":"PostmanRuntime/7.28.4"},"json":{"token":"0e2bd42e-eacb-4051-8dfb-ed8fa2d15b9a"},"method":"POST","origin":"10.132.0.14","url":"http://34.140.55.41/anything"},"schema":{"properties":{"args":{"type":"object"},"data":{"type":"string"},"files":{"type":"object"},"form":{"type":"object"},"headers":{"properties":{"Accept":{"type":"string"},"Accept-Encoding":{"type":"string"},"Cache-Control":{"type":"string"},"Connection":{"type":"string"},"Content-Length":{"type":"string"},"Content-Type":{"type":"string"},"Host":{"type":"string"},"Postman-Token":{"type":"string"},"User-Agent":{"type":"string"}},"required":["Accept","Accept-Encoding","Cache-Control","Connection","Content-Length","Content-Type","Host","Postman-Token","User-Agent"],"type":"object"},"json":{"properties":{"password":{"type":"string"},"token":{"type":"string"},"username":{"type":"string"}},"type":"object"},"method":{"type":"string"},"origin":{"type":"string"},"url":{"type":"string"}},"required":["args","data","files","form","headers","json","method","origin","url"],"type":"object"}}},"description":"OK"}},"summary":"Seen 56 requests","x-endpoints":["b6c1f298-489d-4283-a8bc-c313bb0c5cb9"],"x-kpis":{"avg_rt":0.002233250678250769,"entries":56,"err_rate":0.0,"failures":0,"first_seen":1638431928.639,"hits_rate":0.7719665842863462,"last_active":1638432001.95,"last_seen":1638432001.95,"sessions":2,"sum_duration":72.54200005531311,"sum_rt":0.1620044708251953}}}},"tags":[],"x-ignoredTarget":false}}; } return Promise.resolve(response); } diff --git a/src/webview/src/utils.ts b/src/webview/src/utils.ts index 1478fdb..3983ffd 100644 --- a/src/webview/src/utils.ts +++ b/src/webview/src/utils.ts @@ -21,4 +21,42 @@ export const transformTest = (test: any) => { test.code = test.code.replaceAll('resp = self.', 'resp = requests.'); return test; -} \ No newline at end of file +} + + +// produces a simplified version of the endpoint schema for easier reading +export const getSchemaForViewForEndpointSchema = (endpointSchema: any) => { + const requests = {}; + for (const requestContentType in endpointSchema.requestBody?.content) { + const contentTypeRequestProperties = endpointSchema.requestBody?.content[requestContentType]?.schema?.properties; + if (contentTypeRequestProperties) { + requests[requestContentType] = contentTypeRequestProperties; + } + } + + const responses = {}; + + for (const responseCode in endpointSchema.responses) { + const response = endpointSchema.responses[responseCode]; + if (response.content) { + responses[responseCode] = {}; + for (const contentType in response.content) { + responses[responseCode][contentType] = { + schema: response.content[contentType].schema + } + } + } + } + + const schemaForView = {} as any; + if (Object.keys(endpointSchema.parameters).length) { + schemaForView.parameters = endpointSchema.parameters; + } + if (Object.keys(requests).length) { + schemaForView.requests = requests; + } + if (Object.keys(responses).length) { + schemaForView.responses = responses; + } + return JSON.stringify(schemaForView, null, 4); +}; \ No newline at end of file