Skip to content

Commit 699c0dd

Browse files
authored
CAIP-80 add e2e tests scenarios
* CAIP-80 add basic POC test * CAIP-80 test updated path * CAIP-80 update test * CAIP-80 propagate secret and env vars * CAIP-80 add uuid to test to differentiate test output * CAIP-80 update formatting * CAIP-80 test commit * CAIP-80 feed test is added * CAIP-80 extend timeout * CAIP-80 added separate workflow for e2e * CAIP-80 lock updated * lock back * CAIP-80 fix workflow file * CAIP-80 add e2e marker * CAIP-80 disable coverage for e2e * test workflow for branch * CAIP-80 disable e2e for each commit * CAIP-80 adding typing
1 parent 172b9fe commit 699c0dd

5 files changed

Lines changed: 264 additions & 0 deletions

File tree

.github/workflows/e2e-build.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: e2e test
2+
on:
3+
push:
4+
branches: [ "main" ]
5+
6+
jobs:
7+
e2e-test:
8+
runs-on: ubuntu-latest
9+
10+
steps:
11+
- uses: actions/checkout@v4
12+
13+
- name: Set up Python
14+
id: setup-python
15+
uses: actions/setup-python@v4
16+
with:
17+
python-version: '3.12'
18+
19+
- name: Get pip cache dir
20+
id: pip-cache
21+
run: |
22+
echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
23+
- name: Cache pip dependencies
24+
uses: actions/cache@v4
25+
with:
26+
path: ${{ steps.pip-cache.outputs.dir }}
27+
key: ${{ runner.os }}-pip-${{ steps.setup-python.outputs.python-version }}
28+
29+
- name: Install Poetry
30+
run: pip install poetry
31+
32+
- name: Install Poetry plugins
33+
run: poetry self add poetry-plugin-export
34+
35+
- name: Cache poetry virtualenv
36+
uses: actions/cache@v4
37+
with:
38+
path: ~/.cache/pypoetry/virtualenvs
39+
key: ${{ runner.os }}-poetry-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('poetry.lock') }}
40+
restore-keys: |
41+
${{ runner.os }}-poetry-${{ steps.setup-python.outputs.python-version }}-
42+
- name: Install dependencies
43+
run: poetry install
44+
45+
- name: Run E2E tests
46+
env:
47+
TEST_RSA_KEY: ${{ secrets.TEST_RSA_KEY }}
48+
MSG_BOT_USERNAME: ${{ vars.MSG_BOT_USERNAME }}
49+
FEED_BOT_USERNAME: ${{ vars.FEED_BOT_USERNAME }}
50+
STREAM_ID: ${{ vars.STREAM_ID }}
51+
SYMPHONY_HOST: ${{ vars.SYMPHONY_HOST }}
52+
TEST_USER_ID: ${{ vars.TEST_SYM_USER_ID }}
53+
BOT_USER_ID: ${{ vars.BOT_USER_ID }}
54+
run: poetry run pytest -m e2e --no-cov
55+
timeout-minutes: 10

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ build-backend = "poetry.core.masonry.api"
4949
[tool.pytest.ini_options]
5050
addopts = "--junitxml=test-results/junit.xml --cov --cov-report=html"
5151
testpaths = ["tests"]
52+
markers = ["e2e: marks tests as end-to-end tests"]
5253
norecursedirs = ["*.egg", ".*", "build", "dist", "venv", "legacy"]
5354
junit_logging = "all"
5455

tests/bdk/integration/__init__.py

Whitespace-only changes.

