github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/prober/interpolation/interpolation_test.go (about)

     1  package interpolation
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  
     7  	"github.com/grafana/synthetic-monitoring-agent/internal/testhelper"
     8  	"github.com/stretchr/testify/require"
     9  )
    10  
    11  // mockVariableProvider is a mock implementation of VariableProvider for testing
    12  type mockVariableProvider struct {
    13  	variables map[string]string
    14  }
    15  
    16  func (m *mockVariableProvider) GetVariable(name string) (string, error) {
    17  	if value, exists := m.variables[name]; exists {
    18  		return value, nil
    19  	}
    20  
    21  	return "", fmt.Errorf("variable '%s' not found", name)
    22  }
    23  
    24  func TestResolver_Resolve(t *testing.T) {
    25  	ctx, logger, tenantID := testhelper.CommonTestSetup()
    26  
    27  	// Mock providers
    28  	variableProvider := &mockVariableProvider{
    29  		variables: map[string]string{
    30  			"username":             "admin",
    31  			"domain":               "example.com",
    32  			"random":               "123",
    33  			"variable-with-secret": "${secrets.api-token}",
    34  		},
    35  	}
    36  
    37  	secretProvider := testhelper.NewMockSecretProvider(map[string]string{
    38  		"api-token":       "secret-token-123",
    39  		"db-password":     "secret-password",
    40  		"empty-secret":    "",
    41  		"auth-config":     "username=${username}&token=${api-token}",
    42  		"random-password": "my-password-${random}",
    43  	})
    44  
    45  	testcases := map[string]struct {
    46  		input          string
    47  		secretEnabled  bool
    48  		expectedOutput string
    49  		expectError    bool
    50  	}{
    51  		"empty string": {
    52  			input:          "",
    53  			secretEnabled:  true,
    54  			expectedOutput: "",
    55  			expectError:    false,
    56  		},
    57  		"plaintext only": {
    58  			input:          "hello world",
    59  			secretEnabled:  true,
    60  			expectedOutput: "hello world",
    61  			expectError:    false,
    62  		},
    63  		"secret interpolation only": {
    64  			input:          "${secrets.api-token}",
    65  			secretEnabled:  true,
    66  			expectedOutput: "secret-token-123",
    67  			expectError:    false,
    68  		},
    69  		"variable interpolation only": {
    70  			input:          "${username}",
    71  			secretEnabled:  true,
    72  			expectedOutput: "admin",
    73  			expectError:    false,
    74  		},
    75  		"mixed secret and variable": {
    76  			input:          "Bearer ${secrets.api-token} for ${username}@${domain}",
    77  			secretEnabled:  true,
    78  			expectedOutput: "Bearer secret-token-123 for admin@example.com",
    79  			expectError:    false,
    80  		},
    81  		"multiple secrets": {
    82  			input:          "${secrets.api-token}:${secrets.db-password}",
    83  			secretEnabled:  true,
    84  			expectedOutput: "secret-token-123:secret-password",
    85  			expectError:    false,
    86  		},
    87  		"secrets disabled": {
    88  			input:          "${secrets.api-token}",
    89  			secretEnabled:  false,
    90  			expectedOutput: "${secrets.api-token}",
    91  			expectError:    false,
    92  		},
    93  		"variables still work when secrets disabled": {
    94  			input:          "${username}",
    95  			secretEnabled:  false,
    96  			expectedOutput: "admin",
    97  			expectError:    false,
    98  		},
    99  		"empty secret name": {
   100  			input:          "${secrets.}",
   101  			secretEnabled:  true,
   102  			expectedOutput: "",
   103  			expectError:    true,
   104  		},
   105  		"invalid secret name": {
   106  			input:          "${secrets.invalid-name}",
   107  			secretEnabled:  true,
   108  			expectedOutput: "",
   109  			expectError:    true,
   110  		},
   111  		"missing secret": {
   112  			input:          "${secrets.missing-secret}",
   113  			secretEnabled:  true,
   114  			expectedOutput: "",
   115  			expectError:    true,
   116  		},
   117  		"missing variable": {
   118  			input:          "${missing-variable}",
   119  			secretEnabled:  true,
   120  			expectedOutput: "${missing-variable}",
   121  			expectError:    false,
   122  		},
   123  		"missing variable when secrets disabled": {
   124  			input:          "${missing-variable}",
   125  			secretEnabled:  false,
   126  			expectedOutput: "${missing-variable}",
   127  			expectError:    false,
   128  		},
   129  		"variable with no provider": {
   130  			input:          "${some-variable}",
   131  			secretEnabled:  true,
   132  			expectedOutput: "${some-variable}",
   133  			expectError:    false,
   134  		},
   135  		"secret containing variables": {
   136  			input:          "https://api.example.com/auth?${secrets.auth-config}",
   137  			secretEnabled:  true,
   138  			expectedOutput: "https://api.example.com/auth?username=${username}&token=${api-token}",
   139  			expectError:    false,
   140  		},
   141  		"secret with variable-like pattern": {
   142  			input:          "Password: ${secrets.random-password}",
   143  			secretEnabled:  true,
   144  			expectedOutput: "Password: my-password-${random}",
   145  			expectError:    false,
   146  		},
   147  		"variable with secret": {
   148  			input:          "${variable-with-secret}",
   149  			secretEnabled:  true,
   150  			expectedOutput: "${secrets.api-token}",
   151  			expectError:    false,
   152  		},
   153  		"variables disabled": {
   154  			input:          "Hello ${username} with token ${secrets.api-token}",
   155  			secretEnabled:  true,
   156  			expectedOutput: "Hello ${username} with token secret-token-123",
   157  			expectError:    false,
   158  		},
   159  	}
   160  
   161  	for name, tc := range testcases {
   162  		t.Run(name, func(t *testing.T) {
   163  			var resolver *Resolver
   164  			if name == "variables disabled" {
   165  				// Create resolver with no variable provider to test variables disabled
   166  				resolver = NewResolver(nil, secretProvider, tenantID, logger, tc.secretEnabled)
   167  			} else {
   168  				resolver = NewResolver(variableProvider, secretProvider, tenantID, logger, tc.secretEnabled)
   169  			}
   170  
   171  			actual, err := resolver.Resolve(ctx, tc.input)
   172  
   173  			if tc.expectError {
   174  				require.Error(t, err)
   175  			} else {
   176  				require.NoError(t, err)
   177  				require.Equal(t, tc.expectedOutput, actual)
   178  			}
   179  		})
   180  	}
   181  
   182  	// Test with no variable provider
   183  	t.Run("no variable provider", func(t *testing.T) {
   184  		resolver := NewResolver(nil, secretProvider, tenantID, logger, true)
   185  		actual, err := resolver.Resolve(ctx, "${some-variable}")
   186  		require.NoError(t, err)
   187  		require.Equal(t, "${some-variable}", actual)
   188  	})
   189  }
   190  
   191  func TestIsValidSecretName(t *testing.T) {
   192  	testcases := map[string]struct {
   193  		name     string
   194  		expected bool
   195  	}{
   196  		"valid simple name": {
   197  			name:     "my-secret",
   198  			expected: true,
   199  		},
   200  		"valid with dots": {
   201  			name:     "my.secret.name",
   202  			expected: true,
   203  		},
   204  		"valid with numbers": {
   205  			name:     "secret123",
   206  			expected: true,
   207  		},
   208  		"valid mixed case": {
   209  			name:     "my-secret-name",
   210  			expected: true,
   211  		},
   212  		"empty name": {
   213  			name:     "",
   214  			expected: false,
   215  		},
   216  		"too long": {
   217  			name:     "a" + string(make([]byte, 253)),
   218  			expected: false,
   219  		},
   220  		"starts with dash": {
   221  			name:     "-invalid",
   222  			expected: false,
   223  		},
   224  		"ends with dash": {
   225  			name:     "invalid-",
   226  			expected: false,
   227  		},
   228  		"contains uppercase": {
   229  			name:     "Invalid-Name",
   230  			expected: false,
   231  		},
   232  		"contains special chars": {
   233  			name:     "invalid@name",
   234  			expected: false,
   235  		},
   236  		"starts with dot": {
   237  			name:     ".invalid",
   238  			expected: false,
   239  		},
   240  		"ends with dot": {
   241  			name:     "invalid.",
   242  			expected: false,
   243  		},
   244  	}
   245  
   246  	for name, tc := range testcases {
   247  		t.Run(name, func(t *testing.T) {
   248  			actual := isValidSecretName(tc.name)
   249  			require.Equal(t, tc.expected, actual)
   250  		})
   251  	}
   252  }
   253  
   254  func TestToJavaScript(t *testing.T) {
   255  	testcases := map[string]struct {
   256  		input    string
   257  		expected string
   258  	}{
   259  		"empty string": {
   260  			input:    "",
   261  			expected: "''",
   262  		},
   263  		"plaintext only": {
   264  			input:    "hello world",
   265  			expected: "'hello world'",
   266  		},
   267  		"variable only": {
   268  			input:    "${username}",
   269  			expected: "vars['username']",
   270  		},
   271  		"mixed text and variable": {
   272  			input:    "Hello ${username}!",
   273  			expected: "'Hello '+vars['username']+'!'",
   274  		},
   275  		"multiple variables": {
   276  			input:    "${username}@${domain}",
   277  			expected: "vars['username']+'@'+vars['domain']",
   278  		},
   279  		"complex mixed": {
   280  			input:    "Bearer ${token} for ${username}@${domain}",
   281  			expected: "'Bearer '+vars['token']+' for '+vars['username']+'@'+vars['domain']",
   282  		},
   283  		"with quotes": {
   284  			input:    "Hello \"${username}\"",
   285  			expected: "'Hello \\\"'+vars['username']+'\\\"'",
   286  		},
   287  		"with backslashes": {
   288  			input:    "Path: \\${username}\\",
   289  			expected: "'Path: \\\\'+vars['username']+'\\\\'",
   290  		},
   291  	}
   292  
   293  	for name, tc := range testcases {
   294  		t.Run(name, func(t *testing.T) {
   295  			actual := ToJavaScript(tc.input)
   296  			require.Equal(t, tc.expected, actual)
   297  		})
   298  	}
   299  }