Skip to content

Commit 6ec6075

Browse files
authored
Support for local SMS server for lab testing (#10)
* feat: twilio endpoints * feat: dockerfile for twilio shim * feat: support for automatic twilio shim * build: support for building twilio-shim * feat: addn suport to sms tools and testing * fix: double api key print + cleanup labs on target run * ci: fix source wording
1 parent 147c592 commit 6ec6075

File tree

17 files changed

+431
-31
lines changed

17 files changed

+431
-31
lines changed

Makefile

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,20 @@ patch_templates:
2020
rm -rf $$VENV
2121

2222
tests:
23-
uv run pytest -rs -m e2e
23+
@set -e; \
24+
uv run pytest -rs -v -s -m e2e || exit 0; \
25+
$(MAKE) clean-tests
26+
2427

2528
clean-tests:
26-
@source .venv/bin/activate && \
29+
@. .venv/bin/activate && \
2730
appwrite-lab stop test-lab
2831

29-
.PHONY: patch_templates tests clean-tests build_appwrite_cli build_appwrite_playwright
32+
build_twilio_shim:
33+
cd twilio-shim && docker buildx build -t docker.io/syntaxsdev/twilio-shim:latest -f Dockerfile . --load
34+
35+
push_twilio_shim:
36+
cd twilio-shim && docker buildx build -t docker.io/syntaxsdev/twilio-shim:latest -f Dockerfile . --load --push
37+
38+
39+
.PHONY: patch_templates tests clean-tests build_appwrite_cli build_appwrite_playwright build_twilio_shim push_twilio_shim

appwrite_lab/_orchestrator.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,11 @@ def get_running_pods_by_project(self, name: str):
132132
}
133133
return pods
134134

