pkgcore.bugzilla.testing module

Replay helpers for testing code that talks to Bugzilla.

Subclassing the real urllib handler rather than patching urlopen keeps the genuine urllib.request.Request in the loop, so header, method, body and encoding mistakes are caught, and an unexpected request fails loudly instead of reaching the network.

Nothing here imports pytest, so it is usable from a plain unittest suite or a script; pkgcore.pytest.plugin wraps it in a bugzilla_cassette fixture that downstream projects get for free.

A cassette used as a context manager takes over the opener every client builds, which is what lets it intercept a client constructed somewhere the test can’t reach, such as inside a CLI command:

with Cassette().expect_created(12345) as cassette:
    main(["pkgdev", "bugs", "..."])
assert cassette.calls[0].body["summary"] == "cat/pkg-1: stablereq"

When the client is reachable, skip the patching and hand it the opener:

cassette = Cassette().expect_bugs({"id": 1, ...})
bugs = cassette.client().search()
class pkgcore.bugzilla.testing.Call(method: str, url: str, headers: dict[str, str], body: dict[str, Any] | None)[source]

Bases: object

A request the code under test actually made

body: dict[str, Any] | None
header(name: str) str | None[source]

Look a header up without caring how urllib cased it

headers: dict[str, str]
method: str
property params: dict[str, list[str]]

Query parameters grouped by name

property path: str
property query: list[tuple[str, str]]

Query parameters in order, duplicates preserved

url: str
class pkgcore.bugzilla.testing.Cassette(*recordings: Recording, api_key: str | None = 'fake-api-key-for-tests', base_url: str = 'https://bugs.example.org')[source]

Bases: object

Queued responses, the requests they answered, and a client to drive.

Recordings queued with expect() are consumed in order; once they run out an always() fallback answers, or an unexpected request fails.

always(recording: Recording) Cassette[source]

Answer any request the queue doesn’t cover

assert_drained() None[source]

Fail if queued responses went unused

calls: list[Call]
client(**kwargs: Any) Bugzilla[source]

A client wired to this cassette

creates_bugs(first: int = 1) Cassette[source]

Answer every creation with the next id, for unbounded filing

expect(*recordings: Recording) Cassette[source]

Queue responses, returning self so calls can be chained

expect_bugs(*bugs: dict[str, Any]) Cassette[source]

Queue a search result

expect_changed(bug_id: int, **changes: dict[str, str]) Cassette[source]

Queue a reply to an update

expect_created(*bug_ids: int) Cassette[source]

Queue replies to bug creation

expect_error(code: int, message: str = 'error', status: int = 400) Cassette[source]

Queue a Bugzilla error body

expect_whoami(name: str = 'dev@gentoo.org', real_name: str = 'A Dev', id: int = 7) Cassette[source]
fallback: Recording | None
installed() Generator[Self, None, None][source]

Make every client built while active use this cassette

pending: list[Recording]
transport(**kwargs: Any) UrllibTransport[source]

A transport wired to this cassette

class pkgcore.bugzilla.testing.Recording(body: _Body = None, status: int = 200, raw: bytes | None = None, headers: dict[str, str]=<factory>, content_type: str = 'application/json; charset=UTF-8', reason: str = 'OK')[source]

Bases: object

A canned response.

body is serialized to JSON, or may be a callable taking the Call for responses that vary per request. raw overrides it with exact bytes, for testing non-JSON replies.

body: _Body
content_type: str
headers: dict[str, str]
payload(call: Call) bytes[source]
raw: bytes | None
reason: str
status: int
class pkgcore.bugzilla.testing.ReplayHandler(cassette: Cassette)[source]

Bases: HTTPHandler, HTTPSHandler

Serve queued recordings in order, recording what was asked for.

Subclasses both handlers so urllib.request.build_opener() drops its defaults for either scheme; inheriting only the https one leaves plain http going to the network.

http_open(req: Any) Any
https_open(req: Any) Any[source]
pkgcore.bugzilla.testing.response(body: _Body = None, **kwargs: Any) Recording[source]

Shorthand for Recording