Files
computer-playbook/tests/unit/python/utils/update/test_follow.py
T
kevinveenbirkenbachandClaude Opus 5.5 c068e44d1b fix(update): treat an incomplete Docker Hub response as unreadable
followed_tags caught OSError, ValueError, KeyError and RuntimeError, but
http.client.IncompleteRead, BadStatusLine and LineTooLong derive from
http.client.HTTPException only. One truncated digest response therefore
escaped to collect_updates (docker.py:344, no try) and aborted the whole
Docker update run instead of printing the follow warning for that image.
Every other fetch site of the updater already catches HTTPException
(docker.py, pip.py).

Reported by CodeRabbit on #1370.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 19:16:16 +01:00

93 lines
3.3 KiB
Python

from __future__ import annotations
import contextlib
import http.client
import io
import json
import unittest
import urllib.error
from unittest import mock
from utils.update.follow import followed_tags, tag_digest
UBUNTU = {
"latest": "sha256:lts",
"26.04": "sha256:lts",
"26.10": "sha256:interim",
"rolling": "sha256:interim",
}
FEDORA = {
"latest": "sha256:45",
"44": "sha256:44",
"45": "sha256:45",
"46": "sha256:rawhide",
"rawhide": "sha256:rawhide",
}
def _digests(table: dict[str, str]):
return lambda _image, tag: table[tag]
class TestFollowedTags(unittest.TestCase):
def test_ubuntu_on_the_lts_gets_no_interim_release(self) -> None:
with mock.patch("utils.update.follow.tag_digest", _digests(UBUNTU)):
found = followed_tags("ubuntu", list(UBUNTU), {"follow": "latest"}, "26.04")
self.assertEqual(found, [])
def test_fedora_moves_to_the_stable_release_not_rawhide(self) -> None:
with mock.patch("utils.update.follow.tag_digest", _digests(FEDORA)):
found = followed_tags("fedora", list(FEDORA), {"follow": "latest"}, "44")
self.assertEqual(found, ["45"])
def test_an_entity_without_follow_keeps_every_tag(self) -> None:
tags = list(FEDORA)
self.assertEqual(followed_tags("fedora", tags, {"version": "44"}, "44"), tags)
self.assertEqual(followed_tags("fedora", tags, None, "44"), tags)
def test_an_unreadable_registry_proposes_nothing_and_says_so(self) -> None:
stderr = io.StringIO()
with (
mock.patch("utils.update.follow.tag_digest", side_effect=OSError("429")),
contextlib.redirect_stderr(stderr),
):
found = followed_tags("fedora", list(FEDORA), {"follow": "latest"}, "44")
self.assertEqual(found, [])
self.assertIn("WARN fedora:44", stderr.getvalue())
def test_an_incomplete_response_proposes_nothing_and_says_so(self) -> None:
stderr = io.StringIO()
with (
mock.patch(
"utils.update.follow.get",
side_effect=http.client.IncompleteRead(b"{", 120),
),
contextlib.redirect_stderr(stderr),
):
found = followed_tags("fedora", list(FEDORA), {"follow": "latest"}, "44")
self.assertEqual(found, [])
self.assertIn("WARN fedora:44", stderr.getvalue())
self.assertIn("IncompleteRead", stderr.getvalue())
def test_a_rate_limited_digest_is_retried(self) -> None:
limited = urllib.error.HTTPError("url", 429, "Too Many Requests", {}, None)
body = json.dumps({"digest": "sha256:45"}).encode()
with (
mock.patch("utils.update.follow.get", side_effect=[limited, body]),
mock.patch("utils.update.follow.time.sleep"),
):
self.assertEqual(tag_digest("fedora", "latest"), "sha256:45")
def test_a_persistent_rate_limit_still_raises(self) -> None:
limited = urllib.error.HTTPError("url", 429, "Too Many Requests", {}, None)
with (
mock.patch("utils.update.follow.get", side_effect=[limited] * 3),
mock.patch("utils.update.follow.time.sleep"),
self.assertRaises(urllib.error.HTTPError),
):
tag_digest("fedora", "latest")
if __name__ == "__main__":
unittest.main()