1- //! Live integration tests for the `PrService` stack (GitHub, GitLab, ... )
1+ //! Live integration tests for the `PrService` stack (GitHub, GitLab, Gitea )
22//!
3- //! These tests drive the real provider CLIs (`gh`, `glab`) through
3+ //! These tests drive the real provider CLIs (`gh`, `glab`, `tea` ) through
44//! `pr_service_for`/`PrService` against designated, real, external test
55//! repositories. They are strictly read-only: no create/write operations
66//! against any provider are performed anywhere in this file.
1717//! test repo.
1818//! - `OPERATOR_GITPROVIDER_TEST_PR_GITLAB`: Number of an open GitLab MR on
1919//! that repo with at least one comment.
20+ //! - `OPERATOR_GITPROVIDER_TEST_REPO_GITEA`: Full remote URL of a Gitea test
21+ //! repo. Gitea is self-hosted, so its origin also registers the Gitea host.
22+ //! - `OPERATOR_GITPROVIDER_TEST_PR_GITEA`: Number of an open Gitea PR on that
23+ //! repo with at least one comment.
2024//!
21- //! Each row additionally needs its CLI (`gh`, `glab`) installed and
25+ //! Each row additionally needs its CLI (`gh`, `glab`, `tea` ) installed and
2226//! authenticated. A row skips itself (with `eprintln!`) rather than failing
2327//! the suite when its repo env var is unset or its CLI is unavailable.
2428//!
29+ //! A row env var counts as unset when it is missing **or blank**: CI wires
30+ //! these from repository variables that export an empty string when
31+ //! undefined, and a blank value must skip the row rather than be parsed.
32+ //!
2533//! ## Running Tests
2634//!
2735//! ```bash
28- //! # Both rows
36+ //! # Every row
2937//! OPERATOR_GITPROVIDER_TEST_ENABLED=true \
3038//! OPERATOR_GITPROVIDER_TEST_REPO_GITHUB=https://github.com/owner/repo \
3139//! OPERATOR_GITPROVIDER_TEST_PR_GITHUB=1 \
3240//! OPERATOR_GITPROVIDER_TEST_REPO_GITLAB=https://gitlab.com/owner/repo \
3341//! OPERATOR_GITPROVIDER_TEST_PR_GITLAB=1 \
42+ //! OPERATOR_GITPROVIDER_TEST_REPO_GITEA=https://gitea.example/owner/repo \
43+ //! OPERATOR_GITPROVIDER_TEST_PR_GITEA=1 \
3444//! cargo test --test gitprovider_integration -- --nocapture
3545//!
3646//! # Table-invariant unit test only (no env vars, no CLIs needed)
3949
4050use operator:: api:: pr_service:: pr_service_for;
4151use operator:: api:: PrService ;
52+ use operator:: config:: GitConfig ;
4253use operator:: types:: pr:: { GitProvider , PrState , RepoInfo } ;
4354use std:: collections:: HashSet ;
4455use std:: env;
@@ -53,6 +64,17 @@ fn gitprovider_tests_enabled() -> bool {
5364 . unwrap_or ( false )
5465}
5566
67+ /// A set-but-blank value reads as unset -- see the blank-value note in the
68+ /// module docs.
69+ fn non_blank ( value : String ) -> Option < String > {
70+ ( !value. trim ( ) . is_empty ( ) ) . then_some ( value)
71+ }
72+
73+ /// The value of `var`, or `None` when it is unset or blank.
74+ fn configured ( var : & str ) -> Option < String > {
75+ env:: var ( var) . ok ( ) . and_then ( non_blank)
76+ }
77+
5678/// Macro to skip test if git-provider tests are not configured
5779macro_rules! skip_if_not_configured {
5880 ( ) => {
@@ -66,7 +88,7 @@ macro_rules! skip_if_not_configured {
6688// ─── Provider Table ──────────────────────────────────────────────────────────
6789
6890/// One row of the live provider test table. Adding a new operational
69- /// provider (bitbucket, azure, forgejo, gitea ) is just adding a row here
91+ /// provider (bitbucket, azure, forgejo) is just adding a row here
7092/// plus a `#[tokio::test]` that calls `run_provider_case` for it -- see the
7193/// template comment below `PROVIDER_CASES`.
7294struct ProviderCase {
@@ -78,8 +100,12 @@ struct ProviderCase {
78100 repo_env : & ' static str ,
79101 /// Env var holding the PR/MR number to exercise (must have >=1 comment).
80102 pr_env : & ' static str ,
103+ /// Registers the test repo's origin as this provider's host. `None` for
104+ /// providers resolved by well-known domain; `Some` for self-hosted ones,
105+ /// which are unresolvable until their host is configured.
106+ apply_host : Option < fn ( & mut GitConfig , String ) > ,
81107 /// Builds the `PrService` under test for this row.
82- service : fn ( ) -> Arc < dyn PrService > ,
108+ service : fn ( & GitConfig ) -> Arc < dyn PrService > ,
83109}
84110
85111const PROVIDER_CASES : & [ ProviderCase ] = & [
@@ -88,34 +114,24 @@ const PROVIDER_CASES: &[ProviderCase] = &[
88114 cli : "gh" ,
89115 repo_env : "OPERATOR_GITPROVIDER_TEST_REPO_GITHUB" ,
90116 pr_env : "OPERATOR_GITPROVIDER_TEST_PR_GITHUB" ,
91- service : || {
92- pr_service_for ( GitProvider :: GitHub , & operator:: config:: GitConfig :: default ( ) )
93- . expect ( "github is operational" )
94- } ,
117+ apply_host : None ,
118+ service : |git| pr_service_for ( GitProvider :: GitHub , git) . expect ( "github is operational" ) ,
95119 } ,
96120 ProviderCase {
97121 slug : "gitlab" ,
98122 cli : "glab" ,
99123 repo_env : "OPERATOR_GITPROVIDER_TEST_REPO_GITLAB" ,
100124 pr_env : "OPERATOR_GITPROVIDER_TEST_PR_GITLAB" ,
101- service : || {
102- pr_service_for ( GitProvider :: GitLab , & operator:: config:: GitConfig :: default ( ) )
103- . expect ( "gitlab is operational" )
104- } ,
125+ apply_host : None ,
126+ service : |git| pr_service_for ( GitProvider :: GitLab , git) . expect ( "gitlab is operational" ) ,
105127 } ,
106128 ProviderCase {
107129 slug : "gitea" ,
108130 cli : "tea" ,
109131 repo_env : "OPERATOR_GITPROVIDER_TEST_REPO_GITEA" ,
110132 pr_env : "OPERATOR_GITPROVIDER_TEST_PR_GITEA" ,
111- service : || {
112- let mut config = operator:: config:: GiteaConfig :: default ( ) ;
113- if let Ok ( remote) = env:: var ( "OPERATOR_GITPROVIDER_TEST_REPO_GITEA" ) {
114- let url = url:: Url :: parse ( & remote) . expect ( "Gitea test remote must be HTTPS" ) ;
115- config. host = Some ( url. origin ( ) . ascii_serialization ( ) ) ;
116- }
117- Arc :: new ( operator:: api:: gitea_service:: GiteaService :: new ( config) )
118- } ,
133+ apply_host : Some ( |git, origin| git. gitea . host = Some ( origin) ) ,
134+ service : |git| pr_service_for ( GitProvider :: Gitea , git) . expect ( "gitea is operational" ) ,
119135 } ,
120136 // Template for a future row (documentation only, NOT dead code -- copy
121137 // this into a real `ProviderCase` once the provider gets an operational
@@ -126,12 +142,13 @@ const PROVIDER_CASES: &[ProviderCase] = &[
126142 // cli: "bb" or whatever CLI backs BitbucketService,
127143 // repo_env: "OPERATOR_GITPROVIDER_TEST_REPO_BITBUCKET",
128144 // pr_env: "OPERATOR_GITPROVIDER_TEST_PR_BITBUCKET",
129- // service: || pr_service_for(GitProvider::Bitbucket).expect("bitbucket is operational"),
145+ // apply_host: None,
146+ // service: |git| pr_service_for(GitProvider::Bitbucket, git).expect("operational"),
130147 // },
131148 //
132- // Azure DevOps, Forgejo, and Gitea rows follow the same shape with
133- // `GitProvider::AzureDevOps` / `Forgejo` / `Gitea` and their own
134- // `OPERATOR_GITPROVIDER_TEST_{REPO,PR}_{AZUREDEVOPS,FORGEJO,GITEA}` pair .
149+ // Azure DevOps and Forgejo follow the same shape once `pr_service_for`
150+ // stops returning `UnsupportedProviderError` for them. A self-hosted row
151+ // also needs `apply_host`, as the `gitea` row above does .
135152] ;
136153
137154/// Look up a row by slug (stable against reordering `PROVIDER_CASES`).
@@ -144,19 +161,39 @@ fn case_by_slug(slug: &str) -> &'static ProviderCase {
144161
145162// ─── Shared Row Runner ────────────────────────────────────────────────────────
146163
164+ /// The `GitConfig` a row's service and URL parsing both resolve against.
165+ /// Self-hosted rows register the live repo's own origin as their host.
166+ fn git_config_for ( case : & ProviderCase , repo_url : & str ) -> GitConfig {
167+ let mut git = GitConfig :: default ( ) ;
168+ if let Some ( apply_host) = case. apply_host {
169+ let origin = url:: Url :: parse ( repo_url)
170+ . unwrap_or_else ( |e| {
171+ panic ! (
172+ "{}: {} must be an absolute URL: {e}" ,
173+ case. slug, case. repo_env
174+ )
175+ } )
176+ . origin ( )
177+ . ascii_serialization ( ) ;
178+ apply_host ( & mut git, origin) ;
179+ }
180+ git
181+ }
182+
147183/// Drive one row of `PROVIDER_CASES` through the live `PrService` stack.
148184/// Skips (with `eprintln!`) rather than fails when the row isn't configured
149185/// or its CLI isn't installed/authed -- a missing row must not fail the
150186/// suite.
151187async fn run_provider_case ( case : & ProviderCase ) {
152188 skip_if_not_configured ! ( ) ;
153189
154- let Ok ( repo_url) = env :: var ( case. repo_env ) else {
190+ let Some ( repo_url) = configured ( case. repo_env ) else {
155191 eprintln ! ( "Skipping {} row: {} not set" , case. slug, case. repo_env) ;
156192 return ;
157193 } ;
158194
159- let service = ( case. service ) ( ) ;
195+ let git = git_config_for ( case, & repo_url) ;
196+ let service = ( case. service ) ( & git) ;
160197
161198 // 1. check_available
162199 match service. check_available ( ) . await {
@@ -187,15 +224,6 @@ async fn run_provider_case(case: &ProviderCase) {
187224 eprintln ! ( "{}: authenticated as {user}" , case. slug) ;
188225
189226 // 3. RepoInfo::from_remote_url
190- let mut git = operator:: config:: GitConfig :: default ( ) ;
191- if case. slug == "gitea" {
192- git. gitea . host = Some (
193- url:: Url :: parse ( & repo_url)
194- . unwrap ( )
195- . origin ( )
196- . ascii_serialization ( ) ,
197- ) ;
198- }
199227 let hosts = operator:: types:: pr:: ProviderHosts :: from_config ( & git) . unwrap ( ) ;
200228 let repo_info = RepoInfo :: from_remote_url_with_hosts ( & repo_url, & hosts)
201229 . unwrap_or_else ( |e| panic ! ( "{}: failed to parse repo url {repo_url}: {e}" , case. slug) ) ;
@@ -206,7 +234,7 @@ async fn run_provider_case(case: &ProviderCase) {
206234 case. slug
207235 ) ;
208236
209- let Ok ( pr_number_str) = env :: var ( case. pr_env ) else {
237+ let Some ( pr_number_str) = configured ( case. pr_env ) else {
210238 eprintln ! (
211239 "Skipping remaining {} assertions: {} not set" ,
212240 case. slug, case. pr_env
@@ -272,6 +300,11 @@ async fn test_gitlab_provider_live() {
272300 run_provider_case ( case_by_slug ( "gitlab" ) ) . await ;
273301}
274302
303+ #[ tokio:: test]
304+ async fn test_gitea_provider_live ( ) {
305+ run_provider_case ( case_by_slug ( "gitea" ) ) . await ;
306+ }
307+
275308// ─── CLI Flag Contract (no credentials, no network) ──────────────────────────
276309
277310/// Every long flag an argv builder emits must appear in the installed CLI's
@@ -344,6 +377,23 @@ fn provider_cli_flags_are_accepted_by_the_installed_binary() {
344377
345378// ─── Table Invariants (plain `cargo test`, no gating) ────────────────────────
346379
380+ /// CI wires every row with `${{ vars.X || '' }}`, which exports an empty
381+ /// string when the variable is undefined -- a blank value must read as unset.
382+ #[ test]
383+ fn blank_env_values_are_treated_as_unset ( ) {
384+ for blank in [ "" , " " , "\n " ] {
385+ assert_eq ! (
386+ non_blank( blank. to_string( ) ) ,
387+ None ,
388+ "blank value {blank:?} should read as unset"
389+ ) ;
390+ }
391+ assert_eq ! (
392+ non_blank( "https://github.com/owner/repo" . to_string( ) ) . as_deref( ) ,
393+ Some ( "https://github.com/owner/repo" )
394+ ) ;
395+ }
396+
347397#[ test]
348398fn provider_case_table_env_names_unique_and_slugs_match ( ) {
349399 let mut repo_envs = HashSet :: new ( ) ;
@@ -367,7 +417,7 @@ fn provider_case_table_env_names_unique_and_slugs_match() {
367417 case. slug
368418 ) ;
369419
370- let service = ( case. service ) ( ) ;
420+ let service = ( case. service ) ( & GitConfig :: default ( ) ) ;
371421 assert_eq ! (
372422 service. provider_name( ) ,
373423 case. slug,
@@ -376,8 +426,3 @@ fn provider_case_table_env_names_unique_and_slugs_match() {
376426 ) ;
377427 }
378428}
379-
380- #[ tokio:: test]
381- async fn gitea_provider_live ( ) {
382- run_provider_case ( PROVIDER_CASES . iter ( ) . find ( |c| c. slug == "gitea" ) . unwrap ( ) ) . await ;
383- }
0 commit comments