diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/common_test.go | 191 | ||||
| -rw-r--r-- | lib/config_test.go | 294 | ||||
| -rw-r--r-- | lib/container_test.go | 744 | ||||
| -rw-r--r-- | lib/converter_test.go | 146 | ||||
| -rw-r--r-- | lib/entry_test.go | 612 | ||||
| -rw-r--r-- | lib/error_test.go | 55 | ||||
| -rw-r--r-- | lib/instance_test.go | 604 | ||||
| -rw-r--r-- | lib/lib_test.go | 89 |
8 files changed, 2735 insertions, 0 deletions
diff --git a/lib/common_test.go b/lib/common_test.go new file mode 100644 index 00000000..303c4dc2 --- /dev/null +++ b/lib/common_test.go @@ -0,0 +1,191 @@ +package lib + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestGetRemoteURLContent_Success(t *testing.T) { + // Create 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() + + content, err := GetRemoteURLContent(server.URL) + if err != nil { + t.Fatalf("GetRemoteURLContent failed: %v", err) + } + + if string(content) != "test content" { + t.Errorf("GetRemoteURLContent = %s, want 'test content'", string(content)) + } +} + +func TestGetRemoteURLContent_NotFound(t *testing.T) { + // Create test server that returns 404 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + _, err := GetRemoteURLContent(server.URL) + if err == nil { + t.Error("GetRemoteURLContent should fail for 404") + } +} + +func TestGetRemoteURLContent_InvalidURL(t *testing.T) { + _, err := GetRemoteURLContent("http://invalid-url-that-does-not-exist.local") + if err == nil { + t.Error("GetRemoteURLContent should fail for invalid URL") + } +} + +func TestGetRemoteURLReader_Success(t *testing.T) { + // Create 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() + + reader, err := GetRemoteURLReader(server.URL) + if err != nil { + t.Fatalf("GetRemoteURLReader failed: %v", err) + } + defer reader.Close() + + buf := make([]byte, 1024) + n, _ := reader.Read(buf) + if string(buf[:n]) != "test content" { + t.Errorf("GetRemoteURLReader content = %s, want 'test content'", string(buf[:n])) + } +} + +func TestGetRemoteURLReader_NotFound(t *testing.T) { + // Create test server that returns 404 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + _, err := GetRemoteURLReader(server.URL) + if err == nil { + t.Error("GetRemoteURLReader should fail for 404") + } +} + +func TestGetRemoteURLReader_InvalidURL(t *testing.T) { + _, err := GetRemoteURLReader("http://invalid-url-that-does-not-exist.local") + if err == nil { + t.Error("GetRemoteURLReader should fail for invalid URL") + } +} + +func TestWantedListExtended_UnmarshalJSON_Slice(t *testing.T) { + jsonData := []byte(`["item1", "item2", "item3"]`) + + var w WantedListExtended + err := json.Unmarshal(jsonData, &w) + if err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + + if len(w.TypeSlice) != 3 { + t.Errorf("len(TypeSlice) = %d, want 3", len(w.TypeSlice)) + } + if len(w.TypeMap) != 0 { + t.Errorf("len(TypeMap) = %d, want 0", len(w.TypeMap)) + } + + expectedSlice := []string{"item1", "item2", "item3"} + for i, v := range w.TypeSlice { + if v != expectedSlice[i] { + t.Errorf("TypeSlice[%d] = %s, want %s", i, v, expectedSlice[i]) + } + } +} + +func TestWantedListExtended_UnmarshalJSON_Map(t *testing.T) { + jsonData := []byte(`{"key1": ["value1", "value2"], "key2": ["value3"]}`) + + var w WantedListExtended + err := json.Unmarshal(jsonData, &w) + if err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + + if len(w.TypeSlice) != 0 { + t.Errorf("len(TypeSlice) = %d, want 0", len(w.TypeSlice)) + } + if len(w.TypeMap) != 2 { + t.Errorf("len(TypeMap) = %d, want 2", len(w.TypeMap)) + } + + if len(w.TypeMap["key1"]) != 2 { + t.Errorf("len(TypeMap[key1]) = %d, want 2", len(w.TypeMap["key1"])) + } + if len(w.TypeMap["key2"]) != 1 { + t.Errorf("len(TypeMap[key2]) = %d, want 1", len(w.TypeMap["key2"])) + } +} + +func TestWantedListExtended_UnmarshalJSON_EmptyData(t *testing.T) { + // Test calling UnmarshalJSON directly with empty data + var w WantedListExtended + err := w.UnmarshalJSON([]byte{}) + if err != nil { + t.Fatalf("UnmarshalJSON with empty data failed: %v", err) + } + + if len(w.TypeSlice) != 0 { + t.Errorf("len(TypeSlice) = %d, want 0", len(w.TypeSlice)) + } + if len(w.TypeMap) != 0 { + t.Errorf("len(TypeMap) = %d, want 0", len(w.TypeMap)) + } +} + +func TestWantedListExtended_UnmarshalJSON_Invalid(t *testing.T) { + // Invalid JSON that is neither slice nor map + jsonData := []byte(`123`) + + var w WantedListExtended + err := json.Unmarshal(jsonData, &w) + if err == nil { + t.Error("UnmarshalJSON should fail for invalid format") + } +} + +func TestWantedListExtended_UnmarshalJSON_EmptySlice(t *testing.T) { + jsonData := []byte(`[]`) + + var w WantedListExtended + err := json.Unmarshal(jsonData, &w) + if err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + + if len(w.TypeSlice) != 0 { + t.Errorf("len(TypeSlice) = %d, want 0", len(w.TypeSlice)) + } +} + +func TestWantedListExtended_UnmarshalJSON_EmptyMap(t *testing.T) { + jsonData := []byte(`{}`) + + var w WantedListExtended + err := json.Unmarshal(jsonData, &w) + if err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + + // Empty object is a valid map + if len(w.TypeMap) != 0 { + t.Errorf("len(TypeMap) = %d, want 0", len(w.TypeMap)) + } +} diff --git a/lib/config_test.go b/lib/config_test.go new file mode 100644 index 00000000..6f4e854f --- /dev/null +++ b/lib/config_test.go @@ -0,0 +1,294 @@ +package lib + +import ( + "encoding/json" + "testing" +) + +func TestRegisterInputConfigCreator(t *testing.T) { + // Test registering a new input config creator + testID := "test_input_config_creator_" + t.Name() + fn := func(action Action, data json.RawMessage) (InputConverter, error) { + return nil, nil + } + + err := RegisterInputConfigCreator(testID, fn) + if err != nil { + t.Fatalf("RegisterInputConfigCreator failed: %v", err) + } + + // Test registering duplicate + err = RegisterInputConfigCreator(testID, fn) + if err == nil { + t.Error("RegisterInputConfigCreator should return error for duplicate") + } +} + +func TestRegisterInputConfigCreator_CaseInsensitive(t *testing.T) { + testID := "TEST_INPUT_CONFIG_CASE_" + t.Name() + fn := func(action Action, data json.RawMessage) (InputConverter, error) { + return nil, nil + } + + err := RegisterInputConfigCreator(testID, fn) + if err != nil { + t.Fatalf("RegisterInputConfigCreator failed: %v", err) + } + + // Try to register with lowercase + err = RegisterInputConfigCreator("test_input_config_case_"+t.Name(), fn) + if err == nil { + t.Error("RegisterInputConfigCreator should be case-insensitive") + } +} + +func TestCreateInputConfig_NotFound(t *testing.T) { + _, err := createInputConfig("nonexistent_input_config", ActionAdd, nil) + if err == nil { + t.Error("createInputConfig should return error for unknown type") + } +} + +func TestRegisterOutputConfigCreator(t *testing.T) { + // Test registering a new output config creator + testID := "test_output_config_creator_" + t.Name() + fn := func(action Action, data json.RawMessage) (OutputConverter, error) { + return nil, nil + } + + err := RegisterOutputConfigCreator(testID, fn) + if err != nil { + t.Fatalf("RegisterOutputConfigCreator failed: %v", err) + } + + // Test registering duplicate + err = RegisterOutputConfigCreator(testID, fn) + if err == nil { + t.Error("RegisterOutputConfigCreator should return error for duplicate") + } +} + +func TestRegisterOutputConfigCreator_CaseInsensitive(t *testing.T) { + testID := "TEST_OUTPUT_CONFIG_CASE_" + t.Name() + fn := func(action Action, data json.RawMessage) (OutputConverter, error) { + return nil, nil + } + + err := RegisterOutputConfigCreator(testID, fn) + if err != nil { + t.Fatalf("RegisterOutputConfigCreator failed: %v", err) + } + + // Try to register with lowercase + err = RegisterOutputConfigCreator("test_output_config_case_"+t.Name(), fn) + if err == nil { + t.Error("RegisterOutputConfigCreator should be case-insensitive") + } +} + +func TestCreateOutputConfig_NotFound(t *testing.T) { + _, err := createOutputConfig("nonexistent_output_config", ActionOutput, nil) + if err == nil { + t.Error("createOutputConfig should return error for unknown type") + } +} + +// MockInputConverter for testing +type mockInputConverter struct { + typeName string + action Action + description string +} + +func (m *mockInputConverter) GetType() string { return m.typeName } +func (m *mockInputConverter) GetAction() Action { return m.action } +func (m *mockInputConverter) GetDescription() string { return m.description } +func (m *mockInputConverter) Input(c Container) (Container, error) { + return c, nil +} + +// MockOutputConverter for testing +type mockOutputConverter struct { + typeName string + action Action + description string +} + +func (m *mockOutputConverter) GetType() string { return m.typeName } +func (m *mockOutputConverter) GetAction() Action { return m.action } +func (m *mockOutputConverter) GetDescription() string { return m.description } +func (m *mockOutputConverter) Output(c Container) error { + return nil +} + +func TestInputConvConfigUnmarshalJSON(t *testing.T) { + // Register a mock input config creator + testType := "mock_input_" + t.Name() + RegisterInputConfigCreator(testType, func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + typeName: testType, + action: action, + }, nil + }) + + // Test valid unmarshal + jsonData := []byte(`{"type":"` + testType + `","action":"add","args":{}}`) + var config inputConvConfig + err := json.Unmarshal(jsonData, &config) + if err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + + if config.iType != testType { + t.Errorf("config.iType = %s, want %s", config.iType, testType) + } + if config.action != ActionAdd { + t.Errorf("config.action = %s, want %s", config.action, ActionAdd) + } +} + +func TestInputConvConfigUnmarshalJSON_InvalidAction(t *testing.T) { + jsonData := []byte(`{"type":"sometype","action":"invalid_action","args":{}}`) + var config inputConvConfig + err := json.Unmarshal(jsonData, &config) + if err == nil { + t.Error("UnmarshalJSON should fail for invalid action") + } +} + +func TestInputConvConfigUnmarshalJSON_InvalidJSON(t *testing.T) { + jsonData := []byte(`{invalid json}`) + var config inputConvConfig + err := json.Unmarshal(jsonData, &config) + if err == nil { + t.Error("UnmarshalJSON should fail for invalid JSON") + } +} + +func TestInputConvConfigUnmarshalJSON_UnknownType(t *testing.T) { + jsonData := []byte(`{"type":"unknown_type_123","action":"add","args":{}}`) + var config inputConvConfig + err := json.Unmarshal(jsonData, &config) + if err == nil { + t.Error("UnmarshalJSON should fail for unknown type") + } +} + +func TestOutputConvConfigUnmarshalJSON(t *testing.T) { + // Register a mock output config creator + testType := "mock_output_" + t.Name() + RegisterOutputConfigCreator(testType, func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + typeName: testType, + action: action, + }, nil + }) + + // Test valid unmarshal + jsonData := []byte(`{"type":"` + testType + `","action":"output","args":{}}`) + var config outputConvConfig + err := json.Unmarshal(jsonData, &config) + if err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + + if config.iType != testType { + t.Errorf("config.iType = %s, want %s", config.iType, testType) + } + if config.action != ActionOutput { + t.Errorf("config.action = %s, want %s", config.action, ActionOutput) + } +} + +func TestOutputConvConfigUnmarshalJSON_DefaultAction(t *testing.T) { + // Register a mock output config creator + testType := "mock_output_default_" + t.Name() + RegisterOutputConfigCreator(testType, func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + typeName: testType, + action: action, + }, nil + }) + + // Test unmarshal without action (should default to "output") + jsonData := []byte(`{"type":"` + testType + `","args":{}}`) + var config outputConvConfig + err := json.Unmarshal(jsonData, &config) + if err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + + if config.action != ActionOutput { + t.Errorf("config.action = %s, want %s (default)", config.action, ActionOutput) + } +} + +func TestOutputConvConfigUnmarshalJSON_InvalidAction(t *testing.T) { + jsonData := []byte(`{"type":"sometype","action":"invalid_action","args":{}}`) + var config outputConvConfig + err := json.Unmarshal(jsonData, &config) + if err == nil { + t.Error("UnmarshalJSON should fail for invalid action") + } +} + +func TestOutputConvConfigUnmarshalJSON_InvalidJSON(t *testing.T) { + jsonData := []byte(`{invalid json}`) + var config outputConvConfig + err := json.Unmarshal(jsonData, &config) + if err == nil { + t.Error("UnmarshalJSON should fail for invalid JSON") + } +} + +func TestOutputConvConfigUnmarshalJSON_UnknownType(t *testing.T) { + jsonData := []byte(`{"type":"unknown_type_456","action":"output","args":{}}`) + var config outputConvConfig + err := json.Unmarshal(jsonData, &config) + if err == nil { + t.Error("UnmarshalJSON should fail for unknown type") + } +} + +func TestConfigStruct(t *testing.T) { + // Register mock converters for this test + inputType := "config_test_input_" + t.Name() + outputType := "config_test_output_" + t.Name() + + RegisterInputConfigCreator(inputType, func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + typeName: inputType, + action: action, + }, nil + }) + + RegisterOutputConfigCreator(outputType, func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + typeName: outputType, + action: action, + }, nil + }) + + // Test unmarshaling full config + jsonData := []byte(`{ + "input": [ + {"type":"` + inputType + `","action":"add","args":{}} + ], + "output": [ + {"type":"` + outputType + `","action":"output","args":{}} + ] + }`) + + var cfg config + err := json.Unmarshal(jsonData, &cfg) + if err != nil { + t.Fatalf("UnmarshalJSON failed: %v", err) + } + + if len(cfg.Input) != 1 { + t.Errorf("len(cfg.Input) = %d, want 1", len(cfg.Input)) + } + if len(cfg.Output) != 1 { + t.Errorf("len(cfg.Output) = %d, want 1", len(cfg.Output)) + } +} diff --git a/lib/container_test.go b/lib/container_test.go new file mode 100644 index 00000000..6e6c6390 --- /dev/null +++ b/lib/container_test.go @@ -0,0 +1,744 @@ +package lib + +import ( + "testing" +) + +func TestNewContainer(t *testing.T) { + c := NewContainer() + if c == nil { + t.Fatal("NewContainer returned nil") + } + if c.Len() != 0 { + t.Errorf("NewContainer().Len() = %d, want 0", c.Len()) + } +} + +func TestContainerAdd(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + if c.Len() != 1 { + t.Errorf("Container.Len() = %d, want 1", c.Len()) + } +} + +func TestContainerAdd_ExistingEntry(t *testing.T) { + c := NewContainer() + + // Add first entry + entry1 := NewEntry("test") + if err := entry1.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry1); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Add second entry with same name (should merge) + entry2 := NewEntry("test") + if err := entry2.AddPrefix("10.0.0.0/8"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry2); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Should still be 1 entry + if c.Len() != 1 { + t.Errorf("Container.Len() = %d, want 1", c.Len()) + } +} + +func TestContainerAdd_IgnoreIPv4(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + if err := c.Add(entry, IgnoreIPv4); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Get the entry and verify IPv4 was ignored + e, found := c.GetEntry("test") + if !found { + t.Fatal("Entry not found") + } + + _, err := e.GetIPv4Set() + if err == nil { + t.Error("IPv4 should be ignored") + } +} + +func TestContainerAdd_IgnoreIPv6(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + if err := c.Add(entry, IgnoreIPv6); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Get the entry and verify IPv6 was ignored + e, found := c.GetEntry("test") + if !found { + t.Fatal("Entry not found") + } + + _, err := e.GetIPv6Set() + if err == nil { + t.Error("IPv6 should be ignored") + } +} + +func TestContainerAdd_ExistingEntryWithIPv6(t *testing.T) { + c := NewContainer() + + // Add first entry with only IPv4 + entry1 := NewEntry("test") + if err := entry1.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry1); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Add second entry with IPv6 (same name) + entry2 := NewEntry("test") + if err := entry2.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry2); err != nil { + t.Fatalf("Add failed: %v", err) + } + + e, found := c.GetEntry("test") + if !found { + t.Fatal("Entry not found") + } + + // Now should have both IPv4 and IPv6 + _, err4 := e.GetIPv4Set() + if err4 != nil { + t.Errorf("GetIPv4Set failed: %v", err4) + } + _, err6 := e.GetIPv6Set() + if err6 != nil { + t.Errorf("GetIPv6Set failed: %v", err6) + } +} + +func TestContainerAdd_ExistingEntryIgnoreIPv4(t *testing.T) { + c := NewContainer() + + // Add first entry + entry1 := NewEntry("test") + if err := entry1.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry1); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Add second entry with IgnoreIPv4 - only IPv6 should be added + entry2 := NewEntry("test") + if err := entry2.AddPrefix("10.0.0.0/8"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry2.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry2, IgnoreIPv4); err != nil { + t.Fatalf("Add failed: %v", err) + } + + e, found := c.GetEntry("test") + if !found { + t.Fatal("Entry not found") + } + + // Should have IPv6 now + _, err6 := e.GetIPv6Set() + if err6 != nil { + t.Errorf("GetIPv6Set failed: %v", err6) + } +} + +func TestContainerAdd_ExistingEntryIgnoreIPv6(t *testing.T) { + c := NewContainer() + + // Add first entry with IPv6 + entry1 := NewEntry("test") + if err := entry1.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry1); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Add second entry with IgnoreIPv6 - only IPv4 should be added + entry2 := NewEntry("test") + if err := entry2.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry2.AddPrefix("2002::/16"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry2, IgnoreIPv6); err != nil { + t.Fatalf("Add failed: %v", err) + } + + e, found := c.GetEntry("test") + if !found { + t.Fatal("Entry not found") + } + + // Should have IPv4 now + _, err4 := e.GetIPv4Set() + if err4 != nil { + t.Errorf("GetIPv4Set failed: %v", err4) + } +} + +func TestContainerGetEntry(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Test case insensitivity + testCases := []string{"test", "TEST", "Test", " test ", " TEST "} + for _, tc := range testCases { + e, found := c.GetEntry(tc) + if !found { + t.Errorf("GetEntry(%q) not found", tc) + } + if e == nil { + t.Errorf("GetEntry(%q) returned nil entry", tc) + } + } +} + +func TestContainerGetEntry_NotFound(t *testing.T) { + c := NewContainer() + + e, found := c.GetEntry("nonexistent") + if found { + t.Error("GetEntry for nonexistent entry should return false") + } + if e != nil { + t.Error("GetEntry for nonexistent entry should return nil") + } +} + +func TestContainerLen(t *testing.T) { + c := NewContainer() + + if c.Len() != 0 { + t.Errorf("Empty container Len() = %d, want 0", c.Len()) + } + + // Add entries + for i := 0; i < 5; i++ { + entry := NewEntry("entry" + string(rune('0'+i))) + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + } + + if c.Len() != 5 { + t.Errorf("Container.Len() = %d, want 5", c.Len()) + } +} + +func TestContainerLoop(t *testing.T) { + c := NewContainer() + + // Add entries + names := []string{"entry1", "entry2", "entry3"} + for _, name := range names { + entry := NewEntry(name) + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + } + + // Loop through entries + count := 0 + for range c.Loop() { + count++ + } + + if count != 3 { + t.Errorf("Loop iterated %d times, want 3", count) + } +} + +func TestContainerRemove_CaseRemovePrefix(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("10.0.0.0/8"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Remove one prefix + removeEntry := NewEntry("test") + if err := removeEntry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + if err := c.Remove(removeEntry, CaseRemovePrefix); err != nil { + t.Fatalf("Remove failed: %v", err) + } + + // Entry should still exist + e, found := c.GetEntry("test") + if !found { + t.Error("Entry should still exist after removing prefix") + } + if e == nil { + t.Error("Entry should not be nil") + } +} + +func TestContainerRemove_CaseRemoveEntry(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Remove entire entry + removeEntry := NewEntry("test") + if err := c.Remove(removeEntry, CaseRemoveEntry); err != nil { + t.Fatalf("Remove failed: %v", err) + } + + // Entry should be gone + _, found := c.GetEntry("test") + if found { + t.Error("Entry should be removed") + } +} + +func TestContainerRemove_NotFound(t *testing.T) { + c := NewContainer() + + entry := NewEntry("nonexistent") + err := c.Remove(entry, CaseRemoveEntry) + if err == nil { + t.Error("Remove for nonexistent entry should return error") + } +} + +func TestContainerRemove_UnknownCase(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + removeEntry := NewEntry("test") + err := c.Remove(removeEntry, CaseRemove(999)) + if err == nil { + t.Error("Remove with unknown case should return error") + } +} + +func TestContainerRemove_CaseRemovePrefixIgnoreIPv4(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Remove only IPv6 prefix (ignore IPv4) + removeEntry := NewEntry("test") + if err := removeEntry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + if err := c.Remove(removeEntry, CaseRemovePrefix, IgnoreIPv4); err != nil { + t.Fatalf("Remove failed: %v", err) + } +} + +func TestContainerRemove_CaseRemovePrefixIgnoreIPv6(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Remove only IPv4 prefix (ignore IPv6) + removeEntry := NewEntry("test") + if err := removeEntry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + if err := c.Remove(removeEntry, CaseRemovePrefix, IgnoreIPv6); err != nil { + t.Fatalf("Remove failed: %v", err) + } +} + +func TestContainerRemove_CaseRemoveEntryIgnoreIPv4(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Remove entry with IgnoreIPv4 - should only clear IPv6 + removeEntry := NewEntry("test") + if err := c.Remove(removeEntry, CaseRemoveEntry, IgnoreIPv4); err != nil { + t.Fatalf("Remove failed: %v", err) + } + + // Entry should still exist + e, found := c.GetEntry("test") + if !found { + t.Error("Entry should still exist") + } + + // IPv4 should still exist, IPv6 should be gone + _, err4 := e.GetIPv4Set() + if err4 != nil { + t.Errorf("GetIPv4Set failed: %v", err4) + } +} + +func TestContainerRemove_CaseRemoveEntryIgnoreIPv6(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Remove entry with IgnoreIPv6 - should only clear IPv4 + removeEntry := NewEntry("test") + if err := c.Remove(removeEntry, CaseRemoveEntry, IgnoreIPv6); err != nil { + t.Fatalf("Remove failed: %v", err) + } + + // Entry should still exist + e, found := c.GetEntry("test") + if !found { + t.Error("Entry should still exist") + } + + // IPv6 should still exist, IPv4 should be gone + _, err6 := e.GetIPv6Set() + if err6 != nil { + t.Errorf("GetIPv6Set failed: %v", err6) + } +} + +func TestContainerLookup_IPv4Address(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Lookup IP address + result, found, err := c.Lookup("192.168.1.100") + if err != nil { + t.Fatalf("Lookup failed: %v", err) + } + if !found { + t.Error("IP should be found") + } + if len(result) != 1 || result[0] != "TEST" { + t.Errorf("Lookup result = %v, want [TEST]", result) + } +} + +func TestContainerLookup_IPv4CIDR(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.0.0/16"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Lookup CIDR + result, found, err := c.Lookup("192.168.1.0/24") + if err != nil { + t.Fatalf("Lookup failed: %v", err) + } + if !found { + t.Error("CIDR should be found") + } + if len(result) != 1 || result[0] != "TEST" { + t.Errorf("Lookup result = %v, want [TEST]", result) + } +} + +func TestContainerLookup_IPv6Address(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Lookup IPv6 address + result, found, err := c.Lookup("2001:db8::1") + if err != nil { + t.Fatalf("Lookup failed: %v", err) + } + if !found { + t.Error("IP should be found") + } + if len(result) != 1 || result[0] != "TEST" { + t.Errorf("Lookup result = %v, want [TEST]", result) + } +} + +func TestContainerLookup_IPv6CIDR(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Lookup IPv6 CIDR + result, found, err := c.Lookup("2001:db8:1::/48") + if err != nil { + t.Fatalf("Lookup failed: %v", err) + } + if !found { + t.Error("CIDR should be found") + } + if len(result) != 1 || result[0] != "TEST" { + t.Errorf("Lookup result = %v, want [TEST]", result) + } +} + +func TestContainerLookup_NotFound(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Lookup non-existing IP + result, found, err := c.Lookup("10.0.0.1") + if err != nil { + t.Fatalf("Lookup failed: %v", err) + } + if found { + t.Error("IP should not be found") + } + if len(result) != 0 { + t.Errorf("Lookup result = %v, want empty", result) + } +} + +func TestContainerLookup_InvalidIP(t *testing.T) { + c := NewContainer() + + _, _, err := c.Lookup("invalid") + if err == nil { + t.Error("Lookup with invalid IP should return error") + } +} + +func TestContainerLookup_InvalidCIDR(t *testing.T) { + c := NewContainer() + + _, _, err := c.Lookup("192.168.1.0/33") + if err == nil { + t.Error("Lookup with invalid CIDR should return error") + } +} + +func TestContainerLookup_WithSearchList(t *testing.T) { + c := NewContainer() + + // Add multiple entries + entry1 := NewEntry("entry1") + if err := entry1.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry1); err != nil { + t.Fatalf("Add failed: %v", err) + } + + entry2 := NewEntry("entry2") + if err := entry2.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry2); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Lookup with search list + result, found, err := c.Lookup("192.168.1.100", "entry1") + if err != nil { + t.Fatalf("Lookup failed: %v", err) + } + if !found { + t.Error("IP should be found") + } + if len(result) != 1 || result[0] != "ENTRY1" { + t.Errorf("Lookup result = %v, want [ENTRY1]", result) + } +} + +func TestContainerLookup_WithEmptySearchList(t *testing.T) { + c := NewContainer() + + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Lookup with empty strings in search list (should be ignored) + result, found, err := c.Lookup("192.168.1.100", "", " ") + if err != nil { + t.Fatalf("Lookup failed: %v", err) + } + if !found { + t.Error("IP should be found") + } + if len(result) != 1 { + t.Errorf("Lookup result = %v, want length 1", result) + } +} + +func TestContainerIsValid_NilEntries(t *testing.T) { + // Test with invalid container (nil entries) + c := &container{entries: nil} + + // GetEntry should return false + _, found := c.GetEntry("test") + if found { + t.Error("GetEntry on invalid container should return false") + } + + // Len should return 0 + if c.Len() != 0 { + t.Errorf("Len on invalid container = %d, want 0", c.Len()) + } +} + +func TestContainerRemove_CaseRemovePrefix_NoBuilders(t *testing.T) { + c := NewContainer() + + // Add entry with no builders initially + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := c.Add(entry); err != nil { + t.Fatalf("Add failed: %v", err) + } + + // Try to remove with an entry that has different IP type + removeEntry := NewEntry("test") + if err := removeEntry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // This should not error but should create the builder for the existing entry + if err := c.Remove(removeEntry, CaseRemovePrefix); err != nil { + t.Fatalf("Remove failed: %v", err) + } +} diff --git a/lib/converter_test.go b/lib/converter_test.go new file mode 100644 index 00000000..54aff94d --- /dev/null +++ b/lib/converter_test.go @@ -0,0 +1,146 @@ +package lib + +import ( + "bytes" + "io" + "os" + "testing" +) + +func TestRegisterInputConverter(t *testing.T) { + testName := "test_input_conv_" + t.Name() + mockConv := &mockInputConverter{ + typeName: testName, + action: ActionAdd, + description: "Test input converter", + } + + err := RegisterInputConverter(testName, mockConv) + if err != nil { + t.Fatalf("RegisterInputConverter failed: %v", err) + } + + // Test registering duplicate + err = RegisterInputConverter(testName, mockConv) + if err != ErrDuplicatedConverter { + t.Errorf("RegisterInputConverter duplicate error = %v, want %v", err, ErrDuplicatedConverter) + } +} + +func TestRegisterInputConverter_TrimSpace(t *testing.T) { + testName := " test_input_conv_space_" + t.Name() + " " + mockConv := &mockInputConverter{ + typeName: testName, + action: ActionAdd, + description: "Test input converter", + } + + err := RegisterInputConverter(testName, mockConv) + if err != nil { + t.Fatalf("RegisterInputConverter failed: %v", err) + } + + // Test registering duplicate with trimmed name + err = RegisterInputConverter("test_input_conv_space_"+t.Name(), mockConv) + if err != ErrDuplicatedConverter { + t.Errorf("RegisterInputConverter should detect duplicate after trim") + } +} + +func TestRegisterOutputConverter(t *testing.T) { + testName := "test_output_conv_" + t.Name() + mockConv := &mockOutputConverter{ + typeName: testName, + action: ActionOutput, + description: "Test output converter", + } + + err := RegisterOutputConverter(testName, mockConv) + if err != nil { + t.Fatalf("RegisterOutputConverter failed: %v", err) + } + + // Test registering duplicate + err = RegisterOutputConverter(testName, mockConv) + if err != ErrDuplicatedConverter { + t.Errorf("RegisterOutputConverter duplicate error = %v, want %v", err, ErrDuplicatedConverter) + } +} + +func TestRegisterOutputConverter_TrimSpace(t *testing.T) { + testName := " test_output_conv_space_" + t.Name() + " " + mockConv := &mockOutputConverter{ + typeName: testName, + action: ActionOutput, + description: "Test output converter", + } + + err := RegisterOutputConverter(testName, mockConv) + if err != nil { + t.Fatalf("RegisterOutputConverter failed: %v", err) + } + + // Test registering duplicate with trimmed name + err = RegisterOutputConverter("test_output_conv_space_"+t.Name(), mockConv) + if err != ErrDuplicatedConverter { + t.Errorf("RegisterOutputConverter should detect duplicate after trim") + } +} + +func TestListInputConverter(t *testing.T) { + // Register a converter to ensure there's at least one + testName := "list_input_conv_" + t.Name() + mockConv := &mockInputConverter{ + typeName: testName, + action: ActionAdd, + description: "List test input converter", + } + RegisterInputConverter(testName, mockConv) + + // Capture stdout + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + ListInputConverter() + + w.Close() + os.Stdout = old + + var buf bytes.Buffer + io.Copy(&buf, r) + output := buf.String() + + if len(output) == 0 { + t.Error("ListInputConverter should produce output") + } +} + +func TestListOutputConverter(t *testing.T) { + // Register a converter to ensure there's at least one + testName := "list_output_conv_" + t.Name() + mockConv := &mockOutputConverter{ + typeName: testName, + action: ActionOutput, + description: "List test output converter", + } + RegisterOutputConverter(testName, mockConv) + + // Capture stdout + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + ListOutputConverter() + + w.Close() + os.Stdout = old + + var buf bytes.Buffer + io.Copy(&buf, r) + output := buf.String() + + if len(output) == 0 { + t.Error("ListOutputConverter should produce output") + } +} diff --git a/lib/entry_test.go b/lib/entry_test.go new file mode 100644 index 00000000..c599360e --- /dev/null +++ b/lib/entry_test.go @@ -0,0 +1,612 @@ +package lib + +import ( + "net" + "net/netip" + "testing" +) + +func TestNewEntry(t *testing.T) { + tests := []struct { + name string + want string + }{ + {"test", "TEST"}, + {" Test ", "TEST"}, + {"UPPER", "UPPER"}, + {"lower", "LOWER"}, + {" ", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + entry := NewEntry(tt.name) + if entry.GetName() != tt.want { + t.Errorf("NewEntry(%q).GetName() = %q, want %q", tt.name, entry.GetName(), tt.want) + } + }) + } +} + +func TestEntryAddPrefix_IPv4String(t *testing.T) { + entry := NewEntry("test") + + // Test adding IPv4 address + if err := entry.AddPrefix("192.168.1.1"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + if !entry.hasIPv4Builder() { + t.Error("Expected IPv4 builder to be set") + } + + // Test adding IPv4 CIDR + if err := entry.AddPrefix("10.0.0.0/8"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } +} + +func TestEntryAddPrefix_IPv6String(t *testing.T) { + entry := NewEntry("test") + + // Test adding IPv6 address + if err := entry.AddPrefix("2001:db8::1"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + if !entry.hasIPv6Builder() { + t.Error("Expected IPv6 builder to be set") + } + + // Test adding IPv6 CIDR + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } +} + +func TestEntryAddPrefix_NetIP(t *testing.T) { + entry := NewEntry("test") + + // Test adding net.IP IPv4 + ipv4 := net.ParseIP("192.168.1.1") + if err := entry.AddPrefix(ipv4); err != nil { + t.Fatalf("AddPrefix net.IP IPv4 failed: %v", err) + } + + // Test adding net.IP IPv6 + ipv6 := net.ParseIP("2001:db8::1") + if err := entry.AddPrefix(ipv6); err != nil { + t.Fatalf("AddPrefix net.IP IPv6 failed: %v", err) + } +} + +func TestEntryAddPrefix_NetIPNet(t *testing.T) { + entry := NewEntry("test") + + // Test adding *net.IPNet IPv4 + _, ipnet4, _ := net.ParseCIDR("10.0.0.0/8") + if err := entry.AddPrefix(ipnet4); err != nil { + t.Fatalf("AddPrefix *net.IPNet IPv4 failed: %v", err) + } + + // Test adding *net.IPNet IPv6 + _, ipnet6, _ := net.ParseCIDR("2001:db8::/32") + if err := entry.AddPrefix(ipnet6); err != nil { + t.Fatalf("AddPrefix *net.IPNet IPv6 failed: %v", err) + } +} + +func TestEntryAddPrefix_NetipAddr(t *testing.T) { + entry := NewEntry("test") + + // Test adding netip.Addr IPv4 + addr4 := netip.MustParseAddr("192.168.1.1") + if err := entry.AddPrefix(addr4); err != nil { + t.Fatalf("AddPrefix netip.Addr IPv4 failed: %v", err) + } + + // Test adding netip.Addr IPv6 + addr6 := netip.MustParseAddr("2001:db8::1") + if err := entry.AddPrefix(addr6); err != nil { + t.Fatalf("AddPrefix netip.Addr IPv6 failed: %v", err) + } +} + +func TestEntryAddPrefix_NetipAddrPointer(t *testing.T) { + entry := NewEntry("test") + + // Test adding *netip.Addr IPv4 + addr4 := netip.MustParseAddr("192.168.1.1") + if err := entry.AddPrefix(&addr4); err != nil { + t.Fatalf("AddPrefix *netip.Addr IPv4 failed: %v", err) + } + + // Test adding *netip.Addr IPv6 + addr6 := netip.MustParseAddr("2001:db8::1") + if err := entry.AddPrefix(&addr6); err != nil { + t.Fatalf("AddPrefix *netip.Addr IPv6 failed: %v", err) + } +} + +func TestEntryAddPrefix_NetipPrefix(t *testing.T) { + entry := NewEntry("test") + + // Test adding netip.Prefix IPv4 + prefix4 := netip.MustParsePrefix("10.0.0.0/8") + if err := entry.AddPrefix(prefix4); err != nil { + t.Fatalf("AddPrefix netip.Prefix IPv4 failed: %v", err) + } + + // Test adding netip.Prefix IPv6 + prefix6 := netip.MustParsePrefix("2001:db8::/32") + if err := entry.AddPrefix(prefix6); err != nil { + t.Fatalf("AddPrefix netip.Prefix IPv6 failed: %v", err) + } +} + +func TestEntryAddPrefix_NetipPrefixPointer(t *testing.T) { + entry := NewEntry("test") + + // Test adding *netip.Prefix IPv4 + prefix4 := netip.MustParsePrefix("10.0.0.0/8") + if err := entry.AddPrefix(&prefix4); err != nil { + t.Fatalf("AddPrefix *netip.Prefix IPv4 failed: %v", err) + } + + // Test adding *netip.Prefix IPv6 + prefix6 := netip.MustParsePrefix("2001:db8::/32") + if err := entry.AddPrefix(&prefix6); err != nil { + t.Fatalf("AddPrefix *netip.Prefix IPv6 failed: %v", err) + } +} + +func TestEntryAddPrefix_CommentLine(t *testing.T) { + entry := NewEntry("test") + + // Test comment lines - these should either return ErrCommentLine or ErrInvalidIPType + // because the processPrefix function returns ErrCommentLine for empty strings after + // stripping comments, and AddPrefix then passes nil to add() which returns ErrInvalidIPType + comments := []string{ + "# comment", + "// comment", + "/* comment */", + " # comment with leading spaces", + } + + for _, comment := range comments { + err := entry.AddPrefix(comment) + // After stripping comments, the string is empty, and processPrefix returns ErrCommentLine + // AddPrefix checks for ErrCommentLine and skips it, but then calls add with nil which + // returns ErrInvalidIPType. This is expected behavior. + if err != nil && err != ErrCommentLine && err != ErrInvalidIPType { + t.Errorf("AddPrefix(%q) unexpected error: %v", comment, err) + } + } +} + +func TestEntryAddPrefix_InvalidInput(t *testing.T) { + entry := NewEntry("test") + + // Test invalid inputs + invalidInputs := []string{ + "invalid", + "192.168.1.256", // Invalid IP + "10.0.0.0/33", // Invalid prefix length + "2001:db8::gggg", + } + + for _, input := range invalidInputs { + err := entry.AddPrefix(input) + if err == nil { + t.Errorf("AddPrefix(%q) expected error, got nil", input) + } + } +} + +func TestEntryAddPrefix_UnsupportedType(t *testing.T) { + entry := NewEntry("test") + + err := entry.AddPrefix(12345) // int is not supported + if err != ErrInvalidPrefixType { + t.Errorf("AddPrefix(int) = %v, want %v", err, ErrInvalidPrefixType) + } +} + +func TestEntryRemovePrefix(t *testing.T) { + entry := NewEntry("test") + + // Add some prefixes first + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Remove a prefix + if err := entry.RemovePrefix("192.168.1.0/24"); err != nil { + t.Fatalf("RemovePrefix failed: %v", err) + } + if err := entry.RemovePrefix("2001:db8::/32"); err != nil { + t.Fatalf("RemovePrefix failed: %v", err) + } +} + +func TestEntryRemovePrefix_CommentLine(t *testing.T) { + entry := NewEntry("test") + + // Test comment lines - similar to AddPrefix, after stripping comments, + // the string is empty, and processPrefix returns ErrCommentLine. + // RemovePrefix checks for ErrCommentLine and skips it, but then calls + // remove with nil which returns ErrInvalidIPType. + err := entry.RemovePrefix("# comment") + if err != nil && err != ErrCommentLine && err != ErrInvalidIPType { + t.Errorf("RemovePrefix with comment unexpected error: %v", err) + } +} + +func TestEntryRemovePrefix_NoBuilder(t *testing.T) { + entry := NewEntry("test") + + // Try to remove from empty entry - should not error + if err := entry.RemovePrefix("192.168.1.0/24"); err != nil { + // Error is expected if no builder exists + t.Logf("RemovePrefix from empty entry: %v", err) + } +} + +func TestEntryMarshalPrefix(t *testing.T) { + entry := NewEntry("test") + + // Add prefixes + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Marshal all prefixes + prefixes, err := entry.MarshalPrefix() + if err != nil { + t.Fatalf("MarshalPrefix failed: %v", err) + } + + if len(prefixes) != 2 { + t.Errorf("MarshalPrefix returned %d prefixes, want 2", len(prefixes)) + } +} + +func TestEntryMarshalPrefix_IgnoreIPv4(t *testing.T) { + entry := NewEntry("test") + + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Marshal with IgnoreIPv4 + prefixes, err := entry.MarshalPrefix(IgnoreIPv4) + if err != nil { + t.Fatalf("MarshalPrefix failed: %v", err) + } + + // Should only have IPv6 + for _, p := range prefixes { + if p.Addr().Is4() { + t.Error("Expected no IPv4 prefixes when IgnoreIPv4 is set") + } + } +} + +func TestEntryMarshalPrefix_IgnoreIPv6(t *testing.T) { + entry := NewEntry("test") + + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Marshal with IgnoreIPv6 + prefixes, err := entry.MarshalPrefix(IgnoreIPv6) + if err != nil { + t.Fatalf("MarshalPrefix failed: %v", err) + } + + // Should only have IPv4 + for _, p := range prefixes { + if p.Addr().Is6() { + t.Error("Expected no IPv6 prefixes when IgnoreIPv6 is set") + } + } +} + +func TestEntryMarshalPrefix_Empty(t *testing.T) { + entry := NewEntry("test") + + // Marshal from empty entry + _, err := entry.MarshalPrefix() + if err == nil { + t.Error("MarshalPrefix from empty entry should return error") + } +} + +func TestEntryMarshalIPRange(t *testing.T) { + entry := NewEntry("test") + + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Marshal IP ranges + ranges, err := entry.MarshalIPRange() + if err != nil { + t.Fatalf("MarshalIPRange failed: %v", err) + } + + if len(ranges) != 2 { + t.Errorf("MarshalIPRange returned %d ranges, want 2", len(ranges)) + } +} + +func TestEntryMarshalIPRange_IgnoreIPv4(t *testing.T) { + entry := NewEntry("test") + + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Marshal with IgnoreIPv4 + ranges, err := entry.MarshalIPRange(IgnoreIPv4) + if err != nil { + t.Fatalf("MarshalIPRange failed: %v", err) + } + + // Should only have IPv6 + for _, r := range ranges { + if r.From().Is4() { + t.Error("Expected no IPv4 ranges when IgnoreIPv4 is set") + } + } +} + +func TestEntryMarshalIPRange_IgnoreIPv6(t *testing.T) { + entry := NewEntry("test") + + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Marshal with IgnoreIPv6 + ranges, err := entry.MarshalIPRange(IgnoreIPv6) + if err != nil { + t.Fatalf("MarshalIPRange failed: %v", err) + } + + // Should only have IPv4 + for _, r := range ranges { + if r.From().Is6() { + t.Error("Expected no IPv6 ranges when IgnoreIPv6 is set") + } + } +} + +func TestEntryMarshalIPRange_Empty(t *testing.T) { + entry := NewEntry("test") + + // Marshal from empty entry + _, err := entry.MarshalIPRange() + if err == nil { + t.Error("MarshalIPRange from empty entry should return error") + } +} + +func TestEntryMarshalText(t *testing.T) { + entry := NewEntry("test") + + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Marshal text + cidrList, err := entry.MarshalText() + if err != nil { + t.Fatalf("MarshalText failed: %v", err) + } + + if len(cidrList) != 2 { + t.Errorf("MarshalText returned %d items, want 2", len(cidrList)) + } +} + +func TestEntryMarshalText_IgnoreIPv4(t *testing.T) { + entry := NewEntry("test") + + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Marshal with IgnoreIPv4 + cidrList, err := entry.MarshalText(IgnoreIPv4) + if err != nil { + t.Fatalf("MarshalText failed: %v", err) + } + + if len(cidrList) != 1 { + t.Errorf("MarshalText returned %d items, want 1", len(cidrList)) + } +} + +func TestEntryMarshalText_IgnoreIPv6(t *testing.T) { + entry := NewEntry("test") + + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + // Marshal with IgnoreIPv6 + cidrList, err := entry.MarshalText(IgnoreIPv6) + if err != nil { + t.Fatalf("MarshalText failed: %v", err) + } + + if len(cidrList) != 1 { + t.Errorf("MarshalText returned %d items, want 1", len(cidrList)) + } +} + +func TestEntryMarshalText_Empty(t *testing.T) { + entry := NewEntry("test") + + // Marshal from empty entry + _, err := entry.MarshalText() + if err == nil { + t.Error("MarshalText from empty entry should return error") + } +} + +func TestEntryGetIPv4Set(t *testing.T) { + entry := NewEntry("test") + + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + ipset, err := entry.GetIPv4Set() + if err != nil { + t.Fatalf("GetIPv4Set failed: %v", err) + } + + if ipset == nil { + t.Error("GetIPv4Set returned nil") + } +} + +func TestEntryGetIPv4Set_NoIPv4(t *testing.T) { + entry := NewEntry("test") + + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + _, err := entry.GetIPv4Set() + if err == nil { + t.Error("GetIPv4Set should return error when no IPv4 data") + } +} + +func TestEntryGetIPv6Set(t *testing.T) { + entry := NewEntry("test") + + if err := entry.AddPrefix("2001:db8::/32"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + ipset, err := entry.GetIPv6Set() + if err != nil { + t.Fatalf("GetIPv6Set failed: %v", err) + } + + if ipset == nil { + t.Error("GetIPv6Set returned nil") + } +} + +func TestEntryGetIPv6Set_NoIPv6(t *testing.T) { + entry := NewEntry("test") + + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + t.Fatalf("AddPrefix failed: %v", err) + } + + _, err := entry.GetIPv6Set() + if err == nil { + t.Error("GetIPv6Set should return error when no IPv6 data") + } +} + +func TestEntryAddPrefix_IPv4MappedIPv6(t *testing.T) { + entry := NewEntry("test") + + // Test IPv4-mapped IPv6 prefix + prefix := netip.MustParsePrefix("::ffff:192.168.1.0/120") + if err := entry.AddPrefix(prefix); err != nil { + t.Fatalf("AddPrefix IPv4-mapped IPv6 prefix failed: %v", err) + } + + // Should be stored as IPv4 + if !entry.hasIPv4Builder() { + t.Error("IPv4-mapped IPv6 should be stored as IPv4") + } +} + +func TestEntryAddPrefix_IPv4MappedIPv6InvalidBits(t *testing.T) { + entry := NewEntry("test") + + // Test IPv4-mapped IPv6 prefix with invalid bits (<96) + prefix := netip.MustParsePrefix("::ffff:192.168.1.0/64") + err := entry.AddPrefix(prefix) + if err != ErrInvalidPrefix { + t.Errorf("AddPrefix with invalid IPv4-mapped bits = %v, want %v", err, ErrInvalidPrefix) + } +} + +func TestEntryAddPrefix_InvalidCIDRWithIPv4MappedIPv6(t *testing.T) { + entry := NewEntry("test") + + // This tests the edge case where network.String() contains "::" + // but the address unmaps to IPv4 + err := entry.AddPrefix("::ffff:192.168.1.1/128") + // This should be handled as invalid based on the code logic + if err != nil && err != ErrInvalidCIDR { + t.Logf("AddPrefix with IPv4-mapped IPv6 CIDR: %v", err) + } +} + +func TestEntryAddPrefix_IPv4MappedIPv6PrefixPointer(t *testing.T) { + entry := NewEntry("test") + + // Test *netip.Prefix with IPv4-mapped IPv6 + prefix := netip.MustParsePrefix("::ffff:192.168.1.0/120") + if err := entry.AddPrefix(&prefix); err != nil { + t.Fatalf("AddPrefix *netip.Prefix IPv4-mapped failed: %v", err) + } + + // Should be stored as IPv4 + if !entry.hasIPv4Builder() { + t.Error("IPv4-mapped IPv6 should be stored as IPv4") + } +} + +func TestEntryAddPrefix_IPv4MappedIPv6PrefixPointerInvalidBits(t *testing.T) { + entry := NewEntry("test") + + // Test *netip.Prefix with IPv4-mapped IPv6 invalid bits (<96) + prefix := netip.MustParsePrefix("::ffff:192.168.1.0/64") + err := entry.AddPrefix(&prefix) + if err != ErrInvalidPrefix { + t.Errorf("AddPrefix with invalid IPv4-mapped bits = %v, want %v", err, ErrInvalidPrefix) + } +} diff --git a/lib/error_test.go b/lib/error_test.go new file mode 100644 index 00000000..fb2f280f --- /dev/null +++ b/lib/error_test.go @@ -0,0 +1,55 @@ +package lib + +import ( + "testing" +) + +func TestErrorVariables(t *testing.T) { + tests := []struct { + name string + err error + want string + }{ + {"ErrDuplicatedConverter", ErrDuplicatedConverter, "duplicated converter"}, + {"ErrUnknownAction", ErrUnknownAction, "unknown action"}, + {"ErrNotSupportedFormat", ErrNotSupportedFormat, "not supported format"}, + {"ErrInvalidIPType", ErrInvalidIPType, "invalid IP type"}, + {"ErrInvalidIP", ErrInvalidIP, "invalid IP address"}, + {"ErrInvalidIPLength", ErrInvalidIPLength, "invalid IP address length"}, + {"ErrInvalidIPNet", ErrInvalidIPNet, "invalid IPNet address"}, + {"ErrInvalidCIDR", ErrInvalidCIDR, "invalid CIDR"}, + {"ErrInvalidPrefix", ErrInvalidPrefix, "invalid prefix"}, + {"ErrInvalidPrefixType", ErrInvalidPrefixType, "invalid prefix type"}, + {"ErrCommentLine", ErrCommentLine, "comment line"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.err.Error() != tt.want { + t.Errorf("%s.Error() = %s, want %s", tt.name, tt.err.Error(), tt.want) + } + }) + } +} + +func TestErrorsAreNotNil(t *testing.T) { + errors := []error{ + ErrDuplicatedConverter, + ErrUnknownAction, + ErrNotSupportedFormat, + ErrInvalidIPType, + ErrInvalidIP, + ErrInvalidIPLength, + ErrInvalidIPNet, + ErrInvalidCIDR, + ErrInvalidPrefix, + ErrInvalidPrefixType, + ErrCommentLine, + } + + for _, err := range errors { + if err == nil { + t.Error("Expected error to be non-nil") + } + } +} diff --git a/lib/instance_test.go b/lib/instance_test.go new file mode 100644 index 00000000..dbe6ed10 --- /dev/null +++ b/lib/instance_test.go @@ -0,0 +1,604 @@ +package lib + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" +) + +func TestNewInstance(t *testing.T) { + inst, err := NewInstance() + if err != nil { + t.Fatalf("NewInstance failed: %v", err) + } + if inst == nil { + t.Fatal("NewInstance returned nil") + } +} + +func TestInstanceAddInput(t *testing.T) { + inst, _ := NewInstance() + mockConv := &mockInputConverter{ + typeName: "test", + action: ActionAdd, + description: "Test", + } + + inst.AddInput(mockConv) + + // Verify by running + container := NewContainer() + err := inst.RunInput(container) + if err != nil { + t.Fatalf("RunInput failed: %v", err) + } +} + +func TestInstanceAddOutput(t *testing.T) { + inst, _ := NewInstance() + mockConv := &mockOutputConverter{ + typeName: "test", + action: ActionOutput, + description: "Test", + } + + inst.AddOutput(mockConv) + + // Verify by running + container := NewContainer() + err := inst.RunOutput(container) + if err != nil { + t.Fatalf("RunOutput failed: %v", err) + } +} + +func TestInstanceResetInput(t *testing.T) { + inst, _ := NewInstance() + mockConv := &mockInputConverter{ + typeName: "test", + action: ActionAdd, + description: "Test", + } + + inst.AddInput(mockConv) + inst.ResetInput() + + // After reset, RunInput should not process anything + container := NewContainer() + err := inst.RunInput(container) + if err != nil { + t.Fatalf("RunInput failed: %v", err) + } +} + +func TestInstanceResetOutput(t *testing.T) { + inst, _ := NewInstance() + mockConv := &mockOutputConverter{ + typeName: "test", + action: ActionOutput, + description: "Test", + } + + inst.AddOutput(mockConv) + inst.ResetOutput() + + // After reset, RunOutput should not process anything + container := NewContainer() + err := inst.RunOutput(container) + if err != nil { + t.Fatalf("RunOutput failed: %v", err) + } +} + +func TestInstanceRunInput(t *testing.T) { + inst, _ := NewInstance() + + inputConv := &mockInputConverterWithData{ + mockInputConverter: mockInputConverter{ + typeName: "test", + action: ActionAdd, + description: "Test", + }, + } + + inst.AddInput(inputConv) + + container := NewContainer() + err := inst.RunInput(container) + if err != nil { + t.Fatalf("RunInput failed: %v", err) + } +} + +func TestInstanceRunOutput(t *testing.T) { + inst, _ := NewInstance() + + outputConv := &mockOutputConverter{ + typeName: "test", + action: ActionOutput, + description: "Test", + } + + inst.AddOutput(outputConv) + + container := NewContainer() + err := inst.RunOutput(container) + if err != nil { + t.Fatalf("RunOutput failed: %v", err) + } +} + +func TestInstanceRun_NoInputOrOutput(t *testing.T) { + inst, _ := NewInstance() + + err := inst.Run() + if err == nil { + t.Error("Run should fail when no input or output is specified") + } +} + +func TestInstanceRun_NoInput(t *testing.T) { + inst, _ := NewInstance() + + inst.AddOutput(&mockOutputConverter{ + typeName: "test", + action: ActionOutput, + description: "Test", + }) + + err := inst.Run() + if err == nil { + t.Error("Run should fail when no input is specified") + } +} + +func TestInstanceRun_NoOutput(t *testing.T) { + inst, _ := NewInstance() + + inst.AddInput(&mockInputConverter{ + typeName: "test", + action: ActionAdd, + description: "Test", + }) + + err := inst.Run() + if err == nil { + t.Error("Run should fail when no output is specified") + } +} + +func TestInstanceRun_Success(t *testing.T) { + inst, _ := NewInstance() + + inst.AddInput(&mockInputConverter{ + typeName: "test", + action: ActionAdd, + description: "Test", + }) + + inst.AddOutput(&mockOutputConverter{ + typeName: "test", + action: ActionOutput, + description: "Test", + }) + + err := inst.Run() + if err != nil { + t.Fatalf("Run failed: %v", err) + } +} + +func TestInstanceInitConfig_LocalFile(t *testing.T) { + // Create a temp config file + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.json") + + // Register mock converters for this test + inputType := "instance_test_input_" + t.Name() + outputType := "instance_test_output_" + t.Name() + + RegisterInputConfigCreator(inputType, func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + typeName: inputType, + action: action, + }, nil + }) + + RegisterOutputConfigCreator(outputType, func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + typeName: outputType, + action: action, + }, nil + }) + + configContent := `{ + "input": [{"type":"` + inputType + `","action":"add","args":{}}], + "output": [{"type":"` + outputType + `","action":"output","args":{}}] + }` + + err := os.WriteFile(configPath, []byte(configContent), 0644) + if err != nil { + t.Fatalf("Failed to write config file: %v", err) + } + + inst, _ := NewInstance() + err = inst.InitConfig(configPath) + if err != nil { + t.Fatalf("InitConfig failed: %v", err) + } + + // Should be able to run now + err = inst.Run() + if err != nil { + t.Fatalf("Run failed after InitConfig: %v", err) + } +} + +func TestInstanceInitConfig_LocalFileWithComments(t *testing.T) { + // Create a temp config file with JSON comments + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.json") + + // Register mock converters for this test + inputType := "instance_test_input_comments_" + t.Name() + outputType := "instance_test_output_comments_" + t.Name() + + RegisterInputConfigCreator(inputType, func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + typeName: inputType, + action: action, + }, nil + }) + + RegisterOutputConfigCreator(outputType, func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + typeName: outputType, + action: action, + }, nil + }) + + // JSON with comments and trailing comma + configContent := `{ + // This is a comment + "input": [ + {"type":"` + inputType + `","action":"add","args":{}}, + ], + /* Multi-line comment */ + "output": [ + {"type":"` + outputType + `","action":"output","args":{}}, + ], + }` + + err := os.WriteFile(configPath, []byte(configContent), 0644) + if err != nil { + t.Fatalf("Failed to write config file: %v", err) + } + + inst, _ := NewInstance() + err = inst.InitConfig(configPath) + if err != nil { + t.Fatalf("InitConfig failed: %v", err) + } +} + +func TestInstanceInitConfig_RemoteURL(t *testing.T) { + // Register mock converters for this test + inputType := "instance_test_input_remote_" + t.Name() + outputType := "instance_test_output_remote_" + t.Name() + + RegisterInputConfigCreator(inputType, func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + typeName: inputType, + action: action, + }, nil + }) + + RegisterOutputConfigCreator(outputType, func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + typeName: outputType, + action: action, + }, nil + }) + + configContent := `{ + "input": [{"type":"` + inputType + `","action":"add","args":{}}], + "output": [{"type":"` + outputType + `","action":"output","args":{}}] + }` + + // Create test server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(configContent)) + })) + defer server.Close() + + inst, _ := NewInstance() + err := inst.InitConfig(server.URL) + if err != nil { + t.Fatalf("InitConfig from remote URL failed: %v", err) + } + + // Should be able to run now + err = inst.Run() + if err != nil { + t.Fatalf("Run failed after InitConfig from remote: %v", err) + } +} + +func TestInstanceInitConfig_FileNotFound(t *testing.T) { + inst, _ := NewInstance() + err := inst.InitConfig("/nonexistent/path/to/config.json") + if err == nil { + t.Error("InitConfig should fail for non-existent file") + } +} + +func TestInstanceInitConfig_InvalidJSON(t *testing.T) { + // Create a temp config file with invalid JSON + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.json") + + err := os.WriteFile(configPath, []byte("{invalid json}"), 0644) + if err != nil { + t.Fatalf("Failed to write config file: %v", err) + } + + inst, _ := NewInstance() + err = inst.InitConfig(configPath) + if err == nil { + t.Error("InitConfig should fail for invalid JSON") + } +} + +func TestInstanceInitConfigFromBytes(t *testing.T) { + // Register mock converters for this test + inputType := "instance_test_input_bytes_" + t.Name() + outputType := "instance_test_output_bytes_" + t.Name() + + RegisterInputConfigCreator(inputType, func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + typeName: inputType, + action: action, + }, nil + }) + + RegisterOutputConfigCreator(outputType, func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + typeName: outputType, + action: action, + }, nil + }) + + configContent := []byte(`{ + "input": [{"type":"` + inputType + `","action":"add","args":{}}], + "output": [{"type":"` + outputType + `","action":"output","args":{}}] + }`) + + inst, _ := NewInstance() + err := inst.InitConfigFromBytes(configContent) + if err != nil { + t.Fatalf("InitConfigFromBytes failed: %v", err) + } + + // Should be able to run now + err = inst.Run() + if err != nil { + t.Fatalf("Run failed after InitConfigFromBytes: %v", err) + } +} + +func TestInstanceInitConfigFromBytes_InvalidJSON(t *testing.T) { + inst, _ := NewInstance() + err := inst.InitConfigFromBytes([]byte("{invalid json}")) + if err == nil { + t.Error("InitConfigFromBytes should fail for invalid JSON") + } +} + +// Mock input converter that adds data to container +type mockInputConverterWithData struct { + mockInputConverter +} + +func (m *mockInputConverterWithData) Input(c Container) (Container, error) { + entry := NewEntry("test") + if err := entry.AddPrefix("192.168.1.0/24"); err != nil { + return nil, err + } + if err := c.Add(entry); err != nil { + return nil, err + } + return c, nil +} + +// Mock input converter that returns error +type mockInputConverterWithError struct { + mockInputConverter + err error +} + +func (m *mockInputConverterWithError) Input(c Container) (Container, error) { + return nil, m.err +} + +// Mock output converter that returns error +type mockOutputConverterWithError struct { + mockOutputConverter + err error +} + +func (m *mockOutputConverterWithError) Output(c Container) error { + return m.err +} + +func TestInstanceRunInput_Error(t *testing.T) { + inst, _ := NewInstance() + + inputConv := &mockInputConverterWithError{ + mockInputConverter: mockInputConverter{ + typeName: "test", + action: ActionAdd, + description: "Test", + }, + err: ErrInvalidIP, + } + + inst.AddInput(inputConv) + + container := NewContainer() + err := inst.RunInput(container) + if err != ErrInvalidIP { + t.Errorf("RunInput error = %v, want %v", err, ErrInvalidIP) + } +} + +func TestInstanceRunOutput_Error(t *testing.T) { + inst, _ := NewInstance() + + outputConv := &mockOutputConverterWithError{ + mockOutputConverter: mockOutputConverter{ + typeName: "test", + action: ActionOutput, + description: "Test", + }, + err: ErrNotSupportedFormat, + } + + inst.AddOutput(outputConv) + + container := NewContainer() + err := inst.RunOutput(container) + if err != ErrNotSupportedFormat { + t.Errorf("RunOutput error = %v, want %v", err, ErrNotSupportedFormat) + } +} + +func TestInstanceInitConfig_HTTPSPrefix(t *testing.T) { + // Register mock converters for this test + inputType := "instance_test_https_" + t.Name() + outputType := "instance_test_https_out_" + t.Name() + + RegisterInputConfigCreator(inputType, func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + typeName: inputType, + action: action, + }, nil + }) + + RegisterOutputConfigCreator(outputType, func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + typeName: outputType, + action: action, + }, nil + }) + + configContent := `{ + "input": [{"type":"` + inputType + `","action":"add","args":{}}], + "output": [{"type":"` + outputType + `","action":"output","args":{}}] + }` + + // Create test TLS server + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(configContent)) + })) + defer server.Close() + + // Note: This test will fail due to self-signed certificate + // But it tests the URL prefix detection logic + inst, _ := NewInstance() + _ = inst.InitConfig(server.URL) + // We don't check the error because TLS will fail with self-signed cert +} + +func TestInstanceInitConfig_WithSpaces(t *testing.T) { + // Create a temp config file + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.json") + + // Register mock converters for this test + inputType := "instance_test_spaces_" + t.Name() + outputType := "instance_test_spaces_out_" + t.Name() + + RegisterInputConfigCreator(inputType, func(action Action, data json.RawMessage) (InputConverter, error) { + return &mockInputConverter{ + typeName: inputType, + action: action, + }, nil + }) + + RegisterOutputConfigCreator(outputType, func(action Action, data json.RawMessage) (OutputConverter, error) { + return &mockOutputConverter{ + typeName: outputType, + action: action, + }, nil + }) + + configContent := `{ + "input": [{"type":"` + inputType + `","action":"add","args":{}}], + "output": [{"type":"` + outputType + `","action":"output","args":{}}] + }` + + err := os.WriteFile(configPath, []byte(configContent), 0644) + if err != nil { + t.Fatalf("Failed to write config file: %v", err) + } + + inst, _ := NewInstance() + // Add spaces around the path + err = inst.InitConfig(" " + configPath + " ") + if err != nil { + t.Fatalf("InitConfig with spaces failed: %v", err) + } +} + +func TestInstanceRun_InputError(t *testing.T) { + inst, _ := NewInstance() + + inst.AddInput(&mockInputConverterWithError{ + mockInputConverter: mockInputConverter{ + typeName: "test", + action: ActionAdd, + description: "Test", + }, + err: ErrInvalidIP, + }) + + inst.AddOutput(&mockOutputConverter{ + typeName: "test", + action: ActionOutput, + description: "Test", + }) + + err := inst.Run() + if err != ErrInvalidIP { + t.Errorf("Run should fail with input error, got %v, want %v", err, ErrInvalidIP) + } +} + +func TestInstanceRun_OutputError(t *testing.T) { + inst, _ := NewInstance() + + inst.AddInput(&mockInputConverter{ + typeName: "test", + action: ActionAdd, + description: "Test", + }) + + inst.AddOutput(&mockOutputConverterWithError{ + mockOutputConverter: mockOutputConverter{ + typeName: "test", + action: ActionOutput, + description: "Test", + }, + err: ErrNotSupportedFormat, + }) + + err := inst.Run() + if err != ErrNotSupportedFormat { + t.Errorf("Run should fail with output error, got %v, want %v", err, ErrNotSupportedFormat) + } +} diff --git a/lib/lib_test.go b/lib/lib_test.go new file mode 100644 index 00000000..e58be1b1 --- /dev/null +++ b/lib/lib_test.go @@ -0,0 +1,89 @@ +package lib + +import ( + "testing" +) + +func TestActionConstants(t *testing.T) { + tests := []struct { + action Action + want string + }{ + {ActionAdd, "add"}, + {ActionRemove, "remove"}, + {ActionOutput, "output"}, + } + + for _, tt := range tests { + if string(tt.action) != tt.want { + t.Errorf("Action constant = %s, want %s", tt.action, tt.want) + } + } +} + +func TestIPTypeConstants(t *testing.T) { + tests := []struct { + ipType IPType + want string + }{ + {IPv4, "ipv4"}, + {IPv6, "ipv6"}, + } + + for _, tt := range tests { + if string(tt.ipType) != tt.want { + t.Errorf("IPType constant = %s, want %s", tt.ipType, tt.want) + } + } +} + +func TestCaseRemoveConstants(t *testing.T) { + if CaseRemovePrefix != 0 { + t.Errorf("CaseRemovePrefix = %d, want 0", CaseRemovePrefix) + } + if CaseRemoveEntry != 1 { + t.Errorf("CaseRemoveEntry = %d, want 1", CaseRemoveEntry) + } +} + +func TestActionsRegistry(t *testing.T) { + if !ActionsRegistry[ActionAdd] { + t.Error("ActionAdd should be registered") + } + if !ActionsRegistry[ActionRemove] { + t.Error("ActionRemove should be registered") + } + if !ActionsRegistry[ActionOutput] { + t.Error("ActionOutput should be registered") + } + if ActionsRegistry["unknown"] { + t.Error("unknown action should not be registered") + } +} + +func TestIgnoreIPv4(t *testing.T) { + ipType := IgnoreIPv4() + if ipType != IPv4 { + t.Errorf("IgnoreIPv4() = %s, want %s", ipType, IPv4) + } +} + +func TestIgnoreIPv6(t *testing.T) { + ipType := IgnoreIPv6() + if ipType != IPv6 { + t.Errorf("IgnoreIPv6() = %s, want %s", ipType, IPv6) + } +} + +func TestIgnoreIPOption(t *testing.T) { + // Test that IgnoreIPOption functions return correct types + var opt4 IgnoreIPOption = IgnoreIPv4 + var opt6 IgnoreIPOption = IgnoreIPv6 + + if opt4() != IPv4 { + t.Errorf("IgnoreIPv4 option returned %s, want %s", opt4(), IPv4) + } + if opt6() != IPv6 { + t.Errorf("IgnoreIPv6 option returned %s, want %s", opt6(), IPv6) + } +} |
