An access-control test matrix that finds defects: design principles
Summary
You write negative tests that separate no authentication, no permission, someone else's resource, and a normal request.
Why this matters
An access control test that checked only success responses was green. Even when the route forgot to inject the Header or the scope check was deleted, the test did not break. A verifier must not just list the allowed cases; it has to observe the reason for each rejection separately.
How it works
In each step you build the combination of user, document, and permission yourself. You write assertions on both the response dict and the status code, and you also expose a copy defect that mutates the original scopes. At the end you actually run the 200, 401, 403, and 404 matrix with TestClient.
학생 테스트 → 정상 구현: 실제 시험 모두 통과
└→ 계약 위반 구현: 해당 동작에서 실패
수집 실패·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. Split the Bearer header — test
Test the following public contract of the provided service.py: bearer(header) returns the token when the header starts with exactly 'Bearer ' and is followed by one token with no whitespace. None, an empty token, a different scheme, and extra whitespace are 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: If you split the header into several pieces arbitrarily, a whitespace error can be accepted as a normal token. 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:
header[6:]
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. Copy and return the identity — test
Test the following public contract of the provided service.py: principal(token, users) returns the user {id, scopes} from the token dictionary, but copies the scopes list as well. An unknown token 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: If modifying the returned scopes also changes the original user's permissions, permissions get mixed between requests. 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:
user["scopes"]
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. Compare permissions exactly — test
Test the following public contract of the provided service.py: require_scope(user, scope) returns None when the scope string is exactly present in scopes, and raises PermissionError otherwise. read-all is not read. 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: A substring comparison mistakes a longer permission name for a different permission. 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:
if False:
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. Check ownership separately — test
Test the following public contract of the provided service.py: visible(user, document) is True only when document is not None and owner is exactly equal to the user's id. 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: A missing resource and a resource owned by someone else are bundled into the same decision. 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:
True
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. Choose response fields with an allow list — test
Test the following public contract of the provided service.py: public_document(document) is a new dictionary that has only id and title. It does not include owner or internal_cost. 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 delete fields from the original; assemble a new response. 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:
("id", "title", "owner")
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. Match errors to the HTTP contract — test
Test the following public contract of the provided service.py: authenticate(header, users) connects bearer and principal. ValueError becomes HTTPException(401), and the WWW-Authenticate value in headers is Bearer. 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 lump an authentication failure and an application error together into a single 500. 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:
HTTPException(403,
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. Fix the order of rejection — test
Test the following public contract of the provided service.py: read_document(user, documents, document_id) raises HTTPException(403) if there is no read scope, HTTPException(404) if the document does not exist or belongs to someone else, and otherwise returns the result of public_document. 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: Even after authentication, scope and ownership must each be checked. 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:
HTTPException(403, "not found")
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. Close the boundary in a real request — test
Test the following public contract of the provided service.py: create_app(users, documents) returns a FastAPI app that receives the Authorization header in GET /documents/{document_id} and calls authenticate and read_document. Verify 200, 401, 403, 404, and the removal of private fields with real requests. 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: Even if the functions are each correct, access control is not applied if the route forgets to call them. 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:
authorization: str | None = None
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
A fixed token dictionary is teaching input. Production authentication additionally needs expiry, signatures, revocation, and safe storage. Making the 404s identical also does not remove every inference through response time or access logs. Also check that a rejected request did not change the original data. 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. Split the Bearer header — test → Copy and return the identity — test → Compare permissions exactly — test → Check ownership separately — test → Choose response fields with an allow list — test → Match errors to the HTTP contract — test → Fix the order of rejection — test → Close the boundary in a real request — 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.