tests.test_unsubscribe module

Tests for repomatic.github.unsubscribe.

tests.test_unsubscribe.patch_gh(**mock_kwargs)

The conftest patch_gh, bound to this module’s dispatch points.

tests.test_unsubscribe.test_compute_cutoff(fixed_now, months, expected)[source]

Subtracting months clamps the day and rolls the year over correctly.

A link renders only with both a number and a URL, else the repo name.

tests.test_unsubscribe.test_render_detail_table_empty()[source]

No rows renders to an empty string, not a headerless table.

tests.test_unsubscribe.test_render_detail_table_exact()[source]

Rows render as a headed markdown table with one line each.

tests.test_unsubscribe.test_render_detail_table_humanizes_timestamp()[source]

A real timestamp renders a relative string, not the ‘-’ placeholder.

tests.test_unsubscribe.test_render_report_phase1_dry_run_summary()[source]

Dry-run phase 1 counts candidate rows and marks the mode.

tests.test_unsubscribe.test_render_report_phase1_live_summary()[source]

Live phase 1 reports unsubscribed and failed counts.

tests.test_unsubscribe.test_render_report_backlog_warning(threads_deferred, expect_warning)[source]

The warning fires on work the cap deferred, and on nothing else.

Inspection is not capped, so no thread goes uninspected: what a run leaves behind is eligible threads it declined to act on. That is a remainder a higher cap really does clear, which is what the warning advises.

The manual-run advice links the workflow whose run carries the report.

tests.test_unsubscribe.test_backlog_warning_breaks_only_between_sentences()[source]

A job summary renders every newline as a line break.

Wrapping the prose to a source column therefore leaks those wraps into the rendered warning, which is what made this one read as randomly broken. Each line has to end a sentence instead.

tests.test_unsubscribe.test_unsubscribe_workflow_constant_names_a_bundled_workflow()[source]

The link is only worth rendering while repomatic ships that workflow.

tests.test_unsubscribe.test_render_report_phase2_skipped()[source]

A skipped phase 2 renders its reason inside a warning admonition.

tests.test_unsubscribe.test_render_report_phase2_content()[source]

An active phase 2 renders its summary, search table, and query.

tests.test_unsubscribe.test_render_report_empty_result()[source]

A default result renders both phases plus the attribution footer.

tests.test_unsubscribe.test_fetch_notification_threads_sorts_oldest_first()[source]

The pool comes back oldest first, whatever order the API used.

The order is imposed here, not inherited: the endpoint calls itself sorted by most recent update, and a probe against 1637 real threads found the response ordered by neither updated_at nor id. The arrival order below is deliberately none of those.

tests.test_unsubscribe.test_fetch_notification_threads_skips_malformed_lines()[source]

Blank and unparsable lines are dropped without failing the fetch.

tests.test_unsubscribe.test_fetch_notification_threads_gh_failure_returns_empty()[source]

A gh failure degrades to an empty pool.

tests.test_unsubscribe.test_fetch_notification_threads_query_args()[source]

The fetch requests both subject types, paginated, gated on the cutoff.

tests.test_unsubscribe.test_fetch_notification_threads_sends_the_cutoff_in_utc()[source]

A cutoff from another zone must not stamp its wall clock with a Z.

The suffix is a claim about the instant, so formatting without converting first would shift the window by the offset and silently fetch the wrong threads.

tests.test_unsubscribe.test_get_thread_details_success()[source]

A well-formed response is parsed and returned verbatim.

tests.test_unsubscribe.test_get_thread_details_inaccessible_returns_none()[source]

A gh failure (inaccessible subject) yields None.

tests.test_unsubscribe.test_get_thread_details_malformed_returns_none()[source]

A non-JSON response yields None instead of raising.

tests.test_unsubscribe.test_unsubscribe_rest_thread_success()[source]

A successful unsubscribe deletes the subscription then marks it read.

tests.test_unsubscribe.test_unsubscribe_rest_thread_delete_failure()[source]

A failed DELETE returns False and never attempts the PATCH.

tests.test_unsubscribe.test_unsubscribe_rest_thread_patch_failure()[source]