tests/bdk/integration/e2e_test.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import asyncio
2+
from datetime import datetime, timedelta
3+
from uuid import uuid4
4+
5+
import pytest
6+
import pytest_asyncio
7+
8+
from symphony.bdk.core.config.loader import BdkConfigLoader
9+
from symphony.bdk.core.symphony_bdk import SymphonyBdk
10+
from symphony.bdk.gen.pod_model.v3_room_attributes import V3RoomAttributes
11+
from tests.bdk.integration.helpers import (BOT_USER_ID, FEED_BOT_USERNAME,
12+
MSG_BOT_USERNAME, STREAM_ID,
13+
SYMPHONY_HOST, TEST_RSA_KEY,
14+
TEST_USER_ID, MessageListener,
15+
datafeed_bot_config,
16+
get_test_messages,
17+
messenger_bot_config, send_messages)
18+
19+
pytestmark =[
20+
pytest.mark.asyncio,
21+
pytest.mark.e2e,
22+
pytest.mark.skipif(
23+
not all(
24+
[
25+
STREAM_ID,
26+
MSG_BOT_USERNAME,
27+
FEED_BOT_USERNAME,
28+
SYMPHONY_HOST,
29+
TEST_RSA_KEY,
30+
TEST_USER_ID,
31+
BOT_USER_ID,
32+
]
33+
),
34+
reason="Required environment variables for integration tests are not set "
35+
"(STREAM_ID, MSG_BOT_USERNAME, FEED_BOT_USERNAME, SYMPHONY_HOST, TEST_RSA_KEY, TEST_USER_ID, BOT_USER_ID)",
36+
)
37+
]
38+
NUMBER_OF_MESSAGES = 3
39+
40+
41+
@pytest_asyncio.fixture
42+
async def bdk(messenger_bot_config):
43+
config = BdkConfigLoader.load_from_file(str(messenger_bot_config))
44+
async with SymphonyBdk(config) as bdk:
45+
yield bdk
46+
47+
48+
@pytest.mark.asyncio
49+
async def test_bot_read_write_messages(bdk):
50+
uuid = str(uuid4())
51+
# Given: test execution start time
52+
since = int((datetime.now() - timedelta(seconds=2)).timestamp()) * 1000
53+
# Given: BDK is initialized with config
54+
# When: messages are sent via bot
55+
await send_messages(bdk.messages(), STREAM_ID, since, uuid)
56+
# Then: messages are readable with the same bot
57+
messages = await get_test_messages(bdk, since, uuid)
58+
# Then: Expected messages are posted to the room
59+
assert sorted(messages) == [
60+
f"{uuid}-{i}-{since}" for i in range(NUMBER_OF_MESSAGES)
61+
]
62+
63+
64+
@pytest.mark.asyncio
65+
async def test_bot_creates_stream_add_delete_user(bdk):
66+
test_user = int(TEST_USER_ID)
67+
# Given: Stream bdk creates a room
68+
streams = bdk.streams()
69+
room_result = await streams.create_room(
70+
V3RoomAttributes(name="New fancy room", description="test room")
71+
)
72+
room_id = room_result.room_system_info.id
73+
# When: user is added to the room
74+
await streams.add_member_to_room(test_user, room_id)
75+
members = await streams.list_room_members(room_id)
76+
# Then: user is present in the room
77+
assert test_user in [m.id for m in members.value]
78+
# When: user is removed from the room
79+
await streams.remove_member_from_room(test_user, room_id)
80+
# Then: user is deleted from the room
81+
members_after_removal = await streams.list_room_members(room_id)
82+
assert test_user not in [m.id for m in members_after_removal.value]
83+
84+
85+
@pytest.mark.asyncio
86+
async def test_datafeed_receives_message(bdk: SymphonyBdk, datafeed_bot_config):
87+
"""
88+
Test is running 2 bdk instances at the same time.
89+
Data feed filters its own events so in order to see that feed is working
90+
Two parale bots are added
91+
92+
"""
93+
# Given: message listener is initialized with expected message id
94+
unique_id = str(uuid4())
95+
message_content = f"Message for datafeed test. ID: {unique_id}"
96+
listener = MessageListener(message_to_find=message_content)
97+
# Given: members are added to the room
98+
bdk.datafeed().subscribe(listener)
99+
await bdk.streams().add_member_to_room(int(BOT_USER_ID), STREAM_ID)
100+
datafeed_task = asyncio.create_task(bdk.datafeed().start())
101+
await asyncio.sleep(3)
102+
config = BdkConfigLoader.load_from_file(str(datafeed_bot_config))
103+
async with SymphonyBdk(config) as another_bot:
104+
# When: another bot instance sends a message to the needed room
105+
await another_bot.messages().send_message(STREAM_ID, message_content)
106+
try:
107+
# Then: particular message is received by datafeed instance
108+
await asyncio.wait_for(listener.message_received_event.wait(), timeout=300)
109+
except asyncio.TimeoutError:
110+
pytest.fail("Datafeed did not receive the message within the timeout period.")
111+
finally:
112+
await bdk.datafeed().stop()
113+
await datafeed_task
114+
bdk.datafeed().unsubscribe(listener)
115+
116+
assert listener.message_received_event.is_set()

