From f4e6dfc061acd95c6d2aa21d9e2e5f69e224d2da Mon Sep 17 00:00:00 2001 From: DL6ER Date: Mon, 30 Oct 2023 07:37:28 +0100 Subject: [PATCH] Implement deep-recursion of API arrays Signed-off-by: DL6ER --- src/api/docs/content/specs/clients.yaml | 6 +- src/api/docs/content/specs/endpoints.yaml | 113 ++++++++++++++++++++-- src/api/docs/content/specs/info.yaml | 2 +- src/api/docs/content/specs/network.yaml | 4 + test/api/libs/responseVerifyer.py | 52 +++++++++- 5 files changed, 160 insertions(+), 17 deletions(-) diff --git a/src/api/docs/content/specs/clients.yaml b/src/api/docs/content/specs/clients.yaml index 69d72433..51fdd83f 100644 --- a/src/api/docs/content/specs/clients.yaml +++ b/src/api/docs/content/specs/clients.yaml @@ -246,8 +246,9 @@ components: example: "127.0.0.1,::1" names: type: string - description: Comma-separated list of hostnames - example: "localhost" + nullable: true + description: Comma-separated list of hostnames (if available) + example: "localhost,ip6-localhost" client: type: object properties: @@ -284,6 +285,7 @@ components: description: hostname (only available when {client} is an IP address) type: string readOnly: true + nullable: true example: localhost groups: type: object diff --git a/src/api/docs/content/specs/endpoints.yaml b/src/api/docs/content/specs/endpoints.yaml index 3321108c..0bdf5186 100644 --- a/src/api/docs/content/specs/endpoints.yaml +++ b/src/api/docs/content/specs/endpoints.yaml @@ -37,25 +37,118 @@ components: get: type: array items: - type: string - example: ["/api/dns/blocking", "/api/dns/cache", "/api/dns/port", "/api/domains", "/api/groups", "/api/lists"] + type: object + properties: + uri: + type: string + description: URI + parameters: + type: string + description: Parameters + example: + - uri: "/api/dns/blocking" + parameters: '' + - uri: "/api/dns/cache" + parameters: '' + - uri: "/api/dns/port" + parameters: '' + - uri: "/api/domains" + parameters: '' + - uri: "/api/groups" + parameters: '' + - uri: "/api/lists" + parameters: '' post: type: array items: - type: string - example: ["/api/dns/blocking", "/api/dns/cache", "/api/dns/port", "/api/domains", "/api/groups", "/api/lists"] + type: object + properties: + uri: + type: string + description: URI + parameters: + type: string + description: Parameters + example: + - uri: "/api/dns/blocking" + parameters: '' + - uri: "/api/dns/cache" + parameters: '' + - uri: "/api/dns/port" + parameters: '' + - uri: "/api/domains" + parameters: '' + - uri: "/api/groups" + parameters: '' + - uri: "/api/lists" + parameters: '' put: type: array items: - type: string - example: ["/api/dns/blocking", "/api/dns/cache", "/api/dns/port", "/api/domains", "/api/groups", "/api/lists"] + type: object + properties: + uri: + type: string + description: URI + parameters: + type: string + description: Parameters + example: + - uri: "/api/dns/blocking" + parameters: '' + - uri: "/api/dns/cache" + parameters: '' + - uri: "/api/dns/port" + parameters: '' + - uri: "/api/domains" + parameters: '' + - uri: "/api/groups" + parameters: '' + - uri: "/api/lists" + parameters: '' patch: type: array items: - type: string - example: ["/api/dns/blocking", "/api/dns/cache", "/api/dns/port", "/api/domains", "/api/groups", "/api/lists"] + properties: + uri: + type: string + description: URI + parameters: + type: string + description: Parameters + example: + - uri: "/api/dns/blocking" + parameters: '' + - uri: "/api/dns/cache" + parameters: '' + - uri: "/api/dns/port" + parameters: '' + - uri: "/api/domains" + parameters: '' + - uri: "/api/groups" + parameters: '' + - uri: "/api/lists" + parameters: '' delete: type: array items: - type: string - example: ["/api/dns/blocking", "/api/dns/cache", "/api/dns/port", "/api/domains", "/api/groups", "/api/lists"] + properties: + uri: + type: string + description: URI + parameters: + type: string + description: Parameters + example: + - uri: "/api/dns/blocking" + parameters: '' + - uri: "/api/dns/cache" + parameters: '' + - uri: "/api/dns/port" + parameters: '' + - uri: "/api/domains" + parameters: '' + - uri: "/api/groups" + parameters: '' + - uri: "/api/lists" + parameters: '' diff --git a/src/api/docs/content/specs/info.yaml b/src/api/docs/content/specs/info.yaml index 34a50199..a6c2da8a 100644 --- a/src/api/docs/content/specs/info.yaml +++ b/src/api/docs/content/specs/info.yaml @@ -505,7 +505,7 @@ components: type: number nullable: true description: Critical sensor value (if available, `null` otherwise) - path: + sensor: type: string description: Short path of temperature sensor example: diff --git a/src/api/docs/content/specs/network.yaml b/src/api/docs/content/specs/network.yaml index f94db606..2b0614ac 100644 --- a/src/api/docs/content/specs/network.yaml +++ b/src/api/docs/content/specs/network.yaml @@ -126,6 +126,9 @@ components: type: string nullable: true description: Interface name + default: + type: boolean + description: If the interface is the default gateway carrier: type: boolean description: If the interface is connected @@ -248,6 +251,7 @@ components: name: type: string description: Associated hostname (can be null) + nullable: true example: ubuntu-server lastSeen: type: integer diff --git a/test/api/libs/responseVerifyer.py b/test/api/libs/responseVerifyer.py index 7001c5bd..77323beb 100644 --- a/test/api/libs/responseVerifyer.py +++ b/test/api/libs/responseVerifyer.py @@ -139,8 +139,8 @@ class ResponseVerifyer(): # Check for properties in FTL that are not in the API specs for property in FTLflat.keys(): - if property not in YAMLflat.keys(): - self.errors.append("Property '" + property + "' missing in the API specs") + if property not in YAMLflat.keys() and len([p.startswith(property + ".") for p in YAMLflat.keys()]) == 0: + self.errors.append("Property '" + property + "' missing in the API specs (have " + ",".join(YAMLflat.keys()) + ")") elif expected_mimetype == "application/zip": file_like_object = io.BytesIO(FTLresponse) @@ -214,7 +214,7 @@ class ResponseVerifyer(): all_okay = True # Build flat path of this property - flat_path = ".".join(props) + flat_path = ".".join([str(p) for p in props]) # Check if the property is defined in the API specs if props[-1] not in YAMLprops: @@ -235,6 +235,43 @@ class ResponseVerifyer(): # ... and check them recursively if not self.verify_property(YAMLprop['properties'], YAMLexamples, FTLprop, props + [prop]): all_okay = False + elif YAMLprop['type'] == 'array': + # Check if the FTL response is an array + if type(FTLprop) is not list: + self.errors.append("FTL's response is not an array in " + flat_path) + return False + # Check if the FTL response has the same number of items as the + # YAML examples + elif YAMLexamples is not None: + for t in YAMLexamples: + if 'value' not in YAMLexamples[t]: + self.errors.append(f"Example {flat_path} does not have a 'value' property") + return False + example = YAMLexamples[t]['value'] + # Dive into the example to get to the property we want + example_part = example + for p in props: + if p not in example_part: + self.errors.append(f"Example {t} is missing '{flat_path}'") + return False + example_part = example_part[p] + # Loop over all items in the array ... + for i in range(len(FTLprop)): + # ... and check them recursively if they are objects + if not type(FTLprop[i]) is dict: + if 'properties' in YAMLprop['items']: + self.errors.append(flat_path + " is an array, but the API specs define it as an array of objects") + return False + else: + # Simple array and declared as such, no need for further recursion + continue + if 'properties' not in YAMLprop['items'] and type(FTLprop[i]) is dict: + self.errors.append(flat_path + " is an array of objects, but the API specs define it as a simple array") + return False + + for j in FTLprop[i]: + if not self.verify_property(YAMLprop['items']['properties'], YAMLexamples, FTLprop[i], props + [i, str(j)]): + all_okay = False else: # Check this property @@ -266,11 +303,18 @@ class ResponseVerifyer(): return False example = YAMLexamples[t]['value'] # Dive into the example to get to the property we want + skip_this = False for p in props: - if p not in example: + if type(example) == dict and p not in example: self.errors.append(f"Example {t} does not have an '{p}' item") return False + if type(example) == list and p >= len(example): + # We're out of bounds, so we can't check this example + skip_this = True + break example = example[p] + if skip_this: + continue # Check if the type of the example matches the type we defined in the API specs example_type = type(example) self.YAMLresponse[flat_path].append(example)