A failed PATCH (after a successful DELETE) returns False.

tests.test_unsubscribe.test_graphql_unsubscribe_success()[source]

A successful mutation passes the node id and query, returning True.

tests.test_unsubscribe.test_graphql_unsubscribe_failure()[source]

A failed mutation returns False.

tests.test_unsubscribe.test_get_authenticated_username_strips()[source]

The login is trimmed of trailing whitespace.

tests.test_unsubscribe.test_get_authenticated_username_propagates_error()[source]

A gh failure propagates so the caller can skip phase 2.

tests.test_unsubscribe.test_iter_closed_items_delegates_to_paginator()[source]

The search wraps the shared paginator, bounded by GitHub’s own ceiling.

tests.test_unsubscribe.test_validate_notifications_token_exact_scope(caplog)[source]

A token scoped to exactly ‘notifications’ passes without a warning.

tests.test_unsubscribe.test_validate_notifications_token_extra_scopes_warns(caplog)[source]

Extra scopes on the token log a least-privilege warning.

tests.test_unsubscribe.test_validate_notifications_token_propagates()[source]

A validation failure propagates to the CLI entry point.

tests.test_unsubscribe.test_phase1_live_unsubscribes_closed_stale()[source]

A closed, stale thread is unsubscribed via DELETE + PATCH in live mode.

tests.test_unsubscribe.test_phase1_dry_run_records_candidate_without_mutations()[source]

Dry-run records a candidate row but issues no DELETE or PATCH.

tests.test_unsubscribe.test_phase1_skip_scenarios(detail, counter)[source]

Open, recent, unknown-state, and unparsable threads are each skipped.

tests.test_unsubscribe.test_phase1_inaccessible_subject_skipped()[source]

An inaccessible subject (no details) counts as unknown, not a failure.

tests.test_unsubscribe.test_phase1_delete_failure_records_failed()[source]

A failed DELETE records a FAILED row and skips the PATCH.

tests.test_unsubscribe.test_phase1_cap_defers_but_reports_total()[source]

The cap bounds the unsubscribes, yet the full total is reported.

tests.test_unsubscribe.test_phase1_fetch_failure_skips_phase()[source]

A fetch failure leaves phase 1 empty while phase 2 still runs.

tests.test_unsubscribe.test_phase2_filters_non_subscribed_items()[source]

Only items with viewerSubscription SUBSCRIBED are acted on.

tests.test_unsubscribe.test_phase2_skips_items_active_since_cutoff()[source]

Fresh or timestamp-less items are skipped despite the search filter.

The search query’s day-granular updated:< filter is served by GitHub’s search index, which can lag; the client-side re-check mirrors phase 1.

tests.test_unsubscribe.test_phase2_dry_run_no_mutation_calls()[source]

Dry-run records a candidate row but issues no mutation.

tests.test_unsubscribe.test_phase2_live_unsubscribes_subscribed()[source]

Live mode issues the mutation for a subscribed item.

tests.test_unsubscribe.test_phase2_mutation_failure_records_failed()[source]

A failed mutation records a FAILED row.

A username lookup failure skips phase 2 before any search.

tests.test_unsubscribe.test_phase2_search_failure_skips()[source]

A GraphQL search failure marks phase 2 skipped with a reason.

tests.test_unsubscribe.test_phase2_search_query_built()[source]

The search query embeds the username and the computed cutoff date.

tests.test_unsubscribe.test_batch_subject_details_maps_graphql_onto_the_rest_vocabulary()[source]

One round trip answers many subjects, in the spelling the phase reads.

REST calls a merged pull request closed; GraphQL distinguishes MERGED. The phase compares one vocabulary, so the batch collapses the two.

tests.test_unsubscribe.test_fetch_subject_details_falls_back_to_one_call_each()[source]

A failed batch falls back to one REST call per subject, losing no result.

tests.test_unsubscribe.test_fetch_subject_details_falls_back_for_an_unparsable_url()[source]

A subject URL the pattern cannot read still gets resolved, one call.

tests.test_unsubscribe.test_phase1_batches_every_subject_into_one_round_trip()[source]

The whole pool costs one request, not one per thread.