Azure / openapi-diff

Command line tool to detect breaking changes between two openapi specifications
MIT License
253 stars 34 forks source link

Ensure properties are dereferenced before comparing equality #329

Closed mikeharder closed 2 months ago

mikeharder commented 2 months ago

@johanste, @markcowl, @JeffreyRichter, @allenjzhang, @mikekistler: Please object if anything about this change looks wrong to you.

In summary, the change always calls dereference() on a property with a $ref, before comparing equality.

This looks pretty safe to me and @konrad-jamrozik, and passes the new unit test we wrote (plus all existing tests).

konrad-jamrozik commented 2 months ago

I see the test fails with incompatible properties : bar.

I took the spec:

{
  "swagger": "2.0",
  "info": {
    "title": "compatible-properties-ref",
    "version": "1.0"
  },
  "paths": {
  },
  "definitions": {
    "FooBarString": {
      "type":"object",
      "properties": {
        "bar": {
          "type":"string"
        }
      },
      "allOf": [
        {
          "$ref": "#/definitions/FooBarStringRef"
        }
      ]
    },
    "FooBarStringRef": {
      "type":"object",
      "properties": {
        "bar": {
          "$ref":"#/definitions/MyString"
        }
      }
    },
    "MyString": {
      "type": "string"
    }
  }
}

and pasted it here:

I got no errors in both cases.

image

Looking at that screenshot, both definitions have the same bar. There is no incompatibility anywhere. So the spec appears to be OK. The tool interpretation must be wrong.

Interestingly, when I make MyString have different type and extra key example:

image

Then FooBarString picks up the example but ignores the different type:

image

konrad-jamrozik commented 2 months ago

OK I think I know what is the root-cause. The problem lies in openapi-diff logic.

We throw error from here

Which will happen if this line returns true:

if (!this.isEqual(allOfSchema.properties[key], schemaList[key])) {

On the failing test it evaluates to:

this.isEqual({"$ref":"#/definitions/MyString"}, {"type":"string"})

Now inside the this.isEqual logic the problem lies here:

if ((!parentProperty.type || parentProperty.type === "object") && (!unwrappedProperty.type || unwrappedProperty.type === "object")) {

On the failing test it evaluates to:

(!undefined || undefined === "object") && (!string || string === "object")

which is (true && false) which is false.

As a result, the isEqual will do no more dereferencing, so the parentProperty, which is {"$ref":"#/definitions/MyString"}, won't be dereferenced. Hence isEqual will return with the last return:

return parentProperty.type === unwrappedProperty.type && parentProperty.format === unwrappedProperty.format

which will evaluate like that:

parentProperty.type (undefined) === unwrappedProperty.type (string) && parentProperty.format (undefined) === unwrappedProperty.format (undefined) which is undefined === string && undefined === undefined which is false, hence we throw.

Basically, the logic of isEqual is assuming that it has either dereference both parentProperty and unwrappedProperty, or none, but not possibly only one.

konrad-jamrozik commented 2 months ago

@mikeharder I think you may want to change the PR title before merging to mention it fixes the bug.