openapi-backend icon indicating copy to clipboard operation
openapi-backend copied to clipboard

Error: Unknown operation at OpenAPIValidator.validateRequest at OpenAPIBackend.<anonymous> at async OpenAPIBackend.handleRequest

Open bgolubovicsymphony opened this issue 3 years ago • 1 comments

I am working with MSW and OpenAPI-backend package. I want to mock the booth browser server and test server. I have OpenAPI definition available form with I generate generated.ts for RTK Query (out of scope for this question). I want to use OpenAPI spec to use it with OpenAPI Backend and generate MSW rest worker for browser and for test.

Setup is next:

index.tsx

import worker from './mocks/browser';

if (process.env.NODE_ENV === 'development') {
	worker.start();
}

mock/browser.ts

import { setupWorker, rest } from 'msw';

import { OpenAPIBackend } from 'openapi-backend';
import type { Document } from 'openapi-backend';
import definition from './api.json';


// create our mock backend with openapi-backend
const api = new OpenAPIBackend({ definition: definition as Document });
api.register('notFound', (c, res, ctx) => res(ctx.status(404)));
api.registerHandler('notImplemented', async (c, req, res) => {
	const { status, mock } = await api.mockResponseForOperation(
		c.operation.operationId as string
	);
	return res.status(status).json(mock);
});
api.register('validationFail', (c, res, ctx) =>
	res(ctx.status(400), ctx.json({ error: c.validation.errors }))
);

const worker = setupWorker(
	rest.get('/*', (req) =>
		api.handleRequest({
			...req,
			path: req.url.pathname,
			headers: req.headers.all(),
			method: req.method,
			body: req.body,
		})
	)
);


export default worker;

api.JSON

