Skip to content

Commit f337963

Browse files
committed
Address Copilot comments
Signed-off-by: Sergio Herrera <627709+seherv@users.noreply.github.com>
1 parent 7986977 commit f337963

8 files changed

Lines changed: 379 additions & 17 deletions

File tree

tests/examples/test_crypto.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717

1818
@pytest.fixture()
19-
def crypto_artifacts():
19+
def cleanup_crypto_outputs():
2020
"""Clean up output files written by the crypto example on teardown.
2121
2222
Example RSA and AES keys are in ``examples/crypto/keys/``.
@@ -27,7 +27,7 @@ def crypto_artifacts():
2727

2828

2929
@pytest.mark.example_dir('crypto')
30-
def test_crypto(dapr, crypto_artifacts):
30+
def test_crypto(dapr, cleanup_crypto_outputs):
3131
output = dapr.run(
3232
'--app-id crypto --resources-path ./components/ -- python3 crypto.py',
3333
timeout=30,
@@ -38,7 +38,7 @@ def test_crypto(dapr, crypto_artifacts):
3838

3939

4040
@pytest.mark.example_dir('crypto')
41-
def test_crypto_async(dapr, crypto_artifacts):
41+
def test_crypto_async(dapr, cleanup_crypto_outputs):
4242
output = dapr.run(
4343
'--app-id crypto-async --resources-path ./components/ -- python3 crypto-async.py',
4444
timeout=30,

tests/integration/AGENTS.md

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ tox -e integration -- test_state_store.py -k test_save_and_get
2727

2828
```
2929
tests/integration/
30-
├── conftest.py # DaprTestEnvironment + fixtures (dapr_env, apps_dir, components_dir)
30+
├── conftest.py # DaprTestEnvironment + fixtures (dapr_env, apps_dir, resources_dir, crypto_keys)
3131
├── test_*.py # Test files (one per building block)
3232
├── apps/ # Helper apps started alongside sidecars
3333
│ ├── invoke_receiver.py # gRPC method handler for invoke tests
3434
│ └── pubsub_subscriber.py # Subscriber that persists messages to state store
35-
├── components/ # Dapr component YAMLs loaded by all sidecars
35+
├── resources/ # Dapr component YAMLs loaded by all sidecars
3636
│ ├── statestore.yaml # state.redis (also configured as actor state store)
3737
│ ├── pubsub.yaml # pubsub.redis
3838
│ ├── lockstore.yaml # lock.redis
@@ -41,7 +41,7 @@ tests/integration/
4141
│ ├── localbinding.yaml # bindings.localstorage (rootPath=./.binding-data)
4242
│ ├── cryptostore.yaml # crypto.dapr.localstorage (path=./keys)
4343
│ └── conversation.yaml # conversation.echo
44-
├── keys/ # RSA + symmetric keys for cryptostore
44+
├── keys/ # RSA + symmetric keys for cryptostore (generated at test time, gitignored)
4545
├── secrets.json # Secrets file for localsecretstore component
4646
└── .binding-data/ # Created on demand for localbinding rootPath (gitignored)
4747
```
@@ -54,15 +54,22 @@ Sidecar and client fixtures are **module-scoped** — one sidecar per test file.
5454
|---------|-------|------|-------------|
5555
| `dapr_env` | module | `DaprTestEnvironment` | Manages sidecar lifecycle; call `start_sidecar()` to get a client |
5656
| `apps_dir` | module | `Path` | Path to `tests/integration/apps/` |
57-
| `components_dir` | module | `Path` | Path to `tests/integration/components/` |
58-
| `wait_until` | session | `Callable` | Polling helper `(predicate, timeout=10, interval=0.1)` for eventual-consistency assertions |
59-
| `wait_until_async` | session | `Callable` | Async counterpart of `wait_until` for awaitable predicates |
57+
| `resources_dir` | module | `Path` | Path to `tests/integration/resources/` |
58+
| `crypto_keys` | session | `Path` | Generates ephemeral RSA + AES keys under `tests/integration/keys/` for the cryptostore component (see `tests/crypto_utils.py`) |
6059
| `flush_redis` | session | `None` | Side-effect fixture that clears the `dapr_redis` container once per session |
61-
| `redis_set` | session | `Callable` | Returns `set(key, value, version=1)` that seeds a Dapr configuration value into Redis (`value||version`) |
60+
| `redis_set_config` | session | `Callable` | Returns `_set(key, value, version=1)` that seeds a Dapr configuration value into Redis (`value||version`) |
6261

63-
All four are session-scoped (defined in `tests/conftest.py`) so that module-scoped fixtures can depend on them.
62+
`flush_redis` and `redis_set_config` are session-scoped (defined in `tests/conftest.py`) so module-scoped fixtures can depend on them.
6463

65-
Each test file defines its own module-scoped `client` fixture that calls `dapr_env.start_sidecar(...)`.
64+
Polling helpers are **plain functions**, not fixtures — import them directly:
65+
66+
```python
67+
from tests.wait_utils import wait_until, wait_until_async
68+
```
69+
70+
Both have signature `(condition, timeout=10.0, interval=0.1)` and raise `TimeoutError` if the deadline elapses. `wait_until_async` awaits an awaitable condition.
71+
72+
Each test file defines its own module-scoped fixture (`client` or `sidecar`) that calls `dapr_env.start_sidecar(...)`.
6673

6774
## Building blocks covered
6875

@@ -116,7 +123,7 @@ Some building blocks (invoke, pubsub) require an app process running alongside t
116123

117124
1. Create `test_<building_block>.py`
118125
2. Add a module-scoped `client` fixture that calls `dapr_env.start_sidecar(app_id='test-<name>')`
119-
3. If the building block needs a new Dapr component, add a YAML to `components/`
126+
3. If the building block needs a new Dapr component, add a YAML to `resources/`
120127
4. If the building block needs a running app, add it to `apps/` and pass `app_cmd` / `app_port` to `start_sidecar()`
121128
5. Use unique keys/resource IDs per test to avoid interference (the sidecar is shared within a module)
122129
6. Assert on SDK return types and gRPC status codes, not on string output
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import pytest
2+
3+
from dapr.clients.grpc.conversation import (
4+
ConversationInput,
5+
ConversationInputAlpha2,
6+
create_assistant_message,
7+
create_system_message,
8+
create_user_message,
9+
)
10+
11+
COMPONENT = 'echo'
12+
13+
14+
@pytest.fixture(scope='module')
15+
def client(dapr_env):
16+
return dapr_env.start_sidecar(app_id='test-conversation')
17+
18+
19+
def test_converse_alpha1_echoes_input(client):
20+
response = client.converse_alpha1(
21+
name=COMPONENT,
22+
inputs=[ConversationInput(content='sync hello', role='user')],
23+
)
24+
assert response.outputs[0].result == 'sync hello'
25+
26+
27+
def test_converse_alpha1_with_multiple_inputs(client):
28+
response = client.converse_alpha1(
29+
name=COMPONENT,
30+
inputs=[
31+
ConversationInput(content='one', role='user'),
32+
ConversationInput(content='two', role='user'),
33+
],
34+
)
35+
results = [out.result for out in response.outputs]
36+
# The echo component concatenates all inputs into a single newline-joined output
37+
# rather than echoing each input individually.
38+
assert results == ['one\ntwo']
39+
40+
41+
def test_converse_alpha1_with_temperature(client):
42+
response = client.converse_alpha1(
43+
name=COMPONENT,
44+
inputs=[ConversationInput(content='warm', role='user')],
45+
temperature=0.7,
46+
)
47+
assert response.outputs[0].result == 'warm'
48+
49+
50+
def test_converse_alpha2_echoes_user_message(client):
51+
response = client.converse_alpha2(
52+
name=COMPONENT,
53+
inputs=[ConversationInputAlpha2(messages=[create_user_message('sync world')])],
54+
)
55+
assert response.outputs[0].choices[0].message.content == 'sync world'
56+
57+
58+
def test_converse_alpha2_with_mixed_messages(client):
59+
response = client.converse_alpha2(
60+
name=COMPONENT,
61+
inputs=[
62+
ConversationInputAlpha2(
63+
messages=[
64+
create_system_message('be brief'),
65+
create_user_message('hi'),
66+
create_assistant_message('hello'),
67+
]
68+
)
69+
],
70+
)
71+
assert response.outputs[0].choices[0].message.content

tests/integration/test_crypto.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import pytest
2+
3+
from dapr.clients.grpc._crypto import DecryptOptions, EncryptOptions
4+
5+
CRYPTO_COMPONENT = 'cryptostore'
6+
RSA_KEY = 'rsa-private-key.pem'
7+
SYMMETRIC_KEY = 'symmetric-key-256'
8+
9+
# The crypto API re-emits the alpha warnings on every test run.
10+
pytestmark = pytest.mark.filterwarnings('ignore::UserWarning')
11+
12+
13+
@pytest.fixture(scope='module')
14+
def client(dapr_env, crypto_keys):
15+
return dapr_env.start_sidecar(app_id='test-crypto')
16+
17+
18+
def test_rsa_round_trip(client):
19+
plaintext = b'sync crypto secret'
20+
21+
encrypted_stream = client.encrypt(
22+
data=plaintext,
23+
options=EncryptOptions(
24+
component_name=CRYPTO_COMPONENT,
25+
key_name=RSA_KEY,
26+
key_wrap_algorithm='RSA',
27+
),
28+
)
29+
encrypted = encrypted_stream.read()
30+
assert encrypted != plaintext
31+
32+
decrypted_stream = client.decrypt(
33+
data=encrypted,
34+
options=DecryptOptions(component_name=CRYPTO_COMPONENT, key_name=RSA_KEY),
35+
)
36+
assert decrypted_stream.read() == plaintext
37+
38+
39+
def test_aes_round_trip(client):
40+
plaintext = b'A' * (32 * 1024)
41+
42+
encrypted_stream = client.encrypt(
43+
data=plaintext,
44+
options=EncryptOptions(
45+
component_name=CRYPTO_COMPONENT,
46+
key_name=SYMMETRIC_KEY,
47+
key_wrap_algorithm='AES',
48+
),
49+
)
50+
encrypted = encrypted_stream.read()
51+
52+
decrypted_stream = client.decrypt(
53+
data=encrypted,
54+
options=DecryptOptions(component_name=CRYPTO_COMPONENT, key_name=SYMMETRIC_KEY),
55+
)
56+
assert decrypted_stream.read() == plaintext
57+
58+
59+
def test_string_input_round_trip(client):
60+
plaintext = 'hello dapr crypto'
61+
62+
encrypted_stream = client.encrypt(
63+
data=plaintext,
64+
options=EncryptOptions(
65+
component_name=CRYPTO_COMPONENT,
66+
key_name=RSA_KEY,
67+
key_wrap_algorithm='RSA',
68+
),
69+
)
70+
encrypted = encrypted_stream.read()
71+
72+
decrypted_stream = client.decrypt(
73+
data=encrypted,
74+
options=DecryptOptions(component_name=CRYPTO_COMPONENT, key_name=RSA_KEY),
75+
)
76+
assert decrypted_stream.read().decode() == plaintext
77+
78+
79+
def test_encrypt_with_blank_component_raises(client):
80+
with pytest.raises(ValueError):
81+
client.encrypt(
82+
data=b'payload',
83+
options=EncryptOptions(
84+
component_name='',
85+
key_name=RSA_KEY,
86+
key_wrap_algorithm='RSA',
87+
),
88+
)
89+
90+
91+
def test_decrypt_with_blank_component_raises(client):
92+
with pytest.raises(ValueError):
93+
client.decrypt(
94+
data=b'payload',
95+
options=DecryptOptions(component_name='', key_name=RSA_KEY),
96+
)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import uuid
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
BINDING = 'localbinding'
7+
BINDING_ROOT = Path(__file__).resolve().parent / '.binding-data'
8+
9+
10+
@pytest.fixture(scope='module')
11+
def client(dapr_env):
12+
return dapr_env.start_sidecar(app_id='test-invoke-binding')
13+
14+
15+
def test_create_writes_file_to_disk(client):
16+
file_name = f'binding-{uuid.uuid4().hex[:8]}.txt'
17+
payload = b'hello from sync invoke_binding'
18+
19+
client.invoke_binding(
20+
binding_name=BINDING,
21+
operation='create',
22+
data=payload,
23+
binding_metadata={'fileName': file_name},
24+
)
25+
26+
assert (BINDING_ROOT / file_name).read_bytes() == payload
27+
28+
29+
def test_create_then_get_round_trip(client):
30+
file_name = f'binding-{uuid.uuid4().hex[:8]}.txt'
31+
payload = b'sync round-trip payload'
32+
33+
client.invoke_binding(
34+
binding_name=BINDING,
35+
operation='create',
36+
data=payload,
37+
binding_metadata={'fileName': file_name},
38+
)
39+
response = client.invoke_binding(
40+
binding_name=BINDING,
41+
operation='get',
42+
binding_metadata={'fileName': file_name},
43+
)
44+
45+
assert response.data == payload
46+
47+
48+
def test_create_with_string_payload(client):
49+
file_name = f'binding-{uuid.uuid4().hex[:8]}.txt'
50+
payload = 'sync string payload'
51+
52+
client.invoke_binding(
53+
binding_name=BINDING,
54+
operation='create',
55+
data=payload,
56+
binding_metadata={'fileName': file_name},
57+
)
58+
59+
assert (BINDING_ROOT / file_name).read_text() == payload
60+
61+
62+
def test_delete_removes_file(client):
63+
file_name = f'binding-{uuid.uuid4().hex[:8]}.txt'
64+
file_path = BINDING_ROOT / file_name
65+
66+
client.invoke_binding(
67+
binding_name=BINDING,
68+
operation='create',
69+
data=b'to be deleted',
70+
binding_metadata={'fileName': file_name},
71+
)
72+
assert file_path.exists()
73+
74+
client.invoke_binding(
75+
binding_name=BINDING,
76+
operation='delete',
77+
binding_metadata={'fileName': file_name},
78+
)
79+
assert not file_path.exists()
80+
81+
82+
def test_list_includes_created_file(client):
83+
file_name = f'binding-{uuid.uuid4().hex[:8]}.txt'
84+
client.invoke_binding(
85+
binding_name=BINDING,
86+
operation='create',
87+
data=b'listed',
88+
binding_metadata={'fileName': file_name},
89+
)
90+
91+
response = client.invoke_binding(binding_name=BINDING, operation='list')
92+
assert file_name in response.data.decode()

0 commit comments

Comments
 (0)