"""Challenge 7: Validate an API Response
Check an API response (status code + JSON body):
- status equals the expected status
- body is a JSON object containing every required field with the right type
- fields with expected values match (a list/set/tuple means "any of these")
Returns (is_valid, list_of_errors).
"""
import json
def validate_api_response( response, required_fields, expected_values= None ,
expected_status= 200 ) :
errors = [ ]
if response.get ( "status" ) != expected_status:
errors.append ( f"unexpected status: {response.get('status')}" )
# Body may arrive as a JSON string or an already-parsed dict
body = response.get ( "body" )
if isinstance ( body, str ) :
try :
body = json.loads ( body)
except json.JSONDecodeError :
errors.append ( "body is not valid JSON" )
return False , errors
if not isinstance ( body, dict ) :
errors.append ( "body missing or not a JSON object" )
return False , errors
# Required fields and their types
for field, ftype in required_fields.items ( ) :
if field not in body:
errors.append ( f"missing field: {field}" )
elif not isinstance ( body[ field] , ftype) :
errors.append ( f"wrong type for {field}: expected {ftype.__name__}" )
# Expected values
for field, expected in ( expected_values or { } ) .items ( ) :
if field not in body:
continue # already reported if it was required
allowed = expected if isinstance ( expected, ( list , set , tuple ) ) else [ expected]
if body[ field] not in allowed:
errors.append ( f"invalid value for {field}: {body[field]!r}" )
return ( not errors) , errors
if __name__ == "__main__" :
required = { "id" : int , "name" : str , "state" : str }
expected = { "state" : [ "active" , "pending" ] }
good = { "status" : 200 , "body" : { "id" : 1 , "name" : "svc" , "state" : "active" } }
bad_value = { "status" : 200 , "body" : { "id" : 1 , "name" : "svc" , "state" : "dead" } }
missing = { "status" : 200 , "body" : '{"id": 1}' } # JSON string body
bad_status = { "status" : 500 , "body" : { "id" : 1 , "name" : "x" , "state" : "active" } }
no_body = { "status" : 200 }
for label, resp in [ ( "good" , good) , ( "bad_value" , bad_value) , ( "missing" , missing) ,
( "bad_status" , bad_status) , ( "no_body" , no_body) ] :
print ( label, validate_api_response( resp, required, expected) )
assert validate_api_response( good, required, expected) [ 0 ] is True
assert validate_api_response( bad_value, required, expected) [ 0 ] is False
assert len ( validate_api_response( missing, required, expected) [ 1 ] ) == 2
assert validate_api_response( bad_status, required, expected) [ 0 ] is False
assert validate_api_response( no_body, required, expected) [ 0 ] is False
print ( "Challenge 7 OK" )
IiIiQ2hhbGxlbmdlIDc6IFZhbGlkYXRlIGFuIEFQSSBSZXNwb25zZQpDaGVjayBhbiBBUEkgcmVzcG9uc2UgKHN0YXR1cyBjb2RlICsgSlNPTiBib2R5KToKICAtIHN0YXR1cyBlcXVhbHMgdGhlIGV4cGVjdGVkIHN0YXR1cwogIC0gYm9keSBpcyBhIEpTT04gb2JqZWN0IGNvbnRhaW5pbmcgZXZlcnkgcmVxdWlyZWQgZmllbGQgd2l0aCB0aGUgcmlnaHQgdHlwZQogIC0gZmllbGRzIHdpdGggZXhwZWN0ZWQgdmFsdWVzIG1hdGNoIChhIGxpc3Qvc2V0L3R1cGxlIG1lYW5zICJhbnkgb2YgdGhlc2UiKQpSZXR1cm5zIChpc192YWxpZCwgbGlzdF9vZl9lcnJvcnMpLgoiIiIKCmltcG9ydCBqc29uCgoKZGVmIHZhbGlkYXRlX2FwaV9yZXNwb25zZShyZXNwb25zZSwgcmVxdWlyZWRfZmllbGRzLCBleHBlY3RlZF92YWx1ZXM9Tm9uZSwKICAgICAgICAgICAgICAgICAgICAgICAgICBleHBlY3RlZF9zdGF0dXM9MjAwKToKICAgIGVycm9ycyA9IFtdCgogICAgaWYgcmVzcG9uc2UuZ2V0KCJzdGF0dXMiKSAhPSBleHBlY3RlZF9zdGF0dXM6CiAgICAgICAgZXJyb3JzLmFwcGVuZChmInVuZXhwZWN0ZWQgc3RhdHVzOiB7cmVzcG9uc2UuZ2V0KCdzdGF0dXMnKX0iKQoKICAgICMgQm9keSBtYXkgYXJyaXZlIGFzIGEgSlNPTiBzdHJpbmcgb3IgYW4gYWxyZWFkeS1wYXJzZWQgZGljdAogICAgYm9keSA9IHJlc3BvbnNlLmdldCgiYm9keSIpCiAgICBpZiBpc2luc3RhbmNlKGJvZHksIHN0cik6CiAgICAgICAgdHJ5OgogICAgICAgICAgICBib2R5ID0ganNvbi5sb2Fkcyhib2R5KQogICAgICAgIGV4Y2VwdCBqc29uLkpTT05EZWNvZGVFcnJvcjoKICAgICAgICAgICAgZXJyb3JzLmFwcGVuZCgiYm9keSBpcyBub3QgdmFsaWQgSlNPTiIpCiAgICAgICAgICAgIHJldHVybiBGYWxzZSwgZXJyb3JzCiAgICBpZiBub3QgaXNpbnN0YW5jZShib2R5LCBkaWN0KToKICAgICAgICBlcnJvcnMuYXBwZW5kKCJib2R5IG1pc3Npbmcgb3Igbm90IGEgSlNPTiBvYmplY3QiKQogICAgICAgIHJldHVybiBGYWxzZSwgZXJyb3JzCgogICAgIyBSZXF1aXJlZCBmaWVsZHMgYW5kIHRoZWlyIHR5cGVzCiAgICBmb3IgZmllbGQsIGZ0eXBlIGluIHJlcXVpcmVkX2ZpZWxkcy5pdGVtcygpOgogICAgICAgIGlmIGZpZWxkIG5vdCBpbiBib2R5OgogICAgICAgICAgICBlcnJvcnMuYXBwZW5kKGYibWlzc2luZyBmaWVsZDoge2ZpZWxkfSIpCiAgICAgICAgZWxpZiBub3QgaXNpbnN0YW5jZShib2R5W2ZpZWxkXSwgZnR5cGUpOgogICAgICAgICAgICBlcnJvcnMuYXBwZW5kKGYid3JvbmcgdHlwZSBmb3Ige2ZpZWxkfTogZXhwZWN0ZWQge2Z0eXBlLl9fbmFtZV9ffSIpCgogICAgIyBFeHBlY3RlZCB2YWx1ZXMKICAgIGZvciBmaWVsZCwgZXhwZWN0ZWQgaW4gKGV4cGVjdGVkX3ZhbHVlcyBvciB7fSkuaXRlbXMoKToKICAgICAgICBpZiBmaWVsZCBub3QgaW4gYm9keToKICAgICAgICAgICAgY29udGludWUgICMgYWxyZWFkeSByZXBvcnRlZCBpZiBpdCB3YXMgcmVxdWlyZWQKICAgICAgICBhbGxvd2VkID0gZXhwZWN0ZWQgaWYgaXNpbnN0YW5jZShleHBlY3RlZCwgKGxpc3QsIHNldCwgdHVwbGUpKSBlbHNlIFtleHBlY3RlZF0KICAgICAgICBpZiBib2R5W2ZpZWxkXSBub3QgaW4gYWxsb3dlZDoKICAgICAgICAgICAgZXJyb3JzLmFwcGVuZChmImludmFsaWQgdmFsdWUgZm9yIHtmaWVsZH06IHtib2R5W2ZpZWxkXSFyfSIpCgogICAgcmV0dXJuIChub3QgZXJyb3JzKSwgZXJyb3JzCiAgICAKICAgIAogICAgCgppZiBfX25hbWVfXyA9PSAiX19tYWluX18iOgogICAgcmVxdWlyZWQgPSB7ImlkIjogaW50LCAibmFtZSI6IHN0ciwgInN0YXRlIjogc3RyfQogICAgZXhwZWN0ZWQgPSB7InN0YXRlIjogWyJhY3RpdmUiLCAicGVuZGluZyJdfQoKICAgIGdvb2QgPSB7InN0YXR1cyI6IDIwMCwgImJvZHkiOiB7ImlkIjogMSwgIm5hbWUiOiAic3ZjIiwgInN0YXRlIjogImFjdGl2ZSJ9fQogICAgYmFkX3ZhbHVlID0geyJzdGF0dXMiOiAyMDAsICJib2R5IjogeyJpZCI6IDEsICJuYW1lIjogInN2YyIsICJzdGF0ZSI6ICJkZWFkIn19CiAgICBtaXNzaW5nID0geyJzdGF0dXMiOiAyMDAsICJib2R5IjogJ3siaWQiOiAxfSd9ICAgICAgICAgICMgSlNPTiBzdHJpbmcgYm9keQogICAgYmFkX3N0YXR1cyA9IHsic3RhdHVzIjogNTAwLCAiYm9keSI6IHsiaWQiOiAxLCAibmFtZSI6ICJ4IiwgInN0YXRlIjogImFjdGl2ZSJ9fQogICAgbm9fYm9keSA9IHsic3RhdHVzIjogMjAwfQoKICAgIGZvciBsYWJlbCwgcmVzcCBpbiBbKCJnb29kIiwgZ29vZCksICgiYmFkX3ZhbHVlIiwgYmFkX3ZhbHVlKSwgKCJtaXNzaW5nIiwgbWlzc2luZyksCiAgICAgICAgICAgICAgICAgICAgICAgICgiYmFkX3N0YXR1cyIsIGJhZF9zdGF0dXMpLCAoIm5vX2JvZHkiLCBub19ib2R5KV06CiAgICAgICAgcHJpbnQobGFiZWwsIHZhbGlkYXRlX2FwaV9yZXNwb25zZShyZXNwLCByZXF1aXJlZCwgZXhwZWN0ZWQpKQoKICAgIGFzc2VydCB2YWxpZGF0ZV9hcGlfcmVzcG9uc2UoZ29vZCwgcmVxdWlyZWQsIGV4cGVjdGVkKVswXSBpcyBUcnVlCiAgICBhc3NlcnQgdmFsaWRhdGVfYXBpX3Jlc3BvbnNlKGJhZF92YWx1ZSwgcmVxdWlyZWQsIGV4cGVjdGVkKVswXSBpcyBGYWxzZQogICAgYXNzZXJ0IGxlbih2YWxpZGF0ZV9hcGlfcmVzcG9uc2UobWlzc2luZywgcmVxdWlyZWQsIGV4cGVjdGVkKVsxXSkgPT0gMgogICAgYXNzZXJ0IHZhbGlkYXRlX2FwaV9yZXNwb25zZShiYWRfc3RhdHVzLCByZXF1aXJlZCwgZXhwZWN0ZWQpWzBdIGlzIEZhbHNlCiAgICBhc3NlcnQgdmFsaWRhdGVfYXBpX3Jlc3BvbnNlKG5vX2JvZHksIHJlcXVpcmVkLCBleHBlY3RlZClbMF0gaXMgRmFsc2UKICAgIHByaW50KCJDaGFsbGVuZ2UgNyBPSyIpCg==