pylynqa 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pylynqa/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ """Python client package for the Lynqa API."""
2
+
3
+ __all__ = [
4
+ "BASE_URL",
5
+ "Attachment",
6
+ "CreateAttachment",
7
+ "CreateTestStep",
8
+ "LynqaClient",
9
+ "LynqaClientError",
10
+ "TestData",
11
+ "TestRunContext",
12
+ "TestRunsFilter",
13
+ "TestStep",
14
+ "TextualData",
15
+ "TimePeriod",
16
+ ]
17
+
18
+ from .client import BASE_URL, LynqaClient
19
+ from .models import (
20
+ Attachment,
21
+ CreateAttachment,
22
+ CreateTestStep,
23
+ LynqaClientError,
24
+ TestData,
25
+ TestRunContext,
26
+ TestRunsFilter,
27
+ TestStep,
28
+ TextualData,
29
+ TimePeriod,
30
+ )
@@ -0,0 +1,222 @@
1
+ """Acceptance tests for the Lynqa API — no mocking, real HTTP calls.
2
+
3
+ Requires the environment variable ``LYNQA_API_KEY`` to be set.
4
+
5
+ Run with::
6
+
7
+ LYNQA_API_KEY=lq_live_xxx pytest projects/pylynqa/src/pylynqa/atest -v
8
+ """
9
+
10
+ # ruff: file-ignore[undocumented-public-method,no-self-use]
11
+ import os
12
+ import time
13
+ from datetime import datetime, timezone
14
+
15
+ import pytest
16
+ import responses
17
+
18
+ import pylynqa
19
+ from pylynqa import LynqaClient, TestRunsFilter, TimePeriod
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Constants
23
+ # ---------------------------------------------------------------------------
24
+
25
+ POLL_TIMEOUT_S = 300
26
+ TERMINAL_STATUSES = {"success", "failed", "error", "stopped", "not_run"}
27
+
28
+ TEST_RUN_NAME = f"Automatic test — Google search for lynqa - {datetime.now(tz=timezone.utc).isoformat()}"
29
+ GHERKIN_SCENARIO = (
30
+ "Given go to https://www.google.com/\n"
31
+ "When I look at the search input\n"
32
+ "Then the search input exists\n"
33
+ "When I search for 'lynqa'\n"
34
+ "Then several results are displayed"
35
+ "Then the first results display 'Smartesting'"
36
+ )
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Fixtures
41
+ # ---------------------------------------------------------------------------
42
+
43
+
44
+ @pytest.fixture(scope="module")
45
+ def client() -> LynqaClient:
46
+ """Instantiate a LynqaClient object."""
47
+ api_key = os.environ.get("LYNQA_API_KEY")
48
+ if not api_key:
49
+ pytest.skip("LYNQA_API_KEY environment variable is not set")
50
+ return LynqaClient(api_key=api_key)
51
+
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Helpers
55
+ # ---------------------------------------------------------------------------
56
+
57
+
58
+ def _wait_for_completion(client: LynqaClient, run_id: str) -> None:
59
+ """Poll status until the run reaches a terminal state or the timeout expires."""
60
+ deadline = time.monotonic() + POLL_TIMEOUT_S
61
+ while time.monotonic() < deadline:
62
+ result = client.get_test_run_status(run_id)
63
+ if result["status"] in TERMINAL_STATUSES:
64
+ return
65
+ time.sleep(5)
66
+ pytest.fail(f"Test run {run_id!r} did not complete within {POLL_TIMEOUT_S}s")
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Test scenario
71
+ # ---------------------------------------------------------------------------
72
+
73
+
74
+ class TestApiScenario:
75
+ """End-to-end scenario exercising all major API operations in order."""
76
+
77
+ test_run_id: str = ""
78
+
79
+ _INDEPENDENT_TESTS = (
80
+ "test_health_live",
81
+ "test_health_ready",
82
+ "test_get_account_credits",
83
+ "test_get_purchases",
84
+ "test_get_credit_ledger",
85
+ "test_create_gherkin_test_run",
86
+ )
87
+
88
+ @pytest.fixture(autouse=True) # ruff: ignore[pytest-fixture-autouse]
89
+ def setup(self, request):
90
+ """Set up all tests."""
91
+ # Disable HTTP mocking for all requests to the real API base URL.
92
+ responses.add_passthru(pylynqa.BASE_URL)
93
+ # Skip certain tests if test run not created
94
+ if request.node.name not in self._INDEPENDENT_TESTS and not TestApiScenario.test_run_id:
95
+ pytest.skip("skipped: test_create_gherkin_test_run did not produce a test_run_id")
96
+
97
+ def test_health_live(self, client):
98
+ # Act
99
+ result = client.health_live()
100
+ # Assert
101
+ assert result["status"] == "ok"
102
+
103
+ def test_health_ready(self, client):
104
+ # Act
105
+ result = client.health_ready()
106
+ # Assert
107
+ assert result["ready"]
108
+
109
+ def test_get_account_credits(self, client):
110
+ # Act
111
+ available_credits = client.get_test_execution_credits()
112
+ # Assert
113
+ assert isinstance(available_credits, int)
114
+ assert available_credits >= 0
115
+
116
+ def test_get_purchases(self, client):
117
+ # Act
118
+ purchases = client.get_purchases()
119
+ # Assert
120
+ assert isinstance(purchases, list)
121
+
122
+ def test_get_credit_ledger(self, client):
123
+ # Act
124
+ ledger = client.get_credit_ledger()
125
+ # Assert
126
+ assert isinstance(ledger, list)
127
+ assert len(ledger) > 0
128
+ assert ledger[0]["balanceAfter"] != 0, "Should have at least one credit ledger"
129
+
130
+ def test_create_gherkin_test_run(self, client):
131
+ # Act
132
+ TestApiScenario.test_run_id = client.add_gherkin_test_run(
133
+ url="https://www.google.com/",
134
+ scenario=GHERKIN_SCENARIO,
135
+ name=TEST_RUN_NAME,
136
+ )
137
+ # Assert
138
+ assert TestApiScenario.test_run_id is not None
139
+ if not isinstance(TestApiScenario.test_run_id, str):
140
+ pytest.warns(UserWarning, match="test_run_id is not a str")
141
+ else:
142
+ assert len(TestApiScenario.test_run_id) > 0
143
+
144
+ def test_get_test_run_status(self, client):
145
+ # Act
146
+ result = client.get_test_run_status(TestApiScenario.test_run_id)
147
+ # Assert
148
+ assert "status" in result
149
+ assert result["status"] in {"waiting", "running"} | TERMINAL_STATUSES
150
+
151
+ def test_get_test_run_full_status(self, client):
152
+ # Arrange
153
+ _wait_for_completion(client, TestApiScenario.test_run_id)
154
+ # Act
155
+ result = client.get_test_run_full_status(TestApiScenario.test_run_id)
156
+ # Assert
157
+ assert "status" in result
158
+ assert result["status"] in TERMINAL_STATUSES
159
+ assert "stepStatuses" in result
160
+ assert "steps" in result
161
+ assert len(result["stepStatuses"]) == len(result["steps"])
162
+ assert len(result["stepStatuses"]) > 0
163
+ assert result["type"] == "gherkin"
164
+
165
+ def test_get_test_run(self, client):
166
+ # Act
167
+ result = client.get_test_run(TestApiScenario.test_run_id)
168
+ # Assert
169
+ assert result["name"] == TEST_RUN_NAME
170
+ assert result["url"] == "https://www.google.com/"
171
+ assert result["type"] == "gherkin"
172
+ assert "steps" in result
173
+
174
+ def test_get_step_info(self, client):
175
+ # Arrange
176
+ status_result = client.get_test_run_full_status(TestApiScenario.test_run_id)
177
+ if not status_result.get("stepStatuses"):
178
+ pytest.skip("No steps available in full status")
179
+ # Act
180
+ result = client.get_test_run_step_status(TestApiScenario.test_run_id, 0)
181
+ # Assert
182
+ assert "status" in result
183
+ assert "commands" in result
184
+ assert "assertionsReport" in result
185
+
186
+ def test_get_screenshot_info(self, client):
187
+ # Arrange
188
+ status = client.get_test_run_status(TestApiScenario.test_run_id)
189
+ screenshot_id = status.get("initialReport", {}).get("screenshot")
190
+ if not screenshot_id:
191
+ pytest.skip("No initial screenshot available")
192
+ # Act
193
+ data = client.get_screenshot(TestApiScenario.test_run_id, screenshot_id)
194
+ # Assert
195
+ assert isinstance(data, str)
196
+ assert len(data) > 0
197
+
198
+ def test_query_test_runs(self, client):
199
+ # Act
200
+ result = client.query_test_runs(filters=TestRunsFilter(relative_period=TimePeriod(count=5, unit="m")))
201
+ # Assert
202
+ assert "testRuns" in result
203
+ assert len(result["testRuns"]) > 0
204
+ if not isinstance(TestApiScenario.test_run_id, str):
205
+ pytest.warns(UserWarning, match="test_run_id is not a str")
206
+ assert result["testRuns"][-1].get("id", 0) == str(
207
+ TestApiScenario.test_run_id
208
+ ) # Remove str casting when API ready
209
+
210
+ def test_stop_test_runs(self, client):
211
+ # Act
212
+ result = client.stop_test_runs([TestApiScenario.test_run_id])
213
+ # Assert
214
+ assert "stoppedTestRunIds" in result
215
+ assert isinstance(result["stoppedTestRunIds"], list)
216
+
217
+ def test_delete_test_run(self, client):
218
+ # Act
219
+ client.delete_test_run(TestApiScenario.test_run_id)
220
+ # Assert
221
+ with pytest.raises(pylynqa.LynqaClientError):
222
+ client.get_test_run_status(TestApiScenario.test_run_id)