Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/core/plugins/oas3/components/callbacks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const Callbacks = ({ callbacks, specPath, specSelectors, getComponent }) => {
<OperationContainer
key={`${callbackName}-${operationDTO.path}-${operationDTO.method}`}
op={operationDTO.operation}
tag="callbacks"
tag={`callbacks-${callbackName}`}
method={operationDTO.method}
path={operationDTO.path}
specPath={operationDTO.specPath}
Expand Down
35 changes: 35 additions & 0 deletions test/e2e-cypress/e2e/bugs/5536.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// https://github.com/swagger-api/swagger-ui/issues/5536

describe("#5536: multiple callbacks defined on the same URL", () => {
beforeEach(() => {
cy.visit("/?url=/documents/bugs/5536.yaml")
.get("#operations-default-subscribe")
.click()
.get("#operations-default-subscribe .tab-item")
.contains("Callbacks")
.click()
})

it("should render a separate operation block per callback name", () => {
cy.get("#operations-callbacks-myEvent-post__request_body__callbackUrl_")
.should("exist")
cy.get("#operations-callbacks-otherEvent-post__request_body__callbackUrl_")
.should("exist")
})

it("should expand only the clicked callback's operation block", () => {
cy.get("#operations-callbacks-myEvent-post__request_body__callbackUrl_")
.as("myEventOp")
.should("not.have.class", "is-open")
cy.get("#operations-callbacks-otherEvent-post__request_body__callbackUrl_")
.as("otherEventOp")
.should("not.have.class", "is-open")

cy.get("@myEventOp")
.find(".opblock-summary-control")
.click()

cy.get("@myEventOp").should("have.class", "is-open")
cy.get("@otherEventOp").should("not.have.class", "is-open")
})
})
64 changes: 64 additions & 0 deletions test/e2e-cypress/static/documents/bugs/5536.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
openapi: 3.0.0
info:
version: 1.0.0
title: test
description: example with 2 callbacks using the same URL template
paths:
/subscribe:
post:
summary: Subscribe to a webhook
operationId: subscribe
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
callbackUrl:
type: string
format: uri
example: https://myserver.com/send/callback/here
required:
- callbackUrl
callbacks:
myEvent:
'{$request.body#/callbackUrl}':
post:
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: Some event happened
required:
- message
responses:
'200':
description: Your server returns this code if it accepts the callback
otherEvent:
'{$request.body#/callbackUrl}':
post:
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: integer
format: int32
example: 42
required:
- id
responses:
'200':
description: Your server returns this code if it accepts the callback
responses:
'201':
description: Webhook created
89 changes: 89 additions & 0 deletions test/unit/core/plugins/oas3/callbacks.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* @prettier
*/

import React from "react"
import { shallow } from "enzyme"
import { fromJS, List } from "immutable"
import Callbacks from "core/plugins/oas3/components/callbacks"

describe("<Callbacks/>", () => {
const OperationContainerStub = () => null
OperationContainerStub.displayName = "OperationContainerStub"

const buildOperationDTOs = () => ({
myEvent: [
{
operation: fromJS({ operation: { summary: "myEvent op" } }),
method: "post",
path: "{$request.body#/callbackUrl}",
callbackName: "myEvent",
specPath: List([
"paths",
"/subscribe",
"post",
"callbacks",
"myEvent",
"{$request.body#/callbackUrl}",
"post",
]),
},
],
otherEvent: [
{
operation: fromJS({ operation: { summary: "otherEvent op" } }),
method: "post",
path: "{$request.body#/callbackUrl}",
callbackName: "otherEvent",
specPath: List([
"paths",
"/subscribe",
"post",
"callbacks",
"otherEvent",
"{$request.body#/callbackUrl}",
"post",
]),
},
],
})

const buildProps = (operationDTOs) => ({
callbacks: fromJS({}),
specPath: List(["paths", "/subscribe", "post", "callbacks"]),
specSelectors: {
callbacksOperations: () => operationDTOs,
},
getComponent: (name) => {
if (name === "OperationContainer") return OperationContainerStub
return null
},
})

it("renders one OperationContainer per callback path-item operation", () => {
const wrapper = shallow(<Callbacks {...buildProps(buildOperationDTOs())} />)

const operationContainers = wrapper.find(OperationContainerStub)
expect(operationContainers.length).toEqual(2)
})

it("scopes the tag prop with the callback name so colliding URLs do not share expansion state", () => {
// Regression test for https://github.com/swagger-api/swagger-ui/issues/5536
// When two callbacks declare the same URL template, each OperationContainer
// must receive a unique `tag` so that the layout isShownKey
// (["operations", tag, operationId]) differs between callbacks.
const wrapper = shallow(<Callbacks {...buildProps(buildOperationDTOs())} />)
const operationContainers = wrapper.find(OperationContainerStub)

const tags = operationContainers.map((node) => node.prop("tag"))
expect(tags).toEqual(["callbacks-myEvent", "callbacks-otherEvent"])

// tags must be unique across colliding URL templates
expect(new Set(tags).size).toEqual(tags.length)
})

it("renders 'No callbacks' when the selector returns no operations", () => {
const wrapper = shallow(<Callbacks {...buildProps({})} />)
expect(wrapper.text()).toEqual("No callbacks")
})
})