diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..d3e1d6b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,77 @@ +name: Tests + +on: + push: + branches: + - master + pull_request: + +jobs: + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.16' + cache: true + + - name: Download dependencies + run: go mod download + + - name: Run unit tests + run: | + # Run tests excluding integration and example packages + go test -v -race -coverprofile=coverage.out -covermode=atomic \ + $(go list ./... | grep -v '/integration' | grep -v '/example') + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + if: success() + with: + file: ./coverage.out + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Check for test failures + if: failure() + run: | + echo "::error::Unit tests failed. Please check the logs above." + exit 1 + + build: + name: Build + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.16'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + + - name: Build + run: | + # Build all packages except example (multiple main) and integration (test-only) + go build -v $(go list ./... | grep -v '/example' | grep -v '/integration') + + - name: Test compilation + run: | + # Test compilation for all packages except example + go list ./... | grep -v '/example' | xargs -I {} go test -c -o /dev/null {} + diff --git a/download/downloader.go b/download/downloader.go index 672add8..4a7f267 100644 --- a/download/downloader.go +++ b/download/downloader.go @@ -12,9 +12,10 @@ import ( "time" "github.com/onsi/gomega/gbytes" - "github.com/pivotal-cf/go-pivnet/v7/logger" - "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/disk" "golang.org/x/sync/errgroup" + + "github.com/pivotal-cf/go-pivnet/v7/logger" ) //go:generate counterfeiter -o ./fakes/ranger.go --fake-name Ranger . ranger diff --git a/example/main.go b/example/main.go index 8ed8607..8cc4cef 100644 --- a/example/main.go +++ b/example/main.go @@ -11,8 +11,8 @@ import ( func main() { config := pivnet.ClientConfig{ - Host: pivnet.DefaultHost, - UserAgent: "pivnet-cli-example", + Host: pivnet.DefaultHost, + UserAgent: "pivnet-cli-example", SkipSSLValidation: true, } diff --git a/go.mod b/go.mod index 5b8bddb..a851382 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,12 @@ module github.com/pivotal-cf/go-pivnet/v7 +go 1.16 + require ( github.com/fatih/color v1.7.0 // indirect - github.com/jcmturner/gokrb5/v8 v8.4.4 + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.2 github.com/kr/pretty v0.1.0 // indirect github.com/mattn/go-colorable v0.0.9 // indirect github.com/mattn/go-isatty v0.0.4 // indirect @@ -10,10 +14,14 @@ require ( github.com/onsi/ginkgo v1.6.0 github.com/onsi/gomega v1.4.2 github.com/robdimsdale/sanitizer v0.0.0-20160522134901-ab2334cb7539 - github.com/shirou/gopsutil/v3 v3.24.5 - golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 + github.com/shirou/gopsutil v3.21.11+incompatible + github.com/stretchr/testify v1.9.0 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect + golang.org/x/text v0.3.7 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/cheggaaa/pb.v1 v1.0.26 ) - -go 1.13 diff --git a/go.sum b/go.sum index 9647557..561913d 100644 --- a/go.sum +++ b/go.sum @@ -9,9 +9,6 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= @@ -25,12 +22,13 @@ github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFK github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= -github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= -github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJzodkA= +github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -38,7 +36,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= @@ -51,73 +48,52 @@ github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/robdimsdale/sanitizer v0.0.0-20160522134901-ab2334cb7539 h1:h3AVw1v3JIE9Y1HyjYyiPTG73ywlF4754oiIjkxPjNk= github.com/robdimsdale/sanitizer v0.0.0-20160522134901-ab2334cb7539/go.mod h1:tqCODtkKV+9Tfvt9JURvKCTxJ69bA/OU/QhsaQLK/rc= -github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= -github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pivnet_test.go b/pivnet_test.go index 051d7c2..6e61253 100644 --- a/pivnet_test.go +++ b/pivnet_test.go @@ -2,10 +2,12 @@ package pivnet_test import ( "fmt" - "github.com/pivotal-cf/go-pivnet/v7/go-pivnetfakes" "net/http" + gopivnetfakes "github.com/pivotal-cf/go-pivnet/v7/go-pivnetfakes" + "github.com/onsi/gomega/ghttp" + "github.com/pivotal-cf/go-pivnet/v7" "github.com/pivotal-cf/go-pivnet/v7/logger" "github.com/pivotal-cf/go-pivnet/v7/logger/loggerfakes" @@ -21,10 +23,10 @@ type pivnetErr struct { var _ = Describe("PivnetClient", func() { var ( - server *ghttp.Server - client pivnet.Client - userAgent string - token string + server *ghttp.Server + client pivnet.Client + userAgent string + token string releases pivnet.ReleasesResponse @@ -516,4 +518,373 @@ var _ = Describe("PivnetClient", func() { Expect(req.Header.Get("Content-Type")).To(Equal("application/json")) }) }) + + Describe("NewClientWithProxy", func() { + var ( + proxyConfig pivnet.ClientConfig + ) + + BeforeEach(func() { + proxyConfig = pivnet.ClientConfig{ + Host: server.URL(), + UserAgent: userAgent, + } + }) + + Context("when no proxy auth is configured", func() { + It("creates a client successfully", func() { + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + }) + + It("creates a client with default transport", func() { + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client.HTTP).NotTo(BeNil()) + }) + + It("initializes all client services", func() { + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client.Auth).NotTo(BeNil()) + Expect(client.EULA).NotTo(BeNil()) + Expect(client.ProductFiles).NotTo(BeNil()) + Expect(client.FileGroups).NotTo(BeNil()) + Expect(client.Releases).NotTo(BeNil()) + Expect(client.Products).NotTo(BeNil()) + Expect(client.UserGroups).NotTo(BeNil()) + }) + }) + + Context("when Basic proxy auth is configured", func() { + BeforeEach(func() { + proxyConfig.ProxyAuthConfig = pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeBasic, + ProxyURL: "http://proxy.example.com:8080", + Username: "proxyuser", + Password: "proxypass", + } + }) + + It("creates a client with proxy auth transport", func() { + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + }) + + It("creates a client with custom transport", func() { + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client.HTTP).NotTo(BeNil()) + Expect(client.HTTP.Transport).NotTo(BeNil()) + }) + + It("accepts special characters in username", func() { + proxyConfig.ProxyAuthConfig.Username = "user@domain.com" + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + }) + + It("accepts special characters in password", func() { + proxyConfig.ProxyAuthConfig.Password = "p@$$w0rd!#%" + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + }) + + It("accepts HTTPS proxy URL", func() { + proxyConfig.ProxyAuthConfig.ProxyURL = "https://proxy.example.com:8443" + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + }) + }) + + Context("when SPNEGO proxy auth is configured", func() { + BeforeEach(func() { + proxyConfig.ProxyAuthConfig = pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeSPNEGO, + ProxyURL: "http://proxy.example.com:8080", + Username: "user@REALM.COM", + Password: "password", + Krb5Config: "/tmp/krb5.conf", + } + }) + + It("returns an error when Kerberos login fails", func() { + // SPNEGO will fail because there's no real KDC + _, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to create proxy authenticator")) + }) + + It("returns an error with empty username", func() { + proxyConfig.ProxyAuthConfig.Username = "" + _, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).To(HaveOccurred()) + }) + + It("returns an error with empty password", func() { + proxyConfig.ProxyAuthConfig.Password = "" + _, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).To(HaveOccurred()) + }) + + It("returns an error with empty Krb5Config", func() { + proxyConfig.ProxyAuthConfig.Krb5Config = "" + _, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("when proxy auth type is invalid", func() { + BeforeEach(func() { + proxyConfig.ProxyAuthConfig = pivnet.ProxyAuthConfig{ + AuthType: "invalid", + ProxyURL: "http://proxy.example.com:8080", + } + }) + + It("returns an error", func() { + _, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to create proxy authenticator")) + }) + }) + + Context("when proxy URL is empty but auth type is set", func() { + BeforeEach(func() { + proxyConfig.ProxyAuthConfig = pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeBasic, + ProxyURL: "", + Username: "user", + Password: "pass", + } + }) + + It("returns an error", func() { + _, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("proxy URL is required")) + }) + }) + + Context("when creating proxy auth transport fails", func() { + BeforeEach(func() { + proxyConfig.ProxyAuthConfig = pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeBasic, + ProxyURL: "", // Empty URL will cause validation error + } + }) + + It("returns an error", func() { + _, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("proxy URL is required")) + }) + }) + + Context("with SkipSSLValidation", func() { + BeforeEach(func() { + proxyConfig.SkipSSLValidation = true + }) + + It("creates a client with SSL validation skipped", func() { + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + }) + + It("works with Basic proxy auth", func() { + proxyConfig.ProxyAuthConfig = pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeBasic, + ProxyURL: "http://proxy.example.com:8080", + Username: "user", + Password: "pass", + } + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, proxyConfig, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + }) + }) + }) + + Describe("ProxyAuthConfig", func() { + Context("validation", func() { + It("accepts empty ProxyAuthConfig", func() { + config := pivnet.ClientConfig{ + Host: server.URL(), + UserAgent: userAgent, + ProxyAuthConfig: pivnet.ProxyAuthConfig{}, + } + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, config, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + }) + + It("accepts Basic auth with empty username", func() { + config := pivnet.ClientConfig{ + Host: server.URL(), + UserAgent: userAgent, + ProxyAuthConfig: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeBasic, + ProxyURL: "http://proxy.example.com:8080", + Username: "", + Password: "password", + }, + } + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, config, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + }) + + It("accepts Basic auth with empty password", func() { + config := pivnet.ClientConfig{ + Host: server.URL(), + UserAgent: userAgent, + ProxyAuthConfig: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeBasic, + ProxyURL: "http://proxy.example.com:8080", + Username: "username", + Password: "", + }, + } + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, config, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + }) + + It("accepts Basic auth with both username and password empty", func() { + config := pivnet.ClientConfig{ + Host: server.URL(), + UserAgent: userAgent, + ProxyAuthConfig: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeBasic, + ProxyURL: "http://proxy.example.com:8080", + Username: "", + Password: "", + }, + } + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, config, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + }) + + It("rejects SPNEGO auth without username", func() { + config := pivnet.ClientConfig{ + Host: server.URL(), + UserAgent: userAgent, + ProxyAuthConfig: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeSPNEGO, + ProxyURL: "http://proxy.example.com:8080", + Username: "", + Password: "password", + Krb5Config: "/tmp/krb5.conf", + }, + } + _, err := pivnet.NewClientWithProxy(fakeAccessTokenService, config, fakeLogger) + Expect(err).To(HaveOccurred()) + }) + + It("rejects SPNEGO auth without password", func() { + config := pivnet.ClientConfig{ + Host: server.URL(), + UserAgent: userAgent, + ProxyAuthConfig: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeSPNEGO, + ProxyURL: "http://proxy.example.com:8080", + Username: "user@REALM.COM", + Password: "", + Krb5Config: "/tmp/krb5.conf", + }, + } + _, err := pivnet.NewClientWithProxy(fakeAccessTokenService, config, fakeLogger) + Expect(err).To(HaveOccurred()) + }) + + It("rejects SPNEGO auth without Krb5Config", func() { + config := pivnet.ClientConfig{ + Host: server.URL(), + UserAgent: userAgent, + ProxyAuthConfig: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeSPNEGO, + ProxyURL: "http://proxy.example.com:8080", + Username: "user@REALM.COM", + Password: "password", + Krb5Config: "", + }, + } + _, err := pivnet.NewClientWithProxy(fakeAccessTokenService, config, fakeLogger) + Expect(err).To(HaveOccurred()) + }) + + It("rejects proxy auth without proxy URL", func() { + config := pivnet.ClientConfig{ + Host: server.URL(), + UserAgent: userAgent, + ProxyAuthConfig: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeBasic, + ProxyURL: "", + Username: "user", + Password: "pass", + }, + } + _, err := pivnet.NewClientWithProxy(fakeAccessTokenService, config, fakeLogger) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("proxy URL is required")) + }) + }) + + Context("auth type constants", func() { + It("accepts ProxyAuthTypeBasic constant", func() { + config := pivnet.ClientConfig{ + Host: server.URL(), + UserAgent: userAgent, + ProxyAuthConfig: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeBasic, + ProxyURL: "http://proxy.example.com:8080", + Username: "user", + Password: "pass", + }, + } + client, err := pivnet.NewClientWithProxy(fakeAccessTokenService, config, fakeLogger) + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + }) + + It("rejects uppercase 'BASIC' auth type", func() { + config := pivnet.ClientConfig{ + Host: server.URL(), + UserAgent: userAgent, + ProxyAuthConfig: pivnet.ProxyAuthConfig{ + AuthType: "BASIC", + ProxyURL: "http://proxy.example.com:8080", + Username: "user", + Password: "pass", + }, + } + _, err := pivnet.NewClientWithProxy(fakeAccessTokenService, config, fakeLogger) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unsupported proxy authentication type")) + }) + + It("rejects mixed case 'Basic' auth type", func() { + config := pivnet.ClientConfig{ + Host: server.URL(), + UserAgent: userAgent, + ProxyAuthConfig: pivnet.ProxyAuthConfig{ + AuthType: "Basic", + ProxyURL: "http://proxy.example.com:8080", + Username: "user", + Password: "pass", + }, + } + _, err := pivnet.NewClientWithProxy(fakeAccessTokenService, config, fakeLogger) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unsupported proxy authentication type")) + }) + }) + }) }) diff --git a/proxy_auth_basic_test.go b/proxy_auth_basic_test.go new file mode 100644 index 0000000..bf908f9 --- /dev/null +++ b/proxy_auth_basic_test.go @@ -0,0 +1,298 @@ +package pivnet_test + +import ( + "encoding/base64" + "net/http" + "net/http/httptest" + "strings" + "testing" + + pivnet "github.com/pivotal-cf/go-pivnet/v7" +) + +func TestNewBasicProxyAuth(t *testing.T) { + tests := []struct { + name string + username string + password string + }{ + { + name: "with valid credentials", + username: "testuser", + password: "testpass", + }, + { + name: "with empty username", + username: "", + password: "testpass", + }, + { + name: "with empty password", + username: "testuser", + password: "", + }, + { + name: "with both empty", + username: "", + password: "", + }, + { + name: "with special characters", + username: "user@domain.com", + password: "p@ssw0rd!#$%", + }, + { + name: "with spaces", + username: "user name", + password: "pass word", + }, + { + name: "with colon in password", + username: "user", + password: "pass:word", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + auth := pivnet.NewBasicProxyAuth(tt.username, tt.password) + if auth == nil { + t.Error("expected non-nil BasicProxyAuth") + } + }) + } +} + +func TestBasicProxyAuth_Authenticate(t *testing.T) { + tests := []struct { + name string + username string + password string + expectHeader bool + expectedHeaderValue string + }{ + { + name: "valid credentials", + username: "testuser", + password: "testpass", + expectHeader: true, + expectedHeaderValue: "Basic " + base64.StdEncoding.EncodeToString([]byte("testuser:testpass")), + }, + { + name: "empty username and password - no header", + username: "", + password: "", + expectHeader: false, + expectedHeaderValue: "", + }, + { + name: "empty username with password", + username: "", + password: "testpass", + expectHeader: true, + expectedHeaderValue: "Basic " + base64.StdEncoding.EncodeToString([]byte(":testpass")), + }, + { + name: "username with empty password", + username: "testuser", + password: "", + expectHeader: true, + expectedHeaderValue: "Basic " + base64.StdEncoding.EncodeToString([]byte("testuser:")), + }, + { + name: "credentials with special characters", + username: "user@domain.com", + password: "p@ssw0rd!#$%^&*()", + expectHeader: true, + expectedHeaderValue: "Basic " + base64.StdEncoding.EncodeToString([]byte("user@domain.com:p@ssw0rd!#$%^&*()")), + }, + { + name: "credentials with colon", + username: "user:name", + password: "pass:word", + expectHeader: true, + expectedHeaderValue: "Basic " + base64.StdEncoding.EncodeToString([]byte("user:name:pass:word")), + }, + { + name: "credentials with spaces", + username: "user name", + password: "pass word", + expectHeader: true, + expectedHeaderValue: "Basic " + base64.StdEncoding.EncodeToString([]byte("user name:pass word")), + }, + { + name: "credentials with newlines", + username: "user\nname", + password: "pass\nword", + expectHeader: true, + expectedHeaderValue: "Basic " + base64.StdEncoding.EncodeToString([]byte("user\nname:pass\nword")), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + auth := pivnet.NewBasicProxyAuth(tt.username, tt.password) + req := httptest.NewRequest("GET", "http://example.com", nil) + + err := auth.Authenticate(req) + if err != nil { + t.Errorf("expected no error, but got: %v", err) + return + } + + headerValue := req.Header.Get("Proxy-Authorization") + if tt.expectHeader { + if headerValue == "" { + t.Error("expected Proxy-Authorization header to be set, but it was empty") + return + } + if headerValue != tt.expectedHeaderValue { + t.Errorf("expected header value '%s', got '%s'", tt.expectedHeaderValue, headerValue) + } + + // Verify the header can be decoded + if strings.HasPrefix(headerValue, "Basic ") { + encodedCreds := strings.TrimPrefix(headerValue, "Basic ") + decodedBytes, err := base64.StdEncoding.DecodeString(encodedCreds) + if err != nil { + t.Errorf("failed to decode base64: %v", err) + } + expectedCreds := tt.username + ":" + tt.password + if string(decodedBytes) != expectedCreds { + t.Errorf("decoded credentials '%s' don't match expected '%s'", string(decodedBytes), expectedCreds) + } + } + } else { + if headerValue != "" { + t.Errorf("expected no Proxy-Authorization header, but got: %s", headerValue) + } + } + }) + } +} + +func TestBasicProxyAuth_WithMockProxyServer(t *testing.T) { + t.Run("successful authentication with mock proxy", func(t *testing.T) { + authAttempts := 0 + proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authAttempts++ + + // Check for Proxy-Authorization header + authHeader := r.Header.Get("Proxy-Authorization") + if authHeader == "" { + w.Header().Set("Proxy-Authenticate", "Basic realm=\"Mock Proxy\"") + w.WriteHeader(http.StatusProxyAuthRequired) + w.Write([]byte("Proxy authentication required")) + return + } + + // Decode and verify credentials + expectedAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("mockuser:mockpass")) + if authHeader != expectedAuth { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("Invalid proxy credentials")) + return + } + + // Success + w.WriteHeader(http.StatusOK) + w.Write([]byte("Authenticated through proxy")) + })) + defer proxyServer.Close() + + // Create auth and transport + auth := pivnet.NewBasicProxyAuth("mockuser", "mockpass") + transport, err := pivnet.NewProxyAuthTransport(http.DefaultTransport, auth) + if err != nil { + t.Fatalf("failed to create transport: %v", err) + } + + // Make request + client := &http.Client{Transport: transport} + resp, err := client.Get(proxyServer.URL) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", resp.StatusCode) + } + + if authAttempts != 1 { + t.Errorf("expected 1 auth attempt, got %d", authAttempts) + } + }) + + t.Run("proxy rejects invalid credentials", func(t *testing.T) { + proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Proxy-Authorization") + + // Only accept specific credentials + validAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("validuser:validpass")) + if authHeader != validAuth { + w.Header().Set("Proxy-Authenticate", "Basic realm=\"Mock Proxy\"") + w.WriteHeader(http.StatusProxyAuthRequired) + w.Write([]byte("Invalid credentials")) + return + } + + w.WriteHeader(http.StatusOK) + })) + defer proxyServer.Close() + + // Use invalid credentials + auth := pivnet.NewBasicProxyAuth("invaliduser", "invalidpass") + transport, err := pivnet.NewProxyAuthTransport(http.DefaultTransport, auth) + if err != nil { + t.Fatalf("failed to create transport: %v", err) + } + + client := &http.Client{Transport: transport} + resp, err := client.Get(proxyServer.URL) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusProxyAuthRequired { + t.Errorf("expected status 407, got %d", resp.StatusCode) + } + }) + + t.Run("proxy with special characters in credentials", func(t *testing.T) { + specialUser := "user@domain.com" + specialPass := "p@ss:w0rd!#$%" + + proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Proxy-Authorization") + expectedAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(specialUser+":"+specialPass)) + + if authHeader != expectedAuth { + w.WriteHeader(http.StatusProxyAuthRequired) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("Special chars handled")) + })) + defer proxyServer.Close() + + auth := pivnet.NewBasicProxyAuth(specialUser, specialPass) + transport, err := pivnet.NewProxyAuthTransport(http.DefaultTransport, auth) + if err != nil { + t.Fatalf("failed to create transport: %v", err) + } + + client := &http.Client{Transport: transport} + resp, err := client.Get(proxyServer.URL) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", resp.StatusCode) + } + }) +} diff --git a/proxy_auth_spnego_test.go b/proxy_auth_spnego_test.go new file mode 100644 index 0000000..d0b7e30 --- /dev/null +++ b/proxy_auth_spnego_test.go @@ -0,0 +1,251 @@ +package pivnet_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + pivnet "github.com/pivotal-cf/go-pivnet/v7" +) + +func TestNewSPNEGOProxyAuth(t *testing.T) { + // Create a temporary krb5.conf for testing + tmpDir := t.TempDir() + krb5ConfPath := filepath.Join(tmpDir, "krb5.conf") + + krb5Conf := `[libdefaults] + default_realm = EXAMPLE.COM + dns_lookup_realm = false + dns_lookup_kdc = false + +[realms] + EXAMPLE.COM = { + kdc = kdc.example.com + admin_server = kdc.example.com + } + +[domain_realm] + .example.com = EXAMPLE.COM + example.com = EXAMPLE.COM +` + if err := ioutil.WriteFile(krb5ConfPath, []byte(krb5Conf), 0644); err != nil { + t.Fatalf("failed to create test krb5.conf: %v", err) + } + + tests := []struct { + name string + username string + password string + proxyURL string + krb5ConfigPath string + expectError bool + errorContains string + }{ + { + name: "empty username", + username: "", + password: "password", + proxyURL: "http://proxy.example.com:8080", + krb5ConfigPath: krb5ConfPath, + expectError: true, + errorContains: "username, password, and proxyURL are required", + }, + { + name: "empty password", + username: "user@EXAMPLE.COM", + password: "", + proxyURL: "http://proxy.example.com:8080", + krb5ConfigPath: krb5ConfPath, + expectError: true, + errorContains: "username, password, and proxyURL are required", + }, + { + name: "empty proxy URL", + username: "user@EXAMPLE.COM", + password: "password", + proxyURL: "", + krb5ConfigPath: krb5ConfPath, + expectError: true, + errorContains: "username, password, and proxyURL are required", + }, + { + name: "invalid proxy URL scheme", + username: "user@EXAMPLE.COM", + password: "password", + proxyURL: "ftp://proxy.example.com:8080", + krb5ConfigPath: krb5ConfPath, + expectError: true, + errorContains: "proxy URL must start with http:// or https://", + }, + { + name: "proxy URL without scheme", + username: "user@EXAMPLE.COM", + password: "password", + proxyURL: "proxy.example.com:8080", + krb5ConfigPath: krb5ConfPath, + expectError: true, + errorContains: "proxy URL must start with http:// or https://", + }, + { + name: "path traversal in krb5 config", + username: "user@EXAMPLE.COM", + password: "password", + proxyURL: "http://proxy.example.com:8080", + krb5ConfigPath: "/etc/../../../etc/passwd", + expectError: true, + errorContains: "krb5 config path contains invalid path traversal", + }, + { + name: "relative path traversal in krb5 config", + username: "user@EXAMPLE.COM", + password: "password", + proxyURL: "http://proxy.example.com:8080", + krb5ConfigPath: "../krb5.conf", + expectError: true, + errorContains: "krb5 config path contains invalid path traversal", + }, + { + name: "non-existent krb5 config file", + username: "user@EXAMPLE.COM", + password: "password", + proxyURL: "http://proxy.example.com:8080", + krb5ConfigPath: "/nonexistent/krb5.conf", + expectError: true, + errorContains: "failed to load Kerberos config", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + auth, err := pivnet.NewSPNEGOProxyAuth(tt.username, tt.password, tt.proxyURL, tt.krb5ConfigPath) + + if tt.expectError { + if err == nil { + t.Errorf("expected error containing '%s', but got no error", tt.errorContains) + return + } + if !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("expected error containing '%s', but got: %v", tt.errorContains, err) + } + if auth != nil { + t.Errorf("expected nil auth on error, but got: %v", auth) + } + } else { + if err != nil { + t.Errorf("expected no error, but got: %v", err) + return + } + if auth == nil { + t.Errorf("expected auth to be non-nil") + } + } + }) + } +} + +func TestNewSPNEGOProxyAuth_ConfigValidation(t *testing.T) { + tmpDir := t.TempDir() + + t.Run("missing default_realm in config", func(t *testing.T) { + krb5ConfPath := filepath.Join(tmpDir, "no_realm.conf") + krb5Conf := `[libdefaults] + dns_lookup_realm = false + dns_lookup_kdc = false +` + if err := ioutil.WriteFile(krb5ConfPath, []byte(krb5Conf), 0644); err != nil { + t.Fatalf("failed to create test krb5.conf: %v", err) + } + + _, err := pivnet.NewSPNEGOProxyAuth( + "user", + "pass", + "http://proxy.example.com:8080", + krb5ConfPath, + ) + + if err == nil { + t.Error("expected error for missing realm, but got none") + return + } + + if !strings.Contains(err.Error(), "domain/realm is required") { + t.Errorf("expected error about missing realm, got: %v", err) + } + }) + + t.Run("empty config file", func(t *testing.T) { + krb5ConfPath := filepath.Join(tmpDir, "empty.conf") + if err := ioutil.WriteFile(krb5ConfPath, []byte(""), 0644); err != nil { + t.Fatalf("failed to create test krb5.conf: %v", err) + } + + _, err := pivnet.NewSPNEGOProxyAuth( + "user", + "pass", + "http://proxy.example.com:8080", + krb5ConfPath, + ) + + if err == nil { + t.Error("expected error for empty config, but got none") + } + }) + + t.Run("malformed config file", func(t *testing.T) { + krb5ConfPath := filepath.Join(tmpDir, "malformed.conf") + if err := ioutil.WriteFile(krb5ConfPath, []byte("invalid config content [[["), 0644); err != nil { + t.Fatalf("failed to create test krb5.conf: %v", err) + } + + _, err := pivnet.NewSPNEGOProxyAuth( + "user", + "pass", + "http://proxy.example.com:8080", + krb5ConfPath, + ) + + if err == nil { + t.Error("expected error for malformed config, but got none") + } + }) +} + +func TestSPNEGOProxyAuth_DefaultConfigPath(t *testing.T) { + t.Run("uses KRB5_CONFIG environment variable", func(t *testing.T) { + tmpDir := t.TempDir() + krb5ConfPath := filepath.Join(tmpDir, "custom_krb5.conf") + + krb5Conf := `[libdefaults] + default_realm = CUSTOM.REALM + +[realms] + CUSTOM.REALM = { + kdc = kdc.custom.realm + } +` + if err := ioutil.WriteFile(krb5ConfPath, []byte(krb5Conf), 0644); err != nil { + t.Fatalf("failed to create test krb5.conf: %v", err) + } + + // Set KRB5_CONFIG environment variable + oldEnv := os.Getenv("KRB5_CONFIG") + os.Setenv("KRB5_CONFIG", krb5ConfPath) + defer os.Setenv("KRB5_CONFIG", oldEnv) + + _, err := pivnet.NewSPNEGOProxyAuth( + "user", + "pass", + "http://proxy.custom.realm:8080", + "", // Empty config path should use environment variable + ) + + // Will fail on KDC connection, but should have loaded the config + if err != nil && !strings.Contains(err.Error(), "failed to login to Kerberos") { + if strings.Contains(err.Error(), "failed to load Kerberos config") { + t.Errorf("failed to use KRB5_CONFIG environment variable: %v", err) + } + } + }) +} diff --git a/proxy_auth_test.go b/proxy_auth_test.go new file mode 100644 index 0000000..7ba2547 --- /dev/null +++ b/proxy_auth_test.go @@ -0,0 +1,450 @@ +package pivnet_test + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + pivnet "github.com/pivotal-cf/go-pivnet/v7" +) + +// mockAuthenticator is a mock implementation of ProxyAuthenticator for testing +type mockAuthenticator struct { + authenticateCalled int + authenticateError error + headerToSet string + headerValue string +} + +func (m *mockAuthenticator) Authenticate(req *http.Request) error { + m.authenticateCalled++ + if m.authenticateError != nil { + return m.authenticateError + } + if m.headerToSet != "" { + req.Header.Set(m.headerToSet, m.headerValue) + } + return nil +} + +// mockRoundTripper is a mock implementation of http.RoundTripper for testing +type mockRoundTripper struct { + roundTripCalled int + response *http.Response + err error +} + +func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + m.roundTripCalled++ + if m.err != nil { + return nil, m.err + } + if m.response != nil { + return m.response, nil + } + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: http.NoBody, + Request: req, + }, nil +} + +func TestNewProxyAuthTransport(t *testing.T) { + tests := []struct { + name string + transport http.RoundTripper + authenticator pivnet.ProxyAuthenticator + expectError bool + errorContains string + }{ + { + name: "valid transport and authenticator", + transport: &mockRoundTripper{}, + authenticator: &mockAuthenticator{}, + expectError: false, + }, + { + name: "nil transport", + transport: nil, + authenticator: &mockAuthenticator{}, + expectError: true, + errorContains: "transport cannot be nil", + }, + { + name: "nil authenticator", + transport: &mockRoundTripper{}, + authenticator: nil, + expectError: true, + errorContains: "authenticator cannot be nil", + }, + { + name: "both nil", + transport: nil, + authenticator: nil, + expectError: true, + errorContains: "transport cannot be nil", + }, + { + name: "with http.Transport", + transport: &http.Transport{}, + authenticator: &mockAuthenticator{}, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + transport, err := pivnet.NewProxyAuthTransport(tt.transport, tt.authenticator) + + if tt.expectError { + if err == nil { + t.Errorf("expected error containing '%s', but got no error", tt.errorContains) + return + } + if tt.errorContains != "" && !contains(err.Error(), tt.errorContains) { + t.Errorf("expected error containing '%s', but got: %v", tt.errorContains, err) + } + if transport != nil { + t.Errorf("expected nil transport on error, but got: %v", transport) + } + } else { + if err != nil { + t.Errorf("expected no error, but got: %v", err) + return + } + if transport == nil { + t.Errorf("expected transport to be non-nil") + } + } + }) + } +} + +func TestProxyAuthTransport_RoundTrip(t *testing.T) { + tests := []struct { + name string + authenticateError error + roundTripError error + expectError bool + errorContains string + expectedAuthCalls int + expectedRoundTripCalls int + }{ + { + name: "successful round trip", + authenticateError: nil, + roundTripError: nil, + expectError: false, + expectedAuthCalls: 1, + expectedRoundTripCalls: 1, + }, + { + name: "authentication fails", + authenticateError: fmt.Errorf("auth failed"), + roundTripError: nil, + expectError: true, + errorContains: "failed to authenticate proxy request", + expectedAuthCalls: 1, + expectedRoundTripCalls: 0, + }, + { + name: "round trip fails", + authenticateError: nil, + roundTripError: fmt.Errorf("connection failed"), + expectError: true, + errorContains: "connection failed", + expectedAuthCalls: 1, + expectedRoundTripCalls: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockAuth := &mockAuthenticator{ + authenticateError: tt.authenticateError, + headerToSet: "Proxy-Authorization", + headerValue: "Bearer test-token", + } + mockRT := &mockRoundTripper{ + err: tt.roundTripError, + } + + transport, err := pivnet.NewProxyAuthTransport(mockRT, mockAuth) + if err != nil { + t.Fatalf("failed to create transport: %v", err) + } + + req := httptest.NewRequest("GET", "http://example.com", nil) + resp, err := transport.RoundTrip(req) + + if tt.expectError { + if err == nil { + t.Errorf("expected error containing '%s', but got no error", tt.errorContains) + return + } + if tt.errorContains != "" && !contains(err.Error(), tt.errorContains) { + t.Errorf("expected error containing '%s', but got: %v", tt.errorContains, err) + } + } else { + if err != nil { + t.Errorf("expected no error, but got: %v", err) + return + } + if resp == nil { + t.Errorf("expected response to be non-nil") + } + } + + if mockAuth.authenticateCalled != tt.expectedAuthCalls { + t.Errorf("expected %d authenticate calls, got %d", tt.expectedAuthCalls, mockAuth.authenticateCalled) + } + if mockRT.roundTripCalled != tt.expectedRoundTripCalls { + t.Errorf("expected %d round trip calls, got %d", tt.expectedRoundTripCalls, mockRT.roundTripCalled) + } + }) + } +} + +func TestProxyAuthTransport_GetProxyConnectHeader(t *testing.T) { + t.Run("http.Transport sets GetProxyConnectHeader", func(t *testing.T) { + mockAuth := &mockAuthenticator{ + headerToSet: "Proxy-Authorization", + headerValue: "Bearer connect-token", + } + httpTransport := &http.Transport{} + + transport, err := pivnet.NewProxyAuthTransport(httpTransport, mockAuth) + if err != nil { + t.Fatalf("failed to create transport: %v", err) + } + + // Verify GetProxyConnectHeader was set + if httpTransport.GetProxyConnectHeader == nil { + t.Fatal("expected GetProxyConnectHeader to be set") + } + + // Test the GetProxyConnectHeader function + ctx := context.Background() + proxyURL, _ := url.Parse("http://proxy.example.com:8080") + headers, err := httpTransport.GetProxyConnectHeader(ctx, proxyURL, "example.com:443") + + if err != nil { + t.Errorf("expected no error, but got: %v", err) + } + + if headers.Get("Proxy-Authorization") != "Bearer connect-token" { + t.Errorf("expected Proxy-Authorization header to be set, got: %s", headers.Get("Proxy-Authorization")) + } + + if mockAuth.authenticateCalled != 1 { + t.Errorf("expected 1 authenticate call, got %d", mockAuth.authenticateCalled) + } + + // Ensure transport is properly wrapped + if transport == nil { + t.Error("expected transport to be non-nil") + } + }) + + t.Run("GetProxyConnectHeader handles authentication error", func(t *testing.T) { + mockAuth := &mockAuthenticator{ + authenticateError: fmt.Errorf("auth error"), + } + httpTransport := &http.Transport{} + + _, err := pivnet.NewProxyAuthTransport(httpTransport, mockAuth) + if err != nil { + t.Fatalf("failed to create transport: %v", err) + } + + ctx := context.Background() + proxyURL, _ := url.Parse("http://proxy.example.com:8080") + _, err = httpTransport.GetProxyConnectHeader(ctx, proxyURL, "example.com:443") + + if err == nil { + t.Error("expected error, but got none") + } + if !contains(err.Error(), "failed to authenticate CONNECT request") { + t.Errorf("expected error to contain 'failed to authenticate CONNECT request', got: %v", err) + } + }) + + t.Run("non-http.Transport doesn't set GetProxyConnectHeader", func(t *testing.T) { + mockAuth := &mockAuthenticator{} + mockRT := &mockRoundTripper{} + + transport, err := pivnet.NewProxyAuthTransport(mockRT, mockAuth) + if err != nil { + t.Fatalf("failed to create transport: %v", err) + } + + if transport == nil { + t.Error("expected transport to be non-nil") + } + }) +} + +func TestProxyAuthTransport_WithMockProxy(t *testing.T) { + t.Run("mock proxy requiring authentication", func(t *testing.T) { + // Create a mock proxy server that requires authentication + proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check for Proxy-Authorization header + authHeader := r.Header.Get("Proxy-Authorization") + if authHeader == "" { + w.Header().Set("Proxy-Authenticate", "Basic realm=\"Test Proxy\"") + w.WriteHeader(http.StatusProxyAuthRequired) + w.Write([]byte("Proxy authentication required")) + return + } + + // Validate the auth header + expectedAuth := "Basic dGVzdHVzZXI6dGVzdHBhc3M=" // testuser:testpass + if authHeader != expectedAuth { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("Invalid credentials")) + return + } + + // Authentication successful - proxy the request + w.WriteHeader(http.StatusOK) + w.Write([]byte("Proxy authenticated successfully")) + })) + defer proxyServer.Close() + + // Create authenticator + auth := pivnet.NewBasicProxyAuth("testuser", "testpass") + + // Create transport with proxy + transport, err := pivnet.NewProxyAuthTransport(http.DefaultTransport, auth) + if err != nil { + t.Fatalf("failed to create transport: %v", err) + } + + // Make request through the "proxy" (simulated) + client := &http.Client{Transport: transport} + resp, err := client.Get(proxyServer.URL) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", resp.StatusCode) + } + }) + + t.Run("mock proxy rejecting invalid credentials", func(t *testing.T) { + proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Proxy-Authorization") + + // Always reject with wrong credentials + if authHeader != "Basic Y29ycmVjdDpjcmVkcw==" { // correct:creds + w.Header().Set("Proxy-Authenticate", "Basic realm=\"Test Proxy\"") + w.WriteHeader(http.StatusProxyAuthRequired) + w.Write([]byte("Authentication required")) + return + } + + w.WriteHeader(http.StatusOK) + })) + defer proxyServer.Close() + + // Use wrong credentials + auth := pivnet.NewBasicProxyAuth("wrong", "credentials") + + transport, err := pivnet.NewProxyAuthTransport(http.DefaultTransport, auth) + if err != nil { + t.Fatalf("failed to create transport: %v", err) + } + + client := &http.Client{Transport: transport} + resp, err := client.Get(proxyServer.URL) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + // Should get 407 Proxy Authentication Required + if resp.StatusCode != http.StatusProxyAuthRequired { + t.Errorf("expected status 407, got %d", resp.StatusCode) + } + }) + + t.Run("mock proxy forwarding to target server", func(t *testing.T) { + // Create target server + targetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("Target server response")) + })) + defer targetServer.Close() + + // Create proxy server that actually forwards to target + proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Proxy-Authorization") + if authHeader == "" { + w.WriteHeader(http.StatusProxyAuthRequired) + return + } + + // Actually forward the request to target server + targetResp, err := http.Get(targetServer.URL) + if err != nil { + w.WriteHeader(http.StatusBadGateway) + w.Write([]byte(fmt.Sprintf("Proxy failed to reach target: %v", err))) + return + } + defer targetResp.Body.Close() + + // Copy response from target + body := make([]byte, 1024) + n, _ := targetResp.Body.Read(body) + w.WriteHeader(targetResp.StatusCode) + w.Write(body[:n]) + })) + defer proxyServer.Close() + + auth := pivnet.NewBasicProxyAuth("proxyuser", "proxypass") + transport, err := pivnet.NewProxyAuthTransport(http.DefaultTransport, auth) + if err != nil { + t.Fatalf("failed to create transport: %v", err) + } + + client := &http.Client{Transport: transport} + resp, err := client.Get(proxyServer.URL) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", resp.StatusCode) + } + + // Verify we got the response from the target server + body := make([]byte, 1024) + n, _ := resp.Body.Read(body) + responseBody := string(body[:n]) + if responseBody != "Target server response" { + t.Errorf("expected 'Target server response', got '%s'", responseBody) + } + }) +} + +// Helper function to check if a string contains a substring +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(substr) == 0 || + (len(s) > 0 && len(substr) > 0 && containsHelper(s, substr))) +} + +func containsHelper(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/proxy_authenticator_test.go b/proxy_authenticator_test.go new file mode 100644 index 0000000..a07d88f --- /dev/null +++ b/proxy_authenticator_test.go @@ -0,0 +1,204 @@ +package pivnet_test + +import ( + "strings" + "testing" + + pivnet "github.com/pivotal-cf/go-pivnet/v7" +) + +func TestNewProxyAuthenticator(t *testing.T) { + tests := []struct { + name string + config pivnet.ProxyAuthConfig + expectError bool + errorContains string + expectAuthType string // Expected type of authenticator + }{ + // Basic Authentication - Success Cases + { + name: "basic auth with valid credentials", + config: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeBasic, + Username: "testuser", + Password: "testpass", + ProxyURL: "http://proxy.example.com:8080", + }, + expectError: false, + expectAuthType: "BasicProxyAuth", + }, + { + name: "basic auth with empty username and password", + config: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeBasic, + Username: "", + Password: "", + ProxyURL: "http://proxy.example.com:8080", + }, + expectError: false, + expectAuthType: "BasicProxyAuth", + }, + { + name: "basic auth with special characters in password", + config: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeBasic, + Username: "user", + Password: "p@ssw0rd!#$%", + ProxyURL: "http://proxy.example.com:8080", + }, + expectError: false, + expectAuthType: "BasicProxyAuth", + }, + // SPNEGO Authentication - Error Cases (can't test success without real Kerberos, will handle this in integration tests) + { + name: "spnego with empty username", + config: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeSPNEGO, + Username: "", + Password: "password", + ProxyURL: "http://proxy.example.com:8080", + }, + expectError: true, + errorContains: "username, password, and proxyURL are required", + }, + { + name: "spnego with empty password", + config: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeSPNEGO, + Username: "user@REALM.COM", + Password: "", + ProxyURL: "http://proxy.example.com:8080", + }, + expectError: true, + errorContains: "username, password, and proxyURL are required", + }, + { + name: "spnego with empty proxy URL", + config: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeSPNEGO, + Username: "user@REALM.COM", + Password: "password", + ProxyURL: "", + }, + expectError: true, + errorContains: "username, password, and proxyURL are required", + }, + { + name: "spnego with invalid proxy URL scheme", + config: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeSPNEGO, + Username: "user@REALM.COM", + Password: "password", + ProxyURL: "ftp://proxy.example.com:8080", + }, + expectError: true, + errorContains: "proxy URL must start with http:// or https://", + }, + { + name: "spnego with proxy URL without scheme", + config: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeSPNEGO, + Username: "user@REALM.COM", + Password: "password", + ProxyURL: "proxy.example.com:8080", + }, + expectError: true, + errorContains: "proxy URL must start with http:// or https://", + }, + { + name: "spnego with path traversal in krb5 config", + config: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeSPNEGO, + Username: "user@REALM.COM", + Password: "password", + ProxyURL: "http://proxy.example.com:8080", + Krb5Config: "/etc/../../../etc/passwd", + }, + expectError: true, + errorContains: "krb5 config path contains invalid path traversal", + }, + { + name: "spnego with relative path traversal in krb5 config", + config: pivnet.ProxyAuthConfig{ + AuthType: pivnet.ProxyAuthTypeSPNEGO, + Username: "user@REALM.COM", + Password: "password", + ProxyURL: "http://proxy.example.com:8080", + Krb5Config: "../krb5.conf", + }, + expectError: true, + errorContains: "krb5 config path contains invalid path traversal", + }, + + // General Error Cases + { + name: "empty auth type", + config: pivnet.ProxyAuthConfig{ + AuthType: "", + Username: "user", + Password: "pass", + ProxyURL: "http://proxy.example.com:8080", + }, + expectError: true, + errorContains: "proxy authentication type cannot be empty", + }, + { + name: "unsupported auth type", + config: pivnet.ProxyAuthConfig{ + AuthType: "digest", + Username: "user", + Password: "pass", + ProxyURL: "http://proxy.example.com:8080", + }, + expectError: true, + errorContains: "unsupported proxy authentication type", + }, + { + name: "case insensitive basic auth type", + config: pivnet.ProxyAuthConfig{ + AuthType: "BASIC", + Username: "testuser", + Password: "testpass", + ProxyURL: "http://proxy.example.com:8080", + }, + expectError: true, + }, + { + name: "case insensitive spnego auth type (will fail on krb5 config)", + config: pivnet.ProxyAuthConfig{ + AuthType: "SPNEGO", + Username: "user@REALM.COM", + Password: "password", + ProxyURL: "http://proxy.example.com:8080", + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + authenticator, err := pivnet.NewProxyAuthenticator(tt.config) + + if tt.expectError { + if err == nil { + t.Errorf("expected error containing '%s', but got no error", tt.errorContains) + return + } + if !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("expected error containing '%s', but got: %v", tt.errorContains, err) + } + if authenticator != nil { + t.Errorf("expected nil authenticator on error, but got: %v", authenticator) + } + } else { + if err != nil { + t.Errorf("expected no error, but got: %v", err) + return + } + if authenticator == nil { + t.Errorf("expected authenticator to be non-nil") + } + } + }) + } +}