TT Lab
Get started
Learn Learning paths Courses

Testing Tools in Practice

Test the boundary between CORS and authentication

Continue in TT Lab

Goal

You compare allowed and rejected preflights and prevent regressions that mistake CORS for authentication.

Why it matters

When a request from a disallowed Origin ran on the server, a developer judged it to be a bug in the CORS library. But the browser's read restriction and the server's permission check were different responsibilities. You have to distinguish exactly what the test names and assertions guarantee.

Steps

  1. In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: origin(value) returns the input string if it is an http or https URL that has a host and has no path, query, fragment, or user information. Otherwise it is ValueError. A trailing / is also a path, so it is rejected. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

Prepare this once at the start. Existing files are not overwritten.

mkdir -p /root/work/test-cors-browser-boundary-lab
test -e /root/work/test-cors-browser-boundary-lab/service.py || cp /opt/fixtures/ten_labs/test-cors-browser-boundary-lab/service.py /root/work/test-cors-browser-boundary-lab/service.py
test -e /root/work/test-cors-browser-boundary-lab/test_service.py || cp /opt/fixtures/ten_labs/test-cors-browser-boundary-lab/test_service.py /root/work/test-cors-browser-boundary-lab/test_service.py
cd /root/work/test-cors-browser-boundary-lab
  1. In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: origins(values) is a new list that validates each item with origin and then removes duplicates, keeping the order of first appearance. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

  2. In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: methods(values) allows only GET, POST, PUT, DELETE, and OPTIONS, converts them to uppercase, and removes duplicates. An empty list or any other value is ValueError. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

  3. In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: policy(allowed, credentials) checks that credentials is a bool. If allowed contains '*', it is ValueError, and otherwise it returns {allow_origins:origins(allowed), allow_credentials:credentials}. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

  4. In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: create_app(allowed, credentials=True) is an app that validates the policy and configures CORSMiddleware. It allows only GET and POST, allows the Content-Type and X-Request-ID request headers, and exposes the X-Trace response header. GET /data returns {ok:True} with X-Trace='trace-1'. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

  5. In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: preflight_headers(source, method, requested='X-Request-ID') is a dictionary with three keys: Origin, Access-Control-Request-Method, and Access-Control-Request-Headers. method is uppercase. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

  6. In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: preflight_status(app, source, method, requested='X-Request-ID') sends an OPTIONS request to /data with TestClient and returns the HTTP status. A different origin, DELETE, and an X-Secret header must each give 400. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

  7. In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: cors_observation(app, source) sends GET /data and returns (status, the Access-Control-Allow-Origin value or None, the JSON body). Even for an origin that is not allowed, the 200 body still runs, but the allow-origin header must be absent. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

Notes

Validate the origin format — test

In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: origin(value) returns the input string if it is an http or https URL that has a host and has no path, query, fragment, or user information. Otherwise it is ValueError. A trailing / is also a path, so it is rejected. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

Prepare this once at the start. Existing files are not overwritten.

mkdir -p /root/work/test-cors-browser-boundary-lab
test -e /root/work/test-cors-browser-boundary-lab/service.py || cp /opt/fixtures/ten_labs/test-cors-browser-boundary-lab/service.py /root/work/test-cors-browser-boundary-lab/service.py
test -e /root/work/test-cors-browser-boundary-lab/test_service.py || cp /opt/fixtures/ten_labs/test-cors-browser-boundary-lab/test_service.py /root/work/test-cors-browser-boundary-lab/test_service.py
cd /root/work/test-cors-browser-boundary-lab

If you allow a whole URL as an origin, you can confuse a path or user information with the origin. Do not modify the implementation file. Use pytest.raises to check the expected exception, and assert a concrete expected value for the normal result.

After saving, check with bash /opt/lab/checks/test-cors-browser-boundary-lab/01-contract.sh.

Remove duplicate origins — test

In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: origins(values) is a new list that validates each item with origin and then removes duplicates, keeping the order of first appearance. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

The allow list is a list of exact origins, not a substring match on strings. Do not modify the implementation file. Use pytest.raises to check the expected exception, and assert a concrete expected value for the normal result.

After saving, check with bash /opt/lab/checks/test-cors-browser-boundary-lab/02-contract.sh.

Limit the methods to an allow list — test

In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: methods(values) allows only GET, POST, PUT, DELETE, and OPTIONS, converts them to uppercase, and removes duplicates. An empty list or any other value is ValueError. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

Do not quietly add PATCH or arbitrary methods that were not allowed. Do not modify the implementation file. Use pytest.raises to check the expected exception, and assert a concrete expected value for the normal result.

After saving, check with bash /opt/lab/checks/test-cors-browser-boundary-lab/03-contract.sh.

Do not allow credentials together with an asterisk — test

In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: policy(allowed, credentials) checks that credentials is a bool. If allowed contains '*', it is ValueError, and otherwise it returns {allow_origins:origins(allowed), allow_credentials:credentials}. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

The explicit policy of this lab does not accept an asterisk regardless of whether credentials are allowed. Do not modify the implementation file. Use pytest.raises to check the expected exception, and assert a concrete expected value for the normal result.

After saving, check with bash /opt/lab/checks/test-cors-browser-boundary-lab/04-contract.sh.

Attach the real CORS middleware — test

In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: create_app(allowed, credentials=True) is an app that validates the policy and configures CORSMiddleware. It allows only GET and POST, allows the Content-Type and X-Request-ID request headers, and exposes the X-Trace response header. GET /data returns {ok:True} with X-Trace='trace-1'. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

If you attach headers by hand separately to the preflight and to the real response, the two policies easily drift apart. Do not modify the implementation file. Use pytest.raises to check the expected exception, and assert a concrete expected value for the normal result.

After saving, check with bash /opt/lab/checks/test-cors-browser-boundary-lab/05-contract.sh.

Build the preflight request — test

In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: preflight_headers(source, method, requested='X-Request-ID') is a dictionary with three keys: Origin, Access-Control-Request-Method, and Access-Control-Request-Headers. method is uppercase. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

The method of the actual request is OPTIONS, and the method you want to check is in a separate header. Do not modify the implementation file. Use pytest.raises to check the expected exception, and assert a concrete expected value for the normal result.

After saving, check with bash /opt/lab/checks/test-cors-browser-boundary-lab/06-contract.sh.

Compute the rejection matrix — test

In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: preflight_status(app, source, method, requested='X-Request-ID') sends an OPTIONS request to /data with TestClient and returns the HTTP status. A different origin, DELETE, and an X-Secret header must each give 400. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

Do not mix the three kinds of rejection reasons into one request, or you cannot find the missing policy. Do not modify the implementation file. Use pytest.raises to check the expected exception, and assert a concrete expected value for the normal result.

After saving, check with bash /opt/lab/checks/test-cors-browser-boundary-lab/07-contract.sh.

Observe the difference between CORS and authentication — test

In /root/work/test-cors-browser-boundary-lab/test_service.py, test the following public contract of the provided service.py: cors_observation(app, source) sends GET /data and returns (status, the Access-Control-Allow-Origin value or None, the JSON body). Even for an origin that is not allowed, the 200 body still runs, but the allow-origin header must be absent. It must pass against the correct implementation and be caught, through a failure in the body of an actual test, in an implementation that breaks this contract. Keep the tests from the earlier steps and add a test_ function.

curl and server-to-server requests do not follow the browser's CORS read restriction. Do not modify the implementation file. Use pytest.raises to check the expected exception, and assert a concrete expected value for the normal result.

After saving, check with bash /opt/lab/checks/test-cors-browser-boundary-lab/08-contract.sh.