// Copyright 2020 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package crud import ( "context" "errors" "testing" "github.com/ory/fosite/compose" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/validation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes/fake" coretesting "k8s.io/client-go/testing" ) func TestStorage(t *testing.T) { ctx := context.Background() secretsGVR := schema.GroupVersionResource{ Group: "", Version: "v1", Resource: "secrets", } type testJSON struct { Data string } type mocker interface { AddReactor(verb, resource string, reaction coretesting.ReactionFunc) PrependReactor(verb, resource string, reaction coretesting.ReactionFunc) Tracker() coretesting.ObjectTracker } hmac := compose.NewOAuth2HMACStrategy(&compose.Config{}, []byte("super-secret-32-byte-for-testing"), nil) // test data generation via: // code, signature, err := hmac.GenerateAuthorizeCode(ctx, nil) validateSecretName := validation.NameIsDNSSubdomain // matches k/k const ( namespace = "test-ns" authorizationCode1 = "81qE408EKL-e99gcXo3UnXBz9W05yGm92_hBmvXeadM.R5h38Bmw7yOaWNy0ypB3feh9toM-3T2zlwMXQyeE9B0" authorizationCode2 = "p7aIiOLy-btBBlCro5RWm1QABANKCiC0JmDPhUtfOY4.XXJsYsMWhnSMJi9TXJcPO6SDVO2R_QXImwroxxnQPA8" authorizationCode3 = "skKp1RjGgIwZhT3vaB_k1F3cIj2yp7U8a7UD0xAaemU.5aUhdNmfWLW3yKX8Zfz5ztS5IiiWBgu36Gja-o2xl0I" ) tests := []struct { name string resource string mocks func(*testing.T, mocker) run func(*testing.T, Storage) error wantActions []coretesting.Action wantSecrets []corev1.Secret wantErr string }{ { name: "get non-existent", resource: "authcode", mocks: nil, run: func(t *testing.T, storage Storage) error { _, err := storage.Get(ctx, "not-exists", nil) return err }, wantActions: []coretesting.Action{ coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-authcode-t2fx46yyvs3a"), }, wantSecrets: nil, wantErr: `failed to get authcode for signature not-exists: secrets "pinniped-storage-authcode-t2fx46yyvs3a" not found`, }, { name: "delete non-existent", resource: "tokens", mocks: nil, run: func(t *testing.T, storage Storage) error { return storage.Delete(ctx, "not-a-token") }, wantActions: []coretesting.Action{ coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-tokens-t2fx427lnci6s"), }, wantSecrets: nil, wantErr: `failed to delete tokens for signature not-a-token: secrets "pinniped-storage-tokens-t2fx427lnci6s" not found`, }, { name: "create and get", resource: "access-tokens", mocks: nil, run: func(t *testing.T, storage Storage) error { signature := hmac.AuthorizeCodeSignature(authorizationCode1) require.NotEmpty(t, signature) require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is data := &testJSON{Data: "create-and-get"} rv1, err := storage.Create(ctx, signature, data) require.Empty(t, rv1) // fake client does not set this require.NoError(t, err) out := &testJSON{} rv2, err := storage.Get(ctx, signature, out) require.Empty(t, rv2) // fake client does not set this require.NoError(t, err) require.Equal(t, data, out) return nil }, wantActions: []coretesting.Action{ coretesting.NewCreateAction(secretsGVR, namespace, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-access-tokens-i6mhp4azwdxshgsy3s2mvedxpxuh3nudh3ot3m4xamlugj4e6qoq", ResourceVersion: "", Labels: map[string]string{ "storage.pinniped.dev": "access-tokens", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"create-and-get"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-tokens", }), coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-access-tokens-i6mhp4azwdxshgsy3s2mvedxpxuh3nudh3ot3m4xamlugj4e6qoq"), }, wantSecrets: []corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-access-tokens-i6mhp4azwdxshgsy3s2mvedxpxuh3nudh3ot3m4xamlugj4e6qoq", Namespace: namespace, ResourceVersion: "", Labels: map[string]string{ "storage.pinniped.dev": "access-tokens", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"create-and-get"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/access-tokens", }, }, wantErr: "", }, { name: "get existing", resource: "pandas-are-best", mocks: func(t *testing.T, mock mocker) { err := mock.Tracker().Add(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-pandas-are-best-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq", Namespace: namespace, ResourceVersion: "", Labels: map[string]string{ "storage.pinniped.dev": "pandas-are-best", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"snorlax"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/pandas-are-best", }) require.NoError(t, err) }, run: func(t *testing.T, storage Storage) error { signature := hmac.AuthorizeCodeSignature(authorizationCode2) require.NotEmpty(t, signature) require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is data := &testJSON{Data: "snorlax"} out := &testJSON{} rv1, err := storage.Get(ctx, signature, out) require.Empty(t, rv1) // fake client does not set this require.NoError(t, err) require.Equal(t, data, out) return nil }, wantActions: []coretesting.Action{ coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-pandas-are-best-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq"), }, wantSecrets: []corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-pandas-are-best-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq", Namespace: namespace, ResourceVersion: "", Labels: map[string]string{ "storage.pinniped.dev": "pandas-are-best", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"snorlax"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/pandas-are-best", }, }, wantErr: "", }, { name: "update existing", resource: "stores", mocks: func(t *testing.T, mock mocker) { err := mock.Tracker().Add(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-stores-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba", Namespace: namespace, ResourceVersion: "35", Labels: map[string]string{ "storage.pinniped.dev": "stores", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"pants"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/stores", }) require.NoError(t, err) mock.PrependReactor("update", "secrets", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { secret := action.(coretesting.UpdateAction).GetObject().(*corev1.Secret) secret.ResourceVersion = "45" return false, nil, nil // we mutated the secret in place but we do not "handle" it }) }, run: func(t *testing.T, storage Storage) error { signature := hmac.AuthorizeCodeSignature(authorizationCode3) require.NotEmpty(t, signature) require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is data := &testJSON{Data: "pants"} out := &testJSON{} rv1, err := storage.Get(ctx, signature, out) require.Equal(t, "35", rv1) // set in mock above require.NoError(t, err) require.Equal(t, data, out) newData := &testJSON{Data: "shirts"} rv2, err := storage.Update(ctx, signature, rv1, newData) require.Equal(t, "45", rv2) // mock sets to a higher value on update require.NoError(t, err) newOut := &testJSON{} rv3, err := storage.Get(ctx, signature, newOut) require.Equal(t, "45", rv3) // we should see new rv now require.NoError(t, err) require.Equal(t, newData, newOut) return nil }, wantActions: []coretesting.Action{ coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-stores-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba"), coretesting.NewUpdateAction(secretsGVR, namespace, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-stores-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba", ResourceVersion: "35", // update at initial RV Labels: map[string]string{ "storage.pinniped.dev": "stores", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"shirts"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/stores", }), coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-stores-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba"), }, wantSecrets: []corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-stores-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba", Namespace: namespace, ResourceVersion: "45", // final list at new RV Labels: map[string]string{ "storage.pinniped.dev": "stores", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"shirts"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/stores", }, }, wantErr: "", }, { name: "delete existing", resource: "seals", mocks: func(t *testing.T, mock mocker) { err := mock.Tracker().Add(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-seals-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq", Namespace: namespace, ResourceVersion: "", Labels: map[string]string{ "storage.pinniped.dev": "seals", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"sad-seal"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/seals", }) require.NoError(t, err) }, run: func(t *testing.T, storage Storage) error { signature := hmac.AuthorizeCodeSignature(authorizationCode2) require.NotEmpty(t, signature) require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is return storage.Delete(ctx, signature) }, wantActions: []coretesting.Action{ coretesting.NewDeleteAction(secretsGVR, namespace, "pinniped-storage-seals-lvzgyywdc2dhjdbgf5jvzfyphosigvhnsh6qlse3blumogoqhqhq"), }, wantSecrets: nil, wantErr: "", }, { name: "invalid exiting secret type", resource: "candies", mocks: func(t *testing.T, mock mocker) { err := mock.Tracker().Add(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba", Namespace: namespace, ResourceVersion: "55", Labels: map[string]string{ "storage.pinniped.dev": "candies", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"twizzlers"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/candies-not", }) require.NoError(t, err) }, run: func(t *testing.T, storage Storage) error { signature := hmac.AuthorizeCodeSignature(authorizationCode3) require.NotEmpty(t, signature) require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is out := &testJSON{} rv1, err := storage.Get(ctx, signature, out) require.Empty(t, rv1) require.Empty(t, out.Data) require.True(t, errors.Is(err, ErrSecretTypeMismatch)) require.EqualError(t, err, "secret storage data has incorrect type: storage.pinniped.dev/candies-not must equal storage.pinniped.dev/candies") return nil }, wantActions: []coretesting.Action{ coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba"), }, wantSecrets: []corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba", Namespace: namespace, ResourceVersion: "55", Labels: map[string]string{ "storage.pinniped.dev": "candies", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"twizzlers"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/candies-not", }, }, wantErr: "", }, { name: "invalid exiting secret wrong label", resource: "candies", mocks: func(t *testing.T, mock mocker) { err := mock.Tracker().Add(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba", Namespace: namespace, ResourceVersion: "55", Labels: map[string]string{ "storage.pinniped.dev": "candies-are-bad", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"twizzlers"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/candies", }) require.NoError(t, err) }, run: func(t *testing.T, storage Storage) error { signature := hmac.AuthorizeCodeSignature(authorizationCode3) require.NotEmpty(t, signature) require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is out := &testJSON{} rv1, err := storage.Get(ctx, signature, out) require.Empty(t, rv1) require.Empty(t, out.Data) require.True(t, errors.Is(err, ErrSecretLabelMismatch)) require.EqualError(t, err, "secret storage data has incorrect label: candies-are-bad must equal candies") return nil }, wantActions: []coretesting.Action{ coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba"), }, wantSecrets: []corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba", Namespace: namespace, ResourceVersion: "55", Labels: map[string]string{ "storage.pinniped.dev": "candies-are-bad", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"twizzlers"}`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/candies", }, }, wantErr: "", }, { name: "invalid exiting secret wrong version", resource: "candies", mocks: func(t *testing.T, mock mocker) { err := mock.Tracker().Add(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba", Namespace: namespace, ResourceVersion: "55", Labels: map[string]string{ "storage.pinniped.dev": "candies", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"twizzlers"}`), "pinniped-storage-version": []byte("77"), }, Type: "storage.pinniped.dev/candies", }) require.NoError(t, err) }, run: func(t *testing.T, storage Storage) error { signature := hmac.AuthorizeCodeSignature(authorizationCode3) require.NotEmpty(t, signature) require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is out := &testJSON{} rv1, err := storage.Get(ctx, signature, out) require.Empty(t, rv1) require.Empty(t, out.Data) require.True(t, errors.Is(err, ErrSecretVersionMismatch)) require.EqualError(t, err, "secret storage data has incorrect version") return nil }, wantActions: []coretesting.Action{ coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba"), }, wantSecrets: []corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba", Namespace: namespace, ResourceVersion: "55", Labels: map[string]string{ "storage.pinniped.dev": "candies", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`{"Data":"twizzlers"}`), "pinniped-storage-version": []byte("77"), }, Type: "storage.pinniped.dev/candies", }, }, wantErr: "", }, { name: "invalid exiting secret not json", resource: "candies", mocks: func(t *testing.T, mock mocker) { err := mock.Tracker().Add(&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba", Namespace: namespace, ResourceVersion: "55", Labels: map[string]string{ "storage.pinniped.dev": "candies", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`}}bad data{{`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/candies", }) require.NoError(t, err) }, run: func(t *testing.T, storage Storage) error { signature := hmac.AuthorizeCodeSignature(authorizationCode3) require.NotEmpty(t, signature) require.NotEmpty(t, validateSecretName(signature, false)) // signature is not valid secret name as-is out := &testJSON{} rv1, err := storage.Get(ctx, signature, out) require.Empty(t, rv1) require.Empty(t, out.Data) require.EqualError(t, err, "failed to decode candies for signature 5aUhdNmfWLW3yKX8Zfz5ztS5IiiWBgu36Gja-o2xl0I: invalid character '}' looking for beginning of value") return nil }, wantActions: []coretesting.Action{ coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba"), }, wantSecrets: []corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "pinniped-storage-candies-4wssc5gzt5mlln6iux6gl7hzz3klsirisydaxn7indnpvdnrs5ba", Namespace: namespace, ResourceVersion: "55", Labels: map[string]string{ "storage.pinniped.dev": "candies", }, }, Data: map[string][]byte{ "pinniped-storage-data": []byte(`}}bad data{{`), "pinniped-storage-version": []byte("1"), }, Type: "storage.pinniped.dev/candies", }, }, wantErr: "", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() client := fake.NewSimpleClientset() if tt.mocks != nil { tt.mocks(t, client) } secrets := client.CoreV1().Secrets(namespace) storage := New(tt.resource, secrets) err := tt.run(t, storage) require.Equal(t, tt.wantErr, errString(err)) require.Equal(t, tt.wantActions, client.Actions()) checkSecretActionNames(t, client.Actions()) actualSecrets, err := secrets.List(ctx, metav1.ListOptions{}) require.NoError(t, err) require.Equal(t, tt.wantSecrets, actualSecrets.Items) checkSecretListNames(t, actualSecrets.Items) }) } } func checkSecretActionNames(t *testing.T, actions []coretesting.Action) { t.Helper() for _, action := range actions { name := getName(t, action) assertValidName(t, name) } } func checkSecretListNames(t *testing.T, secrets []corev1.Secret) { t.Helper() for _, secret := range secrets { assertValidName(t, secret.Name) } } func assertValidName(t *testing.T, name string) { t.Helper() validateSecretName := validation.NameIsDNSSubdomain // matches k/k require.NotEmpty(t, name) require.Empty(t, validateSecretName(name, false)) require.Empty(t, validateSecretName(name, true)) // I do not think we actually care about this case } func getName(t *testing.T, action coretesting.Action) string { t.Helper() if getter, ok := action.(interface { GetName() string }); ok { return getter.GetName() } if getter, ok := action.(interface { GetObject() runtime.Object }); ok { accessor, err := meta.Accessor(getter.GetObject()) require.NoError(t, err) return accessor.GetName() } t.Fatalf("failed to get name for action: %#v", action) panic("unreachable") } func errString(err error) string { if err == nil { return "" } return err.Error() }