diff --git a/pkg/plugin/implementation/registry/cmd/plugin_test.go b/pkg/plugin/implementation/registry/cmd/plugin_test.go index e27aece..c460c0a 100644 --- a/pkg/plugin/implementation/registry/cmd/plugin_test.go +++ b/pkg/plugin/implementation/registry/cmd/plugin_test.go @@ -2,249 +2,188 @@ package main import ( "context" - "net/http" - "net/http/httptest" + "errors" + "fmt" "testing" "time" - "github.com/beckn-one/beckn-onix/pkg/model" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/beckn-one/beckn-onix/pkg/plugin/implementation/registry" ) -func TestRegistryProvider_New(t *testing.T) { - tests := []struct { +// mockRegistryClient is a mock implementation of the RegistryLookup interface +// for testing purposes. +type mockRegistryClient struct{} + +func (m *mockRegistryClient) Subscribe(ctx context.Context, subscription interface{}) error { + return nil +} +func (m *mockRegistryClient) Lookup(ctx context.Context, subscription interface{}) ([]interface{}, error) { + return nil, nil +} + +// TestRegistryProvider_ParseConfig tests the configuration parsing logic. +func TestRegistryProvider_ParseConfig(t *testing.T) { + t.Parallel() + provider := registryProvider{} + + testCases := []struct { name string config map[string]string - expectError bool - errorMsg string + expected *registry.Config + expectedErr string }{ { - name: "valid config with all parameters", + name: "should parse a full, valid config", config: map[string]string{ - "url": "http://localhost:8080", - "retry_max": "3", - "retry_wait_min": "100ms", - "retry_wait_max": "500ms", - }, - expectError: false, - }, - { - name: "minimal valid config", - config: map[string]string{ - "url": "http://localhost:8080", - }, - expectError: false, - }, - { - name: "missing URL", - config: map[string]string{}, - expectError: true, - errorMsg: "registry URL cannot be empty", - }, - { - name: "invalid retry_max", - config: map[string]string{ - "url": "http://localhost:8080", - "retry_max": "invalid", - }, - expectError: false, // Invalid values are ignored, not errors - }, - { - name: "invalid retry_wait_min", - config: map[string]string{ - "url": "http://localhost:8080", - "retry_wait_min": "invalid", - }, - expectError: false, // Invalid values are ignored, not errors - }, - { - name: "invalid retry_wait_max", - config: map[string]string{ - "url": "http://localhost:8080", - "retry_wait_max": "invalid", - }, - expectError: false, // Invalid values are ignored, not errors - }, - { - name: "empty URL", - config: map[string]string{ - "url": "", - }, - expectError: true, - errorMsg: "registry URL cannot be empty", - }, - } - - provider := registryProvider{} - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - registry, closer, err := provider.New(ctx, tt.config) - - if tt.expectError { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.errorMsg) - assert.Nil(t, registry) - assert.Nil(t, closer) - } else { - require.NoError(t, err) - assert.NotNil(t, registry) - assert.NotNil(t, closer) - - // Test that closer works - err = closer() - assert.NoError(t, err) - } - }) - } -} - -func TestRegistryProvider_NilContext(t *testing.T) { - provider := registryProvider{} - config := map[string]string{ - "url": "http://localhost:8080", - } - - registry, closer, err := provider.New(context.TODO(), config) - require.Error(t, err) - assert.Contains(t, err.Error(), "context cannot be nil") - assert.Nil(t, registry) - assert.Nil(t, closer) -} - -func TestRegistryProvider_IntegrationTest(t *testing.T) { - // Create a test server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/subscribe": - w.WriteHeader(http.StatusOK) - w.Write([]byte("{}")) - case "/lookup": - w.WriteHeader(http.StatusOK) - w.Write([]byte("[]")) - default: - w.WriteHeader(http.StatusNotFound) - } - })) - defer server.Close() - - provider := registryProvider{} - config := map[string]string{ - "url": server.URL, - "retry_max": "2", - "retry_wait_min": "10ms", - "retry_wait_max": "20ms", - } - - ctx := context.Background() - registry, closer, err := provider.New(ctx, config) - require.NoError(t, err) - require.NotNil(t, registry) - require.NotNil(t, closer) - defer closer() - - subscription := &model.Subscription{ - Subscriber: model.Subscriber{ - SubscriberID: "test-subscriber", - URL: "https://example.com", - Type: "BAP", - Domain: "mobility", - }, - KeyID: "test-key", - SigningPublicKey: "test-signing-key", - EncrPublicKey: "test-encryption-key", - ValidFrom: time.Now(), - ValidUntil: time.Now().Add(24 * time.Hour), - Status: "SUBSCRIBED", - } - - // Test Lookup - results, err := registry.Lookup(ctx, subscription) - require.NoError(t, err) - assert.NotNil(t, results) - assert.Len(t, results, 0) // Empty array response from test server -} - -func TestRegistryProvider_ConfigurationParsing(t *testing.T) { - tests := []struct { - name string - config map[string]string - expectedConfig map[string]interface{} - }{ - { - name: "all parameters set", - config: map[string]string{ - "url": "http://localhost:8080", + "url": "http://test.com", "retry_max": "5", - "retry_wait_min": "200ms", - "retry_wait_max": "1s", + "retry_wait_min": "100ms", + "retry_wait_max": "2s", }, - expectedConfig: map[string]interface{}{ - "url": "http://localhost:8080", - "retry_max": 5, - "retry_wait_min": 200 * time.Millisecond, - "retry_wait_max": 1 * time.Second, + expected: ®istry.Config{ + URL: "http://test.com", + RetryMax: 5, + RetryWaitMin: 100 * time.Millisecond, + RetryWaitMax: 2 * time.Second, }, + expectedErr: "", }, { - name: "only required parameters", + name: "should handle missing optional values", config: map[string]string{ - "url": "https://registry.example.com", + "url": "http://test.com", }, - expectedConfig: map[string]interface{}{ - "url": "https://registry.example.com", + expected: ®istry.Config{ + URL: "http://test.com", }, + expectedErr: "", }, { - name: "invalid numeric values ignored", + name: "should return error for invalid retry_max", config: map[string]string{ - "url": "http://localhost:8080", + "url": "http://test.com", "retry_max": "not-a-number", }, - expectedConfig: map[string]interface{}{ - "url": "http://localhost:8080", - }, + expected: nil, + expectedErr: "invalid retry_max value 'not-a-number'", }, { - name: "invalid duration values ignored", + name: "should return error for invalid retry_wait_min", config: map[string]string{ - "url": "http://localhost:8080", - "retry_wait_min": "not-a-duration", - "retry_wait_max": "also-not-a-duration", + "url": "http://test.com", + "retry_wait_min": "bad-duration", }, - expectedConfig: map[string]interface{}{ - "url": "http://localhost:8080", + expected: nil, + expectedErr: "invalid retry_wait_min value 'bad-duration'", + }, + { + name: "should return error for invalid retry_wait_max", + config: map[string]string{ + "url": "http://test.com", + "retry_wait_max": "30parsecs", }, + expected: nil, + expectedErr: "invalid retry_wait_max value '30parsecs'", }, } - // Create a test server that just returns OK - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("{}")) - })) - defer server.Close() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + parsedConfig, err := provider.parseConfig(tc.config) - provider := registryProvider{} - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Override URL with test server URL for testing - testConfig := make(map[string]string) - for k, v := range tt.config { - testConfig[k] = v + if tc.expectedErr != "" { + if err == nil { + t.Fatalf("expected an error containing '%s' but got none", tc.expectedErr) + } + if e, a := tc.expectedErr, err.Error(); !(a == e || (len(a) > len(e) && a[:len(e)] == e)) { + t.Errorf("expected error message to contain '%s', but got '%s'", e, a) + } + return } - testConfig["url"] = server.URL - - ctx := context.Background() - registry, closer, err := provider.New(ctx, testConfig) - require.NoError(t, err) - require.NotNil(t, registry) - require.NotNil(t, closer) - defer closer() + if err != nil { + t.Fatalf("expected no error, but got: %v", err) + } + if parsedConfig.URL != tc.expected.URL { + t.Errorf("expected URL '%s', got '%s'", tc.expected.URL, parsedConfig.URL) + } + if parsedConfig.RetryMax != tc.expected.RetryMax { + t.Errorf("expected RetryMax %d, got %d", tc.expected.RetryMax, parsedConfig.RetryMax) + } + if parsedConfig.RetryWaitMin != tc.expected.RetryWaitMin { + t.Errorf("expected RetryWaitMin %v, got %v", tc.expected.RetryWaitMin, parsedConfig.RetryWaitMin) + } + if parsedConfig.RetryWaitMax != tc.expected.RetryWaitMax { + t.Errorf("expected RetryWaitMax %v, got %v", tc.expected.RetryWaitMax, parsedConfig.RetryWaitMax) + } }) } } + +// TestRegistryProvider_New tests the plugin's main constructor. +func TestRegistryProvider_New(t *testing.T) { + t.Parallel() + provider := registryProvider{} + originalNewRegistryFunc := newRegistryFunc + + // Cleanup to restore the original function after the test + t.Cleanup(func() { + newRegistryFunc = originalNewRegistryFunc + }) + + t.Run("should return error if context is nil", func(t *testing.T) { + _, _, err := provider.New(nil, map[string]string{}) + if err == nil { + t.Fatal("expected an error for nil context but got none") + } + if err.Error() != "context cannot be nil" { + t.Errorf("expected 'context cannot be nil' error, got '%s'", err.Error()) + } + }) + + t.Run("should return error if config parsing fails", func(t *testing.T) { + config := map[string]string{"retry_max": "invalid"} + _, _, err := provider.New(context.Background(), config) + if err == nil { + t.Fatal("expected an error for bad config but got none") + } + }) + + t.Run("should return error if registry.New fails", func(t *testing.T) { + // Mock the newRegistryFunc to return an error + expectedErr := errors.New("registry creation failed") + newRegistryFunc = func(ctx context.Context, cfg *registry.Config) (*registry.RegistryClient, func() error, error) { + return nil, nil, expectedErr + } + + config := map[string]string{"url": "http://test.com"} + _, _, err := provider.New(context.Background(), config) + if err == nil { + t.Fatal("expected an error from registry.New but got none") + } + if !errors.Is(err, expectedErr) { + t.Errorf("expected error '%v', got '%v'", expectedErr, err) + } + }) + + t.Run("should succeed and return a valid instance", func(t *testing.T) { + // Mock the newRegistryFunc for a successful case + mockCloser := func() error { fmt.Println("closed"); return nil } + newRegistryFunc = func(ctx context.Context, cfg *registry.Config) (*registry.RegistryClient, func() error, error) { + // Return a non-nil client of th correct concrete type + return new(registry.RegistryClient), mockCloser, nil + } + + config := map[string]string{"url": "http://test.com"} + instance, closer, err := provider.New(context.Background(), config) + if err != nil { + t.Fatalf("expected no error, but got: %v", err) + } + if instance == nil { + t.Fatal("expected a non-nil instance") + } + if closer == nil { + t.Fatal("expected a non-nil closer function") + } + }) +} diff --git a/pkg/plugin/implementation/registry/registry_test.go b/pkg/plugin/implementation/registry/registry_test.go index 18f9f96..1498973 100644 --- a/pkg/plugin/implementation/registry/registry_test.go +++ b/pkg/plugin/implementation/registry/registry_test.go @@ -3,496 +3,202 @@ package registry import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/beckn-one/beckn-onix/pkg/model" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -// TestNew tests the New function for creating RegistryClient instances -func TestNew(t *testing.T) { - tests := []struct { +// TestValidate ensures the config validation logic works correctly. +func TestValidate(t *testing.T) { + t.Parallel() + testCases := []struct { name string config *Config - expectError bool - errorMsg string + expectedErr string }{ { - name: "valid config", - config: &Config{ - URL: "http://localhost:8080", - RetryMax: 3, - RetryWaitMin: time.Millisecond * 100, - RetryWaitMax: time.Millisecond * 500, - }, - expectError: false, - }, - { - name: "nil config", + name: "should return error for nil config", config: nil, - expectError: true, - errorMsg: "registry config cannot be nil", + expectedErr: "registry config cannot be nil", }, { - name: "empty URL", - config: &Config{ - URL: "", - }, - expectError: true, - errorMsg: "registry URL cannot be empty", + name: "should return error for empty URL", + config: &Config{URL: ""}, + expectedErr: "registry URL cannot be empty", }, { - name: "minimal valid config", - config: &Config{ - URL: "http://example.com", - }, - expectError: false, + name: "should succeed for valid config", + config: &Config{URL: "http://localhost:8080"}, + expectedErr: "", }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - client, closer, err := New(ctx, tt.config) - - if tt.expectError { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.errorMsg) - assert.Nil(t, client) - assert.Nil(t, closer) - } else { - require.NoError(t, err) - assert.NotNil(t, client) - assert.NotNil(t, closer) - - // Test that closer works without error - err = closer() - assert.NoError(t, err) - - // Verify config is set correctly - assert.Equal(t, tt.config.URL, client.config.URL) - } - }) - } -} - -// TestSubscribeSuccess verifies that the Subscribe function succeeds when the server responds with HTTP 200. -func TestSubscribeSuccess(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify request method and headers - assert.Equal(t, "POST", r.Method) - assert.Equal(t, "application/json", r.Header.Get("Content-Type")) - assert.Contains(t, r.URL.Path, "/subscribe") - - w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte("{}")); err != nil { - t.Errorf("failed to write response: %v", err) - } - })) - defer server.Close() - - config := &Config{ - URL: server.URL, - RetryMax: 3, - RetryWaitMin: time.Millisecond * 100, - RetryWaitMax: time.Millisecond * 500, - } - - ctx := context.Background() - client, closer, err := New(ctx, config) - require.NoError(t, err) - defer closer() - - subscription := &model.Subscription{ - KeyID: "test-key", - SigningPublicKey: "test-signing-key", - EncrPublicKey: "test-encryption-key", - ValidFrom: time.Now(), - ValidUntil: time.Now().Add(24 * time.Hour), - Status: "SUBSCRIBED", - } - - err = client.Subscribe(context.Background(), subscription) - require.NoError(t, err) -} - -// TestSubscribeFailure tests different failure scenarios for Subscribe. -func TestSubscribeFailure(t *testing.T) { - tests := []struct { - name string - responseCode int - responseBody string - expectError bool - errorContains string - setupServer func() *httptest.Server - config *Config - }{ - { - name: "Internal Server Error", - responseCode: http.StatusInternalServerError, - responseBody: "Internal Server Error", - expectError: true, - errorContains: "subscribe request failed with status", - }, - { - name: "Bad Request", - responseCode: http.StatusBadRequest, - responseBody: "Bad Request", - expectError: true, - errorContains: "subscribe request failed with status", - }, - { - name: "Not Found", - responseCode: http.StatusNotFound, - responseBody: "Not Found", - expectError: true, - errorContains: "subscribe request failed with status", - }, - { - name: "Connection Refused", - setupServer: func() *httptest.Server { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) - server.Close() // Close immediately to simulate connection refused - return server - }, - expectError: true, - errorContains: "failed to send subscribe request with retry", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var server *httptest.Server - - if tt.setupServer != nil { - server = tt.setupServer() - } else { - server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.responseCode) - if _, err := w.Write([]byte(tt.responseBody)); err != nil { - t.Errorf("failed to write response: %v", err) - } - })) - defer server.Close() - } - - config := &Config{ - URL: server.URL, - RetryMax: 1, - RetryWaitMin: 1 * time.Millisecond, - RetryWaitMax: 2 * time.Millisecond, - } - - ctx := context.Background() - client, closer, err := New(ctx, config) - require.NoError(t, err) - defer closer() - - subscription := &model.Subscription{ - KeyID: "test-key", - SigningPublicKey: "test-signing-key", - EncrPublicKey: "test-encryption-key", - ValidFrom: time.Now(), - ValidUntil: time.Now().Add(24 * time.Hour), - Status: "SUBSCRIBED", - } - - err = client.Subscribe(context.Background(), subscription) - if tt.expectError { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.errorContains) - } else { - require.NoError(t, err) - } - }) - } -} - -// TestLookupSuccess tests successful lookup scenarios. -func TestLookupSuccess(t *testing.T) { - expectedResponse := []model.Subscription{ - { - Subscriber: model.Subscriber{ - SubscriberID: "123", - URL: "https://example.com", - Type: "BAP", - Domain: "mobility", - }, - KeyID: "test-key", - SigningPublicKey: "test-signing-key", - EncrPublicKey: "test-encryption-key", - ValidFrom: time.Now(), - ValidUntil: time.Now().Add(24 * time.Hour), - Status: "SUBSCRIBED", - }, - { - Subscriber: model.Subscriber{ - SubscriberID: "456", - URL: "https://example2.com", - Type: "BPP", - Domain: "retail", - }, - KeyID: "test-key-2", - SigningPublicKey: "test-signing-key-2", - EncrPublicKey: "test-encryption-key-2", - ValidFrom: time.Now(), - ValidUntil: time.Now().Add(48 * time.Hour), - Status: "SUBSCRIBED", - }, - } - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify request method and headers - assert.Equal(t, "POST", r.Method) - assert.Equal(t, "application/json", r.Header.Get("Content-Type")) - assert.Contains(t, r.URL.Path, "/lookup") - - w.WriteHeader(http.StatusOK) - bodyBytes, _ := json.Marshal(expectedResponse) - if _, err := w.Write(bodyBytes); err != nil { - t.Errorf("failed to write response: %v", err) - } - })) - defer server.Close() - - config := &Config{ - URL: server.URL, - RetryMax: 1, - RetryWaitMin: 1 * time.Millisecond, - RetryWaitMax: 2 * time.Millisecond, - } - - ctx := context.Background() - client, closer, err := New(ctx, config) - require.NoError(t, err) - defer closer() - - subscription := &model.Subscription{ - Subscriber: model.Subscriber{ - SubscriberID: "123", - }, - KeyID: "test-key", - SigningPublicKey: "test-signing-key", - EncrPublicKey: "test-encryption-key", - ValidFrom: time.Now(), - ValidUntil: time.Now().Add(24 * time.Hour), - Status: "SUBSCRIBED", - } - - result, err := client.Lookup(ctx, subscription) - require.NoError(t, err) - require.NotEmpty(t, result) - assert.Len(t, result, 2) - assert.Equal(t, expectedResponse[0].Subscriber.SubscriberID, result[0].Subscriber.SubscriberID) - assert.Equal(t, expectedResponse[1].Subscriber.SubscriberID, result[1].Subscriber.SubscriberID) -} - -// TestLookupFailure tests failure scenarios for the Lookup function. -func TestLookupFailure(t *testing.T) { - tests := []struct { - name string - responseBody interface{} - responseCode int - setupServer func() *httptest.Server - expectError bool - errorContains string - }{ - { - name: "Non-200 status code", - responseBody: "Internal Server Error", - responseCode: http.StatusInternalServerError, - expectError: true, - errorContains: "lookup request failed with status", - }, - { - name: "Invalid JSON response", - responseBody: "Invalid JSON", - responseCode: http.StatusOK, - expectError: true, - errorContains: "failed to unmarshal response body", - }, - { - name: "Connection error", - setupServer: func() *httptest.Server { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) - server.Close() // Close immediately to simulate connection error - return server - }, - expectError: true, - errorContains: "failed to send lookup request with retry", - }, - { - name: "Empty response body with 200 status", - responseBody: "", - responseCode: http.StatusOK, - expectError: true, - errorContains: "failed to unmarshal response body", - }, - { - name: "Valid empty array response", - responseBody: []model.Subscription{}, - responseCode: http.StatusOK, - expectError: false, - }, - } - - for _, tc := range tests { + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - var server *httptest.Server - - if tc.setupServer != nil { - server = tc.setupServer() + err := validate(tc.config) + if tc.expectedErr != "" { + if err == nil { + t.Fatalf("expected an error but got none") + } + if err.Error() != tc.expectedErr { + t.Errorf("expected error message '%s', but got '%s'", tc.expectedErr, err.Error()) + } } else { - server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if tc.responseCode != 0 { - w.WriteHeader(tc.responseCode) - } - if tc.responseBody != nil { - if str, ok := tc.responseBody.(string); ok { - if _, err := w.Write([]byte(str)); err != nil { - t.Errorf("failed to write response: %v", err) - } - } else { - bodyBytes, _ := json.Marshal(tc.responseBody) - if _, err := w.Write(bodyBytes); err != nil { - t.Errorf("failed to write response: %v", err) - } - } - } - })) - defer server.Close() + if err != nil { + t.Fatalf("expected no error, but got: %v", err) + } } + }) + } +} - config := &Config{ - URL: server.URL, - RetryMax: 0, - RetryWaitMin: 1 * time.Millisecond, - RetryWaitMax: 2 * time.Millisecond, - } +// TestNew tests the constructor for the RegistryClient. +func TestNew(t *testing.T) { + t.Parallel() - ctx := context.Background() - client, closer, err := New(ctx, config) - require.NoError(t, err) - defer closer() + t.Run("should fail with invalid config", func(t *testing.T) { + _, _, err := New(context.Background(), &Config{URL: ""}) + if err == nil { + t.Fatal("expected an error for invalid config but got none") + } + }) - subscription := &model.Subscription{ - Subscriber: model.Subscriber{}, + t.Run("should succeed with valid config and set defaults", func(t *testing.T) { + cfg := &Config{URL: "http://test.com"} + client, closer, err := New(context.Background(), cfg) + if err != nil { + t.Fatalf("expected no error, but got: %v", err) + } + if client == nil { + t.Fatal("expected client to be non-nil") + } + if closer == nil { + t.Fatal("expected closer to be non-nil") + } + // Check if default retry settings are applied (go-retryablehttp defaults) + if client.client.RetryMax != 4 { + t.Errorf("expected default RetryMax of 4, but got %d", client.client.RetryMax) + } + }) + + t.Run("should apply custom retry settings", func(t *testing.T) { + cfg := &Config{ + URL: "http://test.com", + RetryMax: 10, + RetryWaitMin: 100 * time.Millisecond, + RetryWaitMax: 1 * time.Second, + } + client, _, err := New(context.Background(), cfg) + if err != nil { + t.Fatalf("expected no error, but got: %v", err) + } + + if client.client.RetryMax != cfg.RetryMax { + t.Errorf("expected RetryMax to be %d, but got %d", cfg.RetryMax, client.client.RetryMax) + } + if client.client.RetryWaitMin != cfg.RetryWaitMin { + t.Errorf("expected RetryWaitMin to be %v, but got %v", cfg.RetryWaitMin, client.client.RetryWaitMin) + } + if client.client.RetryWaitMax != cfg.RetryWaitMax { + t.Errorf("expected RetryWaitMax to be %v, but got %v", cfg.RetryWaitMax, client.client.RetryWaitMax) + } + }) +} + +// TestRegistryClient_Lookup tests the Lookup method. +func TestRegistryClient_Lookup(t *testing.T) { + t.Parallel() + + t.Run("should succeed and unmarshal response", func(t *testing.T) { + expectedSubs := []model.Subscription{ + { KeyID: "test-key", SigningPublicKey: "test-signing-key", EncrPublicKey: "test-encryption-key", ValidFrom: time.Now(), ValidUntil: time.Now().Add(24 * time.Hour), Status: "SUBSCRIBED", - } - - result, err := client.Lookup(ctx, subscription) - if tc.expectError { - require.Error(t, err) - if tc.errorContains != "" { - assert.Contains(t, err.Error(), tc.errorContains) - } - assert.Empty(t, result) - } else { - require.NoError(t, err) - assert.NotNil(t, result) - } - }) - } -} - -// TestContextCancellation tests that operations respect context cancellation -func TestContextCancellation(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Simulate a slow server - time.Sleep(100 * time.Millisecond) - w.WriteHeader(http.StatusOK) - w.Write([]byte("{}")) - })) - defer server.Close() - - config := &Config{ - URL: server.URL, - RetryMax: 0, - RetryWaitMin: 1 * time.Millisecond, - RetryWaitMax: 2 * time.Millisecond, - } - - ctx := context.Background() - client, closer, err := New(ctx, config) - require.NoError(t, err) - defer closer() - - subscription := &model.Subscription{ - KeyID: "test-key", - SigningPublicKey: "test-signing-key", - EncrPublicKey: "test-encryption-key", - ValidFrom: time.Now(), - ValidUntil: time.Now().Add(24 * time.Hour), - Status: "SUBSCRIBED", - } - - t.Run("Subscribe with cancelled context", func(t *testing.T) { - cancelledCtx, cancel := context.WithCancel(context.Background()) - cancel() // Cancel immediately - - err := client.Subscribe(cancelledCtx, subscription) - require.Error(t, err) - assert.Contains(t, err.Error(), "context canceled") - }) - - t.Run("Lookup with cancelled context", func(t *testing.T) { - cancelledCtx, cancel := context.WithCancel(context.Background()) - cancel() // Cancel immediately - - result, err := client.Lookup(cancelledCtx, subscription) - require.Error(t, err) - assert.Contains(t, err.Error(), "context canceled") - assert.Empty(t, result) - }) -} - -// TestRetryConfiguration tests that retry configuration is properly applied -func TestRetryConfiguration(t *testing.T) { - attempts := 0 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - attempts++ - if attempts < 3 { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("Server Error")) - } else { - w.WriteHeader(http.StatusOK) - w.Write([]byte("{}")) + }, + { + KeyID: "test-key-2", + SigningPublicKey: "test-signing-key-2", + EncrPublicKey: "test-encryption-key-2", + ValidFrom: time.Now(), + ValidUntil: time.Now().Add(48 * time.Hour), + Status: "SUBSCRIBED", + }, } - })) - defer server.Close() - config := &Config{ - URL: server.URL, - RetryMax: 3, - RetryWaitMin: 1 * time.Millisecond, - RetryWaitMax: 2 * time.Millisecond, - } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/lookup" { + t.Errorf("expected path '/lookup', got '%s'", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(expectedSubs); err != nil { + t.Fatalf("failed to write response: %v", err) + } + })) + defer server.Close() - ctx := context.Background() - client, closer, err := New(ctx, config) - require.NoError(t, err) - defer closer() + client, closer, err := New(context.Background(), &Config{URL: server.URL}) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer closer() - subscription := &model.Subscription{ - KeyID: "test-key", - SigningPublicKey: "test-signing-key", - EncrPublicKey: "test-encryption-key", - ValidFrom: time.Now(), - ValidUntil: time.Now().Add(24 * time.Hour), - Status: "SUBSCRIBED", - } + results, err := client.Lookup(context.Background(), &model.Subscription{}) + if err != nil { + t.Fatalf("lookup failed: %v", err) + } - // This should succeed after retries - err = client.Subscribe(context.Background(), subscription) - require.NoError(t, err) - assert.GreaterOrEqual(t, attempts, 3) + if len(results) != len(expectedSubs) { + t.Fatalf("expected %d results, but got %d", len(expectedSubs), len(results)) + } + + if results[0].SubscriberID != expectedSubs[0].SubscriberID { + t.Errorf("expected subscriber ID '%s', got '%s'", expectedSubs[0].SubscriberID, results[0].SubscriberID) + } + }) + + t.Run("should fail on non-200 status", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + })) + defer server.Close() + + client, closer, err := New(context.Background(), &Config{URL: server.URL}) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer closer() + + _, err = client.Lookup(context.Background(), &model.Subscription{}) + if err == nil { + t.Fatal("expected an error but got none") + } + }) + + t.Run("should fail on bad JSON response", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `[{"subscriber_id": "bad-json"`) // Malformed JSON + })) + defer server.Close() + + client, closer, err := New(context.Background(), &Config{URL: server.URL, RetryMax: 1}) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer closer() + + _, err = client.Lookup(context.Background(), &model.Subscription{}) + if err == nil { + t.Fatal("expected an unmarshaling error but got none") + } + }) }