Недавно я написал какой-то код для CLI, который читает все секреты на пути хранилища, и я пошел по дороге выяснить Как Фактически установить тестирование некоторых моих функций, которые обращаются к хранилищу через вызовы API.
Главное, мои функции не должен добраться до настоящего случая хранилища, чтобы сделать это тестирование. В качестве основного присоединения к тестированию единиц мы не должны добраться до настоящих API, живых баз данных и т. Д. В модульных тестах. Итак, это означало, что у меня действительно было два варианта:
- Смешайте методы, которые связываются напрямую с API хранилища, используя интерфейсы и инъекцию зависимости.
- Разорвитесь в тестовом сервере хранилища, напишите в нее секреты, и попросите вызовы API прогонте на этот тестовый сервер
К счастью, оба были жизнеспособными вариантами! Я собираюсь пройти через оба варианта как способ работы через издевание этих методов, но второй метод явно превосходит, на мой взгляд. Кроме того, это как хранилище на самом деле Быть протестированным.
Изделка клиента Vault для тестирования методов чтения () и списка ()
Во-первых, давайте создадим некоторые леса ниже. Все это может пойти в main.go
:
var ( ErrSecretNotFound = errors.New("no secret not found at given path") ErrVaultVarsNotFound = errors.New("VAULT_TOKEN and VAULT_ADDR environment variables must be set") ) // 1 type Logicaler interface { List(path string) (*api.Secret, error) Read(path string) (*api.Secret, error) } // 2 type VaultClient struct { client Logicaler } // 3 func NewVaultClient() (*VaultClient, error) { token := os.Getenv("VAULT_TOKEN") vault_addr := os.Getenv("VAULT_ADDR") if token == "" || vault_addr == "" { return &VaultClient{}, ErrVaultVarsNotFound } config := &api.Config{ Address: vault_addr, } client, err := api.NewClient(config) if err != nil { return &VaultClient{}, err } client.SetToken(token) return &VaultClient{ client: client.Logical(), }, nil } // 4 func (v *VaultClient) ReadSecret(endpoint string) ([]string, error) { // 5 secret, err := v.client.Read(endpoint) if err != nil { return []string{}, err } if secret == nil { return []string{}, ErrSecretNotFound } list := []string{} for _, v := range secret.Data["data"].(map[string]interface{}) { list = append(list, v.(string)) } return list, nil } // 6 func (v *VaultClient) ListSecret(path string) ([]string, error) { // 7 secret, err := v.client.List(path) if err != nil { return []string{}, err } if secret == nil { return []string{}, ErrSecretNotFound } list := []string{} for _, v := range secret.Data["keys"].([]interface{}) { list = append(list, v.(string)) } return list, nil }
Некоторые заметки о коде выше, чтобы сделать его приемлемым:
- Мы устанавливаем интерфейс, который требует двух методов, чтобы неявно быть удовлетворены: чтение и список. Это идиоматично, чтобы добраться
Эр
на интерфейсы, поэтому я выбрал этот метод. - Создать
VaultClient
структура, которая принимает одно поле: интерфейсЛогика
Отказ - Создайте новый клиент Vault, который устанавливает реальное соединение с сервером хранилища. Вы можете думать об этом как об этом конструкторе.
- Метод
VaultClient
Читать секрет в хранении - Эта часть важна! Это фактический Связь с хранилищем Отказ Обратите внимание, что это один из двух методов в интерфейсе для удовлетворения. При издевании, вы хотите найти функции или методы, которые передаются непосредственно с API, которые будут издеваться.
- Так же, как 4. Мы перечисляем секреты на пути хранилища.
- Так же, как 5! Это другой метод, который должен быть издеваться и удовлетворен в интерфейсе.
Давайте создадим некоторые дополнительные тестированные леса в нашем main_test.go
Файл до объяснения того, как мы собираемся издеваться с этим:
package main import ( "fmt" "testing" kv "github.com/hashicorp/vault-plugin-secrets-kv" "github.com/hashicorp/vault/api" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/logical" hashivault "github.com/hashicorp/vault/vault" ) // 1 type MockVault struct{} // 2 func (mv *MockVault) List(path string) (*api.Secret, error) { return &api.Secret{}, nil } func (mv *MockVault) Read(path string) (*api.Secret, error) { return &api.Secret{}, nil } // 3 func NewMockVaultClient() *VaultClient { return &VaultClient{ client: &MockVault{}, } }
- Мы создаем клиент Mock Vault для эмуляции фактических методов клиента (чтение и список).
- Мы создаем эти методы (хотя они ничего не возвращают в данный момент), чтобы удовлетворить
Логика
интерфейс. - Создать клиент Mock Vault был
MockVault
структура передается вклиент
Поле, где он ищет переменную типаЛогика
Отказ
С помощью этого кода проверьте все компиляция: Go Build.
.
Теперь, когда у нас есть базовая структура, мы можем создать некоторые данные издевательства для возврата в * API. Секрет
объект. Давайте обновим Читать (строка пути)
Способ возвращения данных издевательства:
func (mv *MockVault) Read(path string) (*api.Secret, error) { return &api.Secret{ Data: map[string]interface{}{ "data": map[string]interface{}{ "value": "fakedata", }, }, }, nil }
Данные
Поле — это фактическое содержание секретных данных. К сожалению, это немного сложно читать из-за нескольких вложенных карта [строка] интерфейс {}
объекты. Стоит выглядеть по определению * API. Секрет
Объект в пакете Vault, чтобы увидеть, как это определено.
У нас есть некоторые данные, возвращаемые в нашу Читать ()
Способ теперь, так что давайте напишем тест на это!
func TestReadSecret(t *testing.T) { vc := NewMockVaultClient() want := []string{"fakedata"} got, _ := vc.ReadSecret("") if !reflect.DeepEqual(want, got) { t.Errorf("got %v but want %v", got, want) } }
Как вы можете видеть, мы теперь создаем NewMockVaultClient ()
вместо фактического клиента. Тогда мы называем методом ReadSecret (строка пути)
, который вызывает методу Читать (строка пути)
. Этот метод реализован в Логика
Интерфейс и, таким образом, звонит в издевательственную (MV * Mockvault) Читать (строка пути)
Метод вместо фактического VaultClient! Наконец, мы тестируем, что массивы точно так же.
Если это запутано, попробуйте реализовать то же самое с Testlistsecret
после. Теперь вы можете проверить это работает с Go Test --run testreadsecret
Отказ
Сверните вверх по тестовым серверу Vault в переходе в тестовые прочитанные () и список ()
Давайте сделаем шаг назад и подумайте о различиях между насмешкой и использованием настоящего тестового клиента хранилища. Поскольку мы не издеваем клиента, нам больше не нужен интерфейс для издевания методов. Мы все равно будем использовать реальные методы, но вызоваясь на тестовый сервер, запутанный в процессе тестирования. Таким образом, мы можем избавиться от интерфейса и отрегулировать некоторые из наших кодов.
Вот леса:
type VaultClient struct { // 1 client *api.Client } func NewVaultClient() (*VaultClient, error) { token := os.Getenv("VAULT_TOKEN") vault_addr := os.Getenv("VAULT_ADDR") if token == "" || vault_addr == "" { return &VaultClient{}, ErrVaultVarsNotFound } config := &api.Config{ Address: vault_addr, } client, err := api.NewClient(config) if err != nil { return &VaultClient{}, err } client.SetToken(token) return &VaultClient{ // 2 client: client, }, nil } func (v *VaultClient) ReadSecret(endpoint string) ([]string, error) { // 3 secret, err := v.client.Logical().Read(endpoint) if err != nil { return []string{}, err } if secret == nil { return []string{}, ErrSecretNotFound } list := []string{} for _, v := range secret.Data["data"].(map[string]interface{}) { list = append(list, v.(string)) } return list, nil } func (v *VaultClient) ListSecret(path string) ([]string, error) { // 4 secret, err := v.client.Logical().List(path) if err != nil { return []string{}, err } if secret == nil { return []string{}, ErrSecretNotFound } list := []string{} for _, v := range secret.Data["keys"].([]interface{}) { list = append(list, v.(string)) } return list, nil }
Я отметил пара пара, чтобы снова отметить, поэтому давайте пройдемся через них.
- Сейчас мы проходим в
* API. Клиент
от пакета хранилища вместоЛогика
интерфейс. Потому что мы больше не издевайтесь, нам не нужен интерфейс. - Увидеть, как передается клиент. Мы больше не звоним
Логично ()
метод. - Как видите, метод звонков выглядит немного по-другому. Каждый раз, когда мы используем клиент, мы работаем
Логично ()
Метод, который используется для возврата клиента для вызовов API-API в логической основе. - Так же, как 3. Теперь вам нужно позвонить
клиент. Логично (). *
Чтобы запустить команды хранилища.
Итак, у нас есть наш код, который вызывает хранилище. Теперь нам нужно написать некоторые тестирования, которые вращаются вверх по тестированию сервера хранилища, чтобы запустить это. Давайте создадим леса для тестового кластера:
// CreateTestVault spins up a Vault server and tests against // an actual Vault instance. Currently, this is only set up for kv v2 func createTestVault(t testing.TB) *hashivault.TestCluster { t.Helper() // CoreConfig parameterizes the Vault core config coreConfig := &hashivault.CoreConfig{ LogicalBackends: map[string]logical.Factory{ "kv": kv.Factory, }, } cluster := hashivault.NewTestCluster(t, coreConfig, &hashivault.TestClusterOptions{ // Handler returns an http.Handler for the API. This can be used on // its own to mount the Vault API within another web server. HandlerFunc: vaulthttp.Handler, }) cluster.Start() // Create KV V2 mount on the path /test // It starts in cluster mode, so you just pick one of the three clients // In this case, Cores[0] is just always picking the first one if err := cluster.Cores[0].Client.Sys().Mount("test", &api.MountInput{ Type: "kv", Options: map[string]string{ "version": "2", }, }); err != nil { t.Fatal(err) } return cluster }
Примечание: Большинство из этого схватило из этого Выпуск GitHub . Стоит читать в совокупности и показывает несколько способов настроить тестовые кластеры.
Комментарии устанавливают большую часть деталей, но важный бит состоит в том, что мы создаем кластер тестового хранилища с /test
путь установлен.
Мы возьмем тестирование чтения для чтения кусочка по частям, поэтому вот начальная настройка, которая создает кластер хранилища, делает клиент и ждет некоторое время после крепления с новым кластером.
func TestReadSecrets(t *testing.T) { cluster := createTestVault(t) defer cluster.Cleanup() vaultClient := cluster.Cores[0].Client // only need a client from 1 of 3 clusters _ = &VaultClient{ client: vaultClient, } // time buffer required after new mount // https://github.com/hashicorp/terraform-provider-vault/issues/677#issuecomment-609116328 // Code 400: Errors: Upgrading from non-versioned to versioned data. This backend will be unavailable for a brief period and will resume service shortly. time.Sleep(2 * time.Second) }
Обратите внимание, мы проходим только в одном ядре клиента. В спине кластера есть 3 ядра. Мы просто выбираем первый.
Далее нам нужно написать в некоторых поддельных данных в путь /тестовое задание
Для того, чтобы прочитать секреты позже. Давайте напишем некоторые данные:
// set up sample data to write into vault testData := []struct { path string key string value string }{ {"test/data/test0", "key0", "data0"}, {"test/data/test1", "key1", "data1"}, {"test/data/test2", "key2", "data2"}, } // write k/v data pairs into vault for _, v := range testData { _, err := vc.client.Logical().Write(v.path, map[string]interface{}{ "data": map[string]interface{}{ v.key: v.value, }, }) if err != nil { t.Fatal(err) } }
В приведенном выше коде мы создаем структуру, которая будет затерена в три раза, чтобы написать пример данных в хранилище.
Наконец, мы создаем тестовую таблицу, которая читает секреты и подтверждает их достоверность.
testTable := []struct { name string endpoint string key string want []string vaultError error }{ // 1 { name: "find a k/v match", endpoint: "test/test0", key: "key0", want: []string{"data0"}, vaultError: nil, }, // 2 { name: "do not find a secret", endpoint: "test/test123", key: "test_0_key", want: []string{"test_0_data"}, vaultError: ErrSecretNotFound, }, } for _, tc := range testTable { t.Run(tc.name, func(t *testing.T) { secrets, err := vc.ReadSecret(tc.endpoint) if err != tc.vaultError { t.Fatal(err) } // 3 for i := 0; i < len(secrets); i++ { if secrets[i] != tc.want[i] { assert.Equal(t, tc.want[i], secrets[i]) } } }) }
- Мы находим действительный матч здесь. Таким образом, у нас не должно быть возвращено никакой ошибки, так вот почему
Vaulterror
этоНиль
Отказ - Мы не найдем действительный матч здесь. Тем не менее, Vault по умолчанию не возвращает ошибку, когда нет совпадения. Возвращает пустой секретный объект. Тем не менее, у нас есть логика, встроенная для размещения для этого
вкус ReadSecret.
. Таким образом, мы проверяемErrsecretnotfownound
Отказ - Вместо использования
отражать
Мы связываемся над объектом секретов. Когда ошибка возвращается, и нет секрета, это просто не будет иметь петлю, так как не требуется сравнение.
Фу! Это много кода. Но теперь у нас есть работающая итерация тестового кластера хранилища, создаваемым, наличие секретов, написанных, а затем запустить наши методы против него.
А для потомки я собираюсь опубликовать весь main_test.go
Файл ниже, потому что было так много дополнений. À très bientôt!
package main import ( "testing" "time" kv "github.com/hashicorp/vault-plugin-secrets-kv" "github.com/hashicorp/vault/api" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/logical" hashivault "github.com/hashicorp/vault/vault" "gotest.tools/assert" ) // CreateTestVault spins up a Vault server and tests against // an actual Vault instance. Currently, this is only set up for // kv v2. Mostly copied from this github issue: // https://github.com/hashicorp/vault/issues/8440 func createTestVault(t testing.TB) *hashivault.TestCluster { t.Helper() // CoreConfig parameterizes the Vault core config coreConfig := &hashivault.CoreConfig{ LogicalBackends: map[string]logical.Factory{ "kv": kv.Factory, }, } cluster := hashivault.NewTestCluster(t, coreConfig, &hashivault.TestClusterOptions{ // Handler returns an http.Handler for the API. This can be used on // its own to mount the Vault API within another web server. HandlerFunc: vaulthttp.Handler, }) cluster.Start() // Create KV V2 mount on the path /test // It starts in cluster mode, so you just pick one of the three clients // In this case, Cores[0] is just always picking the first one if err := cluster.Cores[0].Client.Sys().Mount("test", &api.MountInput{ Type: "kv", Options: map[string]string{ "version": "2", }, }); err != nil { t.Fatal(err) } return cluster } func TestReadSecrets(t *testing.T) { cluster := createTestVault(t) defer cluster.Cleanup() vaultClient := cluster.Cores[0].Client // only need a client from 1 of 3 clusters vc := &VaultClient{ client: vaultClient, } // time buffer required after new mount // https://github.com/hashicorp/terraform-provider-vault/issues/677#issuecomment-609116328 // Code 400: Errors: Upgrading from non-versioned to versioned data. This backend will be unavailable for a brief period and will resume service shortly. time.Sleep(2 * time.Second) testData := []struct { path string key string value string }{ {"test/data/test0", "key0", "data0"}, {"test/data/test1", "key1", "data1"}, {"test/data/test2", "key2", "data2"}, } // write k/v data pairs into vault for _, v := range testData { _, err := vc.client.Logical().Write(v.path, map[string]interface{}{ "data": map[string]interface{}{ v.key: v.value, }, }) if err != nil { t.Fatal(err) } } testTable := []struct { name string endpoint string key string want []string vaultError error }{ // 1 { name: "find a k/v match", endpoint: "test/data/test0", key: "key0", want: []string{"data0"}, vaultError: nil, }, // 2 { name: "do not find a secret", endpoint: "test/data/test123", key: "test_0_key", want: []string{}, vaultError: ErrSecretNotFound, }, } for _, tc := range testTable { t.Run(tc.name, func(t *testing.T) { secrets, err := vc.ReadSecret(tc.endpoint) if err != tc.vaultError { t.Fatal(err) } // 3 for i := 0; i < len(secrets); i++ { if secrets[i] != tc.want[i] { assert.Equal(t, tc.want[i], secrets[i]) } } }) } }
Оригинал: «https://dev.to/lucassha/testing-vault-in-go-3pcg»