diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1910f7cd..e8e5e011 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,3 +23,6 @@ jobs: - name: Build the SDK run: go build ./... + + - name: Run unit tests + run: go test ./... diff --git a/github/authentication/request.go b/github/authentication/request.go new file mode 100644 index 00000000..01255e03 --- /dev/null +++ b/github/authentication/request.go @@ -0,0 +1,49 @@ +package authentication + +import ( + "fmt" + + abs "github.com/microsoft/kiota-abstractions-go" + "github.com/octokit/go-sdk/github/headers" +) + +// Request provides a wrapper around Kiota's abs.RequestInformation type +type Request struct { + *abs.RequestInformation +} + +// WithAuthorization sets the Authorization header to the given token, +// prepended by the AuthType +func (r *Request) WithAuthorization(token string) { + if r.Headers.ContainsKey(headers.AuthorizationKey) { + r.Headers.Remove(headers.AuthorizationKey) + } + r.Headers.Add(headers.AuthorizationKey, fmt.Sprintf("%v %v", headers.AuthType, token)) +} + +// WithUserAgent allows the caller to set the User-Agent string for each request +func (r *Request) WithUserAgent(userAgent string) { + if r.Headers.ContainsKey(headers.UserAgentKey) { + r.Headers.Remove(headers.UserAgentKey) + } + r.Headers.Add(headers.UserAgentKey, userAgent) +} + +// WithDefaultUserAgent sets the default User-Agent string for each request +func (r *Request) WithDefaultUserAgent() { + r.WithUserAgent(headers.UserAgentValue) +} + +// WithAPIVersion sets the API version header for each request +func (r *Request) WithAPIVersion(version string) { + if r.Headers.ContainsKey(headers.APIVersionKey) { + r.Headers.Remove(headers.APIVersionKey) + } + r.Headers.Add(headers.APIVersionKey, version) +} + +// WithDefaultAPIVersion sets the API version header to the default (the version used +// to generate the code) for each request +func (r *Request) WithDefaultAPIVersion() { + r.WithAPIVersion(headers.APIVersionValue) +} diff --git a/github/authentication/token_provider.go b/github/authentication/token_provider.go new file mode 100644 index 00000000..bc558853 --- /dev/null +++ b/github/authentication/token_provider.go @@ -0,0 +1,85 @@ +package authentication + +import ( + "context" + + abs "github.com/microsoft/kiota-abstractions-go" +) + +type TokenProvider struct { + options []TokenProviderOption +} + +type TokenProviderOption func(*TokenProvider, *Request) + +// WithAuthorizationToken sets the AuthorizationToken for each request to the given token. +func WithAuthorizationToken(token string) TokenProviderOption { + return func(t *TokenProvider, r *Request) { + r.WithAuthorization(token) + } +} + +// WithDefaultUserAgent sets the User-Agent string sent for requests to the default +// for this SDK. +func WithDefaultUserAgent() TokenProviderOption { + return func(t *TokenProvider, r *Request) { + r.WithDefaultUserAgent() + } +} + +// WithUserAgent sets the User-Agent string sent with each request. +func WithUserAgent(userAgent string) TokenProviderOption { + return func(t *TokenProvider, r *Request) { + r.WithUserAgent(userAgent) + } +} + +// WithDefaultAPIVersion sets the API version header sent with each request. +func WithDefaultAPIVersion() TokenProviderOption { + return func(t *TokenProvider, r *Request) { + r.WithDefaultAPIVersion() + } +} + +// WithAPIVersion sets the API version header sent with each request. +func WithAPIVersion(version string) TokenProviderOption { + return func(t *TokenProvider, r *Request) { + r.WithAPIVersion(version) + } +} + +// TODO(kfcampbell): implement new constructor with allowedHosts + +// NewTokenProvider creates an instance of TokenProvider with the specified token and options. +func NewTokenProvider(options ...TokenProviderOption) *TokenProvider { + provider := &TokenProvider{ + options: options, + } + + return provider +} + +// defaultHandlers contains our "sensible defaults" for TokenProvider initialization +var defaultHandlers = []TokenProviderOption{WithDefaultUserAgent(), WithDefaultAPIVersion()} + +// AuthenticateRequest applies the default options for each request, then the user's options +// (if present in the TokenProvider). User options are guaranteed to be run in the order they +// were input. +func (t *TokenProvider) AuthenticateRequest(context context.Context, request *abs.RequestInformation, additionalAuthenticationContext map[string]interface{}) error { + reqWrapper := &Request{RequestInformation: request} + + if reqWrapper.Headers == nil { + reqWrapper.Headers = abs.NewRequestHeaders() + } + + for _, option := range defaultHandlers { + option(t, reqWrapper) + } + + // apply user options after defaults + for _, option := range t.options { + option(t, reqWrapper) + } + + return nil +} diff --git a/github/authentication/token_provider_test.go b/github/authentication/token_provider_test.go new file mode 100644 index 00000000..13f8bc5c --- /dev/null +++ b/github/authentication/token_provider_test.go @@ -0,0 +1,181 @@ +package authentication_test + +import ( + "context" + "fmt" + "log" + "os" + "strings" + "testing" + + abstractions "github.com/microsoft/kiota-abstractions-go" + http "github.com/microsoft/kiota-http-go" + "github.com/octokit/go-sdk/github/authentication" + "github.com/octokit/go-sdk/github/headers" + "github.com/octokit/go-sdk/github/octokit" + "github.com/octokit/go-sdk/github/octokit/user" +) + +func TestTokenIsSetInAuthenticatedRequest(t *testing.T) { + token := "help i'm trapped in a Go binary" + provider := authentication.NewTokenProvider(authentication.WithAuthorizationToken(token)) + + reqInfo := abstractions.NewRequestInformation() + addtlContext := make(map[string]interface{}) + + err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext) + if err != nil { + t.Errorf("there should be no error when calling AuthenticateRequest") + } + + if len(reqInfo.Headers.Get(headers.AuthorizationKey)) != 1 { + t.Errorf("there should be exactly one authorization key") + } + + receivedToken := reqInfo.Headers.Get(headers.AuthorizationKey)[0] + if !strings.Contains(receivedToken, token) { + t.Errorf("received token doesn't match up with given token") + } +} + +// TODO(kfcampbell): this code could be refactored to use table-based tests +func TestDefaultRequestOptions(t *testing.T) { + token := "this is not the token you're looking for" + provider := authentication.NewTokenProvider(authentication.WithAuthorizationToken(token)) + reqInfo := abstractions.NewRequestInformation() + addtlContext := make(map[string]interface{}) + + err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext) + if err != nil { + t.Errorf("there should be no error when calling AuthenticateRequest") + } + + apiVersions := reqInfo.Headers.Get(headers.APIVersionKey) + if len(apiVersions) != 1 { + t.Errorf("exactly one API version should be present in the request") + } + + if apiVersions[0] != headers.APIVersionValue { + t.Errorf("default API version is set incorrectly") + } + + userAgents := reqInfo.Headers.Get(headers.UserAgentKey) + if len(userAgents) != 1 { + t.Errorf("exactly one user agent string should be present in the request") + } + + if userAgents[0] != headers.UserAgentValue { + t.Errorf("default user agent string is set incorrectly") + } +} + +func TestOverwritingDefaultRequestOptions(t *testing.T) { + token := "i'm totally a real token" + apiVersion := "i'm totally a real API version" + userAgent := "i'm totally a real user agent" + provider := authentication.NewTokenProvider( + authentication.WithAuthorizationToken(token), + authentication.WithAPIVersion(apiVersion), + authentication.WithUserAgent(userAgent)) + + reqInfo := abstractions.NewRequestInformation() + addtlContext := make(map[string]interface{}) + + err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext) + if err != nil { + t.Errorf("should be no error when calling authenticated request") + } + + apiVersions := reqInfo.Headers.Get(headers.APIVersionKey) + if len(apiVersions) != 1 { + t.Errorf("exactly one API version should be present in the request") + } + + if apiVersions[0] != apiVersion { + t.Errorf("default API version is set incorrectly") + } + + userAgents := reqInfo.Headers.Get(headers.UserAgentKey) + if len(userAgents) != 1 { + t.Errorf("exactly one user agent string should be present in the request") + } + + if userAgents[0] != userAgent { + t.Errorf("default user agent string is set incorrectly") + } + +} + +func TestAnonymousAuthIsAllowed(t *testing.T) { + provider := authentication.NewTokenProvider() + reqInfo := abstractions.NewRequestInformation() + addtlContext := make(map[string]interface{}) + + err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext) + if err != nil { + t.Errorf("should be no error when calling authenticated request") + } + + authorizations := reqInfo.Headers.Get(headers.AuthorizationKey) + if len(authorizations) != 0 { + t.Errorf("no authorization header should be present in the request") + } +} + +func TestTokenSetInRequestIsNotOverwritten(t *testing.T) { + providerToken := "dit dit dit / dat dat dat / dit dit dit" + provider := authentication.NewTokenProvider( + authentication.WithAuthorizationToken(providerToken), + ) + + requestToken := "dit dit dit dit / dit / dit dat dit dit / dit dat dat dit" + requestHeaders := abstractions.NewRequestHeaders() + requestHeaders.Add(headers.AuthType, requestToken) + + reqInfo := abstractions.NewRequestInformation() + reqInfo.Headers = requestHeaders + addtlContext := make(map[string]interface{}) + + err := provider.AuthenticateRequest(context.Background(), reqInfo, addtlContext) + if err != nil { + t.Errorf("AuthenticateRequest should not error") + } + reqInfoToken := reqInfo.Headers.Get(headers.AuthorizationKey)[0] + + if !strings.Contains(reqInfoToken, providerToken) { + t.Errorf("received token doesn't match up with given token") + } +} + +// TODO(kfcampbell): make a more permanent decision about how to structure +// and separately run unit vs. integration tests +func TestHappyPathIntegration(t *testing.T) { + token := os.Getenv("GITHUB_TOKEN") + if token == "" { + t.Skip("in order to run integration tests, ensure a valid GITHUB_TOKEN exists in the environment") + } + + provider := authentication.NewTokenProvider( + authentication.WithAuthorizationToken(token), + ) + + adapter, err := http.NewNetHttpRequestAdapter(provider) + if err != nil { + log.Fatalf("Error creating request adapter: %v", err) + } + headers := abstractions.NewRequestHeaders() + _ = headers.TryAdd("Accept", "application/vnd.github.v3+json") + + client := octokit.NewApiClient(adapter) + emailsRequestConfig := &user.EmailsRequestBuilderGetRequestConfiguration{ + Headers: headers, + } + userEmails, err := client.User().Emails().Get(context.Background(), emailsRequestConfig) + if err != nil { + log.Fatalf("%v\n", err) + } + + for _, v := range userEmails { + fmt.Printf("%v\n", *v.GetEmail()) + } +} diff --git a/github/headers/header_contents.go b/github/headers/header_contents.go new file mode 100644 index 00000000..c70d957f --- /dev/null +++ b/github/headers/header_contents.go @@ -0,0 +1,13 @@ +package headers + +const AuthorizationKey = "Authorization" +const AuthType = "bearer" +const UserAgentKey = "User-Agent" + +// TODO(kfcampbell): get the version and binary name from build settings rather than hard-coding +const UserAgentValue = "go-sdk@v0.0.0" + +const APIVersionKey = "X-GitHub-Api-Version" + +// TODO(kfcampbell): get the version from the generated code somehow +const APIVersionValue = "2022-11-28" diff --git a/go.mod b/go.mod index 2e7677e1..e0ad4df5 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module github.com/octokit/go-sdk go 1.21.4 require ( - github.com/microsoft/kiota-abstractions-go v1.4.0 + github.com/microsoft/kiota-abstractions-go v1.5.2 + github.com/microsoft/kiota-http-go v1.1.1 github.com/microsoft/kiota-serialization-form-go v1.0.0 github.com/microsoft/kiota-serialization-json-go v1.0.4 github.com/microsoft/kiota-serialization-multipart-go v1.0.0 @@ -13,14 +14,14 @@ require ( require ( github.com/cjlapao/common-go v0.0.39 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/std-uritemplate/std-uritemplate/go v0.0.46 // indirect + github.com/std-uritemplate/std-uritemplate/go v0.0.47 // indirect github.com/stretchr/testify v1.8.4 // indirect - go.opentelemetry.io/otel v1.19.0 // indirect - go.opentelemetry.io/otel/metric v1.19.0 // indirect - go.opentelemetry.io/otel/trace v1.19.0 // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6965a94b..ae00e224 100644 --- a/go.sum +++ b/go.sum @@ -3,20 +3,22 @@ github.com/cjlapao/common-go v0.0.39/go.mod h1:M3dzazLjTjEtZJbbxoA5ZDiGCiHmpwqW9 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/microsoft/kiota-abstractions-go v1.4.0 h1:i9+LZ1wQ90xpq/gR2umCA5DgHvgBr3ibzsEdexZGnVY= -github.com/microsoft/kiota-abstractions-go v1.4.0/go.mod h1:NRJnAFg8qqOoX/VQWTe3ZYmcIbLa20LNC+eTqO2j60U= +github.com/microsoft/kiota-abstractions-go v1.5.2 h1:rVGmtb0B557fCxVjHXUrSGXDWTdrIN/9gl6TTBan1fc= +github.com/microsoft/kiota-abstractions-go v1.5.2/go.mod h1:xyBzTVCYrp7QBW4/p+RFi44PHwp/IPn2dZepuV4nF80= +github.com/microsoft/kiota-http-go v1.1.1 h1:W4Olo7Z/MwNZCfkcvH/5eLhnn7koRBMMRhLEnf5MPKo= +github.com/microsoft/kiota-http-go v1.1.1/go.mod h1:QzhhfW5xkoUuT+/ohflpHJvumWeXIxa/Xl0GmQ2M6mY= github.com/microsoft/kiota-serialization-form-go v1.0.0 h1:UNdrkMnLFqUCccQZerKjblsyVgifS11b3WCx+eFEsAI= github.com/microsoft/kiota-serialization-form-go v1.0.0/go.mod h1:h4mQOO6KVTNciMF6azi1J9QB19ujSw3ULKcSNyXXOMA= github.com/microsoft/kiota-serialization-json-go v1.0.4 h1:5TaISWwd2Me8clrK7SqNATo0tv9seOq59y4I5953egQ= @@ -27,16 +29,16 @@ github.com/microsoft/kiota-serialization-text-go v1.0.0 h1:XOaRhAXy+g8ZVpcq7x7a0 github.com/microsoft/kiota-serialization-text-go v1.0.0/go.mod h1:sM1/C6ecnQ7IquQOGUrUldaO5wj+9+v7G2W3sQ3fy6M= 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/std-uritemplate/std-uritemplate/go v0.0.46 h1:rWcEym/hz9YhPTXJELTXfrq48lTx69jNVhv+dOMmyZY= -github.com/std-uritemplate/std-uritemplate/go v0.0.46/go.mod h1:Qov4Ay4U83j37XjgxMYevGJFLbnZ2o9cEOhGufBKgKY= +github.com/std-uritemplate/std-uritemplate/go v0.0.47 h1:erzz/DR4sOzWr0ca2MgSTkMckpLEsDySaTZwVFQq9zw= +github.com/std-uritemplate/std-uritemplate/go v0.0.47/go.mod h1:Qov4Ay4U83j37XjgxMYevGJFLbnZ2o9cEOhGufBKgKY= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= -go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= -go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= -go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= -go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= -go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= 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=