mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	UI: OpenAPI test coverage (#23583)
This commit is contained in:
		| @@ -17,13 +17,17 @@ import { expandOpenApiProps, combineAttributes } from 'vault/utils/openapi-to-at | |||||||
| import fieldToAttrs from 'vault/utils/field-to-attrs'; | import fieldToAttrs from 'vault/utils/field-to-attrs'; | ||||||
| import { resolve, reject } from 'rsvp'; | import { resolve, reject } from 'rsvp'; | ||||||
| import { debug } from '@ember/debug'; | import { debug } from '@ember/debug'; | ||||||
| import { dasherize, capitalize } from '@ember/string'; | import { capitalize } from '@ember/string'; | ||||||
| import { computed } from '@ember/object'; // eslint-disable-line | import { computed } from '@ember/object'; // eslint-disable-line | ||||||
| import { singularize } from 'ember-inflector'; |  | ||||||
| import { withModelValidations } from 'vault/decorators/model-validations'; | import { withModelValidations } from 'vault/decorators/model-validations'; | ||||||
|  |  | ||||||
| import generatedItemAdapter from 'vault/adapters/generated-item-list'; | import generatedItemAdapter from 'vault/adapters/generated-item-list'; | ||||||
| import { sanitizePath } from 'core/utils/sanitize-path'; | import { sanitizePath } from 'core/utils/sanitize-path'; | ||||||
|  | import { | ||||||
|  |   filterPathsByItemType, | ||||||
|  |   pathToHelpUrlSegment, | ||||||
|  |   reducePathsByPathName, | ||||||
|  | } from 'vault/utils/openapi-helpers'; | ||||||
|  |  | ||||||
| export default Service.extend({ | export default Service.extend({ | ||||||
|   attrs: null, |   attrs: null, | ||||||
| @@ -36,6 +40,14 @@ export default Service.extend({ | |||||||
|     }); |     }); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * getNewModel instantiates models which use OpenAPI fully or partially | ||||||
|  |    * @param {string} modelType | ||||||
|  |    * @param {string} backend | ||||||
|  |    * @param {string} apiPath (optional) if passed, this method will call getPaths and build submodels for item types | ||||||
|  |    * @param {*} itemType (optional) used in getPaths for additional models | ||||||
|  |    * @returns void - as side effect, registers model via registerNewModelWithProps | ||||||
|  |    */ | ||||||
|   getNewModel(modelType, backend, apiPath, itemType) { |   getNewModel(modelType, backend, apiPath, itemType) { | ||||||
|     const owner = getOwner(this); |     const owner = getOwner(this); | ||||||
|     const modelName = `model:${modelType}`; |     const modelName = `model:${modelType}`; | ||||||
| @@ -76,12 +88,10 @@ export default Service.extend({ | |||||||
|           const adapter = this.getNewAdapter(pathInfo, itemType); |           const adapter = this.getNewAdapter(pathInfo, itemType); | ||||||
|           owner.register(`adapter:${modelType}`, adapter); |           owner.register(`adapter:${modelType}`, adapter); | ||||||
|         } |         } | ||||||
|         let path; |  | ||||||
|         // if we have an item we want the create info for that itemType |         // if we have an item we want the create info for that itemType | ||||||
|         const paths = itemType ? this.filterPathsByItemType(pathInfo, itemType) : pathInfo.paths; |         const paths = itemType ? filterPathsByItemType(pathInfo, itemType) : pathInfo.paths; | ||||||
|         const createPath = paths.find((path) => path.operations.includes('post') && path.action !== 'Delete'); |         const createPath = paths.find((path) => path.operations.includes('post') && path.action !== 'Delete'); | ||||||
|         path = createPath.path; |         const path = pathToHelpUrlSegment(createPath.path); | ||||||
|         path = path.includes('{') ? path.slice(0, path.indexOf('{') - 1) + '/example' : path; |  | ||||||
|         if (!path) { |         if (!path) { | ||||||
|           // TODO: we don't know if path will ever be falsey |           // TODO: we don't know if path will ever be falsey | ||||||
|           // if it is never falsey we can remove this. |           // if it is never falsey we can remove this. | ||||||
| @@ -99,64 +109,15 @@ export default Service.extend({ | |||||||
|       }); |       }); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   reducePathsByPathName(pathInfo, currentPath) { |   /** | ||||||
|     const pathName = currentPath[0]; |    * getPaths is used to fetch all the openAPI paths available for an auth method, | ||||||
|     const pathDetails = currentPath[1]; |    * to populate the tab navigation in each specific method page | ||||||
|     const displayAttrs = pathDetails['x-vault-displayAttrs']; |    * @param {string} apiPath path of openApi | ||||||
|  |    * @param {string} backend backend name, mostly for debug purposes | ||||||
|     if (!displayAttrs) { |    * @param {string} itemType optional | ||||||
|       return pathInfo; |    * @param {string} itemID optional - ID of specific item being fetched | ||||||
|     } |    * @returns PathsInfo | ||||||
|  |    */ | ||||||
|     let itemType, itemName; |  | ||||||
|     if (displayAttrs.itemType) { |  | ||||||
|       itemType = displayAttrs.itemType; |  | ||||||
|       let items = itemType.split(':'); |  | ||||||
|       itemName = items[items.length - 1]; |  | ||||||
|       items = items.map((item) => dasherize(singularize(item.toLowerCase()))); |  | ||||||
|       itemType = items.join('~*'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (itemType && !pathInfo.itemTypes.includes(itemType)) { |  | ||||||
|       pathInfo.itemTypes.push(itemType); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const operations = []; |  | ||||||
|     if (pathDetails.get) { |  | ||||||
|       operations.push('get'); |  | ||||||
|     } |  | ||||||
|     if (pathDetails.post) { |  | ||||||
|       operations.push('post'); |  | ||||||
|     } |  | ||||||
|     if (pathDetails.delete) { |  | ||||||
|       operations.push('delete'); |  | ||||||
|     } |  | ||||||
|     if (pathDetails.get && pathDetails.get.parameters && pathDetails.get.parameters[0].name === 'list') { |  | ||||||
|       operations.push('list'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pathInfo.paths.push({ |  | ||||||
|       path: pathName, |  | ||||||
|       itemType: itemType || displayAttrs.itemType, |  | ||||||
|       itemName: itemName || pathInfo.itemType || displayAttrs.itemType, |  | ||||||
|       operations, |  | ||||||
|       action: displayAttrs.action, |  | ||||||
|       navigation: displayAttrs.navigation === true, |  | ||||||
|       param: pathName.includes('{') ? pathName.split('{')[1].split('}')[0] : false, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return pathInfo; |  | ||||||
|   }, |  | ||||||
|  |  | ||||||
|   filterPathsByItemType(pathInfo, itemType) { |  | ||||||
|     if (!itemType) { |  | ||||||
|       return pathInfo.paths; |  | ||||||
|     } |  | ||||||
|     return pathInfo.paths.filter((path) => { |  | ||||||
|       return itemType === path.itemType; |  | ||||||
|     }); |  | ||||||
|   }, |  | ||||||
|  |  | ||||||
|   getPaths(apiPath, backend, itemType, itemID) { |   getPaths(apiPath, backend, itemType, itemID) { | ||||||
|     const debugString = |     const debugString = | ||||||
|       itemID && itemType |       itemID && itemType | ||||||
| @@ -167,7 +128,7 @@ export default Service.extend({ | |||||||
|       const pathInfo = help.openapi.paths; |       const pathInfo = help.openapi.paths; | ||||||
|       const paths = Object.entries(pathInfo); |       const paths = Object.entries(pathInfo); | ||||||
|  |  | ||||||
|       return paths.reduce(this.reducePathsByPathName, { |       return paths.reduce(reducePathsByPathName, { | ||||||
|         apiPath, |         apiPath, | ||||||
|         itemType, |         itemType, | ||||||
|         itemTypes: [], |         itemTypes: [], | ||||||
| @@ -229,7 +190,7 @@ export default Service.extend({ | |||||||
|  |  | ||||||
|   getNewAdapter(pathInfo, itemType) { |   getNewAdapter(pathInfo, itemType) { | ||||||
|     // we need list and create paths to set the correct urls for actions |     // we need list and create paths to set the correct urls for actions | ||||||
|     const paths = this.filterPathsByItemType(pathInfo, itemType); |     const paths = filterPathsByItemType(pathInfo, itemType); | ||||||
|     let { apiPath } = pathInfo; |     let { apiPath } = pathInfo; | ||||||
|     const getPath = paths.find((path) => path.operations.includes('get')); |     const getPath = paths.find((path) => path.operations.includes('get')); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										129
									
								
								ui/app/utils/openapi-helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								ui/app/utils/openapi-helpers.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | |||||||
|  | import { dasherize } from '@ember/string'; | ||||||
|  | import { singularize } from 'ember-inflector'; | ||||||
|  |  | ||||||
|  | // TODO: Consolidate with openapi-to-attrs once it's typescript | ||||||
|  |  | ||||||
|  | interface Path { | ||||||
|  |   path: string; | ||||||
|  |   itemType: string; | ||||||
|  |   itemName: string; | ||||||
|  |   operations: string[]; | ||||||
|  |   action: string; | ||||||
|  |   navigation: boolean; | ||||||
|  |   param: string | false; | ||||||
|  | } | ||||||
|  | interface PathsInfo { | ||||||
|  |   apiPath: string; | ||||||
|  |   itemType: string; | ||||||
|  |   itemTypes: string[]; | ||||||
|  |   paths: Path[]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface OpenApiParameter { | ||||||
|  |   description?: string; | ||||||
|  |   in: string; | ||||||
|  |   name: string; | ||||||
|  |   required: boolean; | ||||||
|  |   schema: object; | ||||||
|  | } | ||||||
|  | interface DisplayAttrs { | ||||||
|  |   itemType: string; | ||||||
|  |   action: string; | ||||||
|  |   navigation?: boolean; | ||||||
|  |   description?: string; | ||||||
|  |   name?: string; | ||||||
|  |   group?: string; | ||||||
|  |   value?: string | number; | ||||||
|  |   sensitive?: boolean; | ||||||
|  | } | ||||||
|  | interface OpenApiAction { | ||||||
|  |   parameters: Array<{ name: string }>; | ||||||
|  | } | ||||||
|  | interface OpenApiPath { | ||||||
|  |   description?: string; | ||||||
|  |   parameters: OpenApiParameter[]; | ||||||
|  |   'x-vault-displayAttrs': DisplayAttrs; | ||||||
|  |   get?: OpenApiAction; | ||||||
|  |   post?: OpenApiAction; | ||||||
|  |   delete?: OpenApiAction; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Take object entries from the OpenAPI response and consolidate them into an object which includes itemTypes, operations, and paths | ||||||
|  | export function reducePathsByPathName(pathsInfo: PathsInfo, currentPath: [string, OpenApiPath]): PathsInfo { | ||||||
|  |   const pathName = currentPath[0]; | ||||||
|  |   const pathDetails = currentPath[1]; | ||||||
|  |   const displayAttrs = pathDetails['x-vault-displayAttrs']; | ||||||
|  |   if (!displayAttrs) { | ||||||
|  |     // don't include paths that don't have display attrs | ||||||
|  |     return pathsInfo; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let itemType, itemName; | ||||||
|  |   if (displayAttrs.itemType) { | ||||||
|  |     itemType = displayAttrs.itemType; | ||||||
|  |     let items = itemType.split(':'); | ||||||
|  |     itemName = items[items.length - 1]; | ||||||
|  |     items = items.map((item) => dasherize(singularize(item.toLowerCase()))); | ||||||
|  |     itemType = items.join('~*'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (itemType && !pathsInfo.itemTypes.includes(itemType)) { | ||||||
|  |     pathsInfo.itemTypes.push(itemType); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const operations = []; | ||||||
|  |   if (pathDetails.get) { | ||||||
|  |     operations.push('get'); | ||||||
|  |   } | ||||||
|  |   if (pathDetails.post) { | ||||||
|  |     operations.push('post'); | ||||||
|  |   } | ||||||
|  |   if (pathDetails.delete) { | ||||||
|  |     operations.push('delete'); | ||||||
|  |   } | ||||||
|  |   if (pathDetails.get && pathDetails.get.parameters && pathDetails.get.parameters[0]?.name === 'list') { | ||||||
|  |     operations.push('list'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   pathsInfo.paths.push({ | ||||||
|  |     path: pathName, | ||||||
|  |     itemType: itemType || displayAttrs.itemType, | ||||||
|  |     itemName: itemName || pathsInfo.itemType || displayAttrs.itemType, | ||||||
|  |     operations, | ||||||
|  |     action: displayAttrs.action, | ||||||
|  |     navigation: displayAttrs.navigation === true, | ||||||
|  |     param: _getPathParam(pathName), | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return pathsInfo; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const apiPathRegex = new RegExp(/\{\w+\}/, 'g'); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * getPathParam takes an OpenAPI url and returns the first path param name, if it exists. | ||||||
|  |  * This is an internal method, but exported for testing. | ||||||
|  |  */ | ||||||
|  | export function _getPathParam(pathName: string): string | false { | ||||||
|  |   if (!pathName) return false; | ||||||
|  |   const params = pathName.match(apiPathRegex); | ||||||
|  |   // returns array like ['{username}'] or null | ||||||
|  |   if (!params) return false; | ||||||
|  |   // strip curly brackets from param name | ||||||
|  |   // previous behavior only returned the first param, so we match that for now | ||||||
|  |   return params[0]?.replace(new RegExp('{|}', 'g'), '') || false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function pathToHelpUrlSegment(path: string): string { | ||||||
|  |   if (!path) return ''; | ||||||
|  |   return path.replaceAll(apiPathRegex, 'example'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function filterPathsByItemType(pathInfo: PathsInfo, itemType: string): Path[] { | ||||||
|  |   if (!itemType) { | ||||||
|  |     return pathInfo.paths; | ||||||
|  |   } | ||||||
|  |   return pathInfo.paths.filter((path) => { | ||||||
|  |     return itemType === path.itemType; | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @@ -11,17 +11,17 @@ | |||||||
|   margin: 25px 0; |   margin: 25px 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| /*hide the swagger-ui headers*/ | /* hide the swagger-ui headers */ | ||||||
| .swagger-ember .swagger-ui .filter-container, | .swagger-ember .swagger-ui .filter-container, | ||||||
| .swagger-ember .swagger-ui .information-container.wrapper { | .swagger-ember .swagger-ui .information-container.wrapper { | ||||||
|   display: none; |   display: none; | ||||||
| } | } | ||||||
|  |  | ||||||
| /*some general de-rounding and removing backgrounds and drop shadows*/ | /* some general de-rounding and removing backgrounds and drop shadows */ | ||||||
| .swagger-ember .swagger-ui .btn { | .swagger-ember .swagger-ui .btn { | ||||||
|   border-width: 1px; |   border-width: 1px; | ||||||
|   box-shadow: none; |   box-shadow: none; | ||||||
| 	border-radius: 0px; |   border-radius: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| .swagger-ember .swagger-ui .opblock { | .swagger-ember .swagger-ui .opblock { | ||||||
| @@ -31,8 +31,7 @@ | |||||||
|   box-shadow: none; |   box-shadow: none; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /* customize method, path, description */ | ||||||
| /*customize method, path, description*/ |  | ||||||
| .swagger-ember .swagger-ui .opblock .opblock-summary, | .swagger-ember .swagger-ui .opblock .opblock-summary, | ||||||
| .swagger-ember .swagger-ui .opblock .opblock-summary-description { | .swagger-ember .swagger-ui .opblock .opblock-summary-description { | ||||||
|   display: block; |   display: block; | ||||||
| @@ -49,7 +48,7 @@ | |||||||
| } | } | ||||||
|  |  | ||||||
| .swagger-ember .swagger-ui .opblock .opblock-summary-method, | .swagger-ember .swagger-ui .opblock .opblock-summary-method, | ||||||
| .swagger-ember .swagger-ui .opblock .opblock-summary-path{ | .swagger-ember .swagger-ui .opblock .opblock-summary-path { | ||||||
|   display: inline-block; |   display: inline-block; | ||||||
|   margin: 0; |   margin: 0; | ||||||
|   padding: 0; |   padding: 0; | ||||||
| @@ -60,15 +59,15 @@ | |||||||
|   min-width: auto; |   min-width: auto; | ||||||
|   text-align: left; |   text-align: left; | ||||||
|   font-size: 10px; |   font-size: 10px; | ||||||
| 	box-shadow: 0 0 0 1px currentColor; |   box-shadow: 0 0 0 1px currentcolor; | ||||||
|   position: relative; |   position: relative; | ||||||
|   top: -2px; |   top: -2px; | ||||||
|   padding: 0 2px; |   padding: 0 2px; | ||||||
|   margin-right: 8px; |   margin-right: 8px; | ||||||
| } | } | ||||||
|  |  | ||||||
| /*make tags look like list items */ | /* make tags look like list items */ | ||||||
| .swagger-ember .swagger-ui .opblock-tag{ | .swagger-ember .swagger-ui .opblock-tag { | ||||||
|   font-size: 16px; |   font-size: 16px; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -92,66 +91,70 @@ | |||||||
|   padding-left: 0.75rem; |   padding-left: 0.75rem; | ||||||
|   padding-right: 0.75rem; |   padding-right: 0.75rem; | ||||||
|   position: relative; |   position: relative; | ||||||
|   box-shadow: 0 2px 0 -1px #BAC1CC, 0 -2px 0 -1px #BAC1CC, 0 0 0 1px #BAC1CC, 0 8px 4px -4px rgba(10, 10, 10, 0.1), 0 6px 8px -2px rgba(10, 10, 10, 0.05); |   box-shadow: 0 2px 0 -1px #bac1cc, 0 -2px 0 -1px #bac1cc, 0 0 0 1px #bac1cc, | ||||||
|  |     0 8px 4px -4px rgb(10 10 10 / 10%), 0 6px 8px -2px rgb(10 10 10 / 5%); | ||||||
| } | } | ||||||
|  |  | ||||||
| /*shrink the size of the arrows*/ | /* shrink the size of the arrows */ | ||||||
| .swagger-ember .swagger-ui .expand-methods svg, | .swagger-ember .swagger-ui .expand-methods svg, | ||||||
| .swagger-ember .swagger-ui .expand-operation svg { | .swagger-ember .swagger-ui .expand-operation svg { | ||||||
|   height: 12px; |   height: 12px; | ||||||
|   width: 12px; |   width: 12px; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /* operation box - GET (blue) */ | ||||||
| /*operation box - GET (blue) */ |  | ||||||
| .swagger-ember .swagger-ui .opblock.opblock-get { | .swagger-ember .swagger-ui .opblock.opblock-get { | ||||||
|   background: #f5f8ff; |   background: #f5f8ff; | ||||||
|   border: 1px solid #bfd4ff; |   border: 1px solid #bfd4ff; | ||||||
| } | } | ||||||
|  |  | ||||||
| /*operation label*/ | /* operation label */ | ||||||
| .swagger-ember .swagger-ui .opblock.opblock-get .opblock-summary-method { | .swagger-ember .swagger-ui .opblock.opblock-get .opblock-summary-method { | ||||||
|   color: #1563ff; |   color: #1563ff; | ||||||
|   background: none; |   background: none; | ||||||
| } | } | ||||||
|  /*and expanded tab highlight */ |  | ||||||
|  | /* and expanded tab highlight */ | ||||||
| .swagger-ember .swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after { | .swagger-ember .swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after { | ||||||
|   background: #1563ff; |   background: #1563ff; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /* operation box - POST (green) */ | ||||||
| /*operation box - POST (green) */ |  | ||||||
| .swagger-ember .swagger-ui .opblock.opblock-post { | .swagger-ember .swagger-ui .opblock.opblock-post { | ||||||
|   background: #fafdfa; |   background: #fafdfa; | ||||||
|   border: 1px solid #c6e9c9; |   border: 1px solid #c6e9c9; | ||||||
| } | } | ||||||
|  |  | ||||||
| .swagger-ember .swagger-ui .opblock.opblock-post .opblock-summary-method { | .swagger-ember .swagger-ui .opblock.opblock-post .opblock-summary-method { | ||||||
|   color: #2eb039; |   color: #2eb039; | ||||||
|   background: none; |   background: none; | ||||||
| } | } | ||||||
|  |  | ||||||
| .swagger-ember .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after { | .swagger-ember .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after { | ||||||
|   background: #2eb039; |   background: #2eb039; | ||||||
| } | } | ||||||
|  |  | ||||||
| /*operation box - POST (red) */ | /* operation box - POST (red) */ | ||||||
| .swagger-ember .swagger-ui .opblock.opblock-delete { | .swagger-ember .swagger-ui .opblock.opblock-delete { | ||||||
|   background: #fdfafb; |   background: #fdfafb; | ||||||
|   border: 1px solid #f9ecee; |   border: 1px solid #f9ecee; | ||||||
| } | } | ||||||
|  |  | ||||||
| .swagger-ember .swagger-ui .opblock.opblock-delete .opblock-summary-method { | .swagger-ember .swagger-ui .opblock.opblock-delete .opblock-summary-method { | ||||||
|   color: #c73445; |   color: #c73445; | ||||||
|   background: none; |   background: none; | ||||||
| } | } | ||||||
|  |  | ||||||
| .swagger-ember .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after { | .swagger-ember .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after { | ||||||
|   background: #c73445; |   background: #c73445; | ||||||
| } | } | ||||||
|  |  | ||||||
| /*remove "LOADING" from initial loading spinner*/ | /* remove "LOADING" from initial loading spinner */ | ||||||
| .swagger-ember .swagger-ui .loading-container .loading::after { | .swagger-ember .swagger-ui .loading-container .loading::after { | ||||||
| 	content: ""; |   content: ''; | ||||||
| } | } | ||||||
|  |  | ||||||
| /*add text about requests to a live vault server*/ | /* add text about requests to a live vault server */ | ||||||
| .swagger-ember .swagger-ui .btn.execute::after { | .swagger-ember .swagger-ui .btn.execute::after { | ||||||
| 	content: " - send a request with your token to Vault." |   content: ' - send a request with your token to Vault.'; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										53
									
								
								ui/tests/acceptance/open-api-path-help-test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								ui/tests/acceptance/open-api-path-help-test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | import { module, test } from 'qunit'; | ||||||
|  | import { setupApplicationTest } from 'vault/tests/helpers'; | ||||||
|  | import authPage from 'vault/tests/pages/auth'; | ||||||
|  | import { deleteAuthCmd, deleteEngineCmd, mountAuthCmd, mountEngineCmd, runCmd } from '../helpers/commands'; | ||||||
|  | import { authEngineHelper, secretEngineHelper } from '../helpers/openapi/test-helpers'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This set of tests is for ensuring that backend changes to the OpenAPI spec | ||||||
|  |  * are known by UI developers and adequately addressed in the UI. When changes | ||||||
|  |  * are detected from this set of tests, they should be updated to pass and | ||||||
|  |  * smoke tested to ensure changes to not break the GUI workflow. | ||||||
|  |  * Marked as enterprise so it only runs periodically | ||||||
|  |  */ | ||||||
|  | module('Acceptance | OpenAPI provides expected attributes enterprise', function (hooks) { | ||||||
|  |   setupApplicationTest(hooks); | ||||||
|  |   hooks.beforeEach(function () { | ||||||
|  |     this.pathHelp = this.owner.lookup('service:pathHelp'); | ||||||
|  |     this.store = this.owner.lookup('service:store'); | ||||||
|  |     return authPage.login(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Secret engines that use OpenAPI | ||||||
|  |   ['ssh', 'kmip', 'pki'].forEach(function (testCase) { | ||||||
|  |     return module(`${testCase} engine`, function (hooks) { | ||||||
|  |       hooks.beforeEach(async function () { | ||||||
|  |         this.backend = `${testCase}-openapi`; | ||||||
|  |         await runCmd(mountEngineCmd(testCase, this.backend), false); | ||||||
|  |       }); | ||||||
|  |       hooks.afterEach(async function () { | ||||||
|  |         await runCmd(deleteEngineCmd(this.backend), false); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       secretEngineHelper(test, testCase); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // All auth backends use OpenAPI except aws | ||||||
|  |   ['azure', 'userpass', 'cert', 'gcp', 'github', 'jwt', 'kubernetes', 'ldap', 'okta', 'radius'].forEach( | ||||||
|  |     function (testCase) { | ||||||
|  |       return module(`${testCase} auth`, function (hooks) { | ||||||
|  |         hooks.beforeEach(async function () { | ||||||
|  |           this.mount = `${testCase}-openapi`; | ||||||
|  |           await runCmd(mountAuthCmd(testCase, this.mount), false); | ||||||
|  |         }); | ||||||
|  |         hooks.afterEach(async function () { | ||||||
|  |           await runCmd(deleteAuthCmd(this.backend), false); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         authEngineHelper(test, testCase); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   ); | ||||||
|  | }); | ||||||
							
								
								
									
										1289
									
								
								ui/tests/helpers/openapi/auth-model-attributes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1289
									
								
								ui/tests/helpers/openapi/auth-model-attributes.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1491
									
								
								ui/tests/helpers/openapi/secret-model-attributes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1491
									
								
								ui/tests/helpers/openapi/secret-model-attributes.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										51
									
								
								ui/tests/helpers/openapi/test-helpers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								ui/tests/helpers/openapi/test-helpers.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | import authModelAttributes from './auth-model-attributes'; | ||||||
|  | import secretModelAttributes from './secret-model-attributes'; | ||||||
|  |  | ||||||
|  | export const secretEngineHelper = (test, secretEngine) => { | ||||||
|  |   const engineData = secretModelAttributes[secretEngine]; | ||||||
|  |   if (!engineData) | ||||||
|  |     throw new Error(`No engine attributes found in secret-model-attributes for ${secretEngine}`); | ||||||
|  |  | ||||||
|  |   const modelNames = Object.keys(engineData); | ||||||
|  |   // A given secret engine might have multiple models that are openApi driven | ||||||
|  |   modelNames.forEach((modelName) => { | ||||||
|  |     test(`${modelName} model getProps returns correct attributes`, async function (assert) { | ||||||
|  |       const model = this.store.createRecord(modelName, {}); | ||||||
|  |       const helpUrl = model.getHelpUrl(this.backend); | ||||||
|  |       const result = await this.pathHelp.getProps(helpUrl, this.backend); | ||||||
|  |       const expected = engineData[modelName]; | ||||||
|  |       assert.deepEqual(result, expected, `getProps returns expected attributes for ${modelName}`); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const authEngineHelper = (test, authBackend) => { | ||||||
|  |   const authData = authModelAttributes[authBackend]; | ||||||
|  |   if (!authData) throw new Error(`No auth attributes found in auth-model-attributes for ${authBackend}`); | ||||||
|  |  | ||||||
|  |   const itemNames = Object.keys(authData); | ||||||
|  |   itemNames.forEach((itemName) => { | ||||||
|  |     if (itemName.startsWith('auth-config/')) { | ||||||
|  |       // Config test doesn't need to instantiate a new model | ||||||
|  |       test(`${itemName} model`, async function (assert) { | ||||||
|  |         const model = this.store.createRecord(itemName, {}); | ||||||
|  |         const helpUrl = model.getHelpUrl(this.mount); | ||||||
|  |         const result = await this.pathHelp.getProps(helpUrl, this.mount); | ||||||
|  |         const expected = authData[itemName]; | ||||||
|  |         assert.deepEqual(result, expected, `getProps returns expected attributes for ${itemName}`); | ||||||
|  |       }); | ||||||
|  |     } else { | ||||||
|  |       test.skip(`generated-${itemName}-${authBackend} model`, async function (assert) { | ||||||
|  |         const modelName = `generated-${itemName}-${authBackend}`; | ||||||
|  |         // Generated items need to instantiate the model first via getNewModel | ||||||
|  |         await this.pathHelp.getNewModel(modelName, this.mount, `auth/${this.mount}/`, itemName); | ||||||
|  |         const model = this.store.createRecord(modelName, {}); | ||||||
|  |         // Generated items don't have this method -- helpUrl is calculated in path-help.js line 101 | ||||||
|  |         const helpUrl = model.getHelpUrl(this.mount); | ||||||
|  |         const result = await this.pathHelp.getProps(helpUrl, this.mount); | ||||||
|  |         const expected = authData[modelName]; | ||||||
|  |         assert.deepEqual(result, expected, `getProps returns expected attributes for ${modelName}`); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | }; | ||||||
| @@ -24,13 +24,12 @@ module('Integration | Component | dashboard/replication-state-text', function (h | |||||||
|  |  | ||||||
|   test('it displays replication states', async function (assert) { |   test('it displays replication states', async function (assert) { | ||||||
|     await render( |     await render( | ||||||
|       hbs` |       hbs`<Dashboard::ReplicationStateText | ||||||
|         <Dashboard::ReplicationStateText  |  | ||||||
|   @name={{this.name}} |   @name={{this.name}} | ||||||
|   @version={{this.version}} |   @version={{this.version}} | ||||||
|   @subText={{this.subText}} |   @subText={{this.subText}} | ||||||
|           @clusterStates={{this.clusterStates}} /> |   @clusterStates={{this.clusterStates}} | ||||||
|           ` | />` | ||||||
|     ); |     ); | ||||||
|     assert.dom(SELECTORS.getReplicationTitle('dr-perf', 'DR primary')).hasText('DR primary'); |     assert.dom(SELECTORS.getReplicationTitle('dr-perf', 'DR primary')).hasText('DR primary'); | ||||||
|     assert.dom(SELECTORS.getStateTooltipTitle('dr-perf', 'DR primary')).hasText('running'); |     assert.dom(SELECTORS.getStateTooltipTitle('dr-perf', 'DR primary')).hasText('running'); | ||||||
| @@ -42,13 +41,12 @@ module('Integration | Component | dashboard/replication-state-text', function (h | |||||||
|       isOk: false, |       isOk: false, | ||||||
|     }; |     }; | ||||||
|     await render( |     await render( | ||||||
|       hbs` |       hbs`<Dashboard::ReplicationStateText | ||||||
|         <Dashboard::ReplicationStateText  |  | ||||||
|   @name={{this.name}} |   @name={{this.name}} | ||||||
|   @version={{this.version}} |   @version={{this.version}} | ||||||
|   @subText={{this.subText}} |   @subText={{this.subText}} | ||||||
|           @clusterStates={{this.clusterStates}} /> |   @clusterStates={{this.clusterStates}} | ||||||
|           ` | />` | ||||||
|     ); |     ); | ||||||
|     assert |     assert | ||||||
|       .dom(SELECTORS.getReplicationTitle('dr-perf', 'Performance primary')) |       .dom(SELECTORS.getReplicationTitle('dr-perf', 'Performance primary')) | ||||||
|   | |||||||
| @@ -21,12 +21,9 @@ module('Integration | Component | empty-state', function (hooks) { | |||||||
|  |  | ||||||
|     // Template block usage: |     // Template block usage: | ||||||
|     await render(hbs` |     await render(hbs` | ||||||
|       {{#empty-state |       <EmptyState @title="Empty State Title" @message="This is the empty state message"> | ||||||
|         title="Empty State Title" |  | ||||||
|         message="This is the empty state message" |  | ||||||
|       }} |  | ||||||
|         Actions Link |         Actions Link | ||||||
|       {{/empty-state}} |       </EmptyState> | ||||||
|     `); |     `); | ||||||
|  |  | ||||||
|     assert.dom('.empty-state-title').hasText('Empty State Title', 'renders empty state title'); |     assert.dom('.empty-state-title').hasText('Empty State Title', 'renders empty state title'); | ||||||
|   | |||||||
| @@ -21,9 +21,9 @@ module('Integration | Component | form-error', function (hooks) { | |||||||
|  |  | ||||||
|     // Template block usage: |     // Template block usage: | ||||||
|     await render(hbs` |     await render(hbs` | ||||||
|       {{#form-error}} |       <FormError> | ||||||
|         template block text |         template block text | ||||||
|       {{/form-error}} |       </FormError> | ||||||
|     `); |     `); | ||||||
|  |  | ||||||
|     assert.dom(this.element).hasText('template block text'); |     assert.dom(this.element).hasText('template block text'); | ||||||
|   | |||||||
| @@ -21,9 +21,9 @@ module('Integration | Component | transform-edit-base', function (hooks) { | |||||||
|  |  | ||||||
|     // Template block usage: |     // Template block usage: | ||||||
|     await render(hbs` |     await render(hbs` | ||||||
|       {{#transform-edit-base}} |       <TransformEditBase> | ||||||
|         template block text |         template block text | ||||||
|       {{/transform-edit-base}} |       </TransformEditBase> | ||||||
|     `); |     `); | ||||||
|  |  | ||||||
|     assert.dom(this.element).hasText('template block text'); |     assert.dom(this.element).hasText('template block text'); | ||||||
|   | |||||||
| @@ -13,16 +13,8 @@ module('Integration | Component | transform-role-edit', function (hooks) { | |||||||
|  |  | ||||||
|   skip('it renders', async function (assert) { |   skip('it renders', async function (assert) { | ||||||
|     // TODO: Fill out these tests, merging without to unblock other work |     // TODO: Fill out these tests, merging without to unblock other work | ||||||
|  |  | ||||||
|     await render(hbs`{{transform-role-edit}}`); |  | ||||||
|  |  | ||||||
|     assert.dom(this.element).hasText(''); |  | ||||||
|  |  | ||||||
|     // Template block usage: |  | ||||||
|     await render(hbs` |     await render(hbs` | ||||||
|       {{#transform-role-edit}} |       <TransformRoleEdit /> | ||||||
|         template block text |  | ||||||
|       {{/transform-role-edit}} |  | ||||||
|     `); |     `); | ||||||
|  |  | ||||||
|     assert.dom(this.element).hasText('template block text'); |     assert.dom(this.element).hasText('template block text'); | ||||||
|   | |||||||
| @@ -273,9 +273,7 @@ module('Integration | Component | ttl-picker', function (hooks) { | |||||||
|       this.set('onChange', changeSpy); |       this.set('onChange', changeSpy); | ||||||
|       await render(hbs` |       await render(hbs` | ||||||
|         <TtlPicker |         <TtlPicker | ||||||
|           @label={{this.label}} |           @label={{this.label}} @initialValue="10m" | ||||||
|           @label="clicktest" |  | ||||||
|           @initialValue="10m" |  | ||||||
|           @onChange={{this.onChange}} |           @onChange={{this.onChange}} | ||||||
|         /> |         /> | ||||||
|       `); |       `); | ||||||
| @@ -295,9 +293,7 @@ module('Integration | Component | ttl-picker', function (hooks) { | |||||||
|     test('inputs reflect initial value when toggled on', async function (assert) { |     test('inputs reflect initial value when toggled on', async function (assert) { | ||||||
|       await render(hbs` |       await render(hbs` | ||||||
|         <TtlPicker |         <TtlPicker | ||||||
|           @label={{this.label}} |           @label={{this.label}} @onChange={{this.onChange}} | ||||||
|           @label="inittest" |  | ||||||
|           @onChange={{this.onChange}} |  | ||||||
|           @initialValue="100m" |           @initialValue="100m" | ||||||
|         /> |         /> | ||||||
|       `); |       `); | ||||||
| @@ -311,9 +307,7 @@ module('Integration | Component | ttl-picker', function (hooks) { | |||||||
|     test('it is enabled on init if initialEnabled is true', async function (assert) { |     test('it is enabled on init if initialEnabled is true', async function (assert) { | ||||||
|       await render(hbs` |       await render(hbs` | ||||||
|         <TtlPicker |         <TtlPicker | ||||||
|           @label={{this.label}} |           @label={{this.label}} @onChange={{this.onChange}} | ||||||
|           @label="inittest" |  | ||||||
|           @onChange={{this.onChange}} |  | ||||||
|           @initialValue="100m" |           @initialValue="100m" | ||||||
|           @initialEnabled={{true}} |           @initialEnabled={{true}} | ||||||
|         /> |         /> | ||||||
| @@ -330,9 +324,7 @@ module('Integration | Component | ttl-picker', function (hooks) { | |||||||
|     test('it is enabled on init if initialEnabled evals to truthy', async function (assert) { |     test('it is enabled on init if initialEnabled evals to truthy', async function (assert) { | ||||||
|       await render(hbs` |       await render(hbs` | ||||||
|         <TtlPicker |         <TtlPicker | ||||||
|           @label={{this.label}} |           @label={{this.label}} @onChange={{this.onChange}} | ||||||
|           @label="inittest" |  | ||||||
|           @onChange={{this.onChange}} |  | ||||||
|           @initialValue="100m" |           @initialValue="100m" | ||||||
|           @initialEnabled="100m" |           @initialEnabled="100m" | ||||||
|         /> |         /> | ||||||
| @@ -345,9 +337,7 @@ module('Integration | Component | ttl-picker', function (hooks) { | |||||||
|     test('it converts days to go safe time', async function (assert) { |     test('it converts days to go safe time', async function (assert) { | ||||||
|       await render(hbs` |       await render(hbs` | ||||||
|         <TtlPicker |         <TtlPicker | ||||||
|           @label={{this.label}} |           @label={{this.label}} @initialValue="2d" | ||||||
|           @label="clicktest" |  | ||||||
|           @initialValue="2d" |  | ||||||
|           @onChange={{this.onChange}} |           @onChange={{this.onChange}} | ||||||
|         /> |         /> | ||||||
|       `); |       `); | ||||||
| @@ -367,9 +357,7 @@ module('Integration | Component | ttl-picker', function (hooks) { | |||||||
|     test('it converts to the largest round unit on init', async function (assert) { |     test('it converts to the largest round unit on init', async function (assert) { | ||||||
|       await render(hbs` |       await render(hbs` | ||||||
|         <TtlPicker |         <TtlPicker | ||||||
|           @label={{this.label}} |           @label={{this.label}} @onChange={{this.onChange}} | ||||||
|           @label="convertunits" |  | ||||||
|           @onChange={{this.onChange}} |  | ||||||
|           @initialValue="60000s" |           @initialValue="60000s" | ||||||
|           @initialEnabled="true" |           @initialEnabled="true" | ||||||
|         /> |         /> | ||||||
| @@ -381,9 +369,7 @@ module('Integration | Component | ttl-picker', function (hooks) { | |||||||
|     test('it converts to the largest round unit on init when no unit provided', async function (assert) { |     test('it converts to the largest round unit on init when no unit provided', async function (assert) { | ||||||
|       await render(hbs` |       await render(hbs` | ||||||
|         <TtlPicker |         <TtlPicker | ||||||
|           @label={{this.label}} |           @label={{this.label}} @onChange={{this.onChange}} | ||||||
|           @label="convertunits" |  | ||||||
|           @onChange={{this.onChange}} |  | ||||||
|           @initialValue={{86400}} |           @initialValue={{86400}} | ||||||
|           @initialEnabled="true" |           @initialEnabled="true" | ||||||
|         /> |         /> | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								ui/tests/unit/utils/openapi-helpers-test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								ui/tests/unit/utils/openapi-helpers-test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | import { module, test } from 'qunit'; | ||||||
|  | import { _getPathParam, pathToHelpUrlSegment } from 'vault/utils/openapi-helpers'; | ||||||
|  |  | ||||||
|  | module('Unit | Utility | OpenAPI helper utils', function () { | ||||||
|  |   test(`pathToHelpUrlSegment`, function (assert) { | ||||||
|  |     assert.expect(5); | ||||||
|  |     [ | ||||||
|  |       { path: '/auth/{username}', result: '/auth/example' }, | ||||||
|  |       { path: '{username}/foo', result: 'example/foo' }, | ||||||
|  |       { path: 'foo/{username}/bar', result: 'foo/example/bar' }, | ||||||
|  |       { path: '', result: '' }, | ||||||
|  |       { path: undefined, result: '' }, | ||||||
|  |     ].forEach((test) => { | ||||||
|  |       assert.strictEqual(pathToHelpUrlSegment(test.path), test.result, `translates ${test.path}`); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   test(`_getPathParam`, function (assert) { | ||||||
|  |     assert.expect(7); | ||||||
|  |     [ | ||||||
|  |       { path: '/auth/{username}', result: 'username' }, | ||||||
|  |       { path: '{unicorn}/foo', result: 'unicorn' }, | ||||||
|  |       { path: 'foo/{bigfoot}/bar', result: 'bigfoot' }, | ||||||
|  |       { path: '{alphabet}/bowl/{soup}', result: 'alphabet' }, | ||||||
|  |       { path: 'no/params', result: false }, | ||||||
|  |       { path: '', result: false }, | ||||||
|  |       { path: undefined, result: false }, | ||||||
|  |     ].forEach((test) => { | ||||||
|  |       assert.strictEqual(_getPathParam(test.path), test.result, `returns first param for ${test.path}`); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
		Reference in New Issue
	
	Block a user
	 Chelsea Shaw
					Chelsea Shaw