TT Lab
Get started
Learn Learning paths Courses

Testing Tools in Practice

Test the boundary between CORS and authentication: design principles

Continue in TT Lab

Summary

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

Why this 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.

How it works

Validate Origin as an origin with no path, and test the duplicate and wildcard policies. Set the Access-Control-Request-Method of the OPTIONS request directly. Compare the status and the allow headers of an ordinary request and a preflight, and also record the limitation that TestClient does not implement browser blocking.

학생 테스트 → 정상 구현: 실제 시험 모두 통과
           └→ 계약 위반 구현: 해당 동작에서 실패
수집 실패·0개 실행·강제 종료 ≠ 결함 검출

A worksheet for reading the contract and predicting failures

What follows is not an answer key to memorize an implementation but a step-by-step code review. Each change fragment deliberately breaks the contract. Note that the normal case may still pass after the change. Before running it, predict which input, exception, or state you would observe to expose the difference, and after implementing it, compare that prediction with the result.

1. Validate the origin format — test

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.

Basis for the judgment: 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.

Faulty change fragment to review:

or url.query

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

2. Remove duplicate origins — test

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.

Basis for the judgment: 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.

Faulty change fragment to review:

[origin(value) for value in values]

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

3. Limit the methods to an allow list — test

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.

Basis for the judgment: 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.

Faulty change fragment to review:

"DELETE","OPTIONS","PATCH"

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

4. Do not allow credentials together with an asterisk — test

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.

Basis for the judgment: 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.

Faulty change fragment to review:

not isinstance(credentials, (bool, int))

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

5. Attach the real CORS middleware — test

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.

Basis for the judgment: 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.

Faulty change fragment to review:

expose_headers=[]

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

6. Build the preflight request — test

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.

Basis for the judgment: 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.

Faulty change fragment to review:

method.lower()

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

7. Compute the rejection matrix — test

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.

Basis for the judgment: 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.

Faulty change fragment to review:

client.get("/data",

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

8. Observe the difference between CORS and authentication — test

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.

Basis for the judgment: 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.

Faulty change fragment to review:

source

Compare it with the public contract of the function this fragment sits in. If a single success case does not tell them apart, choose as your observation target an input that should be rejected or the state after a failure.

What it looks like in the field

TestClient is not a browser. It checks CORS response headers and preflights, but it does not implement the browser's own read blocking. Even an ordinary GET that sends a disallowed Origin can execute on the server. Sensitive actions must be protected by separate authentication, authorization, and CSRF policies. You may read the provided implementation, but grading uses a separate copy. Do not work around a defect by checking the wording of the source or by modifying files; check the execution results of the public interface.

What you will do in the next lab

Eight steps lead to one runnable result. Validate the origin format — test → Remove duplicate origins — test → Limit the methods to an allow list — test → Do not allow credentials together with an asterisk — test → Attach the real CORS middleware — test → Build the preflight request — test → Compute the rejection matrix — test → Observe the difference between CORS and authentication — test.

Each step checks actual return values, exceptions, and state changes, not the fact that a function or file exists. After you see the answer, deliberately change a boundary comparison or the cleanup code and check which tests fail. Explain why the earlier tests are kept in the next steps, and write down one operating condition that this lab does not guarantee.