gowalk-cicd 1.0.0
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.
- package/CLAUDE.md +94 -0
- package/README.md +417 -0
- package/action/.daemux-version +1 -0
- package/action/MIGRATION.md +232 -0
- package/action/action.yml +973 -0
- package/action/prompts/generate_dependent_fields.prompt.yml +78 -0
- package/action/prompts/generate_descriptions.prompt.yml +59 -0
- package/action/scripts/app_context_scanner.py +239 -0
- package/action/scripts/asc_build_history.py +284 -0
- package/action/scripts/asc_common.py +246 -0
- package/action/scripts/asc_metadata_applier.py +368 -0
- package/action/scripts/asc_metadata_detector.py +250 -0
- package/action/scripts/asc_version_create.py +365 -0
- package/action/scripts/asc_version_fetch.py +274 -0
- package/action/scripts/asc_version_reuse.py +177 -0
- package/action/scripts/auto_detect.py +391 -0
- package/action/scripts/autoupdate_check.sh +118 -0
- package/action/scripts/cert_factory.py +190 -0
- package/action/scripts/cfg_io.py +27 -0
- package/action/scripts/cfg_resolve.py +147 -0
- package/action/scripts/commit_bot_changes.sh +82 -0
- package/action/scripts/creds_store.py +328 -0
- package/action/scripts/keychain.py +103 -0
- package/action/scripts/lookup_app_id.py +51 -0
- package/action/scripts/manage_marketing_version.py +337 -0
- package/action/scripts/metadata_constants.py +102 -0
- package/action/scripts/mmv_decide_create.py +181 -0
- package/action/scripts/mmv_floor_check.py +265 -0
- package/action/scripts/next_build_number.py +195 -0
- package/action/scripts/pbxproj_editor.py +220 -0
- package/action/scripts/prepare_signing.py +223 -0
- package/action/scripts/profile_io.py +64 -0
- package/action/scripts/profile_manager.py +307 -0
- package/action/scripts/read_config.py +315 -0
- package/action/scripts/resolve_marketing_version.py +173 -0
- package/action/scripts/set_app_store_whats_new.py +317 -0
- package/action/scripts/team_resolver.py +130 -0
- package/action/scripts/test_app_context_scanner.py +167 -0
- package/action/scripts/test_asc_build_history.py +221 -0
- package/action/scripts/test_asc_builds_prerelease.py +219 -0
- package/action/scripts/test_asc_common_timeouts.py +121 -0
- package/action/scripts/test_asc_metadata_applier.py +850 -0
- package/action/scripts/test_asc_metadata_detector.py +445 -0
- package/action/scripts/test_auto_detect.py +432 -0
- package/action/scripts/test_cfg_resolve_credentials.py +48 -0
- package/action/scripts/test_compute_next_version.py +95 -0
- package/action/scripts/test_compute_next_version_patch_backcompat.py +41 -0
- package/action/scripts/test_compute_next_version_strictness.py +67 -0
- package/action/scripts/test_fetch_versions.py +163 -0
- package/action/scripts/test_ground_truth_floor.py +104 -0
- package/action/scripts/test_manage_marketing_version.py +134 -0
- package/action/scripts/test_manage_marketing_version_autoroll.py +149 -0
- package/action/scripts/test_manage_marketing_version_floor.py +170 -0
- package/action/scripts/test_manage_marketing_version_match.py +187 -0
- package/action/scripts/test_mmv_409_classifier.py +231 -0
- package/action/scripts/test_mmv_autobump_persist_gate.py +131 -0
- package/action/scripts/test_mmv_decide_create.py +119 -0
- package/action/scripts/test_mmv_floor_check.py +158 -0
- package/action/scripts/test_mmv_floor_crosscheck.py +132 -0
- package/action/scripts/test_mmv_helpers.py +57 -0
- package/action/scripts/test_next_build_number.py +174 -0
- package/action/scripts/test_read_config_auto_detect.py +257 -0
- package/action/scripts/test_resolve_marketing_version.py +298 -0
- package/action/scripts/test_reuse_stale_editable.py +182 -0
- package/action/scripts/test_set_app_store_whats_new.py +71 -0
- package/action/scripts/test_team_fallback_wiring.py +157 -0
- package/action/scripts/test_team_resolver.py +249 -0
- package/action/scripts/tests_common.py +167 -0
- package/action/scripts/version_utils.py +373 -0
- package/android-action/.daemux-version +1 -0
- package/android-action/action.yml +92 -0
- package/android-action/scripts/android_config.py +130 -0
- package/android-action/scripts/bitrise_deploy.py +160 -0
- package/android-action/scripts/find_bundle.py +25 -0
- package/android-action/scripts/play_preflight.py +69 -0
- package/android-action/scripts/resolve_android.py +70 -0
- package/android-action/scripts/sign_bundle.py +70 -0
- package/android-action/scripts/test_android_config.py +97 -0
- package/android-action/scripts/test_bitrise_deploy.py +124 -0
- package/android-action/scripts/test_sign_bundle.py +82 -0
- package/bin/cli.mjs +63 -0
- package/package.json +57 -0
- package/src/install.mjs +208 -0
- package/templates/deploy.yml +150 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests for asc_build_history.fetch_prerelease_versions and the
|
|
4
|
+
combined-floor wiring that consumes both /preReleaseVersions and
|
|
5
|
+
/builds?include=preReleaseVersion.
|
|
6
|
+
|
|
7
|
+
Why these live in their own module: they exercise asc_build_history
|
|
8
|
+
directly (not via mmv seams), and they're large enough that bundling
|
|
9
|
+
them with the other floor-check tests pushed test_mmv_floor_check.py
|
|
10
|
+
over the 400-line cap. The /builds?include=preReleaseVersion path is
|
|
11
|
+
covered separately in ``test_asc_builds_prerelease.py``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import io
|
|
17
|
+
import sys
|
|
18
|
+
import unittest
|
|
19
|
+
from contextlib import redirect_stderr
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from unittest import mock
|
|
22
|
+
|
|
23
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
24
|
+
|
|
25
|
+
import manage_marketing_version as mmv # noqa: E402
|
|
26
|
+
import asc_version_fetch # noqa: E402
|
|
27
|
+
import asc_build_history # noqa: E402
|
|
28
|
+
|
|
29
|
+
from tests_common import _FakeResponse, _mk_prerelease_entry, _v # noqa: E402
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PrereleaseFetchShapeTests(unittest.TestCase):
|
|
33
|
+
"""The /preReleaseVersions fetcher must use the correct query shape and
|
|
34
|
+
paginate."""
|
|
35
|
+
|
|
36
|
+
def test_prerelease_fetch_client_side_platform_filter(self):
|
|
37
|
+
seen_params: list[dict | None] = []
|
|
38
|
+
payload = {
|
|
39
|
+
"data": [
|
|
40
|
+
{
|
|
41
|
+
"id": "pr-ios",
|
|
42
|
+
"attributes": {"version": "1.0.1", "platform": "IOS"},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "pr-mac",
|
|
46
|
+
"attributes": {"version": "1.0.2", "platform": "MAC_OS"},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "pr-null",
|
|
50
|
+
"attributes": {"version": "1.0.3"},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
"links": {},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def fake_request(method, path, token, *, params=None, **_kw):
|
|
57
|
+
seen_params.append(params)
|
|
58
|
+
return _FakeResponse(payload)
|
|
59
|
+
|
|
60
|
+
buf = io.StringIO()
|
|
61
|
+
with mock.patch.object(
|
|
62
|
+
asc_build_history, "request", side_effect=fake_request), \
|
|
63
|
+
redirect_stderr(buf):
|
|
64
|
+
got = asc_build_history.fetch_prerelease_versions("111", "tok")
|
|
65
|
+
|
|
66
|
+
self.assertTrue(seen_params)
|
|
67
|
+
first = seen_params[0] or {}
|
|
68
|
+
self.assertNotIn("filter[platform]", first)
|
|
69
|
+
self.assertEqual(first.get("limit"), 200)
|
|
70
|
+
self.assertEqual(sorted(got), ["1.0.1", "1.0.3"])
|
|
71
|
+
stderr = buf.getvalue()
|
|
72
|
+
self.assertIn("version=1.0.1", stderr)
|
|
73
|
+
self.assertIn("platform=IOS", stderr)
|
|
74
|
+
self.assertIn("pr-mac", stderr)
|
|
75
|
+
self.assertIn("MAC_OS", stderr)
|
|
76
|
+
self.assertIn("not-ios", stderr)
|
|
77
|
+
self.assertIn("pr-null", stderr)
|
|
78
|
+
|
|
79
|
+
def test_prerelease_fetch_combined_floor_only_counts_ios(self):
|
|
80
|
+
payload = {
|
|
81
|
+
"data": [
|
|
82
|
+
{
|
|
83
|
+
"id": "pr-ios",
|
|
84
|
+
"attributes": {"version": "1.0.4", "platform": "IOS"},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "pr-mac-high",
|
|
88
|
+
"attributes": {"version": "9.9.9", "platform": "MAC_OS"},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
"links": {},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def fake_request(method, path, token, *, params=None, **_kw):
|
|
95
|
+
return _FakeResponse(payload)
|
|
96
|
+
|
|
97
|
+
with mock.patch.object(
|
|
98
|
+
asc_build_history, "request", side_effect=fake_request):
|
|
99
|
+
floor, _ = mmv.get_combined_floor(
|
|
100
|
+
"111", "tok", appstore_versions=[],
|
|
101
|
+
)
|
|
102
|
+
self.assertEqual(floor, "1.0.4")
|
|
103
|
+
|
|
104
|
+
def test_prerelease_fetch_paginated(self):
|
|
105
|
+
seen_params: list[dict | None] = []
|
|
106
|
+
page1 = {
|
|
107
|
+
"data": [
|
|
108
|
+
_mk_prerelease_entry("1.0.1", pid="pr-101"),
|
|
109
|
+
_mk_prerelease_entry("1.0.2", pid="pr-102"),
|
|
110
|
+
],
|
|
111
|
+
"links": {
|
|
112
|
+
"next": "https://api.appstoreconnect.apple.com/v1/apps/111/"
|
|
113
|
+
"preReleaseVersions?cursor=PAGE2",
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
page2 = {
|
|
117
|
+
"data": [_mk_prerelease_entry("1.0.4", pid="pr-104")],
|
|
118
|
+
"links": {},
|
|
119
|
+
}
|
|
120
|
+
responses = [_FakeResponse(page1), _FakeResponse(page2)]
|
|
121
|
+
|
|
122
|
+
def fake_request(method, path, token, *, params=None, **_kw):
|
|
123
|
+
seen_params.append(params)
|
|
124
|
+
return responses.pop(0)
|
|
125
|
+
|
|
126
|
+
with mock.patch.object(
|
|
127
|
+
asc_build_history, "request", side_effect=fake_request):
|
|
128
|
+
got = asc_build_history.fetch_prerelease_versions("111", "tok")
|
|
129
|
+
|
|
130
|
+
self.assertEqual(sorted(got), ["1.0.1", "1.0.2", "1.0.4"])
|
|
131
|
+
self.assertEqual(len(responses), 0)
|
|
132
|
+
for p in seen_params:
|
|
133
|
+
if p is None:
|
|
134
|
+
continue
|
|
135
|
+
self.assertNotIn("filter[platform]", p)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class PlatformNullDefensiveInclusionTests(unittest.TestCase):
|
|
139
|
+
"""Null/missing platform must be INCLUDED defensively."""
|
|
140
|
+
|
|
141
|
+
def test_platform_null_record_included_defensively(self):
|
|
142
|
+
payload = {
|
|
143
|
+
"data": [
|
|
144
|
+
{
|
|
145
|
+
"id": "pr-null",
|
|
146
|
+
"attributes": {"version": "1.0.9"},
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
"links": {},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
def fake_request(method, path, token, *, params=None, **_kw):
|
|
153
|
+
return _FakeResponse(payload)
|
|
154
|
+
|
|
155
|
+
buf = io.StringIO()
|
|
156
|
+
with mock.patch.object(
|
|
157
|
+
asc_build_history, "request", side_effect=fake_request), \
|
|
158
|
+
redirect_stderr(buf):
|
|
159
|
+
versions = asc_build_history.fetch_prerelease_versions(
|
|
160
|
+
"111", "tok",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
self.assertEqual(versions, ["1.0.9"])
|
|
164
|
+
stderr = buf.getvalue()
|
|
165
|
+
self.assertIn("pr-null", stderr)
|
|
166
|
+
self.assertIn("null", stderr.lower())
|
|
167
|
+
|
|
168
|
+
def test_platform_explicit_non_ios_still_excluded(self):
|
|
169
|
+
payload = {
|
|
170
|
+
"data": [
|
|
171
|
+
{
|
|
172
|
+
"id": "pr-mac",
|
|
173
|
+
"attributes": {"version": "9.9.9", "platform": "MAC_OS"},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"id": "pr-tv",
|
|
177
|
+
"attributes": {"version": "8.8.8", "platform": "TV_OS"},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"id": "pr-vision",
|
|
181
|
+
"attributes": {"version": "7.7.7", "platform": "VISION_OS"},
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
"links": {},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
def fake_request(method, path, token, *, params=None, **_kw):
|
|
188
|
+
return _FakeResponse(payload)
|
|
189
|
+
|
|
190
|
+
with mock.patch.object(
|
|
191
|
+
asc_build_history, "request", side_effect=fake_request):
|
|
192
|
+
versions = asc_build_history.fetch_prerelease_versions(
|
|
193
|
+
"111", "tok",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
self.assertEqual(versions, [])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class CombinedFloorBuildsViaSourceTests(unittest.TestCase):
|
|
200
|
+
"""Combined floor per_source must include all three sources."""
|
|
201
|
+
|
|
202
|
+
def test_per_source_breakdown_includes_buildsViaPreRelease(self):
|
|
203
|
+
with mock.patch.object(
|
|
204
|
+
asc_build_history, "fetch_prerelease_versions",
|
|
205
|
+
return_value=["1.0.4"]), \
|
|
206
|
+
mock.patch.object(
|
|
207
|
+
asc_build_history, "fetch_builds_prerelease_versions",
|
|
208
|
+
return_value=["1.0.7"]):
|
|
209
|
+
floor, per_source = asc_version_fetch.get_combined_floor(
|
|
210
|
+
"111", "tok", mmv.semver_tuple,
|
|
211
|
+
appstore_versions=[_v("1.0", "READY_FOR_SALE")],
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
self.assertEqual(floor, "1.0.7")
|
|
215
|
+
self.assertEqual(per_source.get("appStoreVersions"), "1.0")
|
|
216
|
+
self.assertEqual(per_source.get("preReleaseVersions"), "1.0.4")
|
|
217
|
+
self.assertEqual(per_source.get("buildsViaPreRelease"), "1.0.7")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
unittest.main()
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests for asc_build_history.fetch_builds_prerelease_versions, the
|
|
4
|
+
/v1/builds?include=preReleaseVersion path that powers the
|
|
5
|
+
buildsViaPreRelease component of the combined floor.
|
|
6
|
+
|
|
7
|
+
This module owns one large class, ``BuildsPreReleaseRelationshipTests``,
|
|
8
|
+
which was lifted out of ``test_asc_build_history.py`` purely to keep
|
|
9
|
+
that file under the 400-line cap. Both modules share helper imports
|
|
10
|
+
from ``tests_common``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import io
|
|
16
|
+
import sys
|
|
17
|
+
import unittest
|
|
18
|
+
from contextlib import redirect_stderr
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from unittest import mock
|
|
21
|
+
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
23
|
+
|
|
24
|
+
import manage_marketing_version as mmv # noqa: E402
|
|
25
|
+
import asc_version_fetch # noqa: E402
|
|
26
|
+
import asc_build_history # noqa: E402
|
|
27
|
+
|
|
28
|
+
from tests_common import _FakeResponse, _v # noqa: E402
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_data_entry(build_id: str, build_version: str, pr_id: str) -> dict:
|
|
32
|
+
"""One /v1/builds.data entry with a preReleaseVersion relationship."""
|
|
33
|
+
return {
|
|
34
|
+
"id": build_id,
|
|
35
|
+
"type": "builds",
|
|
36
|
+
"attributes": {"version": build_version},
|
|
37
|
+
"relationships": {
|
|
38
|
+
"preReleaseVersion": {
|
|
39
|
+
"data": {"type": "preReleaseVersions", "id": pr_id},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _prerelease_included(
|
|
46
|
+
pr_id: str, pr_version: str, *, platform: str | None = "IOS",
|
|
47
|
+
) -> dict:
|
|
48
|
+
"""One /v1/builds.included entry describing a preReleaseVersion."""
|
|
49
|
+
attrs: dict = {"version": pr_version}
|
|
50
|
+
if platform is not None:
|
|
51
|
+
attrs["platform"] = platform
|
|
52
|
+
return {
|
|
53
|
+
"id": pr_id,
|
|
54
|
+
"type": "preReleaseVersions",
|
|
55
|
+
"attributes": attrs,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_with_prerelease(
|
|
60
|
+
build_id: str, build_version: str,
|
|
61
|
+
pr_id: str, pr_version: str, *, platform: str | None = "IOS",
|
|
62
|
+
) -> dict:
|
|
63
|
+
"""Build the /v1/builds payload pairing one build with one prerelease
|
|
64
|
+
relationship — the canonical fixture shape these tests exercise."""
|
|
65
|
+
return {
|
|
66
|
+
"data": [_build_data_entry(build_id, build_version, pr_id)],
|
|
67
|
+
"included": [
|
|
68
|
+
_prerelease_included(pr_id, pr_version, platform=platform),
|
|
69
|
+
],
|
|
70
|
+
"links": {},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _capture_build_history_request(payload: dict):
|
|
75
|
+
"""Patch asc_build_history.request to return `payload`. Returns
|
|
76
|
+
(seen_paths, seen_params, ctx) where ctx is the unittest.mock patch."""
|
|
77
|
+
seen_paths: list[str] = []
|
|
78
|
+
seen_params: list[dict | None] = []
|
|
79
|
+
|
|
80
|
+
def fake_request(method, path, token, *, params=None, **_kw):
|
|
81
|
+
seen_paths.append(path)
|
|
82
|
+
seen_params.append(params)
|
|
83
|
+
return _FakeResponse(payload)
|
|
84
|
+
|
|
85
|
+
return seen_paths, seen_params, mock.patch.object(
|
|
86
|
+
asc_build_history, "request", side_effect=fake_request,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class BuildsPreReleaseRelationshipTests(unittest.TestCase):
|
|
91
|
+
"""/v1/builds?include=preReleaseVersion reveals preReleaseVersions
|
|
92
|
+
that the direct /preReleaseVersions query might miss."""
|
|
93
|
+
|
|
94
|
+
def test_buildsViaPreRelease_included_in_floor(self):
|
|
95
|
+
payload = _build_with_prerelease(
|
|
96
|
+
"build-1", "42", "pr-ref-106", "1.0.6",
|
|
97
|
+
)
|
|
98
|
+
seen_paths, seen_params, patch_ctx = (
|
|
99
|
+
_capture_build_history_request(payload)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
buf = io.StringIO()
|
|
103
|
+
with patch_ctx, redirect_stderr(buf):
|
|
104
|
+
versions = asc_build_history.fetch_builds_prerelease_versions(
|
|
105
|
+
"111", "tok",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
self.assertEqual(versions, ["1.0.6"])
|
|
109
|
+
self.assertEqual(seen_paths[0], "/builds")
|
|
110
|
+
self.assertEqual(seen_params[0].get("filter[app]"), "111")
|
|
111
|
+
self.assertEqual(seen_params[0].get("include"), "preReleaseVersion")
|
|
112
|
+
|
|
113
|
+
with mock.patch.object(
|
|
114
|
+
asc_build_history, "fetch_prerelease_versions",
|
|
115
|
+
return_value=[]), \
|
|
116
|
+
mock.patch.object(
|
|
117
|
+
asc_build_history, "fetch_builds_prerelease_versions",
|
|
118
|
+
return_value=["1.0.6"]):
|
|
119
|
+
floor, per_source = asc_version_fetch.get_combined_floor(
|
|
120
|
+
"111", "tok", mmv.semver_tuple,
|
|
121
|
+
appstore_versions=[_v("1.0", "READY_FOR_SALE")],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
self.assertEqual(floor, "1.0.6")
|
|
125
|
+
self.assertEqual(per_source.get("buildsViaPreRelease"), "1.0.6")
|
|
126
|
+
|
|
127
|
+
def test_buildsViaPreRelease_uses_top_level_builds_endpoint(self):
|
|
128
|
+
payload = _build_with_prerelease("b1", "1", "pr1", "1.0.9")
|
|
129
|
+
seen_paths, seen_params, patch_ctx = (
|
|
130
|
+
_capture_build_history_request(payload)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
with patch_ctx:
|
|
134
|
+
versions = asc_build_history.fetch_builds_prerelease_versions(
|
|
135
|
+
"111", "tok",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
self.assertEqual(versions, ["1.0.9"])
|
|
139
|
+
self.assertEqual(seen_paths[0], "/builds")
|
|
140
|
+
self.assertNotEqual(seen_paths[0], "/apps/111/builds")
|
|
141
|
+
params = seen_params[0] or {}
|
|
142
|
+
self.assertEqual(params.get("filter[app]"), "111")
|
|
143
|
+
self.assertEqual(params.get("include"), "preReleaseVersion")
|
|
144
|
+
|
|
145
|
+
def test_buildsViaPreRelease_excludes_non_ios_platform(self):
|
|
146
|
+
payload = {
|
|
147
|
+
"data": [
|
|
148
|
+
_build_data_entry("build-mac", "1", "pr-ref-mac"),
|
|
149
|
+
_build_data_entry("build-null", "2", "pr-ref-null"),
|
|
150
|
+
],
|
|
151
|
+
"included": [
|
|
152
|
+
_prerelease_included(
|
|
153
|
+
"pr-ref-mac", "9.9.9", platform="MAC_OS"),
|
|
154
|
+
_prerelease_included(
|
|
155
|
+
"pr-ref-null", "1.0.8", platform=None),
|
|
156
|
+
],
|
|
157
|
+
"links": {},
|
|
158
|
+
}
|
|
159
|
+
_, _, patch_ctx = _capture_build_history_request(payload)
|
|
160
|
+
|
|
161
|
+
with patch_ctx:
|
|
162
|
+
versions = asc_build_history.fetch_builds_prerelease_versions(
|
|
163
|
+
"111", "tok",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
self.assertEqual(versions, ["1.0.8"])
|
|
167
|
+
|
|
168
|
+
def test_buildsViaPreRelease_handles_missing_relationship(self):
|
|
169
|
+
payload = {
|
|
170
|
+
"data": [
|
|
171
|
+
{
|
|
172
|
+
"id": "build-orphan",
|
|
173
|
+
"type": "builds",
|
|
174
|
+
"attributes": {"version": "1"},
|
|
175
|
+
"relationships": {},
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
"included": [],
|
|
179
|
+
"links": {},
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
def fake_request(method, path, token, *, params=None, **_kw):
|
|
183
|
+
return _FakeResponse(payload)
|
|
184
|
+
|
|
185
|
+
with mock.patch.object(
|
|
186
|
+
asc_build_history, "request", side_effect=fake_request):
|
|
187
|
+
versions = asc_build_history.fetch_builds_prerelease_versions(
|
|
188
|
+
"111", "tok",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
self.assertEqual(versions, [])
|
|
192
|
+
|
|
193
|
+
def test_buildsViaPreRelease_paginated(self):
|
|
194
|
+
page1 = _build_with_prerelease("b1", "1", "pr1", "1.0.5")
|
|
195
|
+
page1["links"] = {
|
|
196
|
+
"next": "https://api.appstoreconnect.apple.com/v1/"
|
|
197
|
+
"builds?cursor=PAGE2",
|
|
198
|
+
}
|
|
199
|
+
page2 = _build_with_prerelease("b2", "2", "pr2", "1.0.6")
|
|
200
|
+
responses = [_FakeResponse(page1), _FakeResponse(page2)]
|
|
201
|
+
seen_paths: list[str] = []
|
|
202
|
+
|
|
203
|
+
def fake_request(method, path, token, *, params=None, **_kw):
|
|
204
|
+
seen_paths.append(path)
|
|
205
|
+
return responses.pop(0)
|
|
206
|
+
|
|
207
|
+
with mock.patch.object(
|
|
208
|
+
asc_build_history, "request", side_effect=fake_request):
|
|
209
|
+
versions = asc_build_history.fetch_builds_prerelease_versions(
|
|
210
|
+
"111", "tok",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
self.assertEqual(sorted(versions), ["1.0.5", "1.0.6"])
|
|
214
|
+
self.assertEqual(len(responses), 0)
|
|
215
|
+
self.assertEqual(seen_paths[0], "/builds")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
unittest.main()
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tests for round-12 explicit per-request timeouts in ``asc_common.request``.
|
|
4
|
+
|
|
5
|
+
Without an explicit ``timeout`` kwarg, ``requests.request(...)`` blocks
|
|
6
|
+
forever on a hung TCP socket. CI then ticks until the runner's 6h hard
|
|
7
|
+
limit and a human pages on. The tests here pin:
|
|
8
|
+
|
|
9
|
+
* default ``(connect, read)`` = (10.0, 30.0)
|
|
10
|
+
* env overrides ``ASC_REQUEST_TIMEOUT_CONNECT_SEC`` /
|
|
11
|
+
``ASC_REQUEST_TIMEOUT_READ_SEC`` parse + take effect
|
|
12
|
+
* invalid / negative env values fall back to the defaults so a typo
|
|
13
|
+
can't disable the timeout
|
|
14
|
+
* the timeout is forwarded to ``requests.request`` on every attempt
|
|
15
|
+
(including retries)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import unittest
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from unittest import mock
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
27
|
+
|
|
28
|
+
import asc_common # noqa: E402
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _OkResponse:
|
|
32
|
+
"""Minimal stand-in for a 200 OK response."""
|
|
33
|
+
|
|
34
|
+
status_code = 200
|
|
35
|
+
text = ""
|
|
36
|
+
|
|
37
|
+
def json(self) -> dict:
|
|
38
|
+
return {"data": []}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RequestTimeoutTests(unittest.TestCase):
|
|
42
|
+
"""``request()`` must pass an explicit ``(connect, read)`` timeout."""
|
|
43
|
+
|
|
44
|
+
def _capture_timeout_via_request(self, env: dict) -> tuple[float, float]:
|
|
45
|
+
captured: dict = {}
|
|
46
|
+
|
|
47
|
+
def fake_request(method, url, **kwargs):
|
|
48
|
+
captured["timeout"] = kwargs.get("timeout")
|
|
49
|
+
return _OkResponse()
|
|
50
|
+
|
|
51
|
+
with mock.patch.dict(os.environ, env, clear=False), \
|
|
52
|
+
mock.patch.object(asc_common.requests, "request",
|
|
53
|
+
side_effect=fake_request):
|
|
54
|
+
asc_common.request("GET", "/apps/111", "tok")
|
|
55
|
+
|
|
56
|
+
return captured.get("timeout")
|
|
57
|
+
|
|
58
|
+
def test_default_timeouts_are_10s_connect_30s_read(self):
|
|
59
|
+
timeout = self._capture_timeout_via_request({
|
|
60
|
+
"ASC_REQUEST_TIMEOUT_CONNECT_SEC": "",
|
|
61
|
+
"ASC_REQUEST_TIMEOUT_READ_SEC": "",
|
|
62
|
+
})
|
|
63
|
+
self.assertEqual(timeout, (10.0, 30.0))
|
|
64
|
+
|
|
65
|
+
def test_env_override_takes_effect(self):
|
|
66
|
+
timeout = self._capture_timeout_via_request({
|
|
67
|
+
"ASC_REQUEST_TIMEOUT_CONNECT_SEC": "5",
|
|
68
|
+
"ASC_REQUEST_TIMEOUT_READ_SEC": "60",
|
|
69
|
+
})
|
|
70
|
+
self.assertEqual(timeout, (5.0, 60.0))
|
|
71
|
+
|
|
72
|
+
def test_invalid_env_falls_back_to_default(self):
|
|
73
|
+
"""A typo (e.g. ``CONNECT=ten``) MUST NOT disable the timeout."""
|
|
74
|
+
timeout = self._capture_timeout_via_request({
|
|
75
|
+
"ASC_REQUEST_TIMEOUT_CONNECT_SEC": "ten",
|
|
76
|
+
"ASC_REQUEST_TIMEOUT_READ_SEC": "thirty",
|
|
77
|
+
})
|
|
78
|
+
self.assertEqual(timeout, (10.0, 30.0))
|
|
79
|
+
|
|
80
|
+
def test_non_positive_env_falls_back_to_default(self):
|
|
81
|
+
"""Zero/negative values would disable the timeout; default instead."""
|
|
82
|
+
timeout = self._capture_timeout_via_request({
|
|
83
|
+
"ASC_REQUEST_TIMEOUT_CONNECT_SEC": "0",
|
|
84
|
+
"ASC_REQUEST_TIMEOUT_READ_SEC": "-5",
|
|
85
|
+
})
|
|
86
|
+
self.assertEqual(timeout, (10.0, 30.0))
|
|
87
|
+
|
|
88
|
+
def test_timeout_forwarded_on_every_retry_attempt(self):
|
|
89
|
+
"""A 503 retry must pass the same timeout on the retry call."""
|
|
90
|
+
captured_timeouts: list = []
|
|
91
|
+
responses = iter([_Status(503), _OkResponse()])
|
|
92
|
+
|
|
93
|
+
def fake_request(method, url, **kwargs):
|
|
94
|
+
captured_timeouts.append(kwargs.get("timeout"))
|
|
95
|
+
return next(responses)
|
|
96
|
+
|
|
97
|
+
with mock.patch.dict(
|
|
98
|
+
os.environ,
|
|
99
|
+
{"ASC_REQUEST_TIMEOUT_CONNECT_SEC": "",
|
|
100
|
+
"ASC_REQUEST_TIMEOUT_READ_SEC": ""},
|
|
101
|
+
clear=False), \
|
|
102
|
+
mock.patch.object(asc_common.requests, "request",
|
|
103
|
+
side_effect=fake_request), \
|
|
104
|
+
mock.patch.object(asc_common.time, "sleep"):
|
|
105
|
+
asc_common.request("GET", "/apps/111", "tok")
|
|
106
|
+
|
|
107
|
+
self.assertEqual(len(captured_timeouts), 2)
|
|
108
|
+
self.assertEqual(captured_timeouts[0], (10.0, 30.0))
|
|
109
|
+
self.assertEqual(captured_timeouts[1], (10.0, 30.0))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _Status:
|
|
113
|
+
"""Stand-in for a non-2xx response that should trigger a retry."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, code: int) -> None:
|
|
116
|
+
self.status_code = code
|
|
117
|
+
self.text = f"<error {code}>"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
if __name__ == "__main__":
|
|
121
|
+
unittest.main()
|