fix(ovhcloud): stop a diagnostic step from failing the test (1.2.4)

GET /telephony — the list of billing accounts — answers 500 with an empty body on
at least one service account. Nothing in this integration needs that list: it was
added as a diagnostic, and it promptly failed the very test it exists to explain,
hiding whatever the account-scoped routes had to say behind its own failure. It is
now recorded and stepped over.

When neither product can be listed either, the message names what each code means
rather than repeating the status: 403 is an IAM policy missing the telephony
actions, 404 is a billing account that does not exist under these credentials, and
500 on every telephony route means this service account cannot reach the telephony
API at all — for which the way round is an application key from
api.ovh.com/createToken, which this integration already accepts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN1R4EnNFUV2Xwtd9q98tj
This commit is contained in:
2026-09-19 19:25:00 +00:00
parent 3ca2d6ded2
commit 35f287837b
2 changed files with 37 additions and 18 deletions
@@ -240,7 +240,7 @@ def _queue_id(cfg):
# It also answers its own VERSION. When a test fails identically twice, the first
# question is whether the new code is the code that ran, and nothing on the screen
# could answer it.
VERSION = "1.2.3"
VERSION = "1.2.4"
def _try(step, fn):
@@ -264,20 +264,26 @@ def _walk(cfg, inputs):
out.update({"ok": True, "nichandle": resp.get("nichandle")})
return out
# 1. What can these credentials see at all? It proves the token AND the IAM
# policy in one call, and names the accounts — so a mistyped billing account
# is visible rather than inferred.
# 1. What can these credentials see at all? Useful when it works, and NEVER
# FATAL when it does not.
#
# It is a diagnostic, not a dependency: nothing in this integration needs the
# list of billing accounts, and on a service account this route has been seen
# to answer 500 with an empty body while every account-scoped route underneath
# it works perfectly. A diagnostic step that fails the test it exists to
# explain is worse than no step at all — it hides the answer behind its own
# failure, which is exactly what it did here.
accounts, err = _try("listing telephony billing accounts", lambda: request("GET", "/telephony", cfg))
if err:
out.update({"ok": False, "step": "billing accounts", "detail": err})
return out
accounts = [str(a) for a in (accounts or [])]
out["billing_accounts"] = accounts
if ba not in accounts:
out.update({"ok": False, "step": "billing account",
"detail": "%s is not one of the accounts these credentials can see: [%s]"
% (ba, ", ".join(accounts) or "none")})
return out
out["billing_accounts"] = "could not be listed (" + err + ") — not fatal, carrying on"
else:
accounts = [str(a) for a in (accounts or [])]
out["billing_accounts"] = accounts
if ba not in accounts:
out.update({"ok": False, "step": "billing account",
"detail": "%s is not one of the accounts these credentials can see: [%s]"
% (ba, ", ".join(accounts) or "none")})
return out
# 2. Which product holds the queue, and under what name. Both are listed even on
# success: seeing the OTHER product empty is what tells somebody their number
@@ -294,9 +300,22 @@ def _walk(cfg, inputs):
product = name
break
if product is None:
out.update({"ok": False, "step": "service",
"detail": "%s is not a call queue on %s. Configure the number as a call queue in the "
"control panel, or set telephony_service to one of the names listed above." % (service, ba)})
listed = [n for p in _PRODUCTS for n in (services[p] if isinstance(services[p], list) else [])]
failures = [services[p] for p in _PRODUCTS if not isinstance(services[p], list)]
if not listed and failures:
# Neither product could even be LISTED. That is not a wrong service name,
# it is the account or the credentials — and the three shapes it comes in
# want three different answers, so they are named rather than merged.
detail = ("neither product could be listed on %s. A 403 means the IAM policy is missing the "
"telephony actions; a 404 means this billing account does not exist under these "
"credentials; a 500 on every telephony route means this service account cannot reach "
"the telephony API at all, and the way round is an application key from "
"https://api.ovh.com/createToken/ (fill in application_key, application_secret and "
"consumer_key instead). Details: %s") % (ba, " | ".join(failures))
else:
detail = ("%s is not a call queue on %s. Configure the number as a call queue in the control "
"panel, or set telephony_service to one of the names listed above." % (service, ba))
out.update({"ok": False, "step": "service", "detail": detail})
return out
out["product"] = product
_CACHE["product"] = product