CI/CD hardening: lint, secret scan, coverage gate, auto CVE-fix PRs, GHCR + GitHub mirror publishing (#32)
CI / build-and-scan (push) Successful in 1m37s
CI / build-and-scan (push) Successful in 1m37s
- release changelog: commits rendered as description (link), divider lines dropped - gitleaks secret scan and hadolint on every push/PR - ruff lint/format gate (Python repos) with a pytest --cov-fail-under gate - scheduled CRITICAL Trivy failures attempt an apk upgrade rebuild and open a follow-up PR if it clears the finding, instead of just failing red - images also published to ghcr.io/djeex/<repo> - a matching GitHub Release is created on the GitHub mirror, with a notice pointing back to this repo as the source of truth
This commit was merged in pull request #32.
This commit is contained in:
+55
-34
@@ -38,62 +38,83 @@ def test_missing_product_names_exits(monkeypatch):
|
||||
|
||||
|
||||
def test_default_role_map_is_everyone(monkeypatch):
|
||||
cfg = _reload_env_config(monkeypatch, {
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090, RTX 5080",
|
||||
})
|
||||
cfg = _reload_env_config(
|
||||
monkeypatch,
|
||||
{
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090, RTX 5080",
|
||||
},
|
||||
)
|
||||
assert cfg.DISCORD_ROLE_MAP == {"RTX 5090": "@everyone", "RTX 5080": "@everyone"}
|
||||
|
||||
|
||||
def test_role_count_mismatch_exits(monkeypatch):
|
||||
with pytest.raises(SystemExit):
|
||||
_reload_env_config(monkeypatch, {
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090, RTX 5080",
|
||||
"DISCORD_ROLES": "<@&123456789012345678>",
|
||||
})
|
||||
_reload_env_config(
|
||||
monkeypatch,
|
||||
{
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090, RTX 5080",
|
||||
"DISCORD_ROLES": "<@&123456789012345678>",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_invalid_role_format_exits(monkeypatch):
|
||||
with pytest.raises(SystemExit):
|
||||
_reload_env_config(monkeypatch, {
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090",
|
||||
"DISCORD_ROLES": "not-a-role",
|
||||
})
|
||||
_reload_env_config(
|
||||
monkeypatch,
|
||||
{
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090",
|
||||
"DISCORD_ROLES": "not-a-role",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_valid_role_format_accepted(monkeypatch):
|
||||
cfg = _reload_env_config(monkeypatch, {
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090",
|
||||
"DISCORD_ROLES": "<@&123456789012345678>",
|
||||
})
|
||||
cfg = _reload_env_config(
|
||||
monkeypatch,
|
||||
{
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090",
|
||||
"DISCORD_ROLES": "<@&123456789012345678>",
|
||||
},
|
||||
)
|
||||
assert cfg.DISCORD_ROLE_MAP["RTX 5090"] == "<@&123456789012345678>"
|
||||
|
||||
|
||||
def test_unknown_country_falls_back_to_us(monkeypatch):
|
||||
cfg = _reload_env_config(monkeypatch, {
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090",
|
||||
"COUNTRY": "ZZ",
|
||||
})
|
||||
cfg = _reload_env_config(
|
||||
monkeypatch,
|
||||
{
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090",
|
||||
"COUNTRY": "ZZ",
|
||||
},
|
||||
)
|
||||
assert cfg.currency == "$"
|
||||
|
||||
|
||||
def test_known_country_currency(monkeypatch):
|
||||
cfg = _reload_env_config(monkeypatch, {
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090",
|
||||
"COUNTRY": "GB",
|
||||
})
|
||||
cfg = _reload_env_config(
|
||||
monkeypatch,
|
||||
{
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090",
|
||||
"COUNTRY": "GB",
|
||||
},
|
||||
)
|
||||
assert cfg.currency == "£"
|
||||
|
||||
|
||||
def test_refresh_time_invalid_exits(monkeypatch):
|
||||
with pytest.raises(SystemExit):
|
||||
_reload_env_config(monkeypatch, {
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090",
|
||||
"REFRESH_TIME": "not-a-number",
|
||||
})
|
||||
_reload_env_config(
|
||||
monkeypatch,
|
||||
{
|
||||
"DISCORD_WEBHOOK_URL": VALID_WEBHOOK,
|
||||
"PRODUCT_NAMES": "RTX 5090",
|
||||
"REFRESH_TIME": "not-a-number",
|
||||
},
|
||||
)
|
||||
|
||||
+32
-16
@@ -6,9 +6,7 @@ PRODUCT_NAME = "RTX 5090 Founders Edition"
|
||||
|
||||
SKU_PAYLOAD = {
|
||||
"searchedProducts": {
|
||||
"productDetails": [
|
||||
{"gpu": PRODUCT_NAME, "productSKU": "SKU-1", "productUPC": "ABC123"}
|
||||
]
|
||||
"productDetails": [{"gpu": PRODUCT_NAME, "productSKU": "SKU-1", "productUPC": "ABC123"}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,9 +50,15 @@ def _queue_responses(monkeypatch, checker, *payloads):
|
||||
def test_transition_to_in_stock_sends_notification(monkeypatch):
|
||||
checker = _import_gpu_checker(monkeypatch)
|
||||
calls = []
|
||||
monkeypatch.setattr(checker, "send_discord_notification", lambda *a: calls.append(("in_stock", a)))
|
||||
monkeypatch.setattr(checker, "send_out_of_stock_notification", lambda *a: calls.append(("out_of_stock", a)))
|
||||
monkeypatch.setattr(checker, "send_sku_change_notification", lambda *a: calls.append(("sku_change", a)))
|
||||
monkeypatch.setattr(
|
||||
checker, "send_discord_notification", lambda *a: calls.append(("in_stock", a))
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
checker, "send_out_of_stock_notification", lambda *a: calls.append(("out_of_stock", a))
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
checker, "send_sku_change_notification", lambda *a: calls.append(("sku_change", a))
|
||||
)
|
||||
|
||||
_queue_responses(monkeypatch, checker, SKU_PAYLOAD, _stock_payload(True))
|
||||
checker.check_rtx_50_founders()
|
||||
@@ -67,9 +71,15 @@ def test_transition_to_in_stock_sends_notification(monkeypatch):
|
||||
def test_transition_to_out_of_stock_sends_notification(monkeypatch):
|
||||
checker = _import_gpu_checker(monkeypatch)
|
||||
calls = []
|
||||
monkeypatch.setattr(checker, "send_discord_notification", lambda *a: calls.append(("in_stock", a)))
|
||||
monkeypatch.setattr(checker, "send_out_of_stock_notification", lambda *a: calls.append(("out_of_stock", a)))
|
||||
monkeypatch.setattr(checker, "send_sku_change_notification", lambda *a: calls.append(("sku_change", a)))
|
||||
monkeypatch.setattr(
|
||||
checker, "send_discord_notification", lambda *a: calls.append(("in_stock", a))
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
checker, "send_out_of_stock_notification", lambda *a: calls.append(("out_of_stock", a))
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
checker, "send_sku_change_notification", lambda *a: calls.append(("sku_change", a))
|
||||
)
|
||||
|
||||
_queue_responses(monkeypatch, checker, SKU_PAYLOAD, _stock_payload(True))
|
||||
checker.check_rtx_50_founders()
|
||||
@@ -84,9 +94,15 @@ def test_transition_to_out_of_stock_sends_notification(monkeypatch):
|
||||
def test_no_duplicate_notification_while_still_in_stock(monkeypatch):
|
||||
checker = _import_gpu_checker(monkeypatch)
|
||||
calls = []
|
||||
monkeypatch.setattr(checker, "send_discord_notification", lambda *a: calls.append(("in_stock", a)))
|
||||
monkeypatch.setattr(checker, "send_out_of_stock_notification", lambda *a: calls.append(("out_of_stock", a)))
|
||||
monkeypatch.setattr(checker, "send_sku_change_notification", lambda *a: calls.append(("sku_change", a)))
|
||||
monkeypatch.setattr(
|
||||
checker, "send_discord_notification", lambda *a: calls.append(("in_stock", a))
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
checker, "send_out_of_stock_notification", lambda *a: calls.append(("out_of_stock", a))
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
checker, "send_sku_change_notification", lambda *a: calls.append(("sku_change", a))
|
||||
)
|
||||
|
||||
_queue_responses(monkeypatch, checker, SKU_PAYLOAD, _stock_payload(True))
|
||||
checker.check_rtx_50_founders()
|
||||
@@ -101,7 +117,9 @@ def test_sku_change_triggers_notification_after_first_run(monkeypatch):
|
||||
sku_change_calls = []
|
||||
monkeypatch.setattr(checker, "send_discord_notification", lambda *a: None)
|
||||
monkeypatch.setattr(checker, "send_out_of_stock_notification", lambda *a: None)
|
||||
monkeypatch.setattr(checker, "send_sku_change_notification", lambda *a: sku_change_calls.append(a))
|
||||
monkeypatch.setattr(
|
||||
checker, "send_sku_change_notification", lambda *a: sku_change_calls.append(a)
|
||||
)
|
||||
|
||||
_queue_responses(monkeypatch, checker, SKU_PAYLOAD, _stock_payload(False))
|
||||
checker.check_rtx_50_founders()
|
||||
@@ -109,9 +127,7 @@ def test_sku_change_triggers_notification_after_first_run(monkeypatch):
|
||||
|
||||
changed_payload = {
|
||||
"searchedProducts": {
|
||||
"productDetails": [
|
||||
{"gpu": PRODUCT_NAME, "productSKU": "SKU-2", "productUPC": "ABC123"}
|
||||
]
|
||||
"productDetails": [{"gpu": PRODUCT_NAME, "productSKU": "SKU-2", "productUPC": "ABC123"}]
|
||||
}
|
||||
}
|
||||
_queue_responses(monkeypatch, checker, changed_payload, _stock_payload(False))
|
||||
|
||||
+36
-12
@@ -44,7 +44,9 @@ def test_in_stock_notification_posts_expected_payload(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(notifier.requests, "post", fake_post)
|
||||
|
||||
notifier.send_discord_notification("RTX 5090 Founders Edition", "https://example.com/buy", "1999")
|
||||
notifier.send_discord_notification(
|
||||
"RTX 5090 Founders Edition", "https://example.com/buy", "1999"
|
||||
)
|
||||
|
||||
assert captured["url"] == notifier.DISCORD_WEBHOOK_URL
|
||||
assert captured["json"]["content"] == "@everyone"
|
||||
@@ -54,7 +56,9 @@ def test_in_stock_notification_posts_expected_payload(monkeypatch):
|
||||
|
||||
def test_discord_notification_survives_http_error(monkeypatch):
|
||||
notifier = _import_notifier(monkeypatch, test_mode="False")
|
||||
monkeypatch.setattr(notifier.requests, "post", lambda *a, **k: FakeResponse(status_code=500, text="boom"))
|
||||
monkeypatch.setattr(
|
||||
notifier.requests, "post", lambda *a, **k: FakeResponse(status_code=500, text="boom")
|
||||
)
|
||||
|
||||
notifier.send_discord_notification("RTX 5090 Founders Edition", "https://example.com", "1999")
|
||||
|
||||
@@ -76,7 +80,9 @@ def test_out_of_stock_test_mode_skips_network_call(monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setattr(notifier.requests, "post", lambda *a, **k: calls.append((a, k)))
|
||||
|
||||
notifier.send_out_of_stock_notification("RTX 5090 Founders Edition", "https://example.com", "1999")
|
||||
notifier.send_out_of_stock_notification(
|
||||
"RTX 5090 Founders Edition", "https://example.com", "1999"
|
||||
)
|
||||
|
||||
assert calls == []
|
||||
|
||||
@@ -91,17 +97,23 @@ def test_out_of_stock_notification_posts_on_success(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(notifier.requests, "post", fake_post)
|
||||
|
||||
notifier.send_out_of_stock_notification("RTX 5090 Founders Edition", "https://example.com/buy", "1999")
|
||||
notifier.send_out_of_stock_notification(
|
||||
"RTX 5090 Founders Edition", "https://example.com/buy", "1999"
|
||||
)
|
||||
|
||||
assert captured["json"]["embeds"][0]["url"] == "https://example.com/buy"
|
||||
|
||||
|
||||
def test_out_of_stock_notification_survives_http_error(monkeypatch):
|
||||
notifier = _import_notifier(monkeypatch, test_mode="False")
|
||||
monkeypatch.setattr(notifier.requests, "post", lambda *a, **k: FakeResponse(status_code=500, text="boom"))
|
||||
monkeypatch.setattr(
|
||||
notifier.requests, "post", lambda *a, **k: FakeResponse(status_code=500, text="boom")
|
||||
)
|
||||
|
||||
# Should not raise even though the webhook call "fails"
|
||||
notifier.send_out_of_stock_notification("RTX 5090 Founders Edition", "https://example.com", "1999")
|
||||
notifier.send_out_of_stock_notification(
|
||||
"RTX 5090 Founders Edition", "https://example.com", "1999"
|
||||
)
|
||||
|
||||
|
||||
def test_out_of_stock_notification_survives_connection_error(monkeypatch):
|
||||
@@ -112,7 +124,9 @@ def test_out_of_stock_notification_survives_connection_error(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(notifier.requests, "post", raise_error)
|
||||
|
||||
notifier.send_out_of_stock_notification("RTX 5090 Founders Edition", "https://example.com", "1999")
|
||||
notifier.send_out_of_stock_notification(
|
||||
"RTX 5090 Founders Edition", "https://example.com", "1999"
|
||||
)
|
||||
|
||||
|
||||
def test_sku_change_test_mode_skips_network_call(monkeypatch):
|
||||
@@ -120,16 +134,22 @@ def test_sku_change_test_mode_skips_network_call(monkeypatch):
|
||||
calls = []
|
||||
monkeypatch.setattr(notifier.requests, "post", lambda *a, **k: calls.append((a, k)))
|
||||
|
||||
notifier.send_sku_change_notification("RTX 5090 Founders Edition", "old-sku", "new-sku", "https://example.com")
|
||||
notifier.send_sku_change_notification(
|
||||
"RTX 5090 Founders Edition", "old-sku", "new-sku", "https://example.com"
|
||||
)
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_sku_change_notification_survives_http_error(monkeypatch):
|
||||
notifier = _import_notifier(monkeypatch, test_mode="False")
|
||||
monkeypatch.setattr(notifier.requests, "post", lambda *a, **k: FakeResponse(status_code=500, text="boom"))
|
||||
monkeypatch.setattr(
|
||||
notifier.requests, "post", lambda *a, **k: FakeResponse(status_code=500, text="boom")
|
||||
)
|
||||
|
||||
notifier.send_sku_change_notification("RTX 5090 Founders Edition", "old-sku", "new-sku", "https://example.com")
|
||||
notifier.send_sku_change_notification(
|
||||
"RTX 5090 Founders Edition", "old-sku", "new-sku", "https://example.com"
|
||||
)
|
||||
|
||||
|
||||
def test_sku_change_notification_survives_connection_error(monkeypatch):
|
||||
@@ -140,11 +160,15 @@ def test_sku_change_notification_survives_connection_error(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(notifier.requests, "post", raise_error)
|
||||
|
||||
notifier.send_sku_change_notification("RTX 5090 Founders Edition", "old-sku", "new-sku", "https://example.com")
|
||||
notifier.send_sku_change_notification(
|
||||
"RTX 5090 Founders Edition", "old-sku", "new-sku", "https://example.com"
|
||||
)
|
||||
|
||||
|
||||
def test_sku_change_notification_mentions_role_and_skus(monkeypatch):
|
||||
notifier = _import_notifier(monkeypatch, test_mode="False", discord_roles="<@&123456789012345678>")
|
||||
notifier = _import_notifier(
|
||||
monkeypatch, test_mode="False", discord_roles="<@&123456789012345678>"
|
||||
)
|
||||
captured = {}
|
||||
|
||||
def fake_post(url, json=None, **kwargs):
|
||||
|
||||
Reference in New Issue
Block a user