tests/bdk/integration/helpers.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import asyncio
2+
import os
3+
import re
4+
5+
import pytest
6+
import pytest_asyncio
7+
import yaml
8+
from pathlib import Path
9+
10+
from symphony.bdk.core.config.loader import BdkConfigLoader
11+
from symphony.bdk.core.service.datafeed.real_time_event_listener import \
12+
RealTimeEventListener
13+
from symphony.bdk.core.symphony_bdk import SymphonyBdk
14+
from symphony.bdk.gen.agent_model.v4_initiator import V4Initiator
15+
from symphony.bdk.gen.agent_model.v4_message_sent import V4MessageSent
16+
17+
STREAM_ID = os.getenv("STREAM_ID")
18+
MSG_BOT_USERNAME = os.getenv("MSG_BOT_USERNAME")
19+
FEED_BOT_USERNAME = os.getenv("FEED_BOT_USERNAME")
20+
SYMPHONY_HOST = os.getenv("SYMPHONY_HOST")
21+
TEST_RSA_KEY = os.getenv("TEST_RSA_KEY")
22+
TEST_USER_ID = os.getenv("TEST_USER_ID")
23+
BOT_USER_ID = os.getenv("BOT_USER_ID")
24+
NUMBER_OF_MESSAGES = 3
25+
26+
27+
def generate_config(tmp_dir: Path, bot_username: str):
28+
key_path = tmp_dir / "key.pem"
29+
config_path = tmp_dir / "config.yaml"
30+
31+
bot_config_dict = {
32+
"host": SYMPHONY_HOST,
33+
"bot": {"username": bot_username, "privateKey": {"path": str(key_path)}},
34+
}
35+
36+
key_path.write_text(TEST_RSA_KEY)
37+
with config_path.open("w") as config_file:
38+
yaml.dump(bot_config_dict, config_file)
39+
40+
return config_path
41+
42+
43+
@pytest.fixture
44+
def messenger_bot_config(tmp_path_factory):
45+
tmp_dir = tmp_path_factory.mktemp("bdk_config_messenger")
46+
return generate_config(tmp_dir, MSG_BOT_USERNAME)
47+
48+
49+
@pytest.fixture
50+
def datafeed_bot_config(tmp_path_factory):
51+
tmp_dir = tmp_path_factory.mktemp("bdk_config_feed")
52+
return generate_config(tmp_dir, FEED_BOT_USERNAME)
53+
54+
55+
@pytest_asyncio.fixture
56+
async def bdk(messenger_bot_config):
57+
config = BdkConfigLoader.load_from_file(str(messenger_bot_config))
58+
async with SymphonyBdk(config) as bdk:
59+
yield bdk
60+
61+
62+
async def send_messages(messages, stream_id, since, uuid):
63+
for i in range(NUMBER_OF_MESSAGES):
64+
await messages.send_message(
65+
stream_id, f"<messageML><b>{uuid}-{i}-{since}</b></messageML>"
66+
)
67+
68+
69+
async def get_test_messages(bdk, since, uuid):
70+
messages = await bdk.messages().list_messages(STREAM_ID, since=since)
71+
cleaned_messages_text = [
72+
re.sub(r"<[^>]+>", " ", msg["message"]).strip() for msg in messages
73+
]
74+
return list(
75+
filter(
76+
lambda msg: msg.startswith(uuid),
77+
cleaned_messages_text,
78+
)
79+
)
80+
81+
82+
class MessageListener(RealTimeEventListener):
83+
"""A simple listener to capture a specific message from the datafeed."""
84+
85+
def __init__(self, message_to_find: str):
86+
self._message_to_find = message_to_find
87+
self.message_received_event = asyncio.Event()
88+
89+
async def on_message_sent(self, initiator: V4Initiator, event: V4MessageSent):
90+
message_text = re.sub(r"<[^>]+>", "", event.message.message).strip()
91+
if self._message_to_find in message_text:
92+
self.message_received_event.set()

0 commit comments

Comments
 (0)