diff --git a/lib/common_test.go b/lib/common_test.go new file mode 100644 index 00000000000..ca803c0493b --- /dev/null +++ b/lib/common_test.go @@ -0,0 +1,293 @@ +package lib + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestGetRemoteURLContent(t *testing.T) { + // Create a test server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("test content")) + })) + defer server.Close() + + // Test successful request + content, err := GetRemoteURLContent(server.URL) + if err != nil { + t.Errorf("GetRemoteURLContent() should not return error: %v", err) + } + if string(content) != "test content" { + t.Errorf("GetRemoteURLContent() content = %s; want 'test content'", string(content)) + } +} + +func TestGetRemoteURLContentNotFound(t *testing.T) { + // Create a test server that returns 404 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("Not Found")) + })) + defer server.Close() + + // Test 404 response + _, err := GetRemoteURLContent(server.URL) + if err == nil { + t.Error("GetRemoteURLContent() should return error for 404 response") + } + if !strings.Contains(err.Error(), "404") { + t.Errorf("Error should mention 404 status, got: %v", err) + } +} + +func TestGetRemoteURLContentInvalidURL(t *testing.T) { + // Test with invalid URL + _, err := GetRemoteURLContent("invalid-url") + if err == nil { + t.Error("GetRemoteURLContent() should return error for invalid URL") + } +} + +func TestGetRemoteURLReader(t *testing.T) { + // Create a test server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("test reader content")) + })) + defer server.Close() + + // Test successful request + reader, err := GetRemoteURLReader(server.URL) + if err != nil { + t.Errorf("GetRemoteURLReader() should not return error: %v", err) + } + defer reader.Close() + + if reader == nil { + t.Error("GetRemoteURLReader() should return non-nil reader") + } + + // Read content from reader + buffer := make([]byte, 100) + n, err := reader.Read(buffer) + if err != nil && err.Error() != "EOF" { + t.Errorf("Reading from reader should not return error: %v", err) + } + content := string(buffer[:n]) + if content != "test reader content" { + t.Errorf("Reader content = %s; want 'test reader content'", content) + } +} + +func TestGetRemoteURLReaderNotFound(t *testing.T) { + // Create a test server that returns 404 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + // Test 404 response + _, err := GetRemoteURLReader(server.URL) + if err == nil { + t.Error("GetRemoteURLReader() should return error for 404 response") + } +} + +func TestGetRemoteURLReaderInvalidURL(t *testing.T) { + // Test with invalid URL + _, err := GetRemoteURLReader("invalid-url") + if err == nil { + t.Error("GetRemoteURLReader() should return error for invalid URL") + } +} + +func TestWantedListExtended_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + input string + expectSlice []string + expectMap map[string][]string + expectError bool + sliceSet bool + mapSet bool + }{ + { + name: "Array input", + input: `["item1", "item2", "item3"]`, + expectSlice: []string{"item1", "item2", "item3"}, + expectMap: map[string][]string{}, + expectError: false, + sliceSet: true, + mapSet: false, + }, + { + name: "Object input", + input: `{"key1": ["value1", "value2"], "key2": ["value3"]}`, + expectSlice: []string{}, + expectMap: map[string][]string{"key1": {"value1", "value2"}, "key2": {"value3"}}, + expectError: false, + sliceSet: false, + mapSet: true, + }, + { + name: "Empty array", + input: `[]`, + expectSlice: []string{}, + expectMap: map[string][]string{}, + expectError: false, + sliceSet: true, + mapSet: false, + }, + { + name: "Empty object", + input: `{}`, + expectSlice: []string{}, + expectMap: map[string][]string{}, + expectError: false, + sliceSet: false, + mapSet: true, + }, + { + name: "Empty string input", + input: `""`, + expectSlice: []string{}, + expectMap: map[string][]string{}, + expectError: true, + sliceSet: false, + mapSet: false, + }, + { + name: "Invalid JSON", + input: `{invalid json}`, + expectSlice: nil, + expectMap: nil, + expectError: true, + sliceSet: false, + mapSet: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var wl WantedListExtended + err := json.Unmarshal([]byte(tt.input), &wl) + + if tt.expectError && err == nil { + t.Errorf("UnmarshalJSON() should return error but got nil") + } + if !tt.expectError && err != nil { + t.Errorf("UnmarshalJSON() should not return error but got: %v", err) + } + + if !tt.expectError { + // For slice + if tt.sliceSet { + if len(wl.TypeSlice) != len(tt.expectSlice) { + t.Errorf("TypeSlice length = %d; want %d", len(wl.TypeSlice), len(tt.expectSlice)) + } + for i, expected := range tt.expectSlice { + if i < len(wl.TypeSlice) && wl.TypeSlice[i] != expected { + t.Errorf("TypeSlice[%d] = %s; want %s", i, wl.TypeSlice[i], expected) + } + } + } + + // For map + if tt.mapSet { + if len(wl.TypeMap) != len(tt.expectMap) { + t.Errorf("TypeMap length = %d; want %d", len(wl.TypeMap), len(tt.expectMap)) + } + for key, expectedValues := range tt.expectMap { + actualValues, exists := wl.TypeMap[key] + if !exists { + t.Errorf("TypeMap should contain key %s", key) + continue + } + if len(actualValues) != len(expectedValues) { + t.Errorf("TypeMap[%s] length = %d; want %d", key, len(actualValues), len(expectedValues)) + continue + } + for i, expectedValue := range expectedValues { + if i < len(actualValues) && actualValues[i] != expectedValue { + t.Errorf("TypeMap[%s][%d] = %s; want %s", key, i, actualValues[i], expectedValue) + } + } + } + } + } + }) + } +} + +func TestWantedListExtended_UnmarshalJSON_EmptyData(t *testing.T) { + var wl WantedListExtended + err := wl.UnmarshalJSON([]byte{}) + if err != nil { + t.Errorf("UnmarshalJSON() with empty data should not return error: %v", err) + } + // For empty data, the function returns early and doesn't set anything + if wl.TypeSlice != nil { + t.Error("TypeSlice should be nil for empty data") + } + if wl.TypeMap != nil { + t.Error("TypeMap should be nil for empty data") + } +} + +func TestWantedListExtended_UnmarshalJSON_StringInput(t *testing.T) { + // Test with string input (should fail to parse as both array and object) + var wl WantedListExtended + err := wl.UnmarshalJSON([]byte(`"string value"`)) + if err == nil { + t.Error("UnmarshalJSON() with string input should return error") + } +} + +func TestRemoteContentHTTPMethods(t *testing.T) { + // Test that both functions use GET method + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("Expected GET method, got %s", r.Method) + } + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + })) + defer server.Close() + + // Test GetRemoteURLContent uses GET + _, err := GetRemoteURLContent(server.URL) + if err != nil { + t.Errorf("GetRemoteURLContent() failed: %v", err) + } + + // Test GetRemoteURLReader uses GET + reader, err := GetRemoteURLReader(server.URL) + if err != nil { + t.Errorf("GetRemoteURLReader() failed: %v", err) + } + if reader != nil { + reader.Close() + } +} + +func TestRemoteContentHeaders(t *testing.T) { + // Test server that checks headers + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check that User-Agent is set (Go's http client sets it by default) + if r.UserAgent() == "" { + t.Error("User-Agent header should be set") + } + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) + })) + defer server.Close() + + _, err := GetRemoteURLContent(server.URL) + if err != nil { + t.Errorf("GetRemoteURLContent() failed: %v", err) + } +} \ No newline at end of file diff --git a/lib/config_test.go b/lib/config_test.go new file mode 100644 index 00000000000..de113e1d17c --- /dev/null +++ b/lib/config_test.go @@ -0,0 +1,351 @@ +package lib + +import ( + "encoding/json" + "errors" + "strings" + "testing" +) + +// Mock implementations for testing +type mockInputConfigCreator struct { + shouldError bool + converter InputConverter +} + +func (m *mockInputConfigCreator) create(action Action, data json.RawMessage) (InputConverter, error) { + if m.shouldError { + return nil, errors.New("mock error") + } + return m.converter, nil +} + +type mockOutputConfigCreator struct { + shouldError bool + converter OutputConverter +} + +func (m *mockOutputConfigCreator) create(action Action, data json.RawMessage) (OutputConverter, error) { + if m.shouldError { + return nil, errors.New("mock error") + } + return m.converter, nil +} + +func TestRegisterInputConfigCreator(t *testing.T) { + // Test successful registration + err := RegisterInputConfigCreator("test-input-creator", func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "test"}, + mockActioner: &mockActioner{actionValue: action}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + }, nil + }) + if err != nil { + t.Errorf("RegisterInputConfigCreator() should not return error: %v", err) + } + + // Test duplicate registration + err = RegisterInputConfigCreator("test-input-creator", func(action Action, data json.RawMessage) (InputConverter, error) { + return nil, nil + }) + if err == nil { + t.Error("RegisterInputConfigCreator() should return error for duplicate registration") + } + if !strings.Contains(err.Error(), "already been registered") { + t.Errorf("Error should mention already registered, got: %v", err) + } + + // Test case insensitive registration + err = RegisterInputConfigCreator("TEST-INPUT-CREATOR", func(action Action, data json.RawMessage) (InputConverter, error) { + return nil, nil + }) + if err == nil { + t.Error("RegisterInputConfigCreator() should return error for case-insensitive duplicate") + } +} + +func TestRegisterOutputConfigCreator(t *testing.T) { + // Test successful registration + err := RegisterOutputConfigCreator("test-output-creator", func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "test"}, + mockActioner: &mockActioner{actionValue: action}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + }, nil + }) + if err != nil { + t.Errorf("RegisterOutputConfigCreator() should not return error: %v", err) + } + + // Test duplicate registration + err = RegisterOutputConfigCreator("test-output-creator", func(action Action, data json.RawMessage) (OutputConverter, error) { + return nil, nil + }) + if err == nil { + t.Error("RegisterOutputConfigCreator() should return error for duplicate registration") + } +} + +func TestCreateInputConfig(t *testing.T) { + // Register a test creator first + RegisterInputConfigCreator("test-create-input", func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "test-create-input"}, + mockActioner: &mockActioner{actionValue: action}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + }, nil + }) + + // Test successful creation + converter, err := createInputConfig("test-create-input", ActionAdd, json.RawMessage(`{}`)) + if err != nil { + t.Errorf("createInputConfig() should not return error: %v", err) + } + if converter == nil { + t.Error("createInputConfig() should return non-nil converter") + } + if converter.GetType() != "test-create-input" { + t.Errorf("Converter type = %s; want test-create-input", converter.GetType()) + } + if converter.GetAction() != ActionAdd { + t.Errorf("Converter action = %s; want %s", converter.GetAction(), ActionAdd) + } + + // Test unknown config type + _, err = createInputConfig("unknown-type", ActionAdd, json.RawMessage(`{}`)) + if err == nil { + t.Error("createInputConfig() should return error for unknown type") + } + if !strings.Contains(err.Error(), "unknown config type") { + t.Errorf("Error should mention unknown config type, got: %v", err) + } + + // Test case insensitive lookup + converter, err = createInputConfig("TEST-CREATE-INPUT", ActionRemove, json.RawMessage(`{}`)) + if err != nil { + t.Errorf("createInputConfig() should work case insensitively: %v", err) + } + if converter.GetAction() != ActionRemove { + t.Errorf("Converter action = %s; want %s", converter.GetAction(), ActionRemove) + } +} + +func TestCreateOutputConfig(t *testing.T) { + // Register a test creator first + RegisterOutputConfigCreator("test-create-output", func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "test-create-output"}, + mockActioner: &mockActioner{actionValue: action}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + }, nil + }) + + // Test successful creation + converter, err := createOutputConfig("test-create-output", ActionOutput, json.RawMessage(`{}`)) + if err != nil { + t.Errorf("createOutputConfig() should not return error: %v", err) + } + if converter == nil { + t.Error("createOutputConfig() should return non-nil converter") + } + if converter.GetType() != "test-create-output" { + t.Errorf("Converter type = %s; want test-create-output", converter.GetType()) + } + + // Test unknown config type + _, err = createOutputConfig("unknown-type", ActionOutput, json.RawMessage(`{}`)) + if err == nil { + t.Error("createOutputConfig() should return error for unknown type") + } +} + +func TestInputConvConfig_UnmarshalJSON(t *testing.T) { + // Register a test creator + RegisterInputConfigCreator("test-unmarshal-input", func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "test-unmarshal-input"}, + mockActioner: &mockActioner{actionValue: action}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + }, nil + }) + + tests := []struct { + name string + input string + expectError bool + expectType string + expectAction Action + }{ + { + name: "Valid config", + input: `{"type": "test-unmarshal-input", "action": "add", "args": {}}`, + expectError: false, + expectType: "test-unmarshal-input", + expectAction: ActionAdd, + }, + { + name: "Invalid action", + input: `{"type": "test-unmarshal-input", "action": "invalid", "args": {}}`, + expectError: true, + }, + { + name: "Missing type", + input: `{"action": "add", "args": {}}`, + expectError: true, + }, + { + name: "Unknown type", + input: `{"type": "unknown-type", "action": "add", "args": {}}`, + expectError: true, + }, + { + name: "Invalid JSON", + input: `{invalid json}`, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var config inputConvConfig + err := json.Unmarshal([]byte(tt.input), &config) + + if tt.expectError && err == nil { + t.Errorf("UnmarshalJSON() should return error but got nil") + } + if !tt.expectError && err != nil { + t.Errorf("UnmarshalJSON() should not return error but got: %v", err) + } + + if !tt.expectError { + if config.iType != tt.expectType { + t.Errorf("iType = %s; want %s", config.iType, tt.expectType) + } + if config.action != tt.expectAction { + t.Errorf("action = %s; want %s", config.action, tt.expectAction) + } + if config.converter == nil { + t.Error("converter should not be nil") + } + } + }) + } +} + +func TestOutputConvConfig_UnmarshalJSON(t *testing.T) { + // Register a test creator + RegisterOutputConfigCreator("test-unmarshal-output", func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "test-unmarshal-output"}, + mockActioner: &mockActioner{actionValue: action}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + }, nil + }) + + tests := []struct { + name string + input string + expectError bool + expectType string + expectAction Action + }{ + { + name: "Valid config", + input: `{"type": "test-unmarshal-output", "action": "output", "args": {}}`, + expectError: false, + expectType: "test-unmarshal-output", + expectAction: ActionOutput, + }, + { + name: "Missing action defaults to output", + input: `{"type": "test-unmarshal-output", "args": {}}`, + expectError: false, + expectType: "test-unmarshal-output", + expectAction: ActionOutput, + }, + { + name: "Invalid action", + input: `{"type": "test-unmarshal-output", "action": "invalid", "args": {}}`, + expectError: true, + }, + { + name: "Unknown type", + input: `{"type": "unknown-type", "action": "output", "args": {}}`, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var config outputConvConfig + err := json.Unmarshal([]byte(tt.input), &config) + + if tt.expectError && err == nil { + t.Errorf("UnmarshalJSON() should return error but got nil") + } + if !tt.expectError && err != nil { + t.Errorf("UnmarshalJSON() should not return error but got: %v", err) + } + + if !tt.expectError { + if config.iType != tt.expectType { + t.Errorf("iType = %s; want %s", config.iType, tt.expectType) + } + if config.action != tt.expectAction { + t.Errorf("action = %s; want %s", config.action, tt.expectAction) + } + if config.converter == nil { + t.Error("converter should not be nil") + } + } + }) + } +} + +func TestConfigStruct_UnmarshalJSON(t *testing.T) { + // Register test creators + RegisterInputConfigCreator("test-config-input", func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "test-config-input"}, + mockActioner: &mockActioner{actionValue: action}, + mockDescriptioner: &mockDescriptioner{descValue: "test input"}, + }, nil + }) + RegisterOutputConfigCreator("test-config-output", func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "test-config-output"}, + mockActioner: &mockActioner{actionValue: action}, + mockDescriptioner: &mockDescriptioner{descValue: "test output"}, + }, nil + }) + + configJSON := `{ + "input": [ + {"type": "test-config-input", "action": "add", "args": {}} + ], + "output": [ + {"type": "test-config-output", "action": "output", "args": {}} + ] + }` + + var cfg config + err := json.Unmarshal([]byte(configJSON), &cfg) + if err != nil { + t.Errorf("config UnmarshalJSON() should not return error: %v", err) + } + + if len(cfg.Input) != 1 { + t.Errorf("config should have 1 input, got %d", len(cfg.Input)) + } + if len(cfg.Output) != 1 { + t.Errorf("config should have 1 output, got %d", len(cfg.Output)) + } + + if cfg.Input[0].iType != "test-config-input" { + t.Errorf("input type = %s; want test-config-input", cfg.Input[0].iType) + } + if cfg.Output[0].iType != "test-config-output" { + t.Errorf("output type = %s; want test-config-output", cfg.Output[0].iType) + } +} \ No newline at end of file diff --git a/lib/container_test.go b/lib/container_test.go new file mode 100644 index 00000000000..e9587791c74 --- /dev/null +++ b/lib/container_test.go @@ -0,0 +1,596 @@ +package lib + +import ( + "testing" +) + +func TestNewContainer(t *testing.T) { + container := NewContainer() + if container == nil { + t.Error("NewContainer() should return non-nil container") + } + + if container.Len() != 0 { + t.Errorf("New container should have length 0, got %d", container.Len()) + } +} + +func TestContainerAddAndGet(t *testing.T) { + container := NewContainer() + entry := NewEntry("test") + + // Add prefix to entry + err := entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Add entry to container + err = container.Add(entry) + if err != nil { + t.Errorf("Container.Add() should not return error: %v", err) + } + + // Check container length + if container.Len() != 1 { + t.Errorf("Container length should be 1 after adding entry, got %d", container.Len()) + } + + // Get entry from container + retrievedEntry, found := container.GetEntry("test") + if !found { + t.Error("GetEntry() should find the added entry") + } + if retrievedEntry.GetName() != "TEST" { + t.Errorf("Retrieved entry name should be 'TEST', got '%s'", retrievedEntry.GetName()) + } + + // Test case insensitive retrieval + retrievedEntry, found = container.GetEntry("TEST") + if !found { + t.Error("GetEntry() should be case insensitive") + } + + retrievedEntry, found = container.GetEntry("TeSt") + if !found { + t.Error("GetEntry() should be case insensitive") + } + + // Test retrieval with extra spaces + retrievedEntry, found = container.GetEntry(" test ") + if !found { + t.Error("GetEntry() should handle names with spaces") + } +} + +func TestContainerGetEntryNotFound(t *testing.T) { + container := NewContainer() + + // Try to get non-existent entry + _, found := container.GetEntry("nonexistent") + if found { + t.Error("GetEntry() should return false for non-existent entry") + } + + // Try with empty name + _, found = container.GetEntry("") + if found { + t.Error("GetEntry() should return false for empty name") + } +} + +func TestContainerAddMultipleEntries(t *testing.T) { + container := NewContainer() + + // Create and add multiple entries + entries := []string{"entry1", "entry2", "entry3"} + for _, name := range entries { + entry := NewEntry(name) + err := entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed for %s: %v", name, err) + } + + err = container.Add(entry) + if err != nil { + t.Errorf("Container.Add() failed for %s: %v", name, err) + } + } + + // Check container length + if container.Len() != len(entries) { + t.Errorf("Container length should be %d, got %d", len(entries), container.Len()) + } + + // Verify all entries can be retrieved + for _, name := range entries { + _, found := container.GetEntry(name) + if !found { + t.Errorf("Entry %s should be found in container", name) + } + } +} + +func TestContainerAddDuplicateEntry(t *testing.T) { + container := NewContainer() + entry1 := NewEntry("test") + entry2 := NewEntry("test") + + // Add prefixes to entries + err := entry1.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + err = entry2.AddPrefix("10.0.0.0/8") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Add first entry + err = container.Add(entry1) + if err != nil { + t.Errorf("First Add() should not return error: %v", err) + } + + // Add second entry with same name (should merge) + err = container.Add(entry2) + if err != nil { + t.Errorf("Second Add() should not return error: %v", err) + } + + // Container should still have length 1 + if container.Len() != 1 { + t.Errorf("Container should still have length 1 after adding duplicate, got %d", container.Len()) + } +} + +func TestContainerAddWithIgnoreOptions(t *testing.T) { + container := NewContainer() + entry := NewEntry("test") + + // Add both IPv4 and IPv6 prefixes + err := entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix IPv4 failed: %v", err) + } + err = entry.AddPrefix("2001:db8::/32") + if err != nil { + t.Fatalf("AddPrefix IPv6 failed: %v", err) + } + + // Add with IgnoreIPv4 option + err = container.Add(entry, IgnoreIPv4) + if err != nil { + t.Errorf("Add() with IgnoreIPv4 should not return error: %v", err) + } + + // Create another entry with IgnoreIPv6 option + entry2 := NewEntry("test2") + err = entry2.AddPrefix("10.0.0.0/8") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + err = container.Add(entry2, IgnoreIPv6) + if err != nil { + t.Errorf("Add() with IgnoreIPv6 should not return error: %v", err) + } + + if container.Len() != 2 { + t.Errorf("Container should have length 2, got %d", container.Len()) + } +} + +func TestContainerRemove(t *testing.T) { + container := NewContainer() + entry := NewEntry("test") + + // Add prefix to entry + err := entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Add entry to container + err = container.Add(entry) + if err != nil { + t.Fatalf("Add() failed: %v", err) + } + + // Remove entry with CaseRemovePrefix + err = container.Remove(entry, CaseRemovePrefix) + if err != nil { + t.Errorf("Remove() with CaseRemovePrefix should not return error: %v", err) + } + + // Remove entry with CaseRemoveEntry + err = container.Remove(entry, CaseRemoveEntry) + if err != nil { + t.Errorf("Remove() with CaseRemoveEntry should not return error: %v", err) + } +} + +func TestContainerRemoveWithIgnoreOptions(t *testing.T) { + container := NewContainer() + entry := NewEntry("test") + + // Add both IPv4 and IPv6 prefixes + err := entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix IPv4 failed: %v", err) + } + err = entry.AddPrefix("2001:db8::/32") + if err != nil { + t.Fatalf("AddPrefix IPv6 failed: %v", err) + } + + // Add entry to container + err = container.Add(entry) + if err != nil { + t.Fatalf("Add() failed: %v", err) + } + + // Remove with ignore options + err = container.Remove(entry, CaseRemovePrefix, IgnoreIPv4) + if err != nil { + t.Errorf("Remove() with IgnoreIPv4 should not return error: %v", err) + } + + err = container.Remove(entry, CaseRemovePrefix, IgnoreIPv6) + if err != nil { + t.Errorf("Remove() with IgnoreIPv6 should not return error: %v", err) + } +} + +func TestContainerLoop(t *testing.T) { + container := NewContainer() + entryNames := []string{"entry1", "entry2", "entry3"} + + // Add multiple entries + for _, name := range entryNames { + entry := NewEntry(name) + err := entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed for %s: %v", name, err) + } + + err = container.Add(entry) + if err != nil { + t.Fatalf("Add() failed for %s: %v", name, err) + } + } + + // Loop through entries + count := 0 + foundNames := make(map[string]bool) + + for entry := range container.Loop() { + count++ + foundNames[entry.GetName()] = true + + if entry == nil { + t.Error("Loop() should not return nil entry") + } + } + + if count != len(entryNames) { + t.Errorf("Loop() should iterate %d times, got %d", len(entryNames), count) + } + + // Check that all expected names were found + for _, name := range entryNames { + expectedName := name // Will be converted to uppercase by NewEntry + if name == "entry1" { + expectedName = "ENTRY1" + } else if name == "entry2" { + expectedName = "ENTRY2" + } else if name == "entry3" { + expectedName = "ENTRY3" + } + + if !foundNames[expectedName] { + t.Errorf("Entry %s should be found in loop", expectedName) + } + } +} + +func TestContainerLoopEmpty(t *testing.T) { + container := NewContainer() + + // Loop through empty container + count := 0 + for range container.Loop() { + count++ + } + + if count != 0 { + t.Errorf("Loop() on empty container should iterate 0 times, got %d", count) + } +} + +func TestContainerLookup(t *testing.T) { + container := NewContainer() + + // Add basic test entry + entry := NewEntry("test") + err := entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + err = container.Add(entry) + if err != nil { + t.Fatalf("Add() failed: %v", err) + } + + // Add IPv4 entry for advanced tests (using different IP range to avoid conflicts) + entry4 := NewEntry("ipv4-entry") + err = entry4.AddPrefix("192.168.2.0/24") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + err = container.Add(entry4) + if err != nil { + t.Fatalf("Add() failed: %v", err) + } + + // Add IPv6 entry for advanced tests + entry6 := NewEntry("ipv6-entry") + err = entry6.AddPrefix("2001:db8::/32") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + err = container.Add(entry6) + if err != nil { + t.Fatalf("Add() failed: %v", err) + } + + tests := []struct { + name string + ipOrCidr string + searchList []string + expectFound bool + expectResult []string + expectError bool + }{ + // Basic lookup tests + { + name: "IP in range", + ipOrCidr: "192.168.1.100", + searchList: []string{"test"}, + expectFound: true, + expectResult: []string{"TEST"}, + expectError: false, + }, + { + name: "IP not in range", + ipOrCidr: "10.0.0.1", + searchList: []string{"test"}, + expectFound: false, + expectResult: []string{}, + expectError: false, + }, + { + name: "Invalid IP", + ipOrCidr: "invalid-ip", + searchList: []string{"test"}, + expectFound: false, + expectResult: nil, + expectError: true, + }, + { + name: "Empty search list", + ipOrCidr: "192.168.1.100", + searchList: []string{}, + expectFound: false, + expectResult: []string{}, + expectError: true, + }, + // Advanced lookup tests + { + name: "IPv4 CIDR lookup", + ipOrCidr: "192.168.2.0/25", + searchList: []string{"ipv4-entry"}, + expectFound: true, + expectResult: []string{"IPV4-ENTRY"}, + expectError: false, + }, + { + name: "IPv6 IP lookup", + ipOrCidr: "2001:db8::1", + searchList: []string{"ipv6-entry"}, + expectFound: true, + expectResult: []string{"IPV6-ENTRY"}, + expectError: false, + }, + { + name: "IPv6 CIDR lookup", + ipOrCidr: "2001:db8::/64", + searchList: []string{"ipv6-entry"}, + expectFound: true, + expectResult: []string{"IPV6-ENTRY"}, + expectError: false, + }, + { + name: "Invalid CIDR", + ipOrCidr: "192.168.1.0/99", + searchList: []string{"ipv4-entry"}, + expectFound: false, + expectResult: nil, + expectError: true, + }, + { + name: "Search specific entry only", + ipOrCidr: "192.168.2.100", + searchList: []string{"ipv4-entry"}, + expectFound: true, + expectResult: []string{"IPV4-ENTRY"}, + expectError: false, + }, + { + name: "Search with non-existent entry", + ipOrCidr: "192.168.1.100", + searchList: []string{"non-existent"}, + expectFound: false, + expectResult: []string{}, + expectError: false, + }, + { + name: "Search with empty and whitespace strings", + ipOrCidr: "192.168.2.100", + searchList: []string{"", " ", "ipv4-entry", " "}, + expectFound: true, + expectResult: []string{"IPV4-ENTRY"}, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, found, err := container.Lookup(tt.ipOrCidr, tt.searchList...) + + if tt.expectError && err == nil { + t.Errorf("Lookup() should return error but got nil") + } + if !tt.expectError && err != nil { + t.Errorf("Lookup() should not return error but got: %v", err) + } + if !tt.expectError && found != tt.expectFound { + t.Errorf("Lookup() found = %v; want %v", found, tt.expectFound) + } + if !tt.expectError && tt.expectResult != nil && len(result) != len(tt.expectResult) { + t.Errorf("Lookup() result length = %d; want %d", len(result), len(tt.expectResult)) + } + }) + } +} + + + +// TestContainerAddAdvanced tests complex Add scenarios +func TestContainerAddAdvanced(t *testing.T) { + container := NewContainer() + + // Test adding entry with ignore options + entry1 := NewEntry("test1") + err := entry1.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + err = entry1.AddPrefix("2001:db8::/32") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Add with IPv6 ignore + err = container.Add(entry1, IgnoreIPv6) + if err != nil { + t.Errorf("Add() with IgnoreIPv6 should not return error: %v", err) + } + + // Test merging entries with same name + entry2 := NewEntry("test1") // Same name as entry1 + err = entry2.AddPrefix("10.0.0.0/8") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // This should merge with existing entry + err = container.Add(entry2) + if err != nil { + t.Errorf("Add() merging entries should not return error: %v", err) + } + + // Verify only one entry exists + if container.Len() != 1 { + t.Errorf("Container should have 1 entry after merging, got %d", container.Len()) + } + + // Test adding entry with IPv4 ignore + entry3 := NewEntry("test2") + err = entry3.AddPrefix("172.16.0.0/12") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + err = entry3.AddPrefix("2001:db8:1::/48") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + err = container.Add(entry3, IgnoreIPv4) + if err != nil { + t.Errorf("Add() with IgnoreIPv4 should not return error: %v", err) + } + + if container.Len() != 2 { + t.Errorf("Container should have 2 entries, got %d", container.Len()) + } +} + +// TestContainerLen tests Len function edge cases +func TestContainerLen(t *testing.T) { + // Test with valid empty container + container := NewContainer() + if container.Len() != 0 { + t.Errorf("Empty container Len() should return 0, got %d", container.Len()) + } + + // Test with entries + entry := NewEntry("test") + err := entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + err = container.Add(entry) + if err != nil { + t.Fatalf("Add() failed: %v", err) + } + + if container.Len() != 1 { + t.Errorf("Container with 1 entry Len() should return 1, got %d", container.Len()) + } +} + +// TestContainerRemoveAdvanced tests Remove function with various scenarios +func TestContainerRemoveAdvanced(t *testing.T) { + container := NewContainer() + + // Add an entry with both IPv4 and IPv6 + entry := NewEntry("test") + err := entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + err = entry.AddPrefix("2001:db8::/32") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + err = container.Add(entry) + if err != nil { + t.Fatalf("Add() failed: %v", err) + } + + // Test remove with CaseRemoveEntry + entry2 := NewEntry("test") + err = container.Remove(entry2, CaseRemoveEntry) + if err != nil { + t.Errorf("Remove() with CaseRemoveEntry should not return error: %v", err) + } + + // Entry should be completely removed now + if container.Len() != 0 { + t.Errorf("Container should be empty after CaseRemoveEntry, got %d entries", container.Len()) + } + + // Test removing non-existent entry + entry3 := NewEntry("nonexistent") + err = entry3.AddPrefix("10.0.0.0/8") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + err = container.Remove(entry3, CaseRemoveEntry) + if err == nil { + t.Error("Remove() on non-existent entry should return error") + } +} \ No newline at end of file diff --git a/lib/converter_test.go b/lib/converter_test.go new file mode 100644 index 00000000000..8dcb2b2061e --- /dev/null +++ b/lib/converter_test.go @@ -0,0 +1,211 @@ +package lib + +import ( + "bytes" + "fmt" + "os" + "strings" + "testing" +) + +func TestListInputConverter(t *testing.T) { + // Capture stdout + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + ListInputConverter() + + w.Close() + os.Stdout = old + + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + if !strings.Contains(output, "All available input formats:") { + t.Error("ListInputConverter() should print header") + } +} + +func TestListOutputConverter(t *testing.T) { + // Capture stdout + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + ListOutputConverter() + + w.Close() + os.Stdout = old + + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + if !strings.Contains(output, "All available output formats:") { + t.Error("ListOutputConverter() should print header") + } +} + +func TestRegisterInputConverter(t *testing.T) { + // Create a mock input converter + mockConverter := &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "test-input"}, + mockActioner: &mockActioner{actionValue: ActionAdd}, + mockDescriptioner: &mockDescriptioner{descValue: "test input converter"}, + } + + // Test successful registration + err := RegisterInputConverter("test-input", mockConverter) + if err != nil { + t.Errorf("RegisterInputConverter() should not return error: %v", err) + } + + // Test duplicate registration + err = RegisterInputConverter("test-input", mockConverter) + if err != ErrDuplicatedConverter { + t.Errorf("RegisterInputConverter() should return ErrDuplicatedConverter for duplicate, got: %v", err) + } + + // Test registration with leading/trailing spaces + err = RegisterInputConverter(" test-input2 ", mockConverter) + if err != nil { + t.Errorf("RegisterInputConverter() should handle names with spaces: %v", err) + } +} + +func TestRegisterOutputConverter(t *testing.T) { + // Create a mock output converter + mockConverter := &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "test-output"}, + mockActioner: &mockActioner{actionValue: ActionOutput}, + mockDescriptioner: &mockDescriptioner{descValue: "test output converter"}, + } + + // Test successful registration + err := RegisterOutputConverter("test-output", mockConverter) + if err != nil { + t.Errorf("RegisterOutputConverter() should not return error: %v", err) + } + + // Test duplicate registration + err = RegisterOutputConverter("test-output", mockConverter) + if err != ErrDuplicatedConverter { + t.Errorf("RegisterOutputConverter() should return ErrDuplicatedConverter for duplicate, got: %v", err) + } + + // Test registration with leading/trailing spaces + err = RegisterOutputConverter(" test-output2 ", mockConverter) + if err != nil { + t.Errorf("RegisterOutputConverter() should handle names with spaces: %v", err) + } +} + +func TestConverterRegistryIntegration(t *testing.T) { + // Create mock converters + inputConverter := &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "integration-input"}, + mockActioner: &mockActioner{actionValue: ActionAdd}, + mockDescriptioner: &mockDescriptioner{descValue: "integration test input"}, + } + + outputConverter := &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "integration-output"}, + mockActioner: &mockActioner{actionValue: ActionOutput}, + mockDescriptioner: &mockDescriptioner{descValue: "integration test output"}, + } + + // Register converters + err := RegisterInputConverter("integration-input", inputConverter) + if err != nil { + t.Fatalf("Failed to register input converter: %v", err) + } + + err = RegisterOutputConverter("integration-output", outputConverter) + if err != nil { + t.Fatalf("Failed to register output converter: %v", err) + } + + // Test that they appear in the list + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + ListInputConverter() + ListOutputConverter() + + w.Close() + os.Stdout = old + + var buf bytes.Buffer + buf.ReadFrom(r) + output := buf.String() + + if !strings.Contains(output, "integration-input") { + t.Error("Registered input converter should appear in list") + } + if !strings.Contains(output, "integration-output") { + t.Error("Registered output converter should appear in list") + } + if !strings.Contains(output, "integration test input") { + t.Error("Input converter description should appear in list") + } + if !strings.Contains(output, "integration test output") { + t.Error("Output converter description should appear in list") + } +} + +func TestConverterNaming(t *testing.T) { + tests := []struct { + name string + inputName string + expectedName string + expectError bool + }{ + { + name: "Normal name", + inputName: "test-normal", + expectedName: "test-normal", + expectError: false, + }, + { + name: "Name with spaces", + inputName: " test-spaces ", + expectedName: "test-spaces", + expectError: false, + }, + { + name: "Empty name", + inputName: "", + expectedName: "", + expectError: false, + }, + { + name: "Name with tabs", + inputName: "\ttest-tabs\t", + expectedName: "test-tabs", + expectError: false, + }, + } + + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Use unique names to avoid conflicts + uniqueName := fmt.Sprintf("test-naming-%d", i) + mockConverter := &mockInputConverter{ + mockTyper: &mockTyper{typeValue: uniqueName}, + mockActioner: &mockActioner{actionValue: ActionAdd}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + } + + // The converter name should be trimmed when registered + err := RegisterInputConverter(tt.inputName+fmt.Sprintf("-%d", i), mockConverter) + if tt.expectError && err == nil { + t.Errorf("RegisterInputConverter() should return error but got nil") + } else if !tt.expectError && err != nil { + t.Errorf("RegisterInputConverter() should not return error for name '%s': %v", tt.inputName, err) + } + }) + } +} \ No newline at end of file diff --git a/lib/entry_test.go b/lib/entry_test.go new file mode 100644 index 00000000000..7fd1244ff10 --- /dev/null +++ b/lib/entry_test.go @@ -0,0 +1,979 @@ +package lib + +import ( + "net" + "net/netip" + "testing" +) + +func TestNewEntry(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "Normal name", + input: "test", + expected: "TEST", + }, + { + name: "Lowercase name", + input: "lowercase", + expected: "LOWERCASE", + }, + { + name: "Mixed case name", + input: "MiXeD", + expected: "MIXED", + }, + { + name: "Name with spaces", + input: " test name ", + expected: "TEST NAME", + }, + { + name: "Empty name", + input: "", + expected: "", + }, + { + name: "Name with special characters", + input: "test-name_123", + expected: "TEST-NAME_123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + entry := NewEntry(tt.input) + if entry.GetName() != tt.expected { + t.Errorf("NewEntry(%s).GetName() = %s; want %s", tt.input, entry.GetName(), tt.expected) + } + }) + } +} + +func TestEntryGetName(t *testing.T) { + entry := NewEntry("test") + if entry.GetName() != "TEST" { + t.Errorf("GetName() = %s; want TEST", entry.GetName()) + } +} + +func TestEntryBuilders(t *testing.T) { + entry := NewEntry("test") + + // Test initial state - no builders should exist + if entry.hasIPv4Builder() { + t.Error("New entry should not have IPv4 builder initially") + } + if entry.hasIPv6Builder() { + t.Error("New entry should not have IPv6 builder initially") + } + if entry.hasIPv4Set() { + t.Error("New entry should not have IPv4 set initially") + } + if entry.hasIPv6Set() { + t.Error("New entry should not have IPv6 set initially") + } +} + +func TestEntryAddPrefix(t *testing.T) { + tests := []struct { + name string + prefix string + wantErr bool + }{ + { + name: "Valid IPv4 CIDR", + prefix: "192.168.1.0/24", + wantErr: false, + }, + { + name: "Valid IPv6 CIDR", + prefix: "2001:db8::/32", + wantErr: false, + }, + { + name: "Valid single IPv4", + prefix: "192.168.1.1", + wantErr: false, + }, + { + name: "Valid single IPv6", + prefix: "2001:db8::1", + wantErr: false, + }, + { + name: "Invalid CIDR", + prefix: "invalid-cidr", + wantErr: true, + }, + { + name: "Empty prefix", + prefix: "", + wantErr: true, + }, + { + name: "Invalid IPv4 range", + prefix: "256.256.256.256/24", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + entry := NewEntry("test") + err := entry.AddPrefix(tt.prefix) + + if tt.wantErr && err == nil { + t.Errorf("AddPrefix(%s) should return error but got nil", tt.prefix) + } + if !tt.wantErr && err != nil { + t.Errorf("AddPrefix(%s) should not return error but got: %v", tt.prefix, err) + } + }) + } +} + +func TestEntryGetIPSets(t *testing.T) { + entry := NewEntry("test") + + // Test getting IPv4 set when none exists + _, err := entry.GetIPv4Set() + if err == nil { + t.Error("GetIPv4Set() should return error when no IPv4 set exists") + } + + // Test getting IPv6 set when none exists + _, err = entry.GetIPv6Set() + if err == nil { + t.Error("GetIPv6Set() should return error when no IPv6 set exists") + } + + // Add some prefixes and test getting sets + err = entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + err = entry.AddPrefix("2001:db8::/32") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Now we should be able to get the sets + ipv4Set, err := entry.GetIPv4Set() + if err != nil { + t.Errorf("GetIPv4Set() should not return error after adding IPv4 prefix: %v", err) + } + if ipv4Set == nil { + t.Error("GetIPv4Set() should return non-nil set after adding IPv4 prefix") + } + + ipv6Set, err := entry.GetIPv6Set() + if err != nil { + t.Errorf("GetIPv6Set() should not return error after adding IPv6 prefix: %v", err) + } + if ipv6Set == nil { + t.Error("GetIPv6Set() should return non-nil set after adding IPv6 prefix") + } +} + +func TestEntryMarshalText(t *testing.T) { + entry := NewEntry("test") + + // Test with no prefixes + _, err := entry.MarshalText() + if err == nil { + t.Error("MarshalText() should return error for empty entry") + } + + // Add some prefixes + err = entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + err = entry.AddPrefix("2001:db8::/32") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Test marshaling + cidrs, err := entry.MarshalText() + if err != nil { + t.Errorf("MarshalText() should not return error: %v", err) + } + if len(cidrs) == 0 { + t.Error("MarshalText() should return non-empty slice after adding prefixes") + } + + // Test marshaling with ignore options + cidrs, err = entry.MarshalText(IgnoreIPv6) + if err != nil { + t.Errorf("MarshalText(IgnoreIPv6) should not return error: %v", err) + } + + cidrs, err = entry.MarshalText(IgnoreIPv4) + if err != nil { + t.Errorf("MarshalText(IgnoreIPv4) should not return error: %v", err) + } +} + +func TestEntryMarshalIPRange(t *testing.T) { + entry := NewEntry("test") + + // Test with no prefixes + _, err := entry.MarshalIPRange() + if err == nil { + t.Error("MarshalIPRange() should return error for empty entry") + } + + // Add some prefixes + err = entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + err = entry.AddPrefix("2001:db8::/32") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Test marshaling + ranges, err := entry.MarshalIPRange() + if err != nil { + t.Errorf("MarshalIPRange() should not return error: %v", err) + } + if len(ranges) == 0 { + t.Error("MarshalIPRange() should return non-empty slice after adding prefixes") + } + + // Test marshaling with ignore options + ranges, err = entry.MarshalIPRange(IgnoreIPv6) + if err != nil { + t.Errorf("MarshalIPRange(IgnoreIPv6) should not return error: %v", err) + } + + ranges, err = entry.MarshalIPRange(IgnoreIPv4) + if err != nil { + t.Errorf("MarshalIPRange(IgnoreIPv4) should not return error: %v", err) + } +} + +func TestEntryMarshalPrefix(t *testing.T) { + entry := NewEntry("test") + + // Test with no prefixes + _, err := entry.MarshalPrefix() + if err == nil { + t.Error("MarshalPrefix() should return error for empty entry") + } + + // Add some prefixes + err = entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + err = entry.AddPrefix("2001:db8::/32") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Test marshaling + prefixes, err := entry.MarshalPrefix() + if err != nil { + t.Errorf("MarshalPrefix() should not return error: %v", err) + } + if len(prefixes) == 0 { + t.Error("MarshalPrefix() should return non-empty slice after adding prefixes") + } + + // Test marshaling with ignore options + prefixes, err = entry.MarshalPrefix(IgnoreIPv6) + if err != nil { + t.Errorf("MarshalPrefix(IgnoreIPv6) should not return error: %v", err) + } + + prefixes, err = entry.MarshalPrefix(IgnoreIPv4) + if err != nil { + t.Errorf("MarshalPrefix(IgnoreIPv4) should not return error: %v", err) + } +} + +func TestEntryRemovePrefix(t *testing.T) { + entry := NewEntry("test") + + // Add a prefix first + err := entry.AddPrefix("192.168.1.0/24") + if err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Remove the prefix + err = entry.RemovePrefix("192.168.1.0/24") + if err != nil { + t.Errorf("RemovePrefix should not return error: %v", err) + } + + // Test removing invalid prefix + err = entry.RemovePrefix("invalid-cidr") + if err == nil { + t.Error("RemovePrefix with invalid CIDR should return error") + } + + // Try to remove non-existent prefix (should not error) + err = entry.RemovePrefix("10.0.0.0/8") + if err != nil { + t.Errorf("RemovePrefix() on non-existent prefix should not return error: %v", err) + } +} + +// TestEntryProcessPrefix tests the internal processPrefix function with various input types +func TestEntryProcessPrefix(t *testing.T) { + entry := NewEntry("test") + + tests := []struct { + name string + input interface{} + expectErr bool + expectIP string + }{ + // net.IP tests + { + name: "Valid IPv4 net.IP", + input: net.ParseIP("192.168.1.1"), + expectErr: false, + expectIP: "192.168.1.1", + }, + { + name: "Valid IPv6 net.IP", + input: net.ParseIP("2001:db8::1"), + expectErr: false, + expectIP: "2001:db8::1", + }, + // *net.IPNet tests + { + name: "Valid IPv4 *net.IPNet", + input: func() *net.IPNet { + _, ipnet, _ := net.ParseCIDR("192.168.1.0/24") + return ipnet + }(), + expectErr: false, + expectIP: "192.168.1.0", + }, + { + name: "Valid IPv6 *net.IPNet", + input: func() *net.IPNet { + _, ipnet, _ := net.ParseCIDR("2001:db8::/32") + return ipnet + }(), + expectErr: false, + expectIP: "2001:db8::", + }, + // netip.Addr tests + { + name: "Valid IPv4 netip.Addr", + input: netip.MustParseAddr("192.168.1.1"), + expectErr: false, + expectIP: "192.168.1.1", + }, + { + name: "Valid IPv6 netip.Addr", + input: netip.MustParseAddr("2001:db8::1"), + expectErr: false, + expectIP: "2001:db8::1", + }, + // *netip.Addr tests + { + name: "Valid IPv4 *netip.Addr", + input: func() *netip.Addr { + addr := netip.MustParseAddr("192.168.1.1") + return &addr + }(), + expectErr: false, + expectIP: "192.168.1.1", + }, + { + name: "Valid IPv6 *netip.Addr", + input: func() *netip.Addr { + addr := netip.MustParseAddr("2001:db8::1") + return &addr + }(), + expectErr: false, + expectIP: "2001:db8::1", + }, + // netip.Prefix tests + { + name: "Valid IPv4 netip.Prefix", + input: netip.MustParsePrefix("192.168.1.0/24"), + expectErr: false, + expectIP: "192.168.1.0", + }, + { + name: "Valid IPv6 netip.Prefix", + input: netip.MustParsePrefix("2001:db8::/32"), + expectErr: false, + expectIP: "2001:db8::", + }, + { + name: "IPv4-mapped IPv6 netip.Prefix", + input: netip.MustParsePrefix("::ffff:192.168.1.0/120"), + expectErr: false, + expectIP: "192.168.1.0", + }, + { + name: "Invalid IPv4-mapped IPv6 prefix bits", + input: netip.MustParsePrefix("::ffff:192.168.1.0/95"), + expectErr: true, + }, + // *netip.Prefix tests + { + name: "Valid IPv4 *netip.Prefix", + input: func() *netip.Prefix { + prefix := netip.MustParsePrefix("192.168.1.0/24") + return &prefix + }(), + expectErr: false, + expectIP: "192.168.1.0", + }, + { + name: "Valid IPv6 *netip.Prefix", + input: func() *netip.Prefix { + prefix := netip.MustParsePrefix("2001:db8::/32") + return &prefix + }(), + expectErr: false, + expectIP: "2001:db8::", + }, + // String tests + { + name: "Valid IPv4 string", + input: "192.168.1.1", + expectErr: false, + expectIP: "192.168.1.1", + }, + { + name: "Valid IPv6 string", + input: "2001:db8::1", + expectErr: false, + expectIP: "2001:db8::1", + }, + { + name: "Valid IPv4 CIDR string", + input: "192.168.1.0/24", + expectErr: false, + expectIP: "192.168.1.0", + }, + { + name: "Valid IPv6 CIDR string", + input: "2001:db8::/32", + expectErr: false, + expectIP: "2001:db8::", + }, + { + name: "Invalid string", + input: "invalid-string", + expectErr: true, + }, + // Unsupported type + { + name: "Unsupported type", + input: 123, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + prefix, ipType, err := entry.processPrefix(tt.input) + + if tt.expectErr { + if err == nil { + t.Errorf("processPrefix() should return error for input %v", tt.input) + } + return + } + + if err != nil { + t.Errorf("processPrefix() should not return error for valid input %v: %v", tt.input, err) + return + } + + if prefix == nil { + t.Errorf("processPrefix() should return non-nil prefix for valid input %v", tt.input) + return + } + + if prefix.Addr().String() != tt.expectIP { + t.Errorf("processPrefix() IP = %s; want %s", prefix.Addr().String(), tt.expectIP) + } + + // Verify IP type + if prefix.Addr().Is4() && ipType != IPv4 { + t.Errorf("processPrefix() should return IPv4 type for IPv4 address") + } + if prefix.Addr().Is6() && ipType != IPv6 { + t.Errorf("processPrefix() should return IPv6 type for IPv6 address") + } + }) + } +} + +// TestEntryAddPrefixVariousTypes tests AddPrefix with different input types +func TestEntryAddPrefixVariousTypes(t *testing.T) { + tests := []struct { + name string + input interface{} + expectErr bool + }{ + { + name: "net.IP", + input: net.ParseIP("192.168.1.1"), + expectErr: false, + }, + { + name: "*net.IPNet", + input: func() *net.IPNet { + _, ipnet, _ := net.ParseCIDR("192.168.1.0/24") + return ipnet + }(), + expectErr: false, + }, + { + name: "netip.Addr", + input: netip.MustParseAddr("192.168.1.1"), + expectErr: false, + }, + { + name: "*netip.Addr", + input: func() *netip.Addr { + addr := netip.MustParseAddr("192.168.1.1") + return &addr + }(), + expectErr: false, + }, + { + name: "netip.Prefix", + input: netip.MustParsePrefix("192.168.1.0/24"), + expectErr: false, + }, + { + name: "*netip.Prefix", + input: func() *netip.Prefix { + prefix := netip.MustParsePrefix("192.168.1.0/24") + return &prefix + }(), + expectErr: false, + }, + { + name: "string", + input: "192.168.1.0/24", + expectErr: false, + }, + { + name: "unsupported type", + input: 123, + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + entry := NewEntry("test") + err := entry.AddPrefix(tt.input) + + if tt.expectErr && err == nil { + t.Errorf("AddPrefix() should return error for input %v", tt.input) + } + if !tt.expectErr && err != nil { + t.Errorf("AddPrefix() should not return error for valid input %v: %v", tt.input, err) + } + }) + } +} + +// TestEntryRemoveWithInvalidType tests the remove function with invalid IP type +func TestEntryRemoveWithInvalidType(t *testing.T) { + entry := NewEntry("test") + prefix := netip.MustParsePrefix("192.168.1.0/24") + + // Test remove with invalid IP type - this tests the ErrInvalidIPType path + err := entry.remove(&prefix, IPType("invalid")) + if err == nil { + t.Error("remove() should return error for invalid IP type") + } + if err != ErrInvalidIPType { + t.Errorf("remove() should return ErrInvalidIPType, got %v", err) + } +} + +// TestEntryBuildIPSetErrors tests buildIPSet function error paths +func TestEntryBuildIPSetErrors(t *testing.T) { + entry := NewEntry("test") + + // Test with empty entry - should trigger the "no data" path in buildIPSet + err := entry.buildIPSet() + if err != nil { + // buildIPSet() should not return error for empty entry, it just builds what's available + t.Logf("buildIPSet() returned: %v", err) + } +} + +// TestEntryCIDRMergeRules tests CIDR merge behavior when adding intersecting prefixes +func TestEntryCIDRMergeRules(t *testing.T) { + tests := []struct { + name string + prefixesToAdd []string + expectedPrefixes []string + description string + }{ + { + name: "IPv4 adjacent prefixes merge", + prefixesToAdd: []string{"192.168.0.0/24", "192.168.1.0/24"}, + expectedPrefixes: []string{"192.168.0.0/23"}, + description: "192.168.0.0/24 and 192.168.1.0/24 should merge to 192.168.0.0/23", + }, + { + name: "IPv4 four adjacent prefixes merge", + prefixesToAdd: []string{"192.168.0.0/24", "192.168.1.0/24", "192.168.2.0/24", "192.168.3.0/24"}, + expectedPrefixes: []string{"192.168.0.0/22"}, + description: "Four consecutive /24 prefixes should merge to a single /22", + }, + { + name: "IPv4 non-adjacent prefixes do not merge", + prefixesToAdd: []string{"192.168.0.0/24", "192.168.2.0/24"}, + expectedPrefixes: []string{"192.168.0.0/24", "192.168.2.0/24"}, + description: "Non-adjacent prefixes should remain separate", + }, + { + name: "IPv6 adjacent prefixes merge", + prefixesToAdd: []string{"2001:db8:0:0::/64", "2001:db8:0:1::/64"}, + expectedPrefixes: []string{"2001:db8::/63"}, + description: "Adjacent IPv6 /64 prefixes should merge to /63", + }, + { + name: "Mixed IPv4 and IPv6 prefixes", + prefixesToAdd: []string{"192.168.0.0/24", "192.168.1.0/24", "2001:db8:0:0::/64", "2001:db8:0:1::/64"}, + expectedPrefixes: []string{"192.168.0.0/23", "2001:db8::/63"}, + description: "IPv4 and IPv6 prefixes should merge independently", + }, + { + name: "Single IP addresses merge to CIDR", + prefixesToAdd: []string{"192.168.0.1", "192.168.0.2", "192.168.0.3", "192.168.0.0"}, + expectedPrefixes: []string{"192.168.0.0/30"}, + description: "Individual IP addresses that form a contiguous block should merge", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + entry := NewEntry("test") + + // Add all prefixes + for _, prefix := range tt.prefixesToAdd { + err := entry.AddPrefix(prefix) + if err != nil { + t.Fatalf("Failed to add prefix %s: %v", prefix, err) + } + } + + // Get the resulting prefixes + prefixes, err := entry.MarshalText() + if err != nil { + t.Fatalf("Failed to marshal prefixes: %v", err) + } + + // Verify the expected merge result + if len(prefixes) != len(tt.expectedPrefixes) { + t.Errorf("Expected %d prefixes, got %d. Expected: %v, Got: %v", + len(tt.expectedPrefixes), len(prefixes), tt.expectedPrefixes, prefixes) + } + + // Convert to map for easier comparison (order doesn't matter) + expectedMap := make(map[string]bool) + for _, p := range tt.expectedPrefixes { + expectedMap[p] = true + } + + for _, prefix := range prefixes { + if !expectedMap[prefix] { + t.Errorf("Unexpected prefix %s in result. Expected: %v, Got: %v", + prefix, tt.expectedPrefixes, prefixes) + } + } + + for _, expected := range tt.expectedPrefixes { + found := false + for _, prefix := range prefixes { + if prefix == expected { + found = true + break + } + } + if !found { + t.Errorf("Expected prefix %s not found in result. Expected: %v, Got: %v", + expected, tt.expectedPrefixes, prefixes) + } + } + }) + } +} + +// TestEntryCIDRSplitRules tests CIDR split behavior when removing intersecting prefixes +func TestEntryCIDRSplitRules(t *testing.T) { + tests := []struct { + name string + initialPrefixes []string + prefixesToRemove []string + expectedPrefixes []string + description string + }{ + { + name: "IPv4 remove subset splits CIDR", + initialPrefixes: []string{"192.168.0.0/23"}, + prefixesToRemove: []string{"192.168.1.0/24"}, + expectedPrefixes: []string{"192.168.0.0/24"}, + description: "Removing 192.168.1.0/24 from 192.168.0.0/23 should leave 192.168.0.0/24", + }, + { + name: "IPv4 remove middle of larger block", + initialPrefixes: []string{"192.168.0.0/22"}, + prefixesToRemove: []string{"192.168.1.0/24"}, + expectedPrefixes: []string{"192.168.0.0/24", "192.168.2.0/23"}, + description: "Removing middle /24 from /22 should split into remaining blocks", + }, + { + name: "IPv6 remove subset splits CIDR", + initialPrefixes: []string{"2001:db8::/63"}, + prefixesToRemove: []string{"2001:db8:0:1::/64"}, + expectedPrefixes: []string{"2001:db8::/64"}, + description: "Removing IPv6 /64 from /63 should leave the other /64", + }, + { + name: "Remove entire range leaves empty", + initialPrefixes: []string{"192.168.0.0/24"}, + prefixesToRemove: []string{"192.168.0.0/24"}, + expectedPrefixes: []string{}, + description: "Removing entire range should leave empty entry", + }, + { + name: "Remove non-existent prefix does nothing", + initialPrefixes: []string{"192.168.0.0/24"}, + prefixesToRemove: []string{"10.0.0.0/24"}, + expectedPrefixes: []string{"192.168.0.0/24"}, + description: "Removing non-existent prefix should not affect existing prefixes", + }, + { + name: "Multiple removes create complex split", + initialPrefixes: []string{"192.168.0.0/22"}, + prefixesToRemove: []string{"192.168.0.0/24", "192.168.3.0/24"}, + expectedPrefixes: []string{"192.168.1.0/24", "192.168.2.0/24"}, + description: "Removing first and last /24 from /22 should leave middle two /24s", + }, + { + name: "Remove overlapping ranges", + initialPrefixes: []string{"192.168.0.0/23", "192.168.2.0/24"}, + prefixesToRemove: []string{"192.168.1.0/24"}, + expectedPrefixes: []string{"192.168.0.0/24", "192.168.2.0/24"}, + description: "Removing from one prefix should not affect non-overlapping prefixes", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + entry := NewEntry("test") + + // Add initial prefixes + for _, prefix := range tt.initialPrefixes { + err := entry.AddPrefix(prefix) + if err != nil { + t.Fatalf("Failed to add initial prefix %s: %v", prefix, err) + } + } + + // Remove specified prefixes + for _, prefix := range tt.prefixesToRemove { + err := entry.RemovePrefix(prefix) + if err != nil { + t.Fatalf("Failed to remove prefix %s: %v", prefix, err) + } + } + + // Get the resulting prefixes + prefixes, err := entry.MarshalText() + if err != nil && len(tt.expectedPrefixes) > 0 { + t.Fatalf("Failed to marshal prefixes: %v", err) + } + + // Handle empty result case + if len(tt.expectedPrefixes) == 0 { + if err == nil { + t.Errorf("Expected error for empty entry, but got prefixes: %v", prefixes) + } + return + } + + // Verify the expected split result + if len(prefixes) != len(tt.expectedPrefixes) { + t.Errorf("Expected %d prefixes, got %d. Expected: %v, Got: %v", + len(tt.expectedPrefixes), len(prefixes), tt.expectedPrefixes, prefixes) + } + + // Convert to map for easier comparison (order doesn't matter) + expectedMap := make(map[string]bool) + for _, p := range tt.expectedPrefixes { + expectedMap[p] = true + } + + for _, prefix := range prefixes { + if !expectedMap[prefix] { + t.Errorf("Unexpected prefix %s in result. Expected: %v, Got: %v", + prefix, tt.expectedPrefixes, prefixes) + } + } + + for _, expected := range tt.expectedPrefixes { + found := false + for _, prefix := range prefixes { + if prefix == expected { + found = true + break + } + } + if !found { + t.Errorf("Expected prefix %s not found in result. Expected: %v, Got: %v", + expected, tt.expectedPrefixes, prefixes) + } + } + }) + } +} + +// TestEntryCIDRMergeAndSplitCombined tests complex scenarios with both merge and split operations +func TestEntryCIDRMergeAndSplitCombined(t *testing.T) { + tests := []struct { + name string + operations []struct { + action string // "add" or "remove" + prefix string + } + expectedPrefixes []string + description string + }{ + { + name: "Add, merge, then split", + operations: []struct { + action string + prefix string + }{ + {"add", "192.168.0.0/24"}, + {"add", "192.168.1.0/24"}, // Should merge to 192.168.0.0/23 + {"remove", "192.168.0.128/25"}, // Should split the merged range + }, + expectedPrefixes: []string{"192.168.0.0/25", "192.168.1.0/24"}, + description: "Complex add/merge/remove sequence should work correctly", + }, + { + name: "Build large block then carve out holes", + operations: []struct { + action string + prefix string + }{ + {"add", "10.0.0.0/22"}, // Large block + {"remove", "10.0.1.0/24"}, // Remove middle + {"remove", "10.0.3.0/24"}, // Remove end + }, + expectedPrefixes: []string{"10.0.0.0/24", "10.0.2.0/24"}, + description: "Carving holes in large CIDR block should leave correct fragments", + }, + { + name: "IPv6 complex operations", + operations: []struct { + action string + prefix string + }{ + {"add", "2001:db8:0:0::/64"}, + {"add", "2001:db8:0:1::/64"}, // Should merge to 2001:db8::/63 + {"add", "2001:db8:0:2::/64"}, + {"add", "2001:db8:0:3::/64"}, // All should merge to 2001:db8::/62 + {"remove", "2001:db8:0:2::/64"}, // Remove one, should split + }, + expectedPrefixes: []string{"2001:db8::/63", "2001:db8:0:3::/64"}, + description: "IPv6 merge and split operations should work correctly", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + entry := NewEntry("test") + + // Perform all operations in sequence + for _, op := range tt.operations { + var err error + switch op.action { + case "add": + err = entry.AddPrefix(op.prefix) + case "remove": + err = entry.RemovePrefix(op.prefix) + default: + t.Fatalf("Unknown operation: %s", op.action) + } + + if err != nil { + t.Fatalf("Failed to %s prefix %s: %v", op.action, op.prefix, err) + } + } + + // Get the resulting prefixes + prefixes, err := entry.MarshalText() + if err != nil && len(tt.expectedPrefixes) > 0 { + t.Fatalf("Failed to marshal prefixes: %v", err) + } + + // Handle empty result case + if len(tt.expectedPrefixes) == 0 { + if err == nil { + t.Errorf("Expected error for empty entry, but got prefixes: %v", prefixes) + } + return + } + + // Verify the expected result + if len(prefixes) != len(tt.expectedPrefixes) { + t.Errorf("Expected %d prefixes, got %d. Expected: %v, Got: %v", + len(tt.expectedPrefixes), len(prefixes), tt.expectedPrefixes, prefixes) + } + + // Convert to map for easier comparison (order doesn't matter) + expectedMap := make(map[string]bool) + for _, p := range tt.expectedPrefixes { + expectedMap[p] = true + } + + for _, prefix := range prefixes { + if !expectedMap[prefix] { + t.Errorf("Unexpected prefix %s in result. Expected: %v, Got: %v", + prefix, tt.expectedPrefixes, prefixes) + } + } + + for _, expected := range tt.expectedPrefixes { + found := false + for _, prefix := range prefixes { + if prefix == expected { + found = true + break + } + } + if !found { + t.Errorf("Expected prefix %s not found in result. Expected: %v, Got: %v", + expected, tt.expectedPrefixes, prefixes) + } + } + }) + } +} \ No newline at end of file diff --git a/lib/error_test.go b/lib/error_test.go new file mode 100644 index 00000000000..f32a9b9620c --- /dev/null +++ b/lib/error_test.go @@ -0,0 +1,110 @@ +package lib + +import ( + "errors" + "testing" +) + +func TestErrors(t *testing.T) { + tests := []struct { + name string + err error + expected string + }{ + { + name: "ErrDuplicatedConverter", + err: ErrDuplicatedConverter, + expected: "duplicated converter", + }, + { + name: "ErrUnknownAction", + err: ErrUnknownAction, + expected: "unknown action", + }, + { + name: "ErrNotSupportedFormat", + err: ErrNotSupportedFormat, + expected: "not supported format", + }, + { + name: "ErrInvalidIPType", + err: ErrInvalidIPType, + expected: "invalid IP type", + }, + { + name: "ErrInvalidIP", + err: ErrInvalidIP, + expected: "invalid IP address", + }, + { + name: "ErrInvalidIPLength", + err: ErrInvalidIPLength, + expected: "invalid IP address length", + }, + { + name: "ErrInvalidIPNet", + err: ErrInvalidIPNet, + expected: "invalid IPNet address", + }, + { + name: "ErrInvalidCIDR", + err: ErrInvalidCIDR, + expected: "invalid CIDR", + }, + { + name: "ErrInvalidPrefix", + err: ErrInvalidPrefix, + expected: "invalid prefix", + }, + { + name: "ErrInvalidPrefixType", + err: ErrInvalidPrefixType, + expected: "invalid prefix type", + }, + { + name: "ErrCommentLine", + err: ErrCommentLine, + expected: "comment line", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.err.Error() != tt.expected { + t.Errorf("Error message mismatch. Expected: %s, Got: %s", tt.expected, tt.err.Error()) + } + + // Test that errors are proper error types + if !errors.Is(tt.err, tt.err) { + t.Errorf("Error should be comparable with itself using errors.Is") + } + }) + } +} + +func TestErrorTypes(t *testing.T) { + // Test that all defined errors implement the error interface + errorList := []error{ + ErrDuplicatedConverter, + ErrUnknownAction, + ErrNotSupportedFormat, + ErrInvalidIPType, + ErrInvalidIP, + ErrInvalidIPLength, + ErrInvalidIPNet, + ErrInvalidCIDR, + ErrInvalidPrefix, + ErrInvalidPrefixType, + ErrCommentLine, + } + + for i, err := range errorList { + if err == nil { + t.Errorf("Error at index %d should not be nil", i) + } + + if err.Error() == "" { + t.Errorf("Error at index %d should have a non-empty error message", i) + } + } +} \ No newline at end of file diff --git a/lib/instance_test.go b/lib/instance_test.go new file mode 100644 index 00000000000..7c4c54f3cd1 --- /dev/null +++ b/lib/instance_test.go @@ -0,0 +1,408 @@ +package lib + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestNewInstance(t *testing.T) { + instance, err := NewInstance() + if err != nil { + t.Errorf("NewInstance() should not return error: %v", err) + } + if instance == nil { + t.Error("NewInstance() should return non-nil instance") + } +} + +func TestInstance_AddInput(t *testing.T) { + instance, err := NewInstance() + if err != nil { + t.Fatalf("NewInstance() failed: %v", err) + } + + mockConverter := &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "test"}, + mockActioner: &mockActioner{actionValue: ActionAdd}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + } + + instance.AddInput(mockConverter) + // No direct way to verify, but should not panic +} + +func TestInstance_AddOutput(t *testing.T) { + instance, err := NewInstance() + if err != nil { + t.Fatalf("NewInstance() failed: %v", err) + } + + mockConverter := &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "test"}, + mockActioner: &mockActioner{actionValue: ActionOutput}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + } + + instance.AddOutput(mockConverter) + // No direct way to verify, but should not panic +} + +func TestInstance_ResetInput(t *testing.T) { + instance, err := NewInstance() + if err != nil { + t.Fatalf("NewInstance() failed: %v", err) + } + + // Add some input converters + mockConverter := &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "test"}, + mockActioner: &mockActioner{actionValue: ActionAdd}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + } + instance.AddInput(mockConverter) + instance.AddInput(mockConverter) + + // Reset should clear all inputs + instance.ResetInput() + // No direct way to verify, but should not panic +} + +func TestInstance_ResetOutput(t *testing.T) { + instance, err := NewInstance() + if err != nil { + t.Fatalf("NewInstance() failed: %v", err) + } + + // Add some output converters + mockConverter := &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "test"}, + mockActioner: &mockActioner{actionValue: ActionOutput}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + } + instance.AddOutput(mockConverter) + instance.AddOutput(mockConverter) + + // Reset should clear all outputs + instance.ResetOutput() + // No direct way to verify, but should not panic +} + +func TestInstance_RunInput(t *testing.T) { + instance, err := NewInstance() + if err != nil { + t.Fatalf("NewInstance() failed: %v", err) + } + + container := NewContainer() + + // Test with no input converters + err = instance.RunInput(container) + if err != nil { + t.Errorf("RunInput() with no converters should not return error: %v", err) + } + + // Add a mock input converter + mockConverter := &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "test"}, + mockActioner: &mockActioner{actionValue: ActionAdd}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + } + instance.AddInput(mockConverter) + + // Test with input converter + err = instance.RunInput(container) + if err != nil { + t.Errorf("RunInput() should not return error: %v", err) + } +} + +func TestInstance_RunOutput(t *testing.T) { + instance, err := NewInstance() + if err != nil { + t.Fatalf("NewInstance() failed: %v", err) + } + + container := NewContainer() + + // Test with no output converters + err = instance.RunOutput(container) + if err != nil { + t.Errorf("RunOutput() with no converters should not return error: %v", err) + } + + // Add a mock output converter + mockConverter := &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "test"}, + mockActioner: &mockActioner{actionValue: ActionOutput}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + } + instance.AddOutput(mockConverter) + + // Test with output converter + err = instance.RunOutput(container) + if err != nil { + t.Errorf("RunOutput() should not return error: %v", err) + } +} + +func TestInstance_Run(t *testing.T) { + instance, err := NewInstance() + if err != nil { + t.Fatalf("NewInstance() failed: %v", err) + } + + // Test with no converters - should return error + err = instance.Run() + if err == nil { + t.Error("Run() should return error when no input/output converters are specified") + } + if !strings.Contains(err.Error(), "input type and output type must be specified") { + t.Errorf("Error should mention input/output types, got: %v", err) + } + + // Add input but no output - should return error + mockInputConverter := &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "test"}, + mockActioner: &mockActioner{actionValue: ActionAdd}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + } + instance.AddInput(mockInputConverter) + + err = instance.Run() + if err == nil { + t.Error("Run() should return error when no output converters are specified") + } + + // Add output converter + mockOutputConverter := &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "test"}, + mockActioner: &mockActioner{actionValue: ActionOutput}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + } + instance.AddOutput(mockOutputConverter) + + // Now should work + err = instance.Run() + if err != nil { + t.Errorf("Run() should not return error when both input and output are specified: %v", err) + } +} + +func TestInstance_InitConfig(t *testing.T) { + // Create a temporary config file + tempDir := os.TempDir() + configFile := filepath.Join(tempDir, "test_config.json") + + // Register test creators + RegisterInputConfigCreator("test-file-input", func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "test-file-input"}, + mockActioner: &mockActioner{actionValue: action}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + }, nil + }) + RegisterOutputConfigCreator("test-file-output", func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "test-file-output"}, + mockActioner: &mockActioner{actionValue: action}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + }, nil + }) + + configContent := `{ + "input": [ + {"type": "test-file-input", "action": "add", "args": {}} + ], + "output": [ + {"type": "test-file-output", "action": "output", "args": {}} + ] + }` + + err := os.WriteFile(configFile, []byte(configContent), 0644) + if err != nil { + t.Fatalf("Failed to create test config file: %v", err) + } + defer os.Remove(configFile) + + instance, err := NewInstance() + if err != nil { + t.Fatalf("NewInstance() failed: %v", err) + } + + // Test successful config loading + err = instance.InitConfig(configFile) + if err != nil { + t.Errorf("InitConfig() should not return error: %v", err) + } + + // Test with non-existent file + err = instance.InitConfig("non-existent-file.json") + if err == nil { + t.Error("InitConfig() should return error for non-existent file") + } + + // Test with invalid JSON file + invalidConfigFile := filepath.Join(tempDir, "invalid_config.json") + err = os.WriteFile(invalidConfigFile, []byte(`{invalid json}`), 0644) + if err != nil { + t.Fatalf("Failed to create invalid config file: %v", err) + } + defer os.Remove(invalidConfigFile) + + err = instance.InitConfig(invalidConfigFile) + if err == nil { + t.Error("InitConfig() should return error for invalid JSON") + } +} + +func TestInstance_InitConfigFromBytes(t *testing.T) { + // Register test creators + RegisterInputConfigCreator("test-bytes-input", func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "test-bytes-input"}, + mockActioner: &mockActioner{actionValue: action}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + }, nil + }) + RegisterOutputConfigCreator("test-bytes-output", func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "test-bytes-output"}, + mockActioner: &mockActioner{actionValue: action}, + mockDescriptioner: &mockDescriptioner{descValue: "test"}, + }, nil + }) + + instance, err := NewInstance() + if err != nil { + t.Fatalf("NewInstance() failed: %v", err) + } + + tests := []struct { + name string + content string + expectError bool + }{ + { + name: "Valid config", + content: `{ + "input": [ + {"type": "test-bytes-input", "action": "add", "args": {}} + ], + "output": [ + {"type": "test-bytes-output", "action": "output", "args": {}} + ] + }`, + expectError: false, + }, + { + name: "Valid config with comments (hujson)", + content: `{ + // This is a comment + "input": [ + {"type": "test-bytes-input", "action": "add", "args": {}}, // trailing comma + ], + "output": [ + {"type": "test-bytes-output", "action": "output", "args": {}} + ] + }`, + expectError: false, + }, + { + name: "Invalid JSON", + content: `{invalid json}`, + expectError: true, + }, + { + name: "Empty content", + content: ``, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := instance.InitConfigFromBytes([]byte(tt.content)) + if tt.expectError && err == nil { + t.Errorf("InitConfigFromBytes() should return error but got nil") + } + if !tt.expectError && err != nil { + t.Errorf("InitConfigFromBytes() should not return error but got: %v", err) + } + }) + } +} + +// Error-returning mock converters for testing error scenarios +type errorInputConverter struct { + *mockInputConverter +} + +func (e *errorInputConverter) Input(container Container) (Container, error) { + return nil, errors.New("input error") +} + +type errorOutputConverter struct { + *mockOutputConverter +} + +func (e *errorOutputConverter) Output(container Container) error { + return errors.New("output error") +} + +func TestInstance_RunInput_Error(t *testing.T) { + instance, err := NewInstance() + if err != nil { + t.Fatalf("NewInstance() failed: %v", err) + } + + container := NewContainer() + + // Add an error-returning input converter + errorConverter := &errorInputConverter{ + mockInputConverter: &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "error"}, + mockActioner: &mockActioner{actionValue: ActionAdd}, + mockDescriptioner: &mockDescriptioner{descValue: "error"}, + }, + } + instance.AddInput(errorConverter) + + err = instance.RunInput(container) + if err == nil { + t.Error("RunInput() should return error when input converter fails") + } + if !strings.Contains(err.Error(), "input error") { + t.Errorf("Error should mention input error, got: %v", err) + } +} + +func TestInstance_RunOutput_Error(t *testing.T) { + instance, err := NewInstance() + if err != nil { + t.Fatalf("NewInstance() failed: %v", err) + } + + container := NewContainer() + + // Add an error-returning output converter + errorConverter := &errorOutputConverter{ + mockOutputConverter: &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "error"}, + mockActioner: &mockActioner{actionValue: ActionOutput}, + mockDescriptioner: &mockDescriptioner{descValue: "error"}, + }, + } + instance.AddOutput(errorConverter) + + err = instance.RunOutput(container) + if err == nil { + t.Error("RunOutput() should return error when output converter fails") + } + if !strings.Contains(err.Error(), "output error") { + t.Errorf("Error should mention output error, got: %v", err) + } +} \ No newline at end of file diff --git a/lib/lib_test.go b/lib/lib_test.go new file mode 100644 index 00000000000..95ff0edf560 --- /dev/null +++ b/lib/lib_test.go @@ -0,0 +1,218 @@ +package lib + +import ( + "testing" +) + +func TestConstants(t *testing.T) { + // Test Action constants + if ActionAdd != "add" { + t.Errorf("ActionAdd should be 'add', got: %s", ActionAdd) + } + if ActionRemove != "remove" { + t.Errorf("ActionRemove should be 'remove', got: %s", ActionRemove) + } + if ActionOutput != "output" { + t.Errorf("ActionOutput should be 'output', got: %s", ActionOutput) + } + + // Test IPType constants + if IPv4 != "ipv4" { + t.Errorf("IPv4 should be 'ipv4', got: %s", IPv4) + } + if IPv6 != "ipv6" { + t.Errorf("IPv6 should be 'ipv6', got: %s", IPv6) + } + + // Test CaseRemove constants + if CaseRemovePrefix != 0 { + t.Errorf("CaseRemovePrefix should be 0, got: %d", CaseRemovePrefix) + } + if CaseRemoveEntry != 1 { + t.Errorf("CaseRemoveEntry should be 1, got: %d", CaseRemoveEntry) + } +} + +func TestActionsRegistry(t *testing.T) { + // Test that all defined actions are in the registry + expectedActions := []Action{ActionAdd, ActionRemove, ActionOutput} + + for _, action := range expectedActions { + if !ActionsRegistry[action] { + t.Errorf("Action %s should be registered in ActionsRegistry", action) + } + } + + // Test that the registry contains exactly the expected number of actions + if len(ActionsRegistry) != len(expectedActions) { + t.Errorf("ActionsRegistry should contain exactly %d actions, got: %d", len(expectedActions), len(ActionsRegistry)) + } + + // Test that only valid actions are in the registry + for action, valid := range ActionsRegistry { + if !valid { + t.Errorf("Action %s should have value true in ActionsRegistry", action) + } + + // Check that it's one of our expected actions + found := false + for _, expectedAction := range expectedActions { + if action == expectedAction { + found = true + break + } + } + if !found { + t.Errorf("Unexpected action %s found in ActionsRegistry", action) + } + } +} + +func TestIgnoreIPOptions(t *testing.T) { + // Test IgnoreIPv4 function + if IgnoreIPv4() != IPv4 { + t.Errorf("IgnoreIPv4() should return IPv4, got: %s", IgnoreIPv4()) + } + + // Test IgnoreIPv6 function + if IgnoreIPv6() != IPv6 { + t.Errorf("IgnoreIPv6() should return IPv6, got: %s", IgnoreIPv6()) + } +} + +func TestActionString(t *testing.T) { + // Test Action type string conversion + action := ActionAdd + if string(action) != "add" { + t.Errorf("Action string conversion failed. Expected: add, Got: %s", string(action)) + } +} + +func TestIPTypeString(t *testing.T) { + // Test IPType string conversion + ipType := IPv4 + if string(ipType) != "ipv4" { + t.Errorf("IPType string conversion failed. Expected: ipv4, Got: %s", string(ipType)) + } +} + +func TestCaseRemoveInt(t *testing.T) { + // Test CaseRemove int conversion + caseRemove := CaseRemovePrefix + if int(caseRemove) != 0 { + t.Errorf("CaseRemove int conversion failed. Expected: 0, Got: %d", int(caseRemove)) + } +} + +// Test interface definitions by creating mock implementations +type mockTyper struct { + typeValue string +} + +func (m *mockTyper) GetType() string { + return m.typeValue +} + +type mockActioner struct { + actionValue Action +} + +func (m *mockActioner) GetAction() Action { + return m.actionValue +} + +type mockDescriptioner struct { + descValue string +} + +func (m *mockDescriptioner) GetDescription() string { + return m.descValue +} + +type mockInputConverter struct { + *mockTyper + *mockActioner + *mockDescriptioner +} + +func (m *mockInputConverter) Input(container Container) (Container, error) { + return container, nil +} + +type mockOutputConverter struct { + *mockTyper + *mockActioner + *mockDescriptioner +} + +func (m *mockOutputConverter) Output(container Container) error { + return nil +} + +func TestInterfaces(t *testing.T) { + // Test Typer interface + typer := &mockTyper{typeValue: "test"} + if typer.GetType() != "test" { + t.Errorf("Typer interface implementation failed") + } + + // Test Actioner interface + actioner := &mockActioner{actionValue: ActionAdd} + if actioner.GetAction() != ActionAdd { + t.Errorf("Actioner interface implementation failed") + } + + // Test Descriptioner interface + descriptioner := &mockDescriptioner{descValue: "test description"} + if descriptioner.GetDescription() != "test description" { + t.Errorf("Descriptioner interface implementation failed") + } + + // Test InputConverter interface + inputConverter := &mockInputConverter{ + mockTyper: &mockTyper{typeValue: "input"}, + mockActioner: &mockActioner{actionValue: ActionAdd}, + mockDescriptioner: &mockDescriptioner{descValue: "input desc"}, + } + + if inputConverter.GetType() != "input" { + t.Errorf("InputConverter GetType failed") + } + if inputConverter.GetAction() != ActionAdd { + t.Errorf("InputConverter GetAction failed") + } + if inputConverter.GetDescription() != "input desc" { + t.Errorf("InputConverter GetDescription failed") + } + + container := NewContainer() + result, err := inputConverter.Input(container) + if err != nil { + t.Errorf("InputConverter Input failed: %v", err) + } + if result != container { + t.Errorf("InputConverter Input should return the same container") + } + + // Test OutputConverter interface + outputConverter := &mockOutputConverter{ + mockTyper: &mockTyper{typeValue: "output"}, + mockActioner: &mockActioner{actionValue: ActionOutput}, + mockDescriptioner: &mockDescriptioner{descValue: "output desc"}, + } + + if outputConverter.GetType() != "output" { + t.Errorf("OutputConverter GetType failed") + } + if outputConverter.GetAction() != ActionOutput { + t.Errorf("OutputConverter GetAction failed") + } + if outputConverter.GetDescription() != "output desc" { + t.Errorf("OutputConverter GetDescription failed") + } + + err = outputConverter.Output(container) + if err != nil { + t.Errorf("OutputConverter Output failed: %v", err) + } +} \ No newline at end of file