diff --git a/spec/tests/dev_e2e/013-jwt-replication.md b/spec/tests/dev_e2e/013-jwt-replication.md index 7a3a3c30d..d8ea51e6b 100644 --- a/spec/tests/dev_e2e/013-jwt-replication.md +++ b/spec/tests/dev_e2e/013-jwt-replication.md @@ -20,8 +20,8 @@ the ES config (`reconnect_on_token_change=false`, no file watching). 1. Generate an RSA key pair and sign a JWT token (`sub=user1`, `exp=3600s`). 2. Create the SGW `travel` database (creating its backing CBS bucket and collections) with a `local_jwt` provider registering the public JWK (`issuer=test-issuer`, `client_id=edge-server`, `algorithms=[RS256]`, `register=true`). -3. Add the JWT user `test-provider_user1` with `["*"]` channel access to `travel.airlines`. -4. Insert 5 documents into CBS (`jwt_test_airline_1..5`) with `channels=["*"]`. +3. Add the JWT user `test-provider_user1` with `["airlines"]` channel access to `travel.airlines`. +4. Insert 5 documents into CBS (`jwt_test_airline_1..5`) with `channels=["airlines"]`. 5. Wait until SGW has imported all 5 documents from CBS. 6. Verify the JWT token authenticates against the SGW REST API (`GET /travel/` with `Authorization: Bearer ` returns 200). 7. Configure and start ES with the inline JWT token in `auth.openid_token` and `reconnect_on_token_change=false`. @@ -41,7 +41,7 @@ host (`auth.openid_token.path`), across all `travel` collections. 1. Generate an RSA key pair and sign a JWT token (`sub=user1`, `exp=600s`). 2. Create the SGW `travel` database (creating its backing CBS bucket and collections) with a `local_jwt` provider registering the public JWK, with sync functions for all 5 collections (`airlines`, `airports`, `hotels`, `landmarks`, `routes`). -3. Add the JWT user `test-provider_user1` with `["*"]` channel access to all 5 collections. +3. Add the JWT user `test-provider_user1` with `["United States", "United Kingdom", "France"]` channel access to all 5 collections. 4. Load the `travel` dataset into SGW. 5. Write the JWT token to a file on the ES host (`/home/ec2-user/cert/jwt.txt`). 6. Configure and start ES with `auth.openid_token.path` pointing at the JWT file, syncing all 5 collections. @@ -60,7 +60,7 @@ continues uninterrupted. 1. Generate two RSA key pairs (A and B) and sign Token-A (`kid=test-key-1`) and Token-B (`kid=test-key-2`), both `sub=user1`. 2. Create the SGW `travel` database (creating its backing CBS bucket and collections) with a `local_jwt` provider registering both public JWKs (A and B). -3. Add the JWT user `test-provider_user1` with `["*"]` channel access to `travel.airlines`. +3. Add the JWT user `test-provider_user1` with `["airlines"]` channel access to `travel.airlines`. 4. Insert 5 documents into CBS (`rotation_test_1..5`). 5. Write Token-A to the JWT file on the ES host. 6. Configure and start ES with `auth.openid_token.path` and `reconnect_on_token_change=true`, syncing `travel.airlines`. @@ -81,7 +81,7 @@ and the replicator permanently stops (no retry, since 401 is not transient). 1. Generate an RSA key pair and sign a valid token; generate a second key pair and sign an invalid token (`kid=unknown-key`) whose key is **not** registered with SGW. 2. Create the SGW `travel` database (creating its backing CBS bucket and collections) with a `local_jwt` provider registering **only** the valid public JWK. -3. Add the JWT user `test-provider_user1` with `["*"]` channel access to `travel.airlines`. +3. Add the JWT user `test-provider_user1` with `["airlines"]` channel access to `travel.airlines`. 4. Insert 3 documents into CBS (`disconnect_test_1..3`). 5. Write the valid token to the JWT file and start ES with `reconnect_on_token_change=true`. 6. Wait until replication is idle (confirming the valid token works). @@ -102,7 +102,7 @@ does not treat the content as a valid change. 1. Generate an RSA key pair and sign a valid token. 2. Create the SGW `travel` database (creating its backing CBS bucket and collections) with a `local_jwt` provider registering the public JWK. -3. Add the JWT user `test-provider_user1` with `["*"]` channel access to `travel.airlines`. +3. Add the JWT user `test-provider_user1` with `["airlines"]` channel access to `travel.airlines`. 4. Insert 3 documents into CBS (`filedel_test_1..3`). 5. Write the valid token to the JWT file and start ES with `reconnect_on_token_change=true`. 6. Wait until replication is idle. @@ -123,7 +123,7 @@ re-triggered, after which it recovers. 1. Generate key pairs A, C (valid) and one invalid key. Sign Token-A (`kid=test-key-1`), Token-C (`kid=test-key-3`), and an invalid token (`kid=bad-key`), all `sub=user1`. 2. Create the SGW `travel` database (creating its backing CBS bucket and collections) with a `local_jwt` provider registering public JWKs A and C (not the invalid key). -3. Add the JWT user `test-provider_user1` with `["*"]` channel access to `travel.airlines`. +3. Add the JWT user `test-provider_user1` with `["airlines"]` channel access to `travel.airlines`. 4. Insert 3 documents into CBS (`cycle_test_1..3`). 5. **Phase 1:** Write Token-A to the JWT file, start ES with `reconnect_on_token_change=true`, wait until idle, and verify at least 3 documents replicated. 6. **Phase 2:** Overwrite the JWT file with the invalid token. Poll (up to 60s) until the replicator is removed or `Stopped`/`Offline`, confirming a permanent 401 stop. diff --git a/tests/AGENTS.md b/tests/AGENTS.md index 9c6d140b9..468f5a9b0 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -184,6 +184,8 @@ Located in `dataset/sg/`. Each has a `-sg.json` (data) and `-sg-config.json` (SG - **No docstrings or inline step descriptions on test methods** — the matching `spec/tests/**.md` file is the source of truth for the test flow; `mark_test_step()` text mirrors its numbered steps. Helper functions still need docstrings. - **Platform-agnostic** — must work on every test server (C, .NET, iOS, JVM, JS). Use `self.skip_if_not_platform()` for platform-specific behavior. - **Version gates** — use `self.skip_if_cbl_not(server, ">= 3.3.0")` / `self.skip_if_sgw_not(sg, ">= 4.0.0")` rather than runtime checks. +- **Never put a document in the `*` channel** — not through a `channels` property, nor a sync function. Sync Gateway already gives every document to `*`, so use a named channel. +- **Prefer named channels for user access** — unless the test checks `*` access, do not grant `*` to a user. Grant the channels that the test documents use. - **Check the spec first** — every new/modified test must align with `spec/tests/`. Add or update the spec before changing the code. - **No markdown sidecars for code changes** — do not create `ENHANCEMENT_*.md`, `IMPLEMENTATION_*.md`, etc. PR descriptions cover motivation; the spec covers behavior. diff --git a/tests/dev_e2e/edge_server/test_jwt_rotation.py b/tests/dev_e2e/edge_server/test_jwt_rotation.py index c0a90883b..2cda3a602 100644 --- a/tests/dev_e2e/edge_server/test_jwt_rotation.py +++ b/tests/dev_e2e/edge_server/test_jwt_rotation.py @@ -80,12 +80,13 @@ async def test_replication_with_jwt_file(self, cblpytest: CBLPyTest, dataset_pat await cloud.create_database("travel", payload) # Create user1 with channel access to all travel collections + countries = ["United States", "United Kingdom", "France"] collection_access_input = { - "travel.airlines": ["*"], - "travel.routes": ["*"], - "travel.airports": ["*"], - "travel.landmarks": ["*"], - "travel.hotels": ["*"], + "travel.airlines": countries, + "travel.routes": countries, + "travel.airports": countries, + "travel.landmarks": countries, + "travel.hotels": countries, } access_dict = sgw.create_collection_access_dict(collection_access_input) await sgw.add_user("travel", "test-provider_user1", "pass", access_dict) @@ -182,7 +183,7 @@ async def test_token_rotation_reconnect(self, cblpytest: CBLPyTest, dataset_path await cloud.create_database("travel", payload) # Create JWT user with channel access - collection_access_input = {"travel.airlines": ["*"]} + collection_access_input = {"travel.airlines": ["airlines"]} access_dict = sgw.create_collection_access_dict(collection_access_input) await sgw.add_user("travel", "test-provider_user1", "pass", access_dict) @@ -192,7 +193,7 @@ async def test_token_rotation_reconnect(self, cblpytest: CBLPyTest, dataset_path doc_id = f"rotation_test_{i}" doc = { "id": doc_id, - "channels": ["*"], + "channels": ["airlines"], "type": "airline", "name": f"Rotation Airline {i}", } @@ -236,7 +237,7 @@ async def test_token_rotation_reconnect(self, cblpytest: CBLPyTest, dataset_path doc_id = f"rotation_test_{i}" doc = { "id": doc_id, - "channels": ["*"], + "channels": ["airlines"], "type": "airline", "name": f"Rotation Airline {i}", } @@ -310,7 +311,7 @@ async def test_invalid_token_rotation_causes_401_stop( ) await cloud.create_database("travel", payload) - collection_access_input = {"travel.airlines": ["*"]} + collection_access_input = {"travel.airlines": ["airlines"]} access_dict = sgw.create_collection_access_dict(collection_access_input) await sgw.add_user("travel", "test-provider_user1", "pass", access_dict) @@ -319,7 +320,7 @@ async def test_invalid_token_rotation_causes_401_stop( doc_id = f"disconnect_test_{i}" doc = { "id": doc_id, - "channels": ["*"], + "channels": ["airlines"], "type": "airline", "name": f"Disconnect Airline {i}", } @@ -436,7 +437,7 @@ async def test_corrupt_token_file_content_mid_replication( ) await cloud.create_database("travel", payload) - collection_access_input = {"travel.airlines": ["*"]} + collection_access_input = {"travel.airlines": ["airlines"]} access_dict = sgw.create_collection_access_dict(collection_access_input) await sgw.add_user("travel", "test-provider_user1", "pass", access_dict) @@ -445,7 +446,7 @@ async def test_corrupt_token_file_content_mid_replication( doc_id = f"filedel_test_{i}" doc = { "id": doc_id, - "channels": ["*"], + "channels": ["airlines"], "type": "airline", "name": f"FileDel Airline {i}", } @@ -572,7 +573,7 @@ async def test_valid_invalid_valid_token_cycle( ) await cloud.create_database("travel", payload) - collection_access_input = {"travel.airlines": ["*"]} + collection_access_input = {"travel.airlines": ["airlines"]} access_dict = sgw.create_collection_access_dict(collection_access_input) await sgw.add_user("travel", "test-provider_user1", "pass", access_dict) @@ -582,7 +583,7 @@ async def test_valid_invalid_valid_token_cycle( doc_id = f"cycle_test_{i}" doc = { "id": doc_id, - "channels": ["*"], + "channels": ["airlines"], "type": "airline", "name": f"Cycle Airline {i}", } @@ -653,7 +654,7 @@ async def test_valid_invalid_valid_token_cycle( doc_id = f"cycle_test_{i}" doc = { "id": doc_id, - "channels": ["*"], + "channels": ["airlines"], "type": "airline", "name": f"Cycle Airline {i}", } diff --git a/tests/dev_e2e/edge_server/test_jwt_simple.py b/tests/dev_e2e/edge_server/test_jwt_simple.py index 2528f4cda..31ae2a39d 100644 --- a/tests/dev_e2e/edge_server/test_jwt_simple.py +++ b/tests/dev_e2e/edge_server/test_jwt_simple.py @@ -86,11 +86,11 @@ async def test_jwt_replication_reconnect_false( # STEP 4: Pre-create JWT user with channel access. # SGW auto-registers users on first JWT auth (register=true), but the # auto-created user has no channel access. We pre-create with - # admin_channels=["*"] so it can pull all documents. + # admin_channels=["airlines"] so it can pull the test documents. # Username format: "_" = "test-provider_user1" # ===================================================================== self.mark_test_step("Adding JWT user with channel access.") - input_data = {"travel.airlines": ["*"]} + input_data = {"travel.airlines": ["airlines"]} access_dict = sync_gateway.create_collection_access_dict(input_data) await sync_gateway.add_user(sg_db_name, "test-provider_user1", "pass", access_dict) @@ -105,7 +105,7 @@ async def test_jwt_replication_reconnect_false( doc_id = f"jwt_test_airline_{i}" doc = { "id": doc_id, - "channels": ["*"], + "channels": ["airlines"], "type": "airline", "name": f"JWT Test Airline {i}", }