--- title: Effiapi test suite author: Lars Wirzenius ... [yarn]: https://liw.fi/cmdtest/ [Effireg]: https://effireg.liw.fi/ # Introduction This chapter descibes the effiapi API, using [yarn][] automated scenario tests. It is meant to be understandable to Effi sysadmins, and those writing applications using the API, as well as be an executable test suite for the API. Effiapi is part of [Effireg][], a privacy-aware register of members for associations. The API is a simple RESTful HTTP API using JSON. This means that: * each API call is an HTTP request, with a response * all data is represented as JSON in the body of the request of response * metadata about the data is represeneted as HTTP headers * standard HTTP verbs (POST, PUT, GET, DELETE) are used to indicate the action * standard HTTP status codes are used to indicate result of the request (200 = OK, 404 = not found, etc) Examples will be provided. # Test scenarios ## Manage memberships - happy path This section shows the API calls to manage a memberhip. The test scaffolding starts an effiapi instance, and defines an API client for the administrator, which has all the access. SCENARIO Manage a membership GIVEN An effiapi instance First make sure the register is empty. WHEN admin requests GET /status THEN HTTP status is 200 AND body matches { "resources": 0 } WHEN admin requests GET /search with ... {"cond": [{"where": "meta", "op": ">=", "field": "id", "pattern": ""}]} THEN HTTP status is 200 AND body matches { "resources": [] } Create a member. Check in various ways that it exists. WHEN admin requests POST /memb with body { "fullname": "James Bond" } THEN HTTP status is 201 AND remember header Muck-Id as ID AND remember header Muck-Revision as REV1 WHEN admin requests GET /status THEN HTTP status is 200 AND body matches { "resources": 1 } WHEN admin requests GET /search with ... {"cond": [{"where": "meta", "op": ">=", "field": "id", "pattern": ""}]} THEN HTTP status is 200 AND body matches { "resources": [ "${ID}" ] } WHEN admin requests GET /memb with header Muck-Id: ${ID} THEN HTTP status is 200 AND body matches { "fullname": "James Bond" } AND header Muck-Revision is ${REV1} Update the member. WHEN admin requests PUT /memb with id ${ID}, revision ${REV1}, ... and body { "fullname": "Alfred Pennyworth" } THEN HTTP status is 200 AND remember header Muck-Revision as REV2 WHEN admin requests GET /memb with header Muck-Id: ${ID} THEN HTTP status is 200 AND body matches { "fullname": "Alfred Pennyworth" } AND header Muck-Revision is ${REV2} Delete the member. Check in various ways that it no longer exists. WHEN admin requests DELETE /memb with id ${ID} THEN HTTP status is 200 WHEN admin requests GET /status THEN HTTP status is 200 AND body matches { "resources": 0 } WHEN admin requests GET /search with ... {"cond": [{"where": "meta", "op": ">=", "field": "id", "pattern": ""}]} THEN HTTP status is 200 AND body matches { "resources": [] } WHEN admin requests GET /memb with header Muck-Id: ${ID} THEN HTTP status is 404 Done. FINALLY Effiapi is terminated # Appendix: Yarn scenario step implementations Most of the interesting code is in the `lib.py` module. ## Start and stop effiapi IMPLEMENTS GIVEN An effiapi instance effiapi.write_config() effiapi.start() IMPLEMENTS FINALLY Effiapi is terminated effiapi.terminate() ## Make HTTP requests IMPLEMENTS WHEN admin requests POST /memb with body (.+) body = get_json_match() effiapi.POST('/memb', {}, body) IMPLEMENTS WHEN admin requests GET /status effiapi.GET('/status', {}, None) IMPLEMENTS WHEN admin requests PUT /memb with id (\S+), revision (\S+), and body (.+) rid = get_expanded_match() rev = get_expanded_match() body = get_json_match() headers = { 'Muck-Id': rid, 'Muck-Revision': rev, } effiapi.PUT('/memb', headers, body) IMPLEMENTS WHEN admin requests GET /memb with header (\S+): (\S+) header = get_next_match() value = get_expanded_match() headers = { header: value, } effiapi.GET('/memb', headers, None) IMPLEMENTS WHEN admin requests GET /search with (.+) body = get_expanded_json_match() headers = { 'Content-Type': 'application/json', } effiapi.GET('/search', headers, json.dumps(body)) IMPLEMENTS WHEN admin requests DELETE /memb with id (\S+) rid = get_expanded_match() headers = { 'Muck-id': rid, } effiapi.DELETE('/memb', headers, None) ## Inspect HTTP responses IMPLEMENTS THEN HTTP status is (\d+) expected = int(get_next_match()) actual = effiapi.get_status_code() assertEqual(effiapi.get_status_code(), expected) IMPLEMENTS THEN remember header (\S+) as (.+) header = get_next_match() varname = get_next_match() value = effiapi.get_header(header) save_for_expansion(varname, value) IMPLEMENTS THEN header (\S+) is (.+) header = get_next_match() expected = get_expanded_match() actual = effiapi.get_header(header) assertEqual(actual, expected) IMPLEMENTS THEN body matches (.+) expected = get_expanded_json_match() actual = effiapi.get_json_body() assertEqual(actual, expected)