{
  "openapi": "3.0.1",
  "info": {
    "title": "Fetch API",
    "description": "Source of truth for Fetch dashboard",
    "version": "0.1.5"
  },
  "paths": {
    "/config": {
      "get": {
        "tags": [
          "Configuration"
        ],
        "summary": "Retreive configuration object",
        "description": "Returns configuration object (map) containing configuration parameters for UI (Map<String, String>)",
        "responses": {
          "200": {
            "description": "successfull operation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "string"
                  },
                  "description": "Map serialized to json object.",
                  "example": {
                    "FA_COLOR": "red",
                    "FA_NAME": "fetch"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/links": {
      "get": {
        "tags": [
          "Notifications & Links",
          "Walking Skeleton"
        ],
        "summary": "List all defined links for hospital",
        "description": "Retreives all defined links for hospital. Hospital ID is indirectly obtained from user identity.",
        "responses": {
          "200": {
            "description": "successfull operation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Link"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/kpis": {
      "get": {
        "tags": [
          "KPIS"
        ],
        "summary": "List all KPIs for hospital(s) that current user is managing.",
        "description": "Retreives all KPIs available for hospitals that current user is managing.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/KPI"
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "KPI": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "enum": [
              "revenue",
              "labour"
            ]
          },
          "hospital_id": {
            "type": "string",
            "description": "id of hospital that this KPI describes"
          },
          "goal": {
            "type": "number",
            "description": "full month goal"
          },
          "actual": {
            "type": "number",
            "description": "actual result"
          },
          "mtd_goal": {
            "type": "number",
            "description": "month to date goal, so that we can track projected fulfillment of goal."
          },
          "details": {
            "type": "object",
            "description": "Semi-structured way of describing details of calculation. Every KPI will potentialiy be described differently."
          }
        },
        "required": [
          "id",
          "hospital_id",
          "goal",
          "actual",
          "mtd_goal"
        ]
      },
      "Link": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "unique id of link"
          },
          "hospital_id": {
            "type": "string",
            "description": "id of hospital that this Link is configured for"
          },
          "title": {
            "type": "string",
            "description": "human readable title for URL"
          },
          "description": {
            "type": "string",
            "description": "Description of current link (alt text , or tooltip)"
          },
          "url": {
            "type": "string",
            "description": "Location of external resource"
          },
          "urgent": {
            "type": "boolean",
            "description": "Is urgency of notification elevated?"
          },
          "count": {
            "type": "number",
            "description": "Actual notification value. How many 'tasks' are waiting manager in external system."
          },
          "updated_at": {
            "type": "integer",
            "description": "Date/time of last notification update",
            "format": "int64"
          },
          "children": {
            "type": "array",
            "description": "Since notifications are possibly presented in hierarchy all children of this notification will be gathered here",
            "items": {
              "$ref": "#/components/schemas/Link"
            }
          }
        },
        "required": [
          "id",
          "hospital_id",
          "title",
          "url"
        ],
        "example": [
          {
            "id": 1,
            "hospital_id": "001",
            "title": "Link1",
            "description": "description of Link1",
            "url": "https://www.example.com/link1",
            "urgent": true,
            "count": 1,
            "updated_at": 1631113184221,
            "children": [
              {
                "id": 2,
                "hospital_id": "001",
                "title": "Link2",
                "description": "description of Link2",
                "url": "https://www.example.com/link2",
                "urgent": true,
                "count": 1,
                "updated_at": 1631113184221
              }
            ]
          },
          {
            "id": 3,
            "hospital_id": "002",
            "title": "Link3",
            "description": "description of Link3",
            "url": "https://www.example.com/link3",
            "urgent": false,
            "count": 2,
            "updated_at": 1631113184221
          }
        ]
      }
    }
  }
}

component.tsx

const { data: links, error, isLoading } = useGetLinksQuery({});

Which is fetching localhost:3000/links

The error I am getting is:

mockServiceWorker.js:222 [MSW] Uncaught exception in the request handler for "GET http://localhost:3000/links":

Error: Unknown operation
    at OpenAPIValidator.validateRequest (http://localhost:3000/static/js/vendors~main.chunk.js:63911:13)
    at OpenAPIBackend.<anonymous> (http://localhost:3000/static/js/vendors~main.chunk.js:54246:45)
    at async OpenAPIBackend.handleRequest (http://localhost:3000/static/js/vendors~main.chunk.js:54152:22)

This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses
getResponse @ mockServiceWorker.js:222
async function (async)
getResponse @ mockServiceWorker.js:175
handleRequest @ mockServiceWorker.js:113
async function (async)
handleRequest @ mockServiceWorker.js:112
(anonymous) @ mockServiceWorker.js:271

Network tabs give:

Request URL: http://localhost:3000/links
Request Method: GET
Status Code: 500  (from service worker)
Referrer Policy: strict-origin-when-cross-origin

All related to article: https://dev.to/epilot/testing-react-with-jest-and-openapi-mocks-8gc and https://testing-library.com/docs/react-testing-library/example-intro/

Thank you.

bgolubovicsymphony avatar Sep 13 '21 14:09 bgolubovicsymphony

I am having a similar issue when trying to use setupServer in a client-side Jest test. I'm not sure if I am doing this correctly.* (Is this even possible?)

What I've gathered so far is that, notImplemented handler is properly registered; but on this line ( const routeHandler: Handler = this.handlers[operationId]; ) it tries to get the operationId from the registry and it is not there.

In my head, I was expecting notImplemented to register each operationId available on the fly or something; but it is not. What am I missing?


Update 1: to clarify, I can see the handler notImplemented firing as expected and it grabs the right example from the right operationId (and the response looks right).

export const api = new OpenAPIBackend({
  definition,
  quick: true,
  customizeAjv: (ajv) => {
    const dtFormat = {
      type: 'string',
      validate:
        /^\d\d\d\d-[0-1]\d-[0-3]\dt(?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d\d(?::?\d\d)?)$/i,
    };
    // gets rid of warning `unknown format "date-time" ignored in schema at path`
    // Ref: https://github.com/anttiviljami/openapi-backend/issues/280
    ajv.addFormat('date-time', dtFormat as any);
    return ajv;
  },
  handlers: {
    notImplemented: async (c:any, req: any, res: any) => {
      const { status, mock } = c.api.mockResponseForOperation(c.operation.operationId);
      
      return res.status(status).json(mock); // logging this shows that it is getting the "example" data from the OpenAPI schema.
    },
  }
} as Options);

However, the rest.get() passed to setupServer as a wildcard as explained here is the one ultimately responsible for passing back the response's data to the useHook in RTKQuery.

  rest.get(`${url}*`, async (req, res, ctx) => {
    const path = req.url.pathname.replace('/api/v3', '');
    // convert MSW request to OpenAPIBackend request
    const newReq = {
      method: req.method,
      path,
      headers: { ... },
      body: req.body,
    };

    return api.handleRequest(newReq as unknown as Request, res, ctx);
  })
);

This seems to be where the problem lies for me. When it looks for the operationId in api.handleRequest, it has no registered operationId for mocked data. The only thing I can think of is that we are not supposed to pass any arguments to setupServer; but when I do this, I get:

[MSW] Warning: captured a request without a matching request handler:

        • GET http://localhost:8001/api/v3/oppEQ/snapshot

      If you still wish to intercept this unhandled request, please create a request handler for it.
      Read more: https://mswjs.io/docs/getting-started/mocks

I must be missing something! :)

sam3k avatar Feb 04 '22 19:02 sam3k