kevinswiber/postman2openapi

Allow multiple examples at the item level to allow for reciprocal tests

mefellows opened this issue · 2 comments

First up, thanks for creating this - very cool!

I have a Postman collection that has the following basic product collection:

  • GET /products
  • GET /product/:id
  • POST /products

Screen Shot 2021-05-09 at 10 36 05 pm

At the moment, I believe postman2openapi uses the examples to create the schema, and this seems to work well. The issue is that for the POST endpoint, I have two versions for the 400 and 200 use cases with separate tests and examples. Unfortunately, you can't assign tests to examples so this is the only way to ensure that both response codes are tested (e.g. using Newman CLI).

See this example collection to demonstrate the point:

{
	"info": {
		"name": "Example Products API",
		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
	},
	"item": [
		{
			"name": "POST /products/ (200)",
			"event": [
				{
					"listen": "test",
					"script": {
						"exec": [
							"pm.test(\"Status code is 200\", function () {",
							"    pm.response.to.have.status(200);",
							"});",
							""
						],
						"type": "text/javascript"
					}
				}
			],
			"request": {
				"method": "POST",
				"header": [
					{
						"key": "Content-Type",
						"type": "text",
						"value": "application/json"
					}
				],
				"body": {
					"mode": "raw",
					"raw": "{\n    \"id\": \"09\",\n    \"name\": \"Gem Visa\",\n    \"type\": \"CREDIT_CARD\",\n    \"price\": 99.99,\n    \"version\": \"v1\"\n}"
				},
				"url": {
					"raw": "{{host}}/products",
					"host": [
						"{{host}}"
					],
					"path": [
						"products"
					]
				},
				"description": "Create a product"
			},
			"response": [
				{
					"name": "POST /products/ (200)",
					"originalRequest": {
						"method": "POST",
						"header": [
							{
								"key": "Content-Type",
								"type": "text",
								"value": "application/json"
							}
						],
						"body": {
							"mode": "raw",
							"raw": "{\n    \"id\": \"09\",\n    \"name\": \"Gem Visa\",\n    \"type\": \"CREDIT_CARD\",\n    \"price\": 99.99,\n    \"version\": \"v1\"\n}"
						},
						"url": {
							"raw": "{{host}}/products",
							"host": [
								"{{host}}"
							],
							"path": [
								"products"
							]
						}
					},
					"status": "OK",
					"code": 200,
					"_postman_previewlanguage": "json",
					"header": [
						{
							"key": "X-Powered-By",
							"value": "Express"
						},
						{
							"key": "Access-Control-Allow-Origin",
							"value": "*"
						},
						{
							"key": "Content-Type",
							"value": "application/json; charset=utf-8"
						},
						{
							"key": "Content-Length",
							"value": "79"
						},
						{
							"key": "ETag",
							"value": "W/\"4f-7upA8VUHobjcwMU2JZU+mGYVfEo\""
						},
						{
							"key": "Date",
							"value": "Sun, 09 May 2021 12:29:53 GMT"
						},
						{
							"key": "Connection",
							"value": "keep-alive"
						}
					],
					"cookie": [],
					"body": "{\n    \"id\": \"09\",\n    \"name\": \"Gem Visa\",\n    \"type\": \"CREDIT_CARD\",\n    \"price\": 99.99,\n    \"version\": \"v1\"\n}"
				}
			]
		},
		{
			"name": "POST /products/ (400)",
			"event": [
				{
					"listen": "test",
					"script": {
						"exec": [
							"pm.test(\"Status code is 400\", function () {",
							"    pm.response.to.have.status(400);",
							"});",
							""
						],
						"type": "text/javascript"
					}
				}
			],
			"request": {
				"method": "POST",
				"header": [
					{
						"key": "Content-Type",
						"type": "text",
						"value": "application/json"
					}
				],
				"body": {
					"mode": "raw",
					"raw": "{\n\n}"
				},
				"url": {
					"raw": "{{host}}/products",
					"host": [
						"{{host}}"
					],
					"path": [
						"products"
					]
				},
				"description": "Create a product"
			},
			"response": [
				{
					"name": "POST /products/ (400)",
					"originalRequest": {
						"method": "POST",
						"header": [
							{
								"key": "Content-Type",
								"type": "text",
								"value": "application/json"
							}
						],
						"body": {
							"mode": "raw",
							"raw": "{\n\n}"
						},
						"url": {
							"raw": "{{host}}/products",
							"host": [
								"{{host}}"
							],
							"path": [
								"products"
							]
						}
					},
					"status": "Bad Request",
					"code": 400,
					"_postman_previewlanguage": "json",
					"header": [
						{
							"key": "X-Powered-By",
							"value": "Express"
						},
						{
							"key": "Access-Control-Allow-Origin",
							"value": "*"
						},
						{
							"key": "Content-Type",
							"value": "application/json; charset=utf-8"
						},
						{
							"key": "Content-Length",
							"value": "29"
						},
						{
							"key": "ETag",
							"value": "W/\"1d-pnhbRSD4NZML3cnaJuyottC+RiE\""
						},
						{
							"key": "Date",
							"value": "Sun, 09 May 2021 12:53:30 GMT"
						},
						{
							"key": "Connection",
							"value": "keep-alive"
						}
					],
					"cookie": [],
					"body": "{\n    \"message\": \"invalid product\"\n}"
				}
			]
		}
	]
}

It will generate the following OAS (note the empty 200 types):

---
openapi: 3.0.3
info:
  title: Example Products API
  version: 1.0.0
  contact: {}
servers:
  - url: "{{host}}"
paths:
  /products:
    post:
      summary: POST /products/ (400)
      description: Create a product
      operationId: post/products/(400)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties: {}
            example: {}
      responses:
        "200":
          description: ""
        "400":
          description: POST /products/ (400)
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: string
                example: "*"
            Connection:
              schema:
                type: string
                example: keep-alive
            Content-Length:
              schema:
                type: string
                example: "29"
            Date:
              schema:
                type: string
                example: "Sun, 09 May 2021 12:53:30 GMT"
            ETag:
              schema:
                type: string
                example: "W/\"1d-pnhbRSD4NZML3cnaJuyottC+RiE\""
            X-Powered-By:
              schema:
                type: string
                example: Express
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: invalid product
              examples:
                POST /products/ (400):
                  value:
                    message: invalid product
tags: []

If I re-order the collection, the 400 use case is replaced, so I assume it just replaces some object if it has a matching URL.

I have enough Rust knowledge to be dangerous, and would consider making a PR if you could point me in the right direction.

I can see the potential issues here:

For example, the requestBody would either need to preferentially use any 2xx responses ahead of others if provided, or implement one of the conditional semantics (e.g.oneOf).

There would also need to be some form of ranking mechanism in case of clashes, although I assume this likely exists as examples would have a similar problem, so hopefully it's just a matter of moving some of this logic up a level (he says as if that is so simple).

Fixed in #55.

Awesome, thanks!