135-
def _deploy_service(
135+
def _deploy_compose_service(
136136
self,
137137
project: str,
138-
template_path: Path,
138+
template_paths: list[Path] | Path,
139+
env_file: Path | None = None,
139140
env_vars: dict[str, str] = {},
140141
extra_args: list[str] = [],
141142
):
@@ -144,22 +145,29 @@ def _deploy_service(
144145
145146
Args:
146147
project: The name of the project to deploy the service to.
147-
template_path: The path to the template to use for the service.
148+
template_paths: The path to the template to use for the service.
148149
env_vars: The environment variables to set.
150+
env_file: The path to the environment file to use for the service.
149151
extra_args: Extra arguments to pass to the compose command.
150152
"""
151-
new_env = {**os.environ, **env_vars}
153+
if isinstance(template_paths, Path):
154+
template_paths = [template_paths]
155+
156+
file_overlays = []
157+
for path in template_paths:
158+
file_overlays.extend(["-f", str(path)])
159+
152160
cmd = [
153161
*self.compose,
154-
"-f",
155-
template_path,
162+
*file_overlays,
163+
*(["--env-file", str(env_file)] if env_file else []),
156164
"-p",
157165
project,
158166
*extra_args,
159167
"up",
160168
"-d",
161169
]
162-
return self._run_cmd_safely(cmd, envs=new_env)
170+
return self._run_cmd_safely(cmd, envs=env_vars)
163171

164172
def deploy_appwrite_lab(
165173
self,
@@ -188,24 +196,38 @@ def deploy_appwrite_lab(
188196
error=True, message=f"Lab '{name}' already deployed.", data=None
189197
)
190198
converted_version = version.replace(".", "_")
199+
200+
# Gather paths
191201
template_path = (
192202
Path(__file__).parent
193203
/ "templates"
194204
/ f"docker_compose_{converted_version}.yml"
195205
)
206+
207+
twilio_shim_path = (
208+
Path(__file__).parent
209+
/ "templates"
210+
/ "extras"
211+
/ "twilio-shim"
212+
/ "docker_compose.yml"
213+
)
214+
215+
env_file = Path(__file__).parent / "templates" / "environment" / "dotenv"
196216
if not template_path.exists():
197217
return Response(
198218
error=True, message=f"Template {version} not found.", data=None
199219
)
200220

201-
# Override default env vars
221+
# Override default env vars for port
202222
env_vars = get_env_vars(self.default_env_vars)
203223
if port != 80:
204224
env_vars["_APP_PORT"] = str(port)
205225

206226
# What actually deploys the initial appwrite service
207-
cmd_res = self._deploy_service(
208-
project=name, template_path=template_path, env_vars=env_vars
227+
cmd_res = self._deploy_compose_service(
228+
project=name,
229+
template_paths=[template_path, twilio_shim_path],
230+
env_file=env_file,
209231
)
210232

211233
# Deploy mail server (mailpit)
@@ -216,9 +238,7 @@ def deploy_appwrite_lab(
216238
/ "mailpit"
217239
/ "docker_compose.yml"
218240
)
219-
self._deploy_service(
220-
project=name, template_path=mailpit_template_path, env_vars={}
221-
)
241+
self._deploy_compose_service(project=name, template_paths=mailpit_template_path)
222242
# if CLI, will throw error in actual Response object
223243
if type(cmd_res) is Response and cmd_res.error:
224244
return cmd_res
@@ -246,6 +266,8 @@ def deploy_appwrite_lab(
246266
version=version,
247267
url=url,
248268
**_kwargs,
269+
sms_shim_url="https://localhost:4443",
270+
mailpit_url="http://localhost:8025",
249271
)
250272

251273
lab.generate_missing_config()

appwrite_lab/_state.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import json
22
import os
3-
from pathlib import Path
43

54
from .utils import get_state_path
65

appwrite_lab/labs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def create_api_key(
152152
return Response(
153153
message=f"API key created for {project_name}",
154154
data=api_key.data,
155-
_print_data=True,
155+
_print_data=False,
156156
)
157157

158158
def stop(self, name: str):

appwrite_lab/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ class Lab(_BaseClass):
5151
default_factory=lambda: {"default": Project(None, None, None)}
5252
)
5353
_file: str = field(default="")
54+
sms_shim_url: str = field(default="")
55+
mailpit_url: str = field(default="")
5456

5557
def generate_missing_config(self):
5658
"""Generate missing data config with random values."""

appwrite_lab/templates/environment/dotenv

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ _APP_SMTP_PORT=1025
5555
_APP_SMTP_SECURE=
5656
_APP_SMTP_USERNAME=
5757
_APP_SMTP_PASSWORD=
58-
_APP_SMS_PROVIDER=
59-
_APP_SMS_FROM=
58+
_APP_SMS_PROVIDER=sms://twilio:shim@twilio
59+
_APP_SMS_FROM=+15555555555
6060
_APP_STORAGE_LIMIT=30000000
6161
_APP_STORAGE_PREVIEW_LIMIT=20000000
6262
_APP_STORAGE_ANTIVIRUS=disabled
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
x-sms-env: &sms_env
2+
_APP_SMS_PROVIDER: ${_APP_SMS_PROVIDER:-sms://twilio:shim@twilio}
3+
_APP_SMS_FROM: ${_APP_SMS_FROM:-+15555555555}
4+
5+
networks:
6+
appwrite:
7+
name: appwrite
8+
9+
volumes:
10+
fake_twilio_certs: {}
11+
php_extra_conf: {}
12+
13+
services:
14+
# one-shot: write php ini into the named volume
15+
php-ini-init:
16+
image: busybox:1.36
17+
volumes:
18+
- php_extra_conf:/out
19+
command: >
20+
sh -lc 'printf "%s\n%s\n"
21+
"curl.cainfo=/usr/local/share/ca-certificates/dev-rootCA.pem"
22+
"openssl.cafile=/usr/local/share/ca-certificates/dev-rootCA.pem"
23+
> /out/zz-dev-ca.ini'
24+
25+
twilio-shim:
26+
image: syntaxsdev/twilio-shim:latest
27+
volumes:
28+
- fake_twilio_certs:/certs
29+
ports: ["4443:443"] # dev-only host access
30+
networks:
31+
appwrite:
32+
aliases: [api.twilio.com]
33+
restart: unless-stopped
34+
healthcheck:
35+
test: ["CMD","curl","-k","https://localhost:443/"]
36+
interval: 5s
37+
timeout: 3s
38+
retries: 5
39+
40+
appwrite:
41+
depends_on:
42+
php-ini-init: { condition: service_completed_successfully }
43+
environment:
44+
<<: *sms_env
45+
PHP_INI_SCAN_DIR: /usr/local/etc/php/conf.d:/opt/php-extra-conf.d
46+
volumes:
47+
- fake_twilio_certs:/usr/local/share/ca-certificates:ro
48+
- php_extra_conf:/opt/php-extra-conf.d:ro
49+
networks: [appwrite]
50+
51+
appwrite-worker-messaging:
52+
depends_on:
53+
php-ini-init: { condition: service_completed_successfully }
54+
twilio-shim: { condition: service_healthy }
55+
environment:
56+
<<: *sms_env
57+
PHP_INI_SCAN_DIR: /usr/local/etc/php/conf.d:/opt/php-extra-conf.d
58+
volumes:
59+
- fake_twilio_certs:/usr/local/share/ca-certificates:ro
60+
- php_extra_conf:/opt/php-extra-conf.d:ro
61+
networks: [appwrite]

appwrite_lab/tools/sms.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from appwrite_lab.models import Lab
2+
from httpx import AsyncClient
3+
4+
5+
class SMS:
6+
def __init__(self, lab: Lab):
7+
self.url = lab.sms_shim_url
8+
9+
async def get_messages(self):
10+
async with AsyncClient(verify=False) as client:
11+
response = await client.get(f"{self.url}/inbox")
12+
return response.json().get("messages")
13+
14+
async def clear_messages(self):
15+
async with AsyncClient(verify=False) as client:
16+
response = await client.post(f"{self.url}/clear")
17+
return response.json().get("ok")

pyproject.toml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "appwrite-lab"
7-
version = "0.1.2"
7+
version = "0.1.3"
88
description = "Zero-click Appwrite test environments."
99
readme = "README.md"
1010
requires-python = ">=3.11"
11-
dependencies = ["playwright", "typer>=0.16.0", "python-dotenv>=1.1.0", "pytest>=8.4.1"]
11+
dependencies = [
12+
"playwright",
13+
"typer>=0.16.0",
14+
"python-dotenv>=1.1.0",
15+
"pytest>=8.4.1",
16+
"httpx>=0.28.1",
17+
"pytest-asyncio>=1.1.0",
18+
]
1219
license = "MIT"
1320

1421
[tool.setuptools]
@@ -23,6 +30,5 @@ appwrite-lab = "appwrite_lab.cli.entry:app"
2330
awlab = "appwrite_lab.cli.entry:app"
2431

2532
[tool.pytest.ini_options]
26-
markers = [
27-
"e2e: mark test as e2e",
28-
]
33+
markers = ["e2e: mark test as e2e"]
34+
asyncio_mode = "auto"

tests/conftest.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,20 @@
22
from pathlib import Path
33
from appwrite_lab.models import Lab
44
from appwrite_lab.test_suite import lab_svc, appwrite_file, lab_config, lab
5+
from appwrite_lab.tools.sms import SMS
56

67

78
@pytest.fixture(scope="session")
89
def appwrite_file_path():
910
return Path(__file__).parent / "data" / "appwrite.json"
10-
11+
12+
13+
@pytest.fixture(scope="session")
14+
def sms(lab: Lab):
15+
return SMS(lab)
16+
17+
18+
@pytest.fixture(autouse=True)
19+
async def clear_sms(sms: SMS):
20+
yield
21+
await sms.clear_messages()

0 commit comments

Comments
 (0)