693 lines
19 KiB
Go
693 lines
19 KiB
Go
/*
|
|
Copyright 2016 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package webhook
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
authenticationv1 "k8s.io/api/authentication/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
|
"k8s.io/apiserver/pkg/authentication/token/cache"
|
|
"k8s.io/apiserver/pkg/authentication/user"
|
|
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
|
)
|
|
|
|
// V1Service mocks a remote authentication service.
|
|
type V1Service interface {
|
|
// Review looks at the TokenReviewSpec and provides an authentication
|
|
// response in the TokenReviewStatus.
|
|
Review(*authenticationv1.TokenReview)
|
|
HTTPStatusCode() int
|
|
}
|
|
|
|
// NewV1TestServer wraps a V1Service as an httptest.Server.
|
|
func NewV1TestServer(s V1Service, cert, key, caCert []byte) (*httptest.Server, error) {
|
|
const webhookPath = "/testserver"
|
|
var tlsConfig *tls.Config
|
|
if cert != nil {
|
|
cert, err := tls.X509KeyPair(cert, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
|
|
}
|
|
|
|
if caCert != nil {
|
|
rootCAs := x509.NewCertPool()
|
|
rootCAs.AppendCertsFromPEM(caCert)
|
|
if tlsConfig == nil {
|
|
tlsConfig = &tls.Config{}
|
|
}
|
|
tlsConfig.ClientCAs = rootCAs
|
|
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
|
}
|
|
|
|
serveHTTP := func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Error(w, fmt.Sprintf("unexpected method: %v", r.Method), http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
if r.URL.Path != webhookPath {
|
|
http.Error(w, fmt.Sprintf("unexpected path: %v", r.URL.Path), http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
var review authenticationv1.TokenReview
|
|
bodyData, _ := ioutil.ReadAll(r.Body)
|
|
if err := json.Unmarshal(bodyData, &review); err != nil {
|
|
http.Error(w, fmt.Sprintf("failed to decode body: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
// ensure we received the serialized tokenreview as expected
|
|
if review.APIVersion != "authentication.k8s.io/v1" {
|
|
http.Error(w, fmt.Sprintf("wrong api version: %s", string(bodyData)), http.StatusBadRequest)
|
|
return
|
|
}
|
|
// once we have a successful request, always call the review to record that we were called
|
|
s.Review(&review)
|
|
if s.HTTPStatusCode() < 200 || s.HTTPStatusCode() >= 300 {
|
|
http.Error(w, "HTTP Error", s.HTTPStatusCode())
|
|
return
|
|
}
|
|
type userInfo struct {
|
|
Username string `json:"username"`
|
|
UID string `json:"uid"`
|
|
Groups []string `json:"groups"`
|
|
Extra map[string][]string `json:"extra"`
|
|
}
|
|
type status struct {
|
|
Authenticated bool `json:"authenticated"`
|
|
User userInfo `json:"user"`
|
|
Audiences []string `json:"audiences"`
|
|
}
|
|
|
|
var extra map[string][]string
|
|
if review.Status.User.Extra != nil {
|
|
extra = map[string][]string{}
|
|
for k, v := range review.Status.User.Extra {
|
|
extra[k] = v
|
|
}
|
|
}
|
|
|
|
resp := struct {
|
|
Kind string `json:"kind"`
|
|
APIVersion string `json:"apiVersion"`
|
|
Status status `json:"status"`
|
|
}{
|
|
Kind: "TokenReview",
|
|
APIVersion: authenticationv1.SchemeGroupVersion.String(),
|
|
Status: status{
|
|
review.Status.Authenticated,
|
|
userInfo{
|
|
Username: review.Status.User.Username,
|
|
UID: review.Status.User.UID,
|
|
Groups: review.Status.User.Groups,
|
|
Extra: extra,
|
|
},
|
|
review.Status.Audiences,
|
|
},
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
server := httptest.NewUnstartedServer(http.HandlerFunc(serveHTTP))
|
|
server.TLS = tlsConfig
|
|
server.StartTLS()
|
|
|
|
// Adjust the path to point to our custom path
|
|
serverURL, _ := url.Parse(server.URL)
|
|
serverURL.Path = webhookPath
|
|
server.URL = serverURL.String()
|
|
|
|
return server, nil
|
|
}
|
|
|
|
// A service that can be set to say yes or no to authentication requests.
|
|
type mockV1Service struct {
|
|
allow bool
|
|
statusCode int
|
|
called int
|
|
}
|
|
|
|
func (m *mockV1Service) Review(r *authenticationv1.TokenReview) {
|
|
m.called++
|
|
r.Status.Authenticated = m.allow
|
|
if m.allow {
|
|
r.Status.User.Username = "realHooman@email.com"
|
|
}
|
|
}
|
|
func (m *mockV1Service) Allow() { m.allow = true }
|
|
func (m *mockV1Service) Deny() { m.allow = false }
|
|
func (m *mockV1Service) HTTPStatusCode() int { return m.statusCode }
|
|
|
|
// newV1TokenAuthenticator creates a temporary kubeconfig file from the provided
|
|
// arguments and attempts to load a new WebhookTokenAuthenticator from it.
|
|
func newV1TokenAuthenticator(ctx context.Context, serverURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) {
|
|
tempfile, err := ioutil.TempFile("", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p := tempfile.Name()
|
|
defer os.Remove(p)
|
|
config := v1.Config{
|
|
Clusters: []v1.NamedCluster{
|
|
{
|
|
Cluster: v1.Cluster{Server: serverURL, CertificateAuthorityData: ca},
|
|
},
|
|
},
|
|
AuthInfos: []v1.NamedAuthInfo{
|
|
{
|
|
AuthInfo: v1.AuthInfo{ClientCertificateData: clientCert, ClientKeyData: clientKey},
|
|
},
|
|
},
|
|
}
|
|
if err := json.NewEncoder(tempfile).Encode(config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c, err := tokenReviewInterfaceFromKubeconfig(p, "v1")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
authn, err := newWithBackoff(c, 0, implicitAuds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cache.New(ctx, authn, false, cacheTime, cacheTime), nil
|
|
}
|
|
|
|
func TestV1TLSConfig(t *testing.T) {
|
|
tests := []struct {
|
|
test string
|
|
clientCert, clientKey, clientCA []byte
|
|
serverCert, serverKey, serverCA []byte
|
|
wantErr bool
|
|
}{
|
|
{
|
|
test: "TLS setup between client and server",
|
|
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
|
|
serverCert: serverCert, serverKey: serverKey, serverCA: caCert,
|
|
},
|
|
{
|
|
test: "Server does not require client auth",
|
|
clientCA: caCert,
|
|
serverCert: serverCert, serverKey: serverKey,
|
|
},
|
|
{
|
|
test: "Server does not require client auth, client provides it",
|
|
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
|
|
serverCert: serverCert, serverKey: serverKey,
|
|
},
|
|
{
|
|
test: "Client does not trust server",
|
|
clientCert: clientCert, clientKey: clientKey,
|
|
serverCert: serverCert, serverKey: serverKey,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
test: "Server does not trust client",
|
|
clientCert: clientCert, clientKey: clientKey, clientCA: caCert,
|
|
serverCert: serverCert, serverKey: serverKey, serverCA: badCACert,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
// Plugin does not support insecure configurations.
|
|
test: "Server is using insecure connection",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
// Use a closure so defer statements trigger between loop iterations.
|
|
func() {
|
|
service := new(mockV1Service)
|
|
service.statusCode = 200
|
|
|
|
server, err := NewV1TestServer(service, tt.serverCert, tt.serverKey, tt.serverCA)
|
|
if err != nil {
|
|
t.Errorf("%s: failed to create server: %v", tt.test, err)
|
|
return
|
|
}
|
|
defer server.Close()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
wh, err := newV1TokenAuthenticator(ctx, server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, nil)
|
|
if err != nil {
|
|
t.Errorf("%s: failed to create client: %v", tt.test, err)
|
|
return
|
|
}
|
|
|
|
// Allow all and see if we get an error.
|
|
service.Allow()
|
|
_, authenticated, err := wh.AuthenticateToken(context.Background(), "t0k3n")
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Errorf("expected error making authorization request: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if !authenticated {
|
|
t.Errorf("%s: failed to authenticate token", tt.test)
|
|
return
|
|
}
|
|
|
|
service.Deny()
|
|
_, authenticated, err = wh.AuthenticateToken(context.Background(), "t0k3n")
|
|
if err != nil {
|
|
t.Errorf("%s: unexpectedly failed AuthenticateToken", tt.test)
|
|
}
|
|
if authenticated {
|
|
t.Errorf("%s: incorrectly authenticated token", tt.test)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// recorderV1Service records all token review requests, and responds with the
|
|
// provided TokenReviewStatus.
|
|
type recorderV1Service struct {
|
|
lastRequest authenticationv1.TokenReview
|
|
response authenticationv1.TokenReviewStatus
|
|
}
|
|
|
|
func (rec *recorderV1Service) Review(r *authenticationv1.TokenReview) {
|
|
rec.lastRequest = *r
|
|
r.Status = rec.response
|
|
}
|
|
|
|
func (rec *recorderV1Service) HTTPStatusCode() int { return 200 }
|
|
|
|
func TestV1WebhookTokenAuthenticator(t *testing.T) {
|
|
serv := &recorderV1Service{}
|
|
|
|
s, err := NewV1TestServer(serv, serverCert, serverKey, caCert)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
|
|
expTypeMeta := metav1.TypeMeta{
|
|
APIVersion: "authentication.k8s.io/v1",
|
|
Kind: "TokenReview",
|
|
}
|
|
|
|
tests := []struct {
|
|
description string
|
|
implicitAuds, reqAuds authenticator.Audiences
|
|
serverResponse authenticationv1.TokenReviewStatus
|
|
expectedAuthenticated bool
|
|
expectedUser *user.DefaultInfo
|
|
expectedAuds authenticator.Audiences
|
|
}{
|
|
{
|
|
description: "successful response should pass through all user info.",
|
|
serverResponse: authenticationv1.TokenReviewStatus{
|
|
Authenticated: true,
|
|
User: authenticationv1.UserInfo{
|
|
Username: "somebody",
|
|
},
|
|
},
|
|
expectedAuthenticated: true,
|
|
expectedUser: &user.DefaultInfo{
|
|
Name: "somebody",
|
|
},
|
|
},
|
|
{
|
|
description: "successful response should pass through all user info.",
|
|
serverResponse: authenticationv1.TokenReviewStatus{
|
|
Authenticated: true,
|
|
User: authenticationv1.UserInfo{
|
|
Username: "person@place.com",
|
|
UID: "abcd-1234",
|
|
Groups: []string{"stuff-dev", "main-eng"},
|
|
Extra: map[string]authenticationv1.ExtraValue{"foo": {"bar", "baz"}},
|
|
},
|
|
},
|
|
expectedAuthenticated: true,
|
|
expectedUser: &user.DefaultInfo{
|
|
Name: "person@place.com",
|
|
UID: "abcd-1234",
|
|
Groups: []string{"stuff-dev", "main-eng"},
|
|
Extra: map[string][]string{"foo": {"bar", "baz"}},
|
|
},
|
|
},
|
|
{
|
|
description: "unauthenticated shouldn't even include extra provided info.",
|
|
serverResponse: authenticationv1.TokenReviewStatus{
|
|
Authenticated: false,
|
|
User: authenticationv1.UserInfo{
|
|
Username: "garbage",
|
|
UID: "abcd-1234",
|
|
Groups: []string{"not-actually-used"},
|
|
},
|
|
},
|
|
expectedAuthenticated: false,
|
|
expectedUser: nil,
|
|
},
|
|
{
|
|
description: "unauthenticated shouldn't even include extra provided info.",
|
|
serverResponse: authenticationv1.TokenReviewStatus{
|
|
Authenticated: false,
|
|
},
|
|
expectedAuthenticated: false,
|
|
expectedUser: nil,
|
|
},
|
|
{
|
|
description: "good audience",
|
|
implicitAuds: apiAuds,
|
|
reqAuds: apiAuds,
|
|
serverResponse: authenticationv1.TokenReviewStatus{
|
|
Authenticated: true,
|
|
User: authenticationv1.UserInfo{
|
|
Username: "somebody",
|
|
},
|
|
},
|
|
expectedAuthenticated: true,
|
|
expectedUser: &user.DefaultInfo{
|
|
Name: "somebody",
|
|
},
|
|
expectedAuds: apiAuds,
|
|
},
|
|
{
|
|
description: "good audience",
|
|
implicitAuds: append(apiAuds, "other"),
|
|
reqAuds: apiAuds,
|
|
serverResponse: authenticationv1.TokenReviewStatus{
|
|
Authenticated: true,
|
|
User: authenticationv1.UserInfo{
|
|
Username: "somebody",
|
|
},
|
|
},
|
|
expectedAuthenticated: true,
|
|
expectedUser: &user.DefaultInfo{
|
|
Name: "somebody",
|
|
},
|
|
expectedAuds: apiAuds,
|
|
},
|
|
{
|
|
description: "bad audiences",
|
|
implicitAuds: apiAuds,
|
|
reqAuds: authenticator.Audiences{"other"},
|
|
serverResponse: authenticationv1.TokenReviewStatus{
|
|
Authenticated: false,
|
|
},
|
|
expectedAuthenticated: false,
|
|
},
|
|
{
|
|
description: "bad audiences",
|
|
implicitAuds: apiAuds,
|
|
reqAuds: authenticator.Audiences{"other"},
|
|
// webhook authenticator hasn't been upgraded to support audience.
|
|
serverResponse: authenticationv1.TokenReviewStatus{
|
|
Authenticated: true,
|
|
User: authenticationv1.UserInfo{
|
|
Username: "somebody",
|
|
},
|
|
},
|
|
expectedAuthenticated: false,
|
|
},
|
|
{
|
|
description: "audience aware backend",
|
|
implicitAuds: apiAuds,
|
|
reqAuds: apiAuds,
|
|
serverResponse: authenticationv1.TokenReviewStatus{
|
|
Authenticated: true,
|
|
User: authenticationv1.UserInfo{
|
|
Username: "somebody",
|
|
},
|
|
Audiences: []string(apiAuds),
|
|
},
|
|
expectedAuthenticated: true,
|
|
expectedUser: &user.DefaultInfo{
|
|
Name: "somebody",
|
|
},
|
|
expectedAuds: apiAuds,
|
|
},
|
|
{
|
|
description: "audience aware backend",
|
|
serverResponse: authenticationv1.TokenReviewStatus{
|
|
Authenticated: true,
|
|
User: authenticationv1.UserInfo{
|
|
Username: "somebody",
|
|
},
|
|
Audiences: []string(apiAuds),
|
|
},
|
|
expectedAuthenticated: true,
|
|
expectedUser: &user.DefaultInfo{
|
|
Name: "somebody",
|
|
},
|
|
},
|
|
{
|
|
description: "audience aware backend",
|
|
implicitAuds: apiAuds,
|
|
reqAuds: apiAuds,
|
|
serverResponse: authenticationv1.TokenReviewStatus{
|
|
Authenticated: true,
|
|
User: authenticationv1.UserInfo{
|
|
Username: "somebody",
|
|
},
|
|
Audiences: []string{"other"},
|
|
},
|
|
expectedAuthenticated: false,
|
|
},
|
|
}
|
|
token := "my-s3cr3t-t0ken"
|
|
for _, tt := range tests {
|
|
t.Run(tt.description, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
wh, err := newV1TokenAuthenticator(ctx, s.URL, clientCert, clientKey, caCert, 0, tt.implicitAuds)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if tt.reqAuds != nil {
|
|
ctx = authenticator.WithAudiences(ctx, tt.reqAuds)
|
|
}
|
|
|
|
serv.response = tt.serverResponse
|
|
resp, authenticated, err := wh.AuthenticateToken(ctx, token)
|
|
if err != nil {
|
|
t.Fatalf("authentication failed: %v", err)
|
|
}
|
|
if serv.lastRequest.Spec.Token != token {
|
|
t.Errorf("Server did not see correct token. Got %q, expected %q.",
|
|
serv.lastRequest.Spec.Token, token)
|
|
}
|
|
if !reflect.DeepEqual(serv.lastRequest.TypeMeta, expTypeMeta) {
|
|
t.Errorf("Server did not see correct TypeMeta. Got %v, expected %v",
|
|
serv.lastRequest.TypeMeta, expTypeMeta)
|
|
}
|
|
if authenticated != tt.expectedAuthenticated {
|
|
t.Errorf("Plugin returned incorrect authentication response. Got %t, expected %t.",
|
|
authenticated, tt.expectedAuthenticated)
|
|
}
|
|
if resp != nil && tt.expectedUser != nil && !reflect.DeepEqual(resp.User, tt.expectedUser) {
|
|
t.Errorf("Plugin returned incorrect user. Got %#v, expected %#v",
|
|
resp.User, tt.expectedUser)
|
|
}
|
|
if resp != nil && tt.expectedAuds != nil && !reflect.DeepEqual(resp.Audiences, tt.expectedAuds) {
|
|
t.Errorf("Plugin returned incorrect audiences. Got %#v, expected %#v",
|
|
resp.Audiences, tt.expectedAuds)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type authenticationV1UserInfo authenticationv1.UserInfo
|
|
|
|
func (a *authenticationV1UserInfo) GetName() string { return a.Username }
|
|
func (a *authenticationV1UserInfo) GetUID() string { return a.UID }
|
|
func (a *authenticationV1UserInfo) GetGroups() []string { return a.Groups }
|
|
|
|
func (a *authenticationV1UserInfo) GetExtra() map[string][]string {
|
|
if a.Extra == nil {
|
|
return nil
|
|
}
|
|
ret := map[string][]string{}
|
|
for k, v := range a.Extra {
|
|
ret[k] = []string(v)
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
// Ensure authenticationv1.UserInfo contains the fields necessary to implement the
|
|
// user.Info interface.
|
|
var _ user.Info = (*authenticationV1UserInfo)(nil)
|
|
|
|
// TestWebhookCache verifies that error responses from the server are not
|
|
// cached, but successful responses are. It also ensures that the webhook
|
|
// call is retried on 429 and 500+ errors
|
|
func TestV1WebhookCacheAndRetry(t *testing.T) {
|
|
serv := new(mockV1Service)
|
|
s, err := NewV1TestServer(serv, serverCert, serverKey, caCert)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Create an authenticator that caches successful responses "forever" (100 days).
|
|
wh, err := newV1TokenAuthenticator(ctx, s.URL, clientCert, clientKey, caCert, 2400*time.Hour, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
testcases := []struct {
|
|
description string
|
|
|
|
token string
|
|
allow bool
|
|
code int
|
|
|
|
expectError bool
|
|
expectOk bool
|
|
expectCalls int
|
|
}{
|
|
{
|
|
description: "t0k3n, 500 error, retries and fails",
|
|
|
|
token: "t0k3n",
|
|
allow: false,
|
|
code: 500,
|
|
|
|
expectError: true,
|
|
expectOk: false,
|
|
expectCalls: 5,
|
|
},
|
|
{
|
|
description: "t0k3n, 404 error, fails (but no retry)",
|
|
|
|
token: "t0k3n",
|
|
allow: false,
|
|
code: 404,
|
|
|
|
expectError: true,
|
|
expectOk: false,
|
|
expectCalls: 1,
|
|
},
|
|
{
|
|
description: "t0k3n, 200 response, allowed, succeeds with a single call",
|
|
|
|
token: "t0k3n",
|
|
allow: true,
|
|
code: 200,
|
|
|
|
expectError: false,
|
|
expectOk: true,
|
|
expectCalls: 1,
|
|
},
|
|
{
|
|
description: "t0k3n, 500 response, disallowed, but never called because previous 200 response was cached",
|
|
|
|
token: "t0k3n",
|
|
allow: false,
|
|
code: 500,
|
|
|
|
expectError: false,
|
|
expectOk: true,
|
|
expectCalls: 0,
|
|
},
|
|
|
|
{
|
|
description: "an0th3r_t0k3n, 500 response, disallowed, should be called again with retries",
|
|
|
|
token: "an0th3r_t0k3n",
|
|
allow: false,
|
|
code: 500,
|
|
|
|
expectError: true,
|
|
expectOk: false,
|
|
expectCalls: 5,
|
|
},
|
|
{
|
|
description: "an0th3r_t0k3n, 429 response, disallowed, should be called again with retries",
|
|
|
|
token: "an0th3r_t0k3n",
|
|
allow: false,
|
|
code: 429,
|
|
|
|
expectError: true,
|
|
expectOk: false,
|
|
expectCalls: 5,
|
|
},
|
|
{
|
|
description: "an0th3r_t0k3n, 200 response, allowed, succeeds with a single call",
|
|
|
|
token: "an0th3r_t0k3n",
|
|
allow: true,
|
|
code: 200,
|
|
|
|
expectError: false,
|
|
expectOk: true,
|
|
expectCalls: 1,
|
|
},
|
|
{
|
|
description: "an0th3r_t0k3n, 500 response, disallowed, but never called because previous 200 response was cached",
|
|
|
|
token: "an0th3r_t0k3n",
|
|
allow: false,
|
|
code: 500,
|
|
|
|
expectError: false,
|
|
expectOk: true,
|
|
expectCalls: 0,
|
|
},
|
|
}
|
|
|
|
for _, testcase := range testcases {
|
|
t.Run(testcase.description, func(t *testing.T) {
|
|
serv.allow = testcase.allow
|
|
serv.statusCode = testcase.code
|
|
serv.called = 0
|
|
|
|
_, ok, err := wh.AuthenticateToken(context.Background(), testcase.token)
|
|
hasError := err != nil
|
|
if hasError != testcase.expectError {
|
|
t.Errorf("Webhook returned HTTP %d, expected error=%v, but got error %v", testcase.code, testcase.expectError, err)
|
|
}
|
|
if serv.called != testcase.expectCalls {
|
|
t.Errorf("Expected %d calls, got %d", testcase.expectCalls, serv.called)
|
|
}
|
|
if ok != testcase.expectOk {
|
|
t.Errorf("Expected ok=%v, got %v", testcase.expectOk, ok)
|
|
}
|
|
})
|
|
}
|
|
}
|