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