|
1 | 1 | package beholder_test |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "context" |
4 | 5 | "crypto/ed25519" |
5 | 6 | "encoding/hex" |
| 7 | + "strings" |
6 | 8 | "testing" |
| 9 | + "time" |
7 | 10 |
|
8 | 11 | "github.com/stretchr/testify/assert" |
| 12 | + "github.com/stretchr/testify/mock" |
9 | 13 | "github.com/stretchr/testify/require" |
10 | 14 |
|
11 | 15 | "github.com/smartcontractkit/chainlink-common/pkg/beholder" |
@@ -37,10 +41,157 @@ func TestStaticAuthHeaderProvider(t *testing.T) { |
37 | 41 | } |
38 | 42 |
|
39 | 43 | // Create new header provider |
40 | | - provider := beholder.NewStaticAuthHeaderProvider(testHeaders) |
| 44 | + provider := beholder.NewStaticAuth(testHeaders, false) |
41 | 45 |
|
42 | 46 | // Get headers and verify they match |
43 | 47 | headers, err := provider.Headers(t.Context()) |
44 | 48 | require.NoError(t, err) |
45 | 49 | assert.Equal(t, testHeaders, headers) |
46 | 50 | } |
| 51 | + |
| 52 | +// MockSigner implements the beholder.Signer interface for testing rotating auth |
| 53 | +type MockSigner struct { |
| 54 | + mock.Mock |
| 55 | +} |
| 56 | + |
| 57 | +func (m *MockSigner) Sign(ctx context.Context, keyID []byte, data []byte) ([]byte, error) { |
| 58 | + args := m.Called(ctx, keyID, data) |
| 59 | + return args.Get(0).([]byte), args.Error(1) |
| 60 | +} |
| 61 | + |
| 62 | +func TestRotatingAuth(t *testing.T) { |
| 63 | + // Generate test key pair |
| 64 | + pubKey, privKey, err := ed25519.GenerateKey(nil) |
| 65 | + require.NoError(t, err) |
| 66 | + |
| 67 | + t.Run("creates valid rotating auth headers", func(t *testing.T) { |
| 68 | + |
| 69 | + mockSigner := &MockSigner{} |
| 70 | + |
| 71 | + dummySignature := ed25519.Sign(privKey, []byte("test data")) |
| 72 | + |
| 73 | + mockSigner. |
| 74 | + On("Sign", mock.Anything, mock.MatchedBy(func(keyID []byte) bool { |
| 75 | + return string(keyID) == string(pubKey) // Verify correct public key is passed |
| 76 | + }), mock.Anything). |
| 77 | + Return(dummySignature, nil) |
| 78 | + |
| 79 | + ttl := 5 * time.Minute |
| 80 | + auth := beholder.NewRotatingAuth(pubKey, mockSigner, ttl, false) |
| 81 | + |
| 82 | + headers, err := auth.Headers(t.Context()) |
| 83 | + require.NoError(t, err) |
| 84 | + require.NotEmpty(t, headers) |
| 85 | + |
| 86 | + authHeader := headers["X-Beholder-Node-Auth-Token"] |
| 87 | + require.NotEmpty(t, authHeader) |
| 88 | + |
| 89 | + parts := strings.Split(authHeader, ":") |
| 90 | + require.Len(t, parts, 4, "Auth header should have format version:pubkey_hex:timestamp:signature_hex") |
| 91 | + |
| 92 | + assert.Equal(t, "2", parts[0], "Version should be 2") |
| 93 | + assert.Equal(t, hex.EncodeToString(pubKey), parts[1], "Public key should match") |
| 94 | + assert.NotEmpty(t, parts[2], "Timestamp should not be empty") |
| 95 | + |
| 96 | + // Verify signature is hex encoded |
| 97 | + _, err = hex.DecodeString(parts[3]) |
| 98 | + assert.NoError(t, err, "Signature should be valid hex") |
| 99 | + |
| 100 | + mockSigner.AssertExpectations(t) |
| 101 | + }) |
| 102 | + |
| 103 | + t.Run("reuses headers within TTL", func(t *testing.T) { |
| 104 | + |
| 105 | + mockSigner := &MockSigner{} |
| 106 | + |
| 107 | + dummySignature := ed25519.Sign(privKey, []byte("test data")) |
| 108 | + |
| 109 | + mockSigner. |
| 110 | + On("Sign", mock.Anything, mock.Anything, mock.Anything). |
| 111 | + Return(dummySignature, nil). |
| 112 | + Maybe() |
| 113 | + |
| 114 | + ttl := 5 * time.Minute |
| 115 | + auth := beholder.NewRotatingAuth(pubKey, mockSigner, ttl, false) |
| 116 | + |
| 117 | + headers1, err := auth.Headers(t.Context()) |
| 118 | + require.NoError(t, err) |
| 119 | + |
| 120 | + headers2, err := auth.Headers(t.Context()) |
| 121 | + require.NoError(t, err) |
| 122 | + |
| 123 | + assert.Equal(t, headers1, headers2, "Headers should be reused within TTL") |
| 124 | + |
| 125 | + mockSigner.AssertExpectations(t) |
| 126 | + }) |
| 127 | + |
| 128 | + t.Run("handles signer errors", func(t *testing.T) { |
| 129 | + |
| 130 | + mockSigner := &MockSigner{} |
| 131 | + expectedErr := assert.AnError |
| 132 | + |
| 133 | + mockSigner. |
| 134 | + On("Sign", mock.Anything, mock.Anything, mock.Anything). |
| 135 | + Return([]byte{}, expectedErr) |
| 136 | + |
| 137 | + ttl := 5 * time.Minute |
| 138 | + auth := beholder.NewRotatingAuth(pubKey, mockSigner, ttl, false) |
| 139 | + |
| 140 | + ctx := context.Background() |
| 141 | + headers, err := auth.Headers(ctx) |
| 142 | + require.Error(t, err) |
| 143 | + assert.Nil(t, headers) |
| 144 | + assert.Contains(t, err.Error(), "beholder: failed to sign auth header") |
| 145 | + assert.Contains(t, err.Error(), expectedErr.Error()) |
| 146 | + |
| 147 | + mockSigner.AssertExpectations(t) |
| 148 | + }) |
| 149 | + |
| 150 | + t.Run("implements PerRPCCredentialsProvider interface", func(t *testing.T) { |
| 151 | + |
| 152 | + mockSigner := &MockSigner{} |
| 153 | + dummySignature := ed25519.Sign(privKey, []byte("test data")) |
| 154 | + |
| 155 | + mockSigner. |
| 156 | + On("Sign", mock.Anything, mock.Anything, mock.Anything). |
| 157 | + Return(dummySignature, nil). |
| 158 | + Maybe() |
| 159 | + |
| 160 | + ttl := 5 * time.Minute |
| 161 | + auth := beholder.NewRotatingAuth(pubKey, mockSigner, ttl, false) |
| 162 | + |
| 163 | + creds := auth.Credentials() |
| 164 | + require.NotNil(t, creds) |
| 165 | + |
| 166 | + assert.False(t, creds.RequireTransportSecurity()) |
| 167 | + |
| 168 | + metadata, err := creds.GetRequestMetadata(t.Context()) |
| 169 | + require.NoError(t, err) |
| 170 | + assert.NotEmpty(t, metadata) |
| 171 | + |
| 172 | + mockSigner.AssertExpectations(t) |
| 173 | + }) |
| 174 | + |
| 175 | + t.Run("respects transport security requirement", func(t *testing.T) { |
| 176 | + |
| 177 | + mockSigner := &MockSigner{} |
| 178 | + dummySignature := ed25519.Sign(privKey, []byte("test data")) |
| 179 | + |
| 180 | + mockSigner. |
| 181 | + On("Sign", mock.Anything, mock.Anything, mock.Anything). |
| 182 | + Return(dummySignature, nil). |
| 183 | + Maybe() |
| 184 | + |
| 185 | + ttl := 5 * time.Minute |
| 186 | + // transport security required |
| 187 | + authSecure := beholder.NewRotatingAuth(pubKey, mockSigner, ttl, true) |
| 188 | + credsSecure := authSecure.Credentials() |
| 189 | + assert.True(t, credsSecure.RequireTransportSecurity()) |
| 190 | + // transport security not required |
| 191 | + authInsecure := beholder.NewRotatingAuth(pubKey, mockSigner, ttl, false) |
| 192 | + credsInsecure := authInsecure.Credentials() |
| 193 | + assert.False(t, credsInsecure.RequireTransportSecurity()) |
| 194 | + |
| 195 | + mockSigner.AssertExpectations(t) |
| 196 | + }) |
| 197 | +} |
0 commit comments