client_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. /*
  2. Copyright © The ESO Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. // /*
  14. // Licensed under the Apache License, Version 2.0 (the "License");
  15. // you may not use this file except in compliance with the License.
  16. // You may obtain a copy of the License at
  17. //
  18. // https://www.apache.org/licenses/LICENSE-2.0
  19. //
  20. // Unless required by applicable law or agreed to in writing, software
  21. // distributed under the License is distributed on an "AS IS" BASIS,
  22. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  23. // See the License for the specific language governing permissions and
  24. // limitations under the License.
  25. // */
  26. package github
  27. import (
  28. "context"
  29. "crypto/rand"
  30. "crypto/rsa"
  31. "crypto/x509"
  32. "encoding/pem"
  33. "errors"
  34. "fmt"
  35. "net/http"
  36. "net/http/httptest"
  37. "testing"
  38. "github.com/bradleyfalzon/ghinstallation/v2"
  39. github "github.com/google/go-github/v56/github"
  40. "github.com/stretchr/testify/assert"
  41. "github.com/stretchr/testify/require"
  42. corev1 "k8s.io/api/core/v1"
  43. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  44. "k8s.io/apimachinery/pkg/runtime"
  45. "sigs.k8s.io/controller-runtime/pkg/client/fake"
  46. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  47. esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  48. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  49. )
  50. func TestConfigureSecretClientRoutesBySecretTypeAndScope(t *testing.T) {
  51. tests := []struct {
  52. name string
  53. provider *esv1.GithubProvider
  54. wantListPath string
  55. wantRepoGet bool
  56. }{
  57. {
  58. name: "omitted type uses Actions organization secrets",
  59. provider: &esv1.GithubProvider{
  60. Organization: "acme",
  61. },
  62. wantListPath: "/orgs/acme/actions/secrets",
  63. },
  64. {
  65. name: "explicit Actions uses repository secrets",
  66. provider: &esv1.GithubProvider{
  67. SecretType: esv1.GithubSecretTypeActions,
  68. Organization: "acme",
  69. Repository: "widgets",
  70. },
  71. wantListPath: "/repos/acme/widgets/actions/secrets",
  72. },
  73. {
  74. name: "Actions environment remains supported",
  75. provider: &esv1.GithubProvider{
  76. SecretType: esv1.GithubSecretTypeActions,
  77. Organization: "acme",
  78. Repository: "widgets",
  79. Environment: "production",
  80. },
  81. wantListPath: "/repositories/42/environments/production/secrets",
  82. wantRepoGet: true,
  83. },
  84. {
  85. name: "Dependabot uses organization secrets",
  86. provider: &esv1.GithubProvider{
  87. SecretType: esv1.GithubSecretTypeDependabot,
  88. Organization: "acme",
  89. },
  90. wantListPath: "/orgs/acme/dependabot/secrets",
  91. },
  92. {
  93. name: "Dependabot uses repository secrets without repository lookup",
  94. provider: &esv1.GithubProvider{
  95. SecretType: esv1.GithubSecretTypeDependabot,
  96. Organization: "acme",
  97. Repository: "widgets",
  98. },
  99. wantListPath: "/repos/acme/widgets/dependabot/secrets",
  100. },
  101. }
  102. for _, tt := range tests {
  103. t.Run(tt.name, func(t *testing.T) {
  104. var gotPaths []string
  105. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  106. gotPaths = append(gotPaths, r.URL.Path)
  107. if r.URL.Path == "/repos/acme/widgets" {
  108. _, _ = fmt.Fprint(w, `{"id":42}`)
  109. return
  110. }
  111. if r.URL.Path != tt.wantListPath {
  112. http.Error(w, "unexpected request", http.StatusNotFound)
  113. return
  114. }
  115. _, _ = fmt.Fprint(w, `{"total_count":0,"secrets":[]}`)
  116. }))
  117. t.Cleanup(server.Close)
  118. g := &Client{provider: tt.provider}
  119. secretType, err := validateGithubProvider(tt.provider)
  120. require.NoError(t, err)
  121. require.NoError(t, g.configureSecretClient(context.Background(), newGithubTestClient(t, server), secretType))
  122. _, _, err = g.listSecretsFn(context.Background())
  123. require.NoError(t, err)
  124. if tt.wantRepoGet {
  125. assert.Equal(t, []string{"/repos/acme/widgets", tt.wantListPath}, gotPaths)
  126. } else {
  127. assert.Equal(t, []string{tt.wantListPath}, gotPaths)
  128. }
  129. })
  130. }
  131. }
  132. type getSecretFn func(ctx context.Context, ref esv1.PushSecretRemoteRef) (*github.Secret, *github.Response, error)
  133. func withGetSecretFn(secret *github.Secret, response *github.Response, err error) getSecretFn {
  134. return func(_ context.Context, _ esv1.PushSecretRemoteRef) (*github.Secret, *github.Response, error) {
  135. return secret, response, err
  136. }
  137. }
  138. type getPublicKeyFn func(ctx context.Context) (*github.PublicKey, *github.Response, error)
  139. func withGetPublicKeyFn(key *github.PublicKey, response *github.Response, err error) getPublicKeyFn {
  140. return func(_ context.Context) (*github.PublicKey, *github.Response, error) {
  141. return key, response, err
  142. }
  143. }
  144. type createOrUpdateSecretFn func(ctx context.Context, encryptedSecret *github.EncryptedSecret) (*github.Response, error)
  145. func withCreateOrUpdateSecretFn(response *github.Response, err error) createOrUpdateSecretFn {
  146. return func(_ context.Context, _ *github.EncryptedSecret) (*github.Response, error) {
  147. return response, err
  148. }
  149. }
  150. func TestSecretExists(t *testing.T) {
  151. type testCase struct {
  152. name string
  153. prov *esv1.GithubProvider
  154. remoteRef esv1.PushSecretData
  155. getSecretFn getSecretFn
  156. wantErr error
  157. exists bool
  158. }
  159. tests := []testCase{
  160. {
  161. name: "getSecret fail",
  162. getSecretFn: withGetSecretFn(nil, nil, errors.New("boom")),
  163. exists: false,
  164. wantErr: errors.New("error fetching secret"),
  165. },
  166. {
  167. name: "no secret",
  168. getSecretFn: withGetSecretFn(nil, nil, nil),
  169. exists: false,
  170. },
  171. {
  172. name: "with secret",
  173. getSecretFn: withGetSecretFn(&github.Secret{}, nil, nil),
  174. exists: true,
  175. },
  176. }
  177. for _, test := range tests {
  178. t.Run(test.name, func(t *testing.T) {
  179. g := Client{
  180. provider: test.prov,
  181. }
  182. g.getSecretFn = test.getSecretFn
  183. ok, err := g.SecretExists(context.TODO(), test.remoteRef)
  184. assert.Equal(t, test.exists, ok)
  185. if test.wantErr == nil {
  186. assert.NoError(t, err)
  187. } else {
  188. assert.ErrorContains(t, err, test.wantErr.Error())
  189. }
  190. })
  191. }
  192. }
  193. func TestPushSecret(t *testing.T) {
  194. type testCase struct {
  195. name string
  196. prov *esv1.GithubProvider
  197. secret *corev1.Secret
  198. remoteRef esv1.PushSecretData
  199. getSecretFn getSecretFn
  200. getPublicKeyFn getPublicKeyFn
  201. createOrUpdateFn createOrUpdateSecretFn
  202. wantErr error
  203. }
  204. tests := []testCase{
  205. {
  206. name: "failGetSecretFn",
  207. getSecretFn: withGetSecretFn(nil, nil, errors.New("boom")),
  208. wantErr: errors.New("error fetching secret"),
  209. },
  210. {
  211. name: "failGetPublicKey",
  212. getSecretFn: withGetSecretFn(&github.Secret{
  213. Name: "foo",
  214. }, nil, nil),
  215. getPublicKeyFn: withGetPublicKeyFn(nil, nil, errors.New("boom")),
  216. wantErr: errors.New("error fetching public key"),
  217. },
  218. {
  219. name: "failDecodeKey",
  220. getSecretFn: withGetSecretFn(&github.Secret{
  221. Name: "foo",
  222. }, nil, nil),
  223. getPublicKeyFn: withGetPublicKeyFn(&github.PublicKey{
  224. Key: new("broken"),
  225. KeyID: new("123"),
  226. }, nil, nil),
  227. wantErr: errors.New("unable to decode public key"),
  228. },
  229. {
  230. name: "failSecretData",
  231. getSecretFn: withGetSecretFn(&github.Secret{
  232. Name: "foo",
  233. }, nil, nil),
  234. getPublicKeyFn: withGetPublicKeyFn(&github.PublicKey{
  235. Key: new("Cg=="),
  236. KeyID: new("123"),
  237. }, nil, nil),
  238. secret: &corev1.Secret{
  239. Data: map[string][]byte{
  240. "foo": []byte("bar"),
  241. },
  242. },
  243. remoteRef: esv1alpha1.PushSecretData{
  244. Match: esv1alpha1.PushSecretMatch{
  245. SecretKey: "bar",
  246. },
  247. },
  248. wantErr: errors.New("not found in secret"),
  249. },
  250. {
  251. name: "failSecretData",
  252. getSecretFn: withGetSecretFn(&github.Secret{
  253. Name: "foo",
  254. }, nil, nil),
  255. getPublicKeyFn: withGetPublicKeyFn(&github.PublicKey{
  256. Key: new("Zm9vYmFyCg=="),
  257. KeyID: new("123"),
  258. }, nil, nil),
  259. secret: &corev1.Secret{
  260. Data: map[string][]byte{
  261. "foo": []byte("bingg"),
  262. },
  263. },
  264. remoteRef: esv1alpha1.PushSecretData{
  265. Match: esv1alpha1.PushSecretMatch{
  266. SecretKey: "foo",
  267. },
  268. },
  269. createOrUpdateFn: withCreateOrUpdateSecretFn(nil, errors.New("boom")),
  270. wantErr: errors.New("failed to create secret"),
  271. },
  272. {
  273. name: "Success",
  274. getSecretFn: withGetSecretFn(&github.Secret{
  275. Name: "foo",
  276. }, nil, nil),
  277. getPublicKeyFn: withGetPublicKeyFn(&github.PublicKey{
  278. Key: new("Zm9vYmFyCg=="),
  279. KeyID: new("123"),
  280. }, nil, nil),
  281. secret: &corev1.Secret{
  282. Data: map[string][]byte{
  283. "foo": []byte("bingg"),
  284. },
  285. },
  286. remoteRef: esv1alpha1.PushSecretData{
  287. Match: esv1alpha1.PushSecretMatch{
  288. SecretKey: "foo",
  289. },
  290. },
  291. createOrUpdateFn: withCreateOrUpdateSecretFn(nil, nil),
  292. },
  293. }
  294. for _, test := range tests {
  295. t.Run(test.name, func(t *testing.T) {
  296. g := Client{
  297. provider: test.prov,
  298. }
  299. g.getSecretFn = test.getSecretFn
  300. g.getPublicKeyFn = test.getPublicKeyFn
  301. g.createOrUpdateFn = test.createOrUpdateFn
  302. err := g.PushSecret(context.TODO(), test.secret, test.remoteRef)
  303. if test.wantErr == nil {
  304. assert.NoError(t, err)
  305. } else {
  306. assert.ErrorContains(t, err, test.wantErr.Error())
  307. }
  308. })
  309. }
  310. }
  311. func TestPushSecretSelectedRepos(t *testing.T) {
  312. validKey := withGetPublicKeyFn(&github.PublicKey{
  313. Key: new("Zm9vYmFyCg=="),
  314. KeyID: new("123"),
  315. }, nil, nil)
  316. secret := &corev1.Secret{Data: map[string][]byte{"foo": []byte("bingg")}}
  317. ref := esv1alpha1.PushSecretData{
  318. Match: esv1alpha1.PushSecretMatch{SecretKey: "foo"},
  319. }
  320. t.Run("selected visibility preserves existing repositories", func(t *testing.T) {
  321. var pushed *github.EncryptedSecret
  322. g := Client{provider: &esv1.GithubProvider{}}
  323. g.getSecretFn = withGetSecretFn(&github.Secret{Name: "foo", Visibility: "selected"}, nil, nil)
  324. g.getPublicKeyFn = validKey
  325. g.listSelectedReposFn = func(_ context.Context, _ string) (github.SelectedRepoIDs, error) {
  326. return github.SelectedRepoIDs{1, 2, 3}, nil
  327. }
  328. g.createOrUpdateFn = func(_ context.Context, es *github.EncryptedSecret) (*github.Response, error) {
  329. pushed = es
  330. return nil, nil
  331. }
  332. require.NoError(t, g.PushSecret(context.TODO(), secret, ref))
  333. require.NotNil(t, pushed)
  334. assert.Equal(t, "selected", pushed.Visibility)
  335. assert.Equal(t, github.SelectedRepoIDs{1, 2, 3}, pushed.SelectedRepositoryIDs)
  336. })
  337. t.Run("list selected repos error is propagated", func(t *testing.T) {
  338. g := Client{provider: &esv1.GithubProvider{}}
  339. g.getSecretFn = withGetSecretFn(&github.Secret{Name: "foo", Visibility: "selected"}, nil, nil)
  340. g.getPublicKeyFn = validKey
  341. g.listSelectedReposFn = func(_ context.Context, _ string) (github.SelectedRepoIDs, error) {
  342. return nil, errors.New("boom")
  343. }
  344. g.createOrUpdateFn = withCreateOrUpdateSecretFn(nil, nil)
  345. err := g.PushSecret(context.TODO(), secret, ref)
  346. assert.ErrorContains(t, err, "failed to list selected repositories")
  347. })
  348. t.Run("non-selected visibility does not set repositories", func(t *testing.T) {
  349. var pushed *github.EncryptedSecret
  350. called := false
  351. g := Client{provider: &esv1.GithubProvider{}}
  352. g.getSecretFn = withGetSecretFn(&github.Secret{Name: "foo", Visibility: "all"}, nil, nil)
  353. g.getPublicKeyFn = validKey
  354. g.listSelectedReposFn = func(_ context.Context, _ string) (github.SelectedRepoIDs, error) {
  355. called = true
  356. return github.SelectedRepoIDs{9}, nil
  357. }
  358. g.createOrUpdateFn = func(_ context.Context, es *github.EncryptedSecret) (*github.Response, error) {
  359. pushed = es
  360. return nil, nil
  361. }
  362. require.NoError(t, g.PushSecret(context.TODO(), secret, ref))
  363. require.NotNil(t, pushed)
  364. assert.False(t, called)
  365. assert.Equal(t, "all", pushed.Visibility)
  366. assert.Nil(t, pushed.SelectedRepositoryIDs)
  367. })
  368. }
  369. func TestResolveOrgSecretVisibility(t *testing.T) {
  370. ptr := func(s string) *string { return &s }
  371. tests := []struct {
  372. name string
  373. nilProvider bool
  374. providerViz string
  375. existing *github.Secret
  376. want string
  377. }{
  378. {
  379. name: "nil provider, no existing secret — defaults to all",
  380. nilProvider: true,
  381. existing: nil,
  382. want: "all",
  383. },
  384. {
  385. name: "nil provider, existing secret has private — preserves private",
  386. nilProvider: true,
  387. existing: &github.Secret{Visibility: *ptr("private")},
  388. want: "private",
  389. },
  390. {
  391. name: "provider unset, no existing secret — defaults to all",
  392. providerViz: "",
  393. existing: nil,
  394. want: "all",
  395. },
  396. {
  397. name: "provider unset, existing secret has all — preserves all",
  398. providerViz: "",
  399. existing: &github.Secret{Visibility: *ptr("all")},
  400. want: "all",
  401. },
  402. {
  403. name: "provider unset, existing secret has private — preserves private",
  404. providerViz: "",
  405. existing: &github.Secret{Visibility: *ptr("private")},
  406. want: "private",
  407. },
  408. {
  409. name: "provider set to private, no existing secret",
  410. providerViz: "private",
  411. existing: nil,
  412. want: "private",
  413. },
  414. {
  415. name: "provider set to private, existing secret has all — provider wins",
  416. providerViz: "private",
  417. existing: &github.Secret{Visibility: *ptr("all")},
  418. want: "private",
  419. },
  420. {
  421. name: "provider set to all, existing secret has private — provider wins",
  422. providerViz: "all",
  423. existing: &github.Secret{Visibility: *ptr("private")},
  424. want: "all",
  425. },
  426. }
  427. for _, tt := range tests {
  428. t.Run(tt.name, func(t *testing.T) {
  429. g := &Client{}
  430. if !tt.nilProvider {
  431. g.provider = &esv1.GithubProvider{
  432. OrgSecretVisibility: tt.providerViz,
  433. }
  434. }
  435. got := g.resolveOrgSecretVisibility(tt.existing)
  436. assert.Equal(t, tt.want, got)
  437. })
  438. }
  439. }
  440. // generateTestPrivateKey generates a PEM-encoded RSA private key for testing.
  441. func generateTestPrivateKey() (string, error) {
  442. privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
  443. if err != nil {
  444. return "", err
  445. }
  446. privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
  447. privateKeyPEM := pem.EncodeToMemory(&pem.Block{
  448. Type: "RSA PRIVATE KEY",
  449. Bytes: privateKeyBytes,
  450. })
  451. return string(privateKeyPEM), nil
  452. }
  453. func TestAuthWithPrivateKey(t *testing.T) {
  454. // Generate a valid private key for testing
  455. privateKeyPEM, err := generateTestPrivateKey()
  456. require.NoError(t, err)
  457. tests := []struct {
  458. name string
  459. provider *esv1.GithubProvider
  460. secret *corev1.Secret
  461. wantErr bool
  462. wantBaseURL string
  463. wantUploadURL string
  464. checkTransport bool
  465. }{
  466. {
  467. name: "GitHub.com (default)",
  468. provider: &esv1.GithubProvider{
  469. AppID: 1,
  470. InstallationID: 1,
  471. URL: "https://github.com/",
  472. Auth: esv1.GithubAppAuth{
  473. PrivateKey: esmeta.SecretKeySelector{
  474. Name: "test-secret",
  475. Key: "private-key",
  476. },
  477. },
  478. },
  479. secret: &corev1.Secret{
  480. ObjectMeta: metav1.ObjectMeta{
  481. Name: "test-secret",
  482. Namespace: "default",
  483. },
  484. Data: map[string][]byte{
  485. "private-key": []byte(privateKeyPEM),
  486. },
  487. },
  488. wantErr: false,
  489. wantBaseURL: "https://api.github.com/",
  490. checkTransport: false, // For default GitHub, we don't modify transport
  491. },
  492. {
  493. name: "GitHub Enterprise with custom URL",
  494. provider: &esv1.GithubProvider{
  495. AppID: 1,
  496. InstallationID: 1,
  497. URL: "https://github.enterprise.com/",
  498. Auth: esv1.GithubAppAuth{
  499. PrivateKey: esmeta.SecretKeySelector{
  500. Name: "test-secret",
  501. Key: "private-key",
  502. },
  503. },
  504. },
  505. secret: &corev1.Secret{
  506. ObjectMeta: metav1.ObjectMeta{
  507. Name: "test-secret",
  508. Namespace: "default",
  509. },
  510. Data: map[string][]byte{
  511. "private-key": []byte(privateKeyPEM),
  512. },
  513. },
  514. wantErr: false,
  515. wantBaseURL: "https://github.enterprise.com/api/v3/",
  516. wantUploadURL: "https://github.enterprise.com/api/uploads/",
  517. checkTransport: true,
  518. },
  519. {
  520. name: "GitHub Enterprise with separate upload URL",
  521. provider: &esv1.GithubProvider{
  522. AppID: 1,
  523. InstallationID: 1,
  524. URL: "https://github.enterprise.com/api/v3",
  525. UploadURL: "https://uploads.github.enterprise.com/api/v3",
  526. Auth: esv1.GithubAppAuth{
  527. PrivateKey: esmeta.SecretKeySelector{
  528. Name: "test-secret",
  529. Key: "private-key",
  530. },
  531. },
  532. },
  533. secret: &corev1.Secret{
  534. ObjectMeta: metav1.ObjectMeta{
  535. Name: "test-secret",
  536. Namespace: "default",
  537. },
  538. Data: map[string][]byte{
  539. "private-key": []byte(privateKeyPEM),
  540. },
  541. },
  542. wantErr: false,
  543. wantBaseURL: "https://github.enterprise.com/api/v3/",
  544. wantUploadURL: "https://uploads.github.enterprise.com/api/v3/api/uploads/",
  545. checkTransport: true,
  546. },
  547. {
  548. name: "Empty URL (default to github.com)",
  549. provider: &esv1.GithubProvider{
  550. AppID: 1,
  551. InstallationID: 1,
  552. URL: "",
  553. Auth: esv1.GithubAppAuth{
  554. PrivateKey: esmeta.SecretKeySelector{
  555. Name: "test-secret",
  556. Key: "private-key",
  557. },
  558. },
  559. },
  560. secret: &corev1.Secret{
  561. ObjectMeta: metav1.ObjectMeta{
  562. Name: "test-secret",
  563. Namespace: "default",
  564. },
  565. Data: map[string][]byte{
  566. "private-key": []byte(privateKeyPEM),
  567. },
  568. },
  569. wantErr: false,
  570. wantBaseURL: "https://api.github.com/",
  571. checkTransport: false,
  572. },
  573. }
  574. for _, tt := range tests {
  575. t.Run(tt.name, func(t *testing.T) {
  576. // Create fake Kubernetes client with the secret
  577. scheme := runtime.NewScheme()
  578. _ = corev1.AddToScheme(scheme)
  579. fakeClient := fake.NewClientBuilder().
  580. WithScheme(scheme).
  581. WithObjects(tt.secret).
  582. Build()
  583. // Create the GitHub client
  584. client := &Client{
  585. crClient: fakeClient,
  586. provider: tt.provider,
  587. namespace: "default",
  588. storeKind: "SecretStore",
  589. }
  590. // Call AuthWithPrivateKey
  591. ghClient, err := client.AuthWithPrivateKey(context.Background())
  592. if tt.wantErr {
  593. assert.Error(t, err)
  594. return
  595. }
  596. require.NoError(t, err)
  597. require.NotNil(t, ghClient)
  598. // Verify the BaseURL is set correctly
  599. assert.Equal(t, tt.wantBaseURL, ghClient.BaseURL.String())
  600. // If UploadURL is specified, verify it
  601. if tt.wantUploadURL != "" {
  602. assert.Equal(t, tt.wantUploadURL, ghClient.UploadURL.String())
  603. }
  604. // For GitHub Enterprise, verify the transport BaseURL is also set
  605. if tt.checkTransport {
  606. transport := ghClient.Client().Transport
  607. require.NotNil(t, transport)
  608. // Type assert to ghinstallation.Transport
  609. ghTransport, ok := transport.(*ghinstallation.Transport)
  610. require.True(t, ok, "Expected transport to be *ghinstallation.Transport")
  611. // Verify the BaseURL is set on the transport
  612. assert.Equal(t, tt.wantBaseURL, ghTransport.BaseURL,
  613. "Transport BaseURL should match the enterprise URL")
  614. }
  615. })
  616. }
  617. }