| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package secretstore_test |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "testing" |
| 8 | |
| 9 | "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore" |
| 10 | "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore/backends" |
| 11 | "github.com/stretchr/testify/assert" |
| 12 | "github.com/stretchr/testify/require" |
| 13 | ) |
| 14 | |
| 15 | func TestProviderSchemaAndValidationParity(t *testing.T) { |
| 16 | svc := secretstore.NewService(backends.Creators()...) |
| 17 | |
| 18 | tests := map[string]struct { |
| 19 | kind secretstore.StoreKind |
| 20 | valid map[string]any |
| 21 | invalid map[string]any |
| 22 | wantErrContains string |
| 23 | assertSchemaShape func(t *testing.T, schema map[string]any) |
| 24 | }{ |
| 25 | "aws": { |
| 26 | kind: secretstore.KindAWSSM, |
| 27 | valid: map[string]any{ |
| 28 | "name": "aws_prod", |
| 29 | "auth_mode": "env", |
| 30 | "region": "us-east-1", |
| 31 | }, |
| 32 | invalid: map[string]any{ |
| 33 | "name": "aws_prod", |
| 34 | "auth_mode": "env", |
| 35 | }, |
| 36 | wantErrContains: "region is required", |
| 37 | assertSchemaShape: func(t *testing.T, schema map[string]any) { |
| 38 | jsonSchema := schema["jsonSchema"].(map[string]any) |
| 39 | assert.Contains(t, jsonSchema["required"], "auth_mode") |
| 40 | assert.Contains(t, jsonSchema["required"], "region") |
| 41 | _, ok := jsonSchema["allOf"] |
| 42 | assert.False(t, ok) |
| 43 | }, |
| 44 | }, |
| 45 | "azure": { |
| 46 | kind: secretstore.KindAzureKV, |
| 47 | valid: map[string]any{ |
| 48 | "name": "azure_prod", |
| 49 | "mode": "managed_identity", |
| 50 | }, |
| 51 | invalid: map[string]any{ |
| 52 | "name": "azure_prod", |
| 53 | "mode": "service_principal", |
| 54 | "mode_service_principal": map[string]any{ |
| 55 | "client_id": "client-id", |
| 56 | }, |
| 57 | }, |
| 58 | wantErrContains: "mode_service_principal.tenant_id is required", |
| 59 | assertSchemaShape: func(t *testing.T, schema map[string]any) { |
| 60 | jsonSchema := schema["jsonSchema"].(map[string]any) |
| 61 | uiSchema := schema["uiSchema"].(map[string]any) |
| 62 | assert.Contains(t, jsonSchema["required"], "mode") |
| 63 | deps := jsonSchema["dependencies"].(map[string]any) |
| 64 | assert.Contains(t, deps, "mode") |
| 65 | modeServicePrincipal := uiSchema["mode_service_principal"].(map[string]any) |
| 66 | clientSecret := modeServicePrincipal["client_secret"].(map[string]any) |
| 67 | assert.Equal(t, "password", clientSecret["ui:widget"]) |
| 68 | }, |
| 69 | }, |
| 70 | "azure default requires mode": { |
| 71 | kind: secretstore.KindAzureKV, |
| 72 | valid: map[string]any{ |
| 73 | "name": "azure_default", |
| 74 | "mode": "default", |
| 75 | }, |
| 76 | invalid: map[string]any{ |
| 77 | "name": "azure_default", |
| 78 | }, |
| 79 | wantErrContains: "mode is required", |
| 80 | }, |
| 81 | "gcp": { |
| 82 | kind: secretstore.KindGCPSM, |
| 83 | valid: map[string]any{ |
| 84 | "name": "gcp_prod", |
| 85 | "mode": "metadata", |
| 86 | }, |
| 87 | invalid: map[string]any{ |
| 88 | "name": "gcp_prod", |
| 89 | "mode": "service_account_file", |
| 90 | "mode_service_account_file": map[string]any{}, |
| 91 | }, |
| 92 | wantErrContains: "mode_service_account_file.path is required", |
| 93 | assertSchemaShape: func(t *testing.T, schema map[string]any) { |
| 94 | jsonSchema := schema["jsonSchema"].(map[string]any) |
| 95 | assert.Contains(t, jsonSchema["required"], "mode") |
| 96 | deps := jsonSchema["dependencies"].(map[string]any) |
| 97 | assert.Contains(t, deps, "mode") |
| 98 | }, |
| 99 | }, |
| 100 | "vault": { |
| 101 | kind: secretstore.KindVault, |
| 102 | valid: map[string]any{ |
| 103 | "name": "vault_prod", |
| 104 | "mode": "token", |
| 105 | "mode_token": map[string]any{ |
| 106 | "token": "vault-token", |
| 107 | }, |
| 108 | "addr": "https://vault.example", |
| 109 | }, |
| 110 | invalid: map[string]any{ |
| 111 | "name": "vault_prod", |
| 112 | "mode": "token_file", |
| 113 | "addr": "https://vault.example", |
| 114 | }, |
| 115 | wantErrContains: "mode_token_file is required", |
| 116 | assertSchemaShape: func(t *testing.T, schema map[string]any) { |
| 117 | jsonSchema := schema["jsonSchema"].(map[string]any) |
| 118 | uiSchema := schema["uiSchema"].(map[string]any) |
| 119 | assert.Contains(t, jsonSchema["required"], "addr") |
| 120 | assert.Contains(t, jsonSchema["required"], "mode") |
| 121 | assert.NotContains(t, jsonSchema["required"], "kind") |
| 122 | deps := jsonSchema["dependencies"].(map[string]any) |
| 123 | assert.Contains(t, deps, "mode") |
| 124 | modeToken := uiSchema["mode_token"].(map[string]any) |
| 125 | token := modeToken["token"].(map[string]any) |
| 126 | assert.Equal(t, "password", token["ui:widget"]) |
| 127 | }, |
| 128 | }, |
| 129 | } |
| 130 | |
| 131 | for name, tc := range tests { |
| 132 | t.Run(name, func(t *testing.T) { |
| 133 | displayName, ok := svc.DisplayName(tc.kind) |
| 134 | require.True(t, ok) |
| 135 | assert.NotEmpty(t, displayName) |
| 136 | |
| 137 | schema, ok := svc.Schema(tc.kind) |
| 138 | require.True(t, ok) |
| 139 | schemaObj := decodeSchema(t, schema) |
| 140 | _, ok = schemaObj["jsonSchema"].(map[string]any) |
| 141 | require.True(t, ok) |
| 142 | _, ok = schemaObj["uiSchema"].(map[string]any) |
| 143 | require.True(t, ok) |
| 144 | if tc.assertSchemaShape != nil { |
| 145 | tc.assertSchemaShape(t, schemaObj) |
| 146 | } |
| 147 | |
| 148 | err := svc.Validate(context.Background(), newStoreFromConfig(t, svc, tc.kind, tc.valid)) |
| 149 | require.NoError(t, err) |
| 150 | |
| 151 | err = svc.Validate(context.Background(), newStoreFromConfig(t, svc, tc.kind, tc.invalid)) |
| 152 | require.Error(t, err) |
| 153 | assert.ErrorContains(t, err, tc.wantErrContains) |
| 154 | }) |
| 155 | } |
| 156 | } |