summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoranthropic-code-agent[bot] <[email protected]>2026-03-06 20:53:28 +0000
committeranthropic-code-agent[bot] <[email protected]>2026-03-06 20:53:28 +0000
commitcd2ba4b2110cb81171ba2af2c95c9fd2e0ffc9d5 (patch)
tree5723ae5127597a84e94b4f3ba6ce746a191bf91e
parent698dc71e82a0c9d507e327b15b7ac9108412f373 (diff)
Add comprehensive unit tests for lib package with 92.6% coverageclaude/write-unit-tests-lib-package
Co-authored-by: Loyalsoldier <[email protected]>
-rw-r--r--lib/common_test.go293
-rw-r--r--lib/config_test.go437
-rw-r--r--lib/container_test.go635
-rw-r--r--lib/converter_test.go357
-rw-r--r--lib/coverage2_test.go261
-rw-r--r--lib/coverage_test.go221
-rw-r--r--lib/entry_test.go558
-rw-r--r--lib/error_test.go62
-rw-r--r--lib/instance_test.go609
-rw-r--r--lib/lib_test.go86
10 files changed, 3519 insertions, 0 deletions
diff --git a/lib/common_test.go b/lib/common_test.go
new file mode 100644
index 00000000..40212363
--- /dev/null
+++ b/lib/common_test.go
@@ -0,0 +1,293 @@
+package lib
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestGetRemoteURLContent(t *testing.T) {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ wantErr bool
+ errMessage string
+ want string
+ }{
+ {
+ name: "successful request",
+ handler: func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("test content"))
+ },
+ wantErr: false,
+ want: "test content",
+ },
+ {
+ name: "404 not found",
+ handler: func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ },
+ wantErr: true,
+ errMessage: "404 Not Found",
+ },
+ {
+ name: "500 internal server error",
+ handler: func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusInternalServerError)
+ },
+ wantErr: true,
+ errMessage: "500 Internal Server Error",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ server := httptest.NewServer(tt.handler)
+ defer server.Close()
+
+ got, err := GetRemoteURLContent(server.URL)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("GetRemoteURLContent() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if tt.wantErr {
+ if err != nil && tt.errMessage != "" && err.Error() != fmt.Sprintf("failed to get remote content -> %s: %s", server.URL, tt.errMessage) {
+ t.Errorf("GetRemoteURLContent() error message = %v, want substring %v", err.Error(), tt.errMessage)
+ }
+ return
+ }
+ if string(got) != tt.want {
+ t.Errorf("GetRemoteURLContent() = %q, want %q", string(got), tt.want)
+ }
+ })
+ }
+}
+
+func TestGetRemoteURLContentInvalidURL(t *testing.T) {
+ _, err := GetRemoteURLContent("http://invalid-url-that-does-not-exist-12345.com")
+ if err == nil {
+ t.Error("GetRemoteURLContent() expected error for invalid URL, got nil")
+ }
+}
+
+func TestGetRemoteURLReader(t *testing.T) {
+ tests := []struct {
+ name string
+ handler http.HandlerFunc
+ wantErr bool
+ errMessage string
+ want string
+ }{
+ {
+ name: "successful request",
+ handler: func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte("test reader content"))
+ },
+ wantErr: false,
+ want: "test reader content",
+ },
+ {
+ name: "404 not found",
+ handler: func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusNotFound)
+ },
+ wantErr: true,
+ errMessage: "404 Not Found",
+ },
+ {
+ name: "403 forbidden",
+ handler: func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusForbidden)
+ },
+ wantErr: true,
+ errMessage: "403 Forbidden",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ server := httptest.NewServer(tt.handler)
+ defer server.Close()
+
+ reader, err := GetRemoteURLReader(server.URL)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("GetRemoteURLReader() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if tt.wantErr {
+ if err != nil && tt.errMessage != "" && err.Error() != fmt.Sprintf("failed to get remote content -> %s: %s", server.URL, tt.errMessage) {
+ t.Errorf("GetRemoteURLReader() error message = %v, want substring %v", err.Error(), tt.errMessage)
+ }
+ return
+ }
+ defer reader.Close()
+ got, err := io.ReadAll(reader)
+ if err != nil {
+ t.Errorf("Failed to read from reader: %v", err)
+ }
+ if string(got) != tt.want {
+ t.Errorf("GetRemoteURLReader() content = %q, want %q", string(got), tt.want)
+ }
+ })
+ }
+}
+
+func TestGetRemoteURLReaderInvalidURL(t *testing.T) {
+ _, err := GetRemoteURLReader("http://invalid-url-that-does-not-exist-12345.com")
+ if err == nil {
+ t.Error("GetRemoteURLReader() expected error for invalid URL, got nil")
+ }
+}
+
+func TestGetIgnoreIPType(t *testing.T) {
+ tests := []struct {
+ name string
+ onlyIPType IPType
+ want IPType
+ }{
+ {
+ name: "IPv4 returns IgnoreIPv6",
+ onlyIPType: IPv4,
+ want: IPv6,
+ },
+ {
+ name: "IPv6 returns IgnoreIPv4",
+ onlyIPType: IPv6,
+ want: IPv4,
+ },
+ {
+ name: "empty string returns nil",
+ onlyIPType: IPType(""),
+ want: IPType(""),
+ },
+ {
+ name: "invalid type returns nil",
+ onlyIPType: IPType("invalid"),
+ want: IPType(""),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := GetIgnoreIPType(tt.onlyIPType)
+ if got == nil && tt.want == "" {
+ // nil is expected
+ return
+ }
+ if got == nil {
+ t.Errorf("GetIgnoreIPType(%q) = nil, want %q", tt.onlyIPType, tt.want)
+ return
+ }
+ result := got()
+ if result != tt.want {
+ t.Errorf("GetIgnoreIPType(%q)() = %q, want %q", tt.onlyIPType, result, tt.want)
+ }
+ })
+ }
+}
+
+func TestWantedListExtended_UnmarshalJSON(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ wantSlice []string
+ wantMap map[string][]string
+ wantErr bool
+ }{
+ {
+ name: "slice format",
+ input: `["item1", "item2", "item3"]`,
+ wantSlice: []string{"item1", "item2", "item3"},
+ wantMap: map[string][]string{},
+ wantErr: false,
+ },
+ {
+ name: "map format",
+ input: `{"key1": ["val1", "val2"], "key2": ["val3"]}`,
+ wantSlice: []string{},
+ wantMap: map[string][]string{"key1": {"val1", "val2"}, "key2": {"val3"}},
+ wantErr: false,
+ },
+ {
+ name: "empty slice",
+ input: `[]`,
+ wantSlice: []string{},
+ wantMap: map[string][]string{},
+ wantErr: false,
+ },
+ {
+ name: "empty map",
+ input: `{}`,
+ wantSlice: []string{},
+ wantMap: map[string][]string{},
+ wantErr: false,
+ },
+ {
+ name: "invalid json",
+ input: `{invalid}`,
+ wantSlice: nil,
+ wantMap: nil,
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ var w WantedListExtended
+ err := json.Unmarshal([]byte(tt.input), &w)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("WantedListExtended.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if tt.wantErr {
+ return
+ }
+
+ // Check slice
+ if tt.wantSlice == nil && w.TypeSlice != nil {
+ t.Errorf("WantedListExtended.TypeSlice = %v, want nil", w.TypeSlice)
+ } else if tt.wantSlice != nil {
+ if len(w.TypeSlice) != len(tt.wantSlice) {
+ t.Errorf("WantedListExtended.TypeSlice length = %d, want %d", len(w.TypeSlice), len(tt.wantSlice))
+ } else {
+ for i, v := range tt.wantSlice {
+ if w.TypeSlice[i] != v {
+ t.Errorf("WantedListExtended.TypeSlice[%d] = %q, want %q", i, w.TypeSlice[i], v)
+ }
+ }
+ }
+ }
+
+ // Check map
+ if tt.wantMap == nil && w.TypeMap != nil {
+ t.Errorf("WantedListExtended.TypeMap = %v, want nil", w.TypeMap)
+ } else if tt.wantMap != nil {
+ if len(w.TypeMap) != len(tt.wantMap) {
+ t.Errorf("WantedListExtended.TypeMap length = %d, want %d", len(w.TypeMap), len(tt.wantMap))
+ } else {
+ for k, v := range tt.wantMap {
+ gotV, ok := w.TypeMap[k]
+ if !ok {
+ t.Errorf("WantedListExtended.TypeMap missing key %q", k)
+ continue
+ }
+ if len(gotV) != len(v) {
+ t.Errorf("WantedListExtended.TypeMap[%q] length = %d, want %d", k, len(gotV), len(v))
+ continue
+ }
+ for i, val := range v {
+ if gotV[i] != val {
+ t.Errorf("WantedListExtended.TypeMap[%q][%d] = %q, want %q", k, i, gotV[i], val)
+ }
+ }
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/lib/config_test.go b/lib/config_test.go
new file mode 100644
index 00000000..3f2cff9c
--- /dev/null
+++ b/lib/config_test.go
@@ -0,0 +1,437 @@
+package lib
+
+import (
+ "encoding/json"
+ "testing"
+)
+
+func TestRegisterInputConfigCreator(t *testing.T) {
+ // Clear cache
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ err := RegisterInputConfigCreator("test", creator)
+ if err != nil {
+ t.Errorf("RegisterInputConfigCreator() error = %v, want nil", err)
+ }
+
+ // Verify creator was registered
+ if _, ok := inputConfigCreatorCache["test"]; !ok {
+ t.Error("Creator not found in inputConfigCreatorCache")
+ }
+}
+
+func TestRegisterInputConfigCreator_Duplicate(t *testing.T) {
+ // Clear cache
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ // Register first time
+ err := RegisterInputConfigCreator("test", creator)
+ if err != nil {
+ t.Errorf("RegisterInputConfigCreator() first call error = %v, want nil", err)
+ }
+
+ // Register duplicate
+ err = RegisterInputConfigCreator("test", creator)
+ if err == nil {
+ t.Error("RegisterInputConfigCreator() duplicate expected error, got nil")
+ }
+ if err.Error() != "config creator has already been registered" {
+ t.Errorf("RegisterInputConfigCreator() duplicate error = %v, want 'config creator has already been registered'", err)
+ }
+}
+
+func TestRegisterInputConfigCreator_CaseInsensitive(t *testing.T) {
+ // Clear cache
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ // Register with uppercase
+ err := RegisterInputConfigCreator("TEST", creator)
+ if err != nil {
+ t.Errorf("RegisterInputConfigCreator() error = %v, want nil", err)
+ }
+
+ // Verify lowercase key is used
+ if _, ok := inputConfigCreatorCache["test"]; !ok {
+ t.Error("Creator not found with lowercase key")
+ }
+}
+
+func TestRegisterOutputConfigCreator(t *testing.T) {
+ // Clear cache
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ err := RegisterOutputConfigCreator("test", creator)
+ if err != nil {
+ t.Errorf("RegisterOutputConfigCreator() error = %v, want nil", err)
+ }
+
+ // Verify creator was registered
+ if _, ok := outputConfigCreatorCache["test"]; !ok {
+ t.Error("Creator not found in outputConfigCreatorCache")
+ }
+}
+
+func TestRegisterOutputConfigCreator_Duplicate(t *testing.T) {
+ // Clear cache
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ // Register first time
+ err := RegisterOutputConfigCreator("test", creator)
+ if err != nil {
+ t.Errorf("RegisterOutputConfigCreator() first call error = %v, want nil", err)
+ }
+
+ // Register duplicate
+ err = RegisterOutputConfigCreator("test", creator)
+ if err == nil {
+ t.Error("RegisterOutputConfigCreator() duplicate expected error, got nil")
+ }
+ if err.Error() != "config creator has already been registered" {
+ t.Errorf("RegisterOutputConfigCreator() duplicate error = %v, want 'config creator has already been registered'", err)
+ }
+}
+
+func TestRegisterOutputConfigCreator_CaseInsensitive(t *testing.T) {
+ // Clear cache
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ // Register with uppercase
+ err := RegisterOutputConfigCreator("TEST", creator)
+ if err != nil {
+ t.Errorf("RegisterOutputConfigCreator() error = %v, want nil", err)
+ }
+
+ // Verify lowercase key is used
+ if _, ok := outputConfigCreatorCache["test"]; !ok {
+ t.Error("Creator not found with lowercase key")
+ }
+}
+
+func TestCreateInputConfig(t *testing.T) {
+ // Clear cache and register creator
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ RegisterInputConfigCreator("test", creator)
+
+ // Create config
+ conv, err := createInputConfig("test", ActionAdd, json.RawMessage(`{}`))
+ if err != nil {
+ t.Errorf("createInputConfig() error = %v, want nil", err)
+ }
+ if conv == nil {
+ t.Error("createInputConfig() returned nil converter")
+ }
+ if conv.GetType() != "test" {
+ t.Errorf("createInputConfig() type = %q, want %q", conv.GetType(), "test")
+ }
+}
+
+func TestCreateInputConfig_CaseInsensitive(t *testing.T) {
+ // Clear cache and register creator
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ RegisterInputConfigCreator("test", creator)
+
+ // Create config with uppercase
+ conv, err := createInputConfig("TEST", ActionAdd, json.RawMessage(`{}`))
+ if err != nil {
+ t.Errorf("createInputConfig() error = %v, want nil", err)
+ }
+ if conv == nil {
+ t.Error("createInputConfig() returned nil converter")
+ }
+}
+
+func TestCreateInputConfig_NotFound(t *testing.T) {
+ // Clear cache
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+
+ // Try to create non-existent config
+ _, err := createInputConfig("notfound", ActionAdd, json.RawMessage(`{}`))
+ if err == nil {
+ t.Error("createInputConfig() with unknown type expected error, got nil")
+ }
+ if err.Error() != "unknown config type" {
+ t.Errorf("createInputConfig() error = %v, want 'unknown config type'", err)
+ }
+}
+
+func TestCreateOutputConfig(t *testing.T) {
+ // Clear cache and register creator
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ RegisterOutputConfigCreator("test", creator)
+
+ // Create config
+ conv, err := createOutputConfig("test", ActionOutput, json.RawMessage(`{}`))
+ if err != nil {
+ t.Errorf("createOutputConfig() error = %v, want nil", err)
+ }
+ if conv == nil {
+ t.Error("createOutputConfig() returned nil converter")
+ }
+ if conv.GetType() != "test" {
+ t.Errorf("createOutputConfig() type = %q, want %q", conv.GetType(), "test")
+ }
+}
+
+func TestCreateOutputConfig_CaseInsensitive(t *testing.T) {
+ // Clear cache and register creator
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ RegisterOutputConfigCreator("test", creator)
+
+ // Create config with uppercase
+ conv, err := createOutputConfig("TEST", ActionOutput, json.RawMessage(`{}`))
+ if err != nil {
+ t.Errorf("createOutputConfig() error = %v, want nil", err)
+ }
+ if conv == nil {
+ t.Error("createOutputConfig() returned nil converter")
+ }
+}
+
+func TestCreateOutputConfig_NotFound(t *testing.T) {
+ // Clear cache
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ // Try to create non-existent config
+ _, err := createOutputConfig("notfound", ActionOutput, json.RawMessage(`{}`))
+ if err == nil {
+ t.Error("createOutputConfig() with unknown type expected error, got nil")
+ }
+ if err.Error() != "unknown config type" {
+ t.Errorf("createOutputConfig() error = %v, want 'unknown config type'", err)
+ }
+}
+
+func TestInputConvConfig_UnmarshalJSON(t *testing.T) {
+ // Clear cache and register creator
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ RegisterInputConfigCreator("test", creator)
+
+ // Test valid JSON
+ jsonData := `{"type": "test", "action": "add", "args": {}}`
+ var config inputConvConfig
+ err := json.Unmarshal([]byte(jsonData), &config)
+ if err != nil {
+ t.Errorf("inputConvConfig.UnmarshalJSON() error = %v, want nil", err)
+ }
+ if config.iType != "test" {
+ t.Errorf("inputConvConfig.iType = %q, want %q", config.iType, "test")
+ }
+ if config.action != ActionAdd {
+ t.Errorf("inputConvConfig.action = %q, want %q", config.action, ActionAdd)
+ }
+}
+
+func TestInputConvConfig_UnmarshalJSON_InvalidAction(t *testing.T) {
+ // Clear cache and register creator
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ RegisterInputConfigCreator("test", creator)
+
+ // Test invalid action
+ jsonData := `{"type": "test", "action": "invalid", "args": {}}`
+ var config inputConvConfig
+ err := json.Unmarshal([]byte(jsonData), &config)
+ if err == nil {
+ t.Error("inputConvConfig.UnmarshalJSON() with invalid action expected error, got nil")
+ }
+}
+
+func TestInputConvConfig_UnmarshalJSON_InvalidJSON(t *testing.T) {
+ jsonData := `{invalid json}`
+ var config inputConvConfig
+ err := json.Unmarshal([]byte(jsonData), &config)
+ if err == nil {
+ t.Error("inputConvConfig.UnmarshalJSON() with invalid JSON expected error, got nil")
+ }
+}
+
+func TestInputConvConfig_UnmarshalJSON_UnknownType(t *testing.T) {
+ // Clear cache
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+
+ jsonData := `{"type": "unknown", "action": "add", "args": {}}`
+ var config inputConvConfig
+ err := json.Unmarshal([]byte(jsonData), &config)
+ if err == nil {
+ t.Error("inputConvConfig.UnmarshalJSON() with unknown type expected error, got nil")
+ }
+}
+
+func TestOutputConvConfig_UnmarshalJSON(t *testing.T) {
+ // Clear cache and register creator
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ RegisterOutputConfigCreator("test", creator)
+
+ // Test valid JSON
+ jsonData := `{"type": "test", "action": "output", "args": {}}`
+ var config outputConvConfig
+ err := json.Unmarshal([]byte(jsonData), &config)
+ if err != nil {
+ t.Errorf("outputConvConfig.UnmarshalJSON() error = %v, want nil", err)
+ }
+ if config.iType != "test" {
+ t.Errorf("outputConvConfig.iType = %q, want %q", config.iType, "test")
+ }
+ if config.action != ActionOutput {
+ t.Errorf("outputConvConfig.action = %q, want %q", config.action, ActionOutput)
+ }
+}
+
+func TestOutputConvConfig_UnmarshalJSON_DefaultAction(t *testing.T) {
+ // Clear cache and register creator
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ RegisterOutputConfigCreator("test", creator)
+
+ // Test without action (should default to "output")
+ jsonData := `{"type": "test", "args": {}}`
+ var config outputConvConfig
+ err := json.Unmarshal([]byte(jsonData), &config)
+ if err != nil {
+ t.Errorf("outputConvConfig.UnmarshalJSON() error = %v, want nil", err)
+ }
+ if config.action != ActionOutput {
+ t.Errorf("outputConvConfig.action = %q, want %q (default)", config.action, ActionOutput)
+ }
+}
+
+func TestOutputConvConfig_UnmarshalJSON_InvalidAction(t *testing.T) {
+ // Clear cache and register creator
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ creator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "test", action: action, description: "test"}, nil
+ }
+
+ RegisterOutputConfigCreator("test", creator)
+
+ // Test invalid action
+ jsonData := `{"type": "test", "action": "invalid", "args": {}}`
+ var config outputConvConfig
+ err := json.Unmarshal([]byte(jsonData), &config)
+ if err == nil {
+ t.Error("outputConvConfig.UnmarshalJSON() with invalid action expected error, got nil")
+ }
+}
+
+func TestOutputConvConfig_UnmarshalJSON_InvalidJSON(t *testing.T) {
+ jsonData := `{invalid json}`
+ var config outputConvConfig
+ err := json.Unmarshal([]byte(jsonData), &config)
+ if err == nil {
+ t.Error("outputConvConfig.UnmarshalJSON() with invalid JSON expected error, got nil")
+ }
+}
+
+func TestOutputConvConfig_UnmarshalJSON_UnknownType(t *testing.T) {
+ // Clear cache
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ jsonData := `{"type": "unknown", "action": "output", "args": {}}`
+ var config outputConvConfig
+ err := json.Unmarshal([]byte(jsonData), &config)
+ if err == nil {
+ t.Error("outputConvConfig.UnmarshalJSON() with unknown type expected error, got nil")
+ }
+}
+
+func TestConfig_UnmarshalJSON(t *testing.T) {
+ // Clear caches and register creators
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ inputCreator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "testin", action: action, description: "test input"}, nil
+ }
+ outputCreator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "testout", action: action, description: "test output"}, nil
+ }
+
+ RegisterInputConfigCreator("testin", inputCreator)
+ RegisterOutputConfigCreator("testout", outputCreator)
+
+ // Test full config
+ jsonData := `{
+ "input": [
+ {"type": "testin", "action": "add", "args": {}}
+ ],
+ "output": [
+ {"type": "testout", "action": "output", "args": {}}
+ ]
+ }`
+
+ var cfg config
+ err := json.Unmarshal([]byte(jsonData), &cfg)
+ if err != nil {
+ t.Errorf("config.UnmarshalJSON() error = %v, want nil", err)
+ }
+
+ if len(cfg.Input) != 1 {
+ t.Errorf("config.Input length = %d, want 1", len(cfg.Input))
+ }
+ if len(cfg.Output) != 1 {
+ t.Errorf("config.Output length = %d, want 1", len(cfg.Output))
+ }
+}
diff --git a/lib/container_test.go b/lib/container_test.go
new file mode 100644
index 00000000..d394b580
--- /dev/null
+++ b/lib/container_test.go
@@ -0,0 +1,635 @@
+package lib
+
+import (
+ "testing"
+)
+
+func TestNewContainer(t *testing.T) {
+ container := NewContainer()
+ if container == nil {
+ t.Fatal("NewContainer() returned nil")
+ }
+ if container.Len() != 0 {
+ t.Errorf("NewContainer().Len() = %d, want 0", container.Len())
+ }
+}
+
+func TestContainer_GetEntry(t *testing.T) {
+ container := NewContainer()
+
+ // Get non-existent entry
+ _, found := container.GetEntry("notfound")
+ if found {
+ t.Error("Container.GetEntry() found non-existent entry")
+ }
+
+ // Add an entry
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ container.Add(entry)
+
+ // Get existing entry (case insensitive, trimmed)
+ tests := []string{"test", "TEST", " test ", "TeSt"}
+ for _, name := range tests {
+ got, found := container.GetEntry(name)
+ if !found {
+ t.Errorf("Container.GetEntry(%q) not found, want found", name)
+ continue
+ }
+ if got.GetName() != "TEST" {
+ t.Errorf("Container.GetEntry(%q).GetName() = %q, want %q", name, got.GetName(), "TEST")
+ }
+ }
+}
+
+func TestContainer_Len(t *testing.T) {
+ container := NewContainer()
+
+ if container.Len() != 0 {
+ t.Errorf("Empty container.Len() = %d, want 0", container.Len())
+ }
+
+ // Add entries
+ entry1 := NewEntry("entry1")
+ entry1.AddPrefix("192.168.1.0/24")
+ container.Add(entry1)
+
+ if container.Len() != 1 {
+ t.Errorf("Container.Len() after 1 add = %d, want 1", container.Len())
+ }
+
+ entry2 := NewEntry("entry2")
+ entry2.AddPrefix("10.0.0.0/8")
+ container.Add(entry2)
+
+ if container.Len() != 2 {
+ t.Errorf("Container.Len() after 2 adds = %d, want 2", container.Len())
+ }
+}
+
+func TestContainer_Loop(t *testing.T) {
+ container := NewContainer()
+
+ // Add multiple entries
+ entry1 := NewEntry("entry1")
+ entry1.AddPrefix("192.168.1.0/24")
+ container.Add(entry1)
+
+ entry2 := NewEntry("entry2")
+ entry2.AddPrefix("10.0.0.0/8")
+ container.Add(entry2)
+
+ entry3 := NewEntry("entry3")
+ entry3.AddPrefix("2001:db8::/32")
+ container.Add(entry3)
+
+ // Loop through entries
+ count := 0
+ names := make(map[string]bool)
+ for entry := range container.Loop() {
+ count++
+ names[entry.GetName()] = true
+ }
+
+ if count != 3 {
+ t.Errorf("Container.Loop() iterated %d times, want 3", count)
+ }
+
+ expectedNames := map[string]bool{"ENTRY1": true, "ENTRY2": true, "ENTRY3": true}
+ for name := range expectedNames {
+ if !names[name] {
+ t.Errorf("Container.Loop() missing entry %q", name)
+ }
+ }
+}
+
+func TestContainer_Add_NewEntry(t *testing.T) {
+ container := NewContainer()
+
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+
+ err := container.Add(entry)
+ if err != nil {
+ t.Errorf("Container.Add() error = %v, want nil", err)
+ }
+
+ if container.Len() != 1 {
+ t.Errorf("Container.Len() = %d, want 1", container.Len())
+ }
+
+ // Verify entry exists
+ got, found := container.GetEntry("test")
+ if !found {
+ t.Fatal("Container.GetEntry() not found after Add")
+ }
+ if got.GetName() != "TEST" {
+ t.Errorf("Added entry name = %q, want %q", got.GetName(), "TEST")
+ }
+}
+
+func TestContainer_Add_ExistingEntry(t *testing.T) {
+ container := NewContainer()
+
+ // Add first entry
+ entry1 := NewEntry("test")
+ entry1.AddPrefix("192.168.1.0/24")
+ container.Add(entry1)
+
+ // Add second entry with same name
+ entry2 := NewEntry("test")
+ entry2.AddPrefix("10.0.0.0/8")
+ err := container.Add(entry2)
+ if err != nil {
+ t.Errorf("Container.Add() existing entry error = %v, want nil", err)
+ }
+
+ // Should still have only 1 entry
+ if container.Len() != 1 {
+ t.Errorf("Container.Len() = %d, want 1", container.Len())
+ }
+
+ // Verify both prefixes are in the entry
+ got, _ := container.GetEntry("test")
+ cidrs, err := got.MarshalText()
+ if err != nil {
+ t.Fatalf("MarshalText() error = %v", err)
+ }
+ if len(cidrs) != 2 {
+ t.Errorf("Entry has %d prefixes, want 2", len(cidrs))
+ }
+}
+
+func TestContainer_Add_WithIgnoreIPv4(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with both IPv4 and IPv6
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ entry.AddPrefix("2001:db8::/32")
+ container.Add(entry, IgnoreIPv4)
+
+ // Should only have IPv6
+ got, _ := container.GetEntry("test")
+ _, err := got.GetIPv4Set()
+ if err == nil {
+ t.Error("Entry should not have IPv4 set when added with IgnoreIPv4")
+ }
+
+ _, err = got.GetIPv6Set()
+ if err != nil {
+ t.Errorf("Entry.GetIPv6Set() error = %v, want nil", err)
+ }
+}
+
+func TestContainer_Add_WithIgnoreIPv6(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with both IPv4 and IPv6
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ entry.AddPrefix("2001:db8::/32")
+ container.Add(entry, IgnoreIPv6)
+
+ // Should only have IPv4
+ got, _ := container.GetEntry("test")
+ _, err := got.GetIPv4Set()
+ if err != nil {
+ t.Errorf("Entry.GetIPv4Set() error = %v, want nil", err)
+ }
+
+ _, err = got.GetIPv6Set()
+ if err == nil {
+ t.Error("Entry should not have IPv6 set when added with IgnoreIPv6")
+ }
+}
+
+func TestContainer_Add_ExistingWithIgnoreOptions(t *testing.T) {
+ container := NewContainer()
+
+ // Add first entry with IPv4
+ entry1 := NewEntry("test")
+ entry1.AddPrefix("192.168.1.0/24")
+ container.Add(entry1)
+
+ // Add second entry with IPv6, ignoring IPv4
+ entry2 := NewEntry("test")
+ entry2.AddPrefix("2001:db8::/32")
+ container.Add(entry2, IgnoreIPv4)
+
+ // Should have only IPv6 now
+ got, _ := container.GetEntry("test")
+ _, err := got.GetIPv6Set()
+ if err != nil {
+ t.Errorf("Entry.GetIPv6Set() error = %v, want nil", err)
+ }
+}
+
+func TestContainer_Remove_NotFound(t *testing.T) {
+ container := NewContainer()
+
+ entry := NewEntry("notfound")
+ err := container.Remove(entry, CaseRemoveEntry)
+ if err == nil {
+ t.Error("Container.Remove() on non-existent entry expected error, got nil")
+ }
+}
+
+func TestContainer_Remove_CaseRemoveEntry(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ container.Add(entry)
+
+ // Remove entire entry
+ err := container.Remove(entry, CaseRemoveEntry)
+ if err != nil {
+ t.Errorf("Container.Remove() error = %v, want nil", err)
+ }
+
+ // Should not be found anymore
+ _, found := container.GetEntry("test")
+ if found {
+ t.Error("Entry still found after CaseRemoveEntry")
+ }
+
+ if container.Len() != 0 {
+ t.Errorf("Container.Len() = %d, want 0 after remove", container.Len())
+ }
+}
+
+func TestContainer_Remove_CaseRemovePrefix(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with multiple prefixes
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ entry.AddPrefix("10.0.0.0/8")
+ container.Add(entry)
+
+ // Remove one prefix
+ removeEntry := NewEntry("test")
+ removeEntry.AddPrefix("192.168.1.0/24")
+ err := container.Remove(removeEntry, CaseRemovePrefix)
+ if err != nil {
+ t.Errorf("Container.Remove() error = %v, want nil", err)
+ }
+
+ // Entry should still exist
+ got, found := container.GetEntry("test")
+ if !found {
+ t.Fatal("Entry not found after CaseRemovePrefix")
+ }
+
+ // Should have only one prefix left
+ cidrs, err := got.MarshalText()
+ if err != nil {
+ t.Fatalf("MarshalText() error = %v", err)
+ }
+ if len(cidrs) != 1 {
+ t.Errorf("Entry has %d prefixes, want 1 after removal", len(cidrs))
+ }
+}
+
+func TestContainer_Remove_WithIgnoreIPv4(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with both IPv4 and IPv6
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ entry.AddPrefix("2001:db8::/32")
+ container.Add(entry)
+
+ // Remove IPv6 only (ignoring IPv4)
+ removeEntry := NewEntry("test")
+ removeEntry.AddPrefix("2001:db8::/32")
+ err := container.Remove(removeEntry, CaseRemovePrefix, IgnoreIPv4)
+ if err != nil {
+ t.Errorf("Container.Remove() error = %v, want nil", err)
+ }
+
+ // IPv4 should still exist
+ got, _ := container.GetEntry("test")
+ _, err = got.GetIPv4Set()
+ if err != nil {
+ t.Errorf("IPv4 set should still exist after removing IPv6 with IgnoreIPv4")
+ }
+}
+
+func TestContainer_Remove_WithIgnoreIPv6(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with both IPv4 and IPv6
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ entry.AddPrefix("2001:db8::/32")
+ container.Add(entry)
+
+ // Remove IPv4 only (ignoring IPv6)
+ removeEntry := NewEntry("test")
+ removeEntry.AddPrefix("192.168.1.0/24")
+ err := container.Remove(removeEntry, CaseRemovePrefix, IgnoreIPv6)
+ if err != nil {
+ t.Errorf("Container.Remove() error = %v, want nil", err)
+ }
+
+ // IPv6 should still exist
+ got, _ := container.GetEntry("test")
+ _, err = got.GetIPv6Set()
+ if err != nil {
+ t.Errorf("IPv6 set should still exist after removing IPv4 with IgnoreIPv6")
+ }
+}
+
+func TestContainer_Remove_CaseRemoveEntry_WithIgnoreIPv4(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with both IPv4 and IPv6
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ entry.AddPrefix("2001:db8::/32")
+ container.Add(entry)
+
+ // Remove IPv6 only (CaseRemoveEntry with IgnoreIPv4)
+ err := container.Remove(entry, CaseRemoveEntry, IgnoreIPv4)
+ if err != nil {
+ t.Errorf("Container.Remove() error = %v, want nil", err)
+ }
+
+ // Entry should still exist but only with IPv4
+ got, found := container.GetEntry("test")
+ if !found {
+ t.Fatal("Entry should still exist")
+ }
+
+ _, err = got.GetIPv4Set()
+ if err != nil {
+ t.Errorf("IPv4 set should still exist")
+ }
+
+ _, err = got.GetIPv6Set()
+ if err == nil {
+ t.Error("IPv6 set should not exist")
+ }
+}
+
+func TestContainer_Remove_CaseRemoveEntry_WithIgnoreIPv6(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with both IPv4 and IPv6
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ entry.AddPrefix("2001:db8::/32")
+ container.Add(entry)
+
+ // Remove IPv4 only (CaseRemoveEntry with IgnoreIPv6)
+ err := container.Remove(entry, CaseRemoveEntry, IgnoreIPv6)
+ if err != nil {
+ t.Errorf("Container.Remove() error = %v, want nil", err)
+ }
+
+ // Entry should still exist but only with IPv6
+ got, found := container.GetEntry("test")
+ if !found {
+ t.Fatal("Entry should still exist")
+ }
+
+ _, err = got.GetIPv6Set()
+ if err != nil {
+ t.Errorf("IPv6 set should still exist")
+ }
+
+ _, err = got.GetIPv4Set()
+ if err == nil {
+ t.Error("IPv4 set should not exist")
+ }
+}
+
+func TestContainer_Remove_InvalidCase(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ container.Add(entry)
+
+ // Try to remove with invalid case
+ err := container.Remove(entry, CaseRemove(999))
+ if err == nil {
+ t.Error("Container.Remove() with invalid case expected error, got nil")
+ }
+}
+
+func TestContainer_Lookup_IPv4(t *testing.T) {
+ container := NewContainer()
+
+ // Add entries
+ entry1 := NewEntry("entry1")
+ entry1.AddPrefix("192.168.1.0/24")
+ container.Add(entry1)
+
+ entry2 := NewEntry("entry2")
+ entry2.AddPrefix("10.0.0.0/8")
+ container.Add(entry2)
+
+ // Lookup IPv4 address
+ results, found, err := container.Lookup("192.168.1.1")
+ if err != nil {
+ t.Errorf("Container.Lookup() error = %v, want nil", err)
+ }
+ if !found {
+ t.Error("Container.Lookup() found = false, want true")
+ }
+ if len(results) != 1 {
+ t.Errorf("Container.Lookup() returned %d results, want 1", len(results))
+ }
+ if len(results) > 0 && results[0] != "ENTRY1" {
+ t.Errorf("Container.Lookup() result = %q, want %q", results[0], "ENTRY1")
+ }
+
+ // Lookup IPv4 address in second entry
+ results, found, err = container.Lookup("10.1.2.3")
+ if err != nil {
+ t.Errorf("Container.Lookup() error = %v, want nil", err)
+ }
+ if !found {
+ t.Error("Container.Lookup() found = false, want true")
+ }
+ if len(results) != 1 {
+ t.Errorf("Container.Lookup() returned %d results, want 1", len(results))
+ }
+ if len(results) > 0 && results[0] != "ENTRY2" {
+ t.Errorf("Container.Lookup() result = %q, want %q", results[0], "ENTRY2")
+ }
+}
+
+func TestContainer_Lookup_IPv6(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry
+ entry := NewEntry("entry1")
+ entry.AddPrefix("2001:db8::/32")
+ container.Add(entry)
+
+ // Lookup IPv6 address
+ results, found, err := container.Lookup("2001:db8::1")
+ if err != nil {
+ t.Errorf("Container.Lookup() error = %v, want nil", err)
+ }
+ if !found {
+ t.Error("Container.Lookup() found = false, want true")
+ }
+ if len(results) != 1 {
+ t.Errorf("Container.Lookup() returned %d results, want 1", len(results))
+ }
+ if len(results) > 0 && results[0] != "ENTRY1" {
+ t.Errorf("Container.Lookup() result = %q, want %q", results[0], "ENTRY1")
+ }
+}
+
+func TestContainer_Lookup_CIDR(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry
+ entry := NewEntry("entry1")
+ entry.AddPrefix("192.168.0.0/16")
+ container.Add(entry)
+
+ // Lookup CIDR
+ results, found, err := container.Lookup("192.168.1.0/24")
+ if err != nil {
+ t.Errorf("Container.Lookup() error = %v, want nil", err)
+ }
+ if !found {
+ t.Error("Container.Lookup() found = false, want true")
+ }
+ if len(results) != 1 {
+ t.Errorf("Container.Lookup() returned %d results, want 1", len(results))
+ }
+}
+
+func TestContainer_Lookup_NotFound(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry
+ entry := NewEntry("entry1")
+ entry.AddPrefix("192.168.1.0/24")
+ container.Add(entry)
+
+ // Lookup non-matching address
+ results, found, err := container.Lookup("10.0.0.1")
+ if err != nil {
+ t.Errorf("Container.Lookup() error = %v, want nil", err)
+ }
+ if found {
+ t.Error("Container.Lookup() found = true, want false")
+ }
+ if len(results) != 0 {
+ t.Errorf("Container.Lookup() returned %d results, want 0", len(results))
+ }
+}
+
+func TestContainer_Lookup_WithSearchList(t *testing.T) {
+ container := NewContainer()
+
+ // Add multiple entries
+ entry1 := NewEntry("entry1")
+ entry1.AddPrefix("192.168.1.0/24")
+ container.Add(entry1)
+
+ entry2 := NewEntry("entry2")
+ entry2.AddPrefix("192.168.1.0/24")
+ container.Add(entry2)
+
+ entry3 := NewEntry("entry3")
+ entry3.AddPrefix("192.168.1.0/24")
+ container.Add(entry3)
+
+ // Lookup with search list
+ results, found, err := container.Lookup("192.168.1.1", "entry1", "entry3")
+ if err != nil {
+ t.Errorf("Container.Lookup() error = %v, want nil", err)
+ }
+ if !found {
+ t.Error("Container.Lookup() found = false, want true")
+ }
+ if len(results) != 2 {
+ t.Errorf("Container.Lookup() returned %d results, want 2", len(results))
+ }
+
+ // Verify results contain only searched entries
+ resultMap := make(map[string]bool)
+ for _, r := range results {
+ resultMap[r] = true
+ }
+ if !resultMap["ENTRY1"] || !resultMap["ENTRY3"] {
+ t.Errorf("Container.Lookup() results = %v, want ENTRY1 and ENTRY3", results)
+ }
+ if resultMap["ENTRY2"] {
+ t.Error("Container.Lookup() should not include ENTRY2")
+ }
+}
+
+func TestContainer_Lookup_InvalidIP(t *testing.T) {
+ container := NewContainer()
+
+ // Lookup invalid IP
+ _, _, err := container.Lookup("invalid")
+ if err == nil {
+ t.Error("Container.Lookup() with invalid IP expected error, got nil")
+ }
+}
+
+func TestContainer_Lookup_InvalidCIDR(t *testing.T) {
+ container := NewContainer()
+
+ // Lookup invalid CIDR
+ _, _, err := container.Lookup("192.168.1.0/33")
+ if err == nil {
+ t.Error("Container.Lookup() with invalid CIDR expected error, got nil")
+ }
+}
+
+func TestContainer_Lookup_SearchListCaseInsensitive(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry
+ entry := NewEntry("MyEntry")
+ entry.AddPrefix("192.168.1.0/24")
+ container.Add(entry)
+
+ // Lookup with different case
+ results, found, err := container.Lookup("192.168.1.1", "myentry", "MYENTRY", " MyEntry ")
+ if err != nil {
+ t.Errorf("Container.Lookup() error = %v, want nil", err)
+ }
+ if !found {
+ t.Error("Container.Lookup() found = false, want true")
+ }
+ if len(results) != 1 {
+ t.Errorf("Container.Lookup() returned %d results, want 1", len(results))
+ }
+}
+
+func TestContainer_Lookup_EmptySearchListEntries(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry
+ entry := NewEntry("entry1")
+ entry.AddPrefix("192.168.1.0/24")
+ container.Add(entry)
+
+ // Lookup with empty/whitespace search list entries (should be ignored)
+ results, found, err := container.Lookup("192.168.1.1", "", " ", "entry1")
+ if err != nil {
+ t.Errorf("Container.Lookup() error = %v, want nil", err)
+ }
+ if !found {
+ t.Error("Container.Lookup() found = false, want true")
+ }
+ if len(results) != 1 {
+ t.Errorf("Container.Lookup() returned %d results, want 1", len(results))
+ }
+}
diff --git a/lib/converter_test.go b/lib/converter_test.go
new file mode 100644
index 00000000..be58ee1d
--- /dev/null
+++ b/lib/converter_test.go
@@ -0,0 +1,357 @@
+package lib
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "strings"
+ "testing"
+)
+
+// Mock converters for testing
+type mockInputConverter struct {
+ typ string
+ action Action
+ description string
+}
+
+func (m *mockInputConverter) GetType() string { return m.typ }
+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
+}
+
+type mockOutputConverter struct {
+ typ string
+ action Action
+ description string
+}
+
+func (m *mockOutputConverter) GetType() string { return m.typ }
+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 TestRegisterInputConverter(t *testing.T) {
+ // Clear any existing converters
+ inputConverterMap = make(map[string]InputConverter)
+
+ converter := &mockInputConverter{
+ typ: "test",
+ action: ActionAdd,
+ description: "Test input converter",
+ }
+
+ err := RegisterInputConverter("test", converter)
+ if err != nil {
+ t.Errorf("RegisterInputConverter() error = %v, want nil", err)
+ }
+
+ // Verify converter was registered
+ if _, ok := inputConverterMap["test"]; !ok {
+ t.Error("Converter not found in inputConverterMap")
+ }
+}
+
+func TestRegisterInputConverter_Duplicate(t *testing.T) {
+ // Clear any existing converters
+ inputConverterMap = make(map[string]InputConverter)
+
+ converter := &mockInputConverter{
+ typ: "test",
+ action: ActionAdd,
+ description: "Test input converter",
+ }
+
+ // Register first time
+ err := RegisterInputConverter("test", converter)
+ if err != nil {
+ t.Errorf("RegisterInputConverter() first call error = %v, want nil", err)
+ }
+
+ // Register duplicate
+ err = RegisterInputConverter("test", converter)
+ if err != ErrDuplicatedConverter {
+ t.Errorf("RegisterInputConverter() duplicate error = %v, want %v", err, ErrDuplicatedConverter)
+ }
+}
+
+func TestRegisterInputConverter_Trimming(t *testing.T) {
+ // Clear any existing converters
+ inputConverterMap = make(map[string]InputConverter)
+
+ converter := &mockInputConverter{
+ typ: "test",
+ action: ActionAdd,
+ description: "Test input converter",
+ }
+
+ // Register with spaces
+ err := RegisterInputConverter(" test ", converter)
+ if err != nil {
+ t.Errorf("RegisterInputConverter() error = %v, want nil", err)
+ }
+
+ // Verify trimmed name is used
+ if _, ok := inputConverterMap["test"]; !ok {
+ t.Error("Converter not found with trimmed name in inputConverterMap")
+ }
+}
+
+func TestRegisterOutputConverter(t *testing.T) {
+ // Clear any existing converters
+ outputConverterMap = make(map[string]OutputConverter)
+
+ converter := &mockOutputConverter{
+ typ: "test",
+ action: ActionOutput,
+ description: "Test output converter",
+ }
+
+ err := RegisterOutputConverter("test", converter)
+ if err != nil {
+ t.Errorf("RegisterOutputConverter() error = %v, want nil", err)
+ }
+
+ // Verify converter was registered
+ if _, ok := outputConverterMap["test"]; !ok {
+ t.Error("Converter not found in outputConverterMap")
+ }
+}
+
+func TestRegisterOutputConverter_Duplicate(t *testing.T) {
+ // Clear any existing converters
+ outputConverterMap = make(map[string]OutputConverter)
+
+ converter := &mockOutputConverter{
+ typ: "test",
+ action: ActionOutput,
+ description: "Test output converter",
+ }
+
+ // Register first time
+ err := RegisterOutputConverter("test", converter)
+ if err != nil {
+ t.Errorf("RegisterOutputConverter() first call error = %v, want nil", err)
+ }
+
+ // Register duplicate
+ err = RegisterOutputConverter("test", converter)
+ if err != ErrDuplicatedConverter {
+ t.Errorf("RegisterOutputConverter() duplicate error = %v, want %v", err, ErrDuplicatedConverter)
+ }
+}
+
+func TestRegisterOutputConverter_Trimming(t *testing.T) {
+ // Clear any existing converters
+ outputConverterMap = make(map[string]OutputConverter)
+
+ converter := &mockOutputConverter{
+ typ: "test",
+ action: ActionOutput,
+ description: "Test output converter",
+ }
+
+ // Register with spaces
+ err := RegisterOutputConverter(" test ", converter)
+ if err != nil {
+ t.Errorf("RegisterOutputConverter() error = %v, want nil", err)
+ }
+
+ // Verify trimmed name is used
+ if _, ok := outputConverterMap["test"]; !ok {
+ t.Error("Converter not found with trimmed name in outputConverterMap")
+ }
+}
+
+func TestListInputConverter(t *testing.T) {
+ // Clear and setup converters
+ inputConverterMap = make(map[string]InputConverter)
+
+ converter1 := &mockInputConverter{
+ typ: "test1",
+ action: ActionAdd,
+ description: "Test input converter 1",
+ }
+ converter2 := &mockInputConverter{
+ typ: "test2",
+ action: ActionAdd,
+ description: "Test input converter 2",
+ }
+
+ RegisterInputConverter("test1", converter1)
+ RegisterInputConverter("test2", converter2)
+
+ // 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()
+
+ // Verify output contains expected strings
+ if !strings.Contains(output, "All available input formats:") {
+ t.Error("ListInputConverter() output missing header")
+ }
+ if !strings.Contains(output, "test1") {
+ t.Error("ListInputConverter() output missing test1")
+ }
+ if !strings.Contains(output, "test2") {
+ t.Error("ListInputConverter() output missing test2")
+ }
+ if !strings.Contains(output, "Test input converter 1") {
+ t.Error("ListInputConverter() output missing description 1")
+ }
+ if !strings.Contains(output, "Test input converter 2") {
+ t.Error("ListInputConverter() output missing description 2")
+ }
+}
+
+func TestListInputConverter_Empty(t *testing.T) {
+ // Clear converters
+ inputConverterMap = make(map[string]InputConverter)
+
+ // 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()
+
+ // Verify output contains header
+ if !strings.Contains(output, "All available input formats:") {
+ t.Error("ListInputConverter() output missing header")
+ }
+}
+
+func TestListOutputConverter(t *testing.T) {
+ // Clear and setup converters
+ outputConverterMap = make(map[string]OutputConverter)
+
+ converter1 := &mockOutputConverter{
+ typ: "test1",
+ action: ActionOutput,
+ description: "Test output converter 1",
+ }
+ converter2 := &mockOutputConverter{
+ typ: "test2",
+ action: ActionOutput,
+ description: "Test output converter 2",
+ }
+
+ RegisterOutputConverter("test1", converter1)
+ RegisterOutputConverter("test2", converter2)
+
+ // 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()
+
+ // Verify output contains expected strings
+ if !strings.Contains(output, "All available output formats:") {
+ t.Error("ListOutputConverter() output missing header")
+ }
+ if !strings.Contains(output, "test1") {
+ t.Error("ListOutputConverter() output missing test1")
+ }
+ if !strings.Contains(output, "test2") {
+ t.Error("ListOutputConverter() output missing test2")
+ }
+ if !strings.Contains(output, "Test output converter 1") {
+ t.Error("ListOutputConverter() output missing description 1")
+ }
+ if !strings.Contains(output, "Test output converter 2") {
+ t.Error("ListOutputConverter() output missing description 2")
+ }
+}
+
+func TestListOutputConverter_Empty(t *testing.T) {
+ // Clear converters
+ outputConverterMap = make(map[string]OutputConverter)
+
+ // 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()
+
+ // Verify output contains header
+ if !strings.Contains(output, "All available output formats:") {
+ t.Error("ListOutputConverter() output missing header")
+ }
+}
+
+func TestListConverters_Sorted(t *testing.T) {
+ // Clear and setup converters in non-alphabetical order
+ inputConverterMap = make(map[string]InputConverter)
+
+ converterZ := &mockInputConverter{typ: "z", action: ActionAdd, description: "Z"}
+ converterA := &mockInputConverter{typ: "a", action: ActionAdd, description: "A"}
+ converterM := &mockInputConverter{typ: "m", action: ActionAdd, description: "M"}
+
+ RegisterInputConverter("z", converterZ)
+ RegisterInputConverter("a", converterA)
+ RegisterInputConverter("m", converterM)
+
+ // 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()
+
+ // Find positions of each converter in output
+ posA := strings.Index(output, "- a")
+ posM := strings.Index(output, "- m")
+ posZ := strings.Index(output, "- z")
+
+ if posA == -1 || posM == -1 || posZ == -1 {
+ t.Error("ListInputConverter() missing one or more converters")
+ }
+
+ // Verify alphabetical order
+ if !(posA < posM && posM < posZ) {
+ t.Error("ListInputConverter() not sorted alphabetically")
+ }
+}
diff --git a/lib/coverage2_test.go b/lib/coverage2_test.go
new file mode 100644
index 00000000..ff7181e5
--- /dev/null
+++ b/lib/coverage2_test.go
@@ -0,0 +1,261 @@
+package lib
+
+import (
+ "net"
+ "testing"
+)
+
+// Additional comprehensive coverage tests
+
+func TestEntry_ProcessPrefix_NetIPNetEdgeCases(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Test with *net.IPNet IPv6
+ _, ipnet, _ := net.ParseCIDR("2001:db8::/32")
+ err := entry.AddPrefix(ipnet)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(*net.IPNet IPv6) error = %v, want nil", err)
+ }
+}
+
+func TestContainer_AddExistingWithNilBuilders(t *testing.T) {
+ container := NewContainer()
+
+ // Add first entry with IPv4
+ entry1 := NewEntry("test")
+ entry1.AddPrefix("192.168.1.0/24")
+ container.Add(entry1)
+
+ // Add second entry with IPv6 and default ignore option (no ignore)
+ entry2 := NewEntry("test")
+ entry2.AddPrefix("2001:db8::/32")
+ err := container.Add(entry2, nil)
+ if err != nil {
+ t.Errorf("Container.Add() with nil option error = %v, want nil", err)
+ }
+}
+
+func TestContainer_RemoveWithNilBuilders(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with IPv4
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ container.Add(entry)
+
+ // Remove with entry that has no builders created
+ removeEntry := NewEntry("test")
+ removeEntry.AddPrefix("10.0.0.0/8")
+
+ // Remove prefixes with no ignore option
+ err := container.Remove(removeEntry, CaseRemovePrefix, nil)
+ if err != nil {
+ t.Errorf("Container.Remove() with nil option error = %v, want nil", err)
+ }
+}
+
+func TestEntry_MarshalWithNilOption(t *testing.T) {
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+
+ // Test with nil option
+ prefixes, err := entry.MarshalPrefix(nil)
+ if err != nil {
+ t.Errorf("Entry.MarshalPrefix(nil) error = %v, want nil", err)
+ }
+ if len(prefixes) != 1 {
+ t.Errorf("Entry.MarshalPrefix(nil) returned %d prefixes, want 1", len(prefixes))
+ }
+
+ ipranges, err := entry.MarshalIPRange(nil)
+ if err != nil {
+ t.Errorf("Entry.MarshalIPRange(nil) error = %v, want nil", err)
+ }
+ if len(ipranges) != 1 {
+ t.Errorf("Entry.MarshalIPRange(nil) returned %d ranges, want 1", len(ipranges))
+ }
+
+ cidrs, err := entry.MarshalText(nil)
+ if err != nil {
+ t.Errorf("Entry.MarshalText(nil) error = %v, want nil", err)
+ }
+ if len(cidrs) != 1 {
+ t.Errorf("Entry.MarshalText(nil) returned %d CIDRs, want 1", len(cidrs))
+ }
+}
+
+func TestContainer_LookupIPv6Prefix(t *testing.T) {
+ container := NewContainer()
+
+ // Add IPv6 entry
+ entry := NewEntry("entry1")
+ entry.AddPrefix("2001:db8::/32")
+ container.Add(entry)
+
+ // Lookup IPv6 CIDR
+ results, found, err := container.Lookup("2001:db8:1::/48")
+ if err != nil {
+ t.Errorf("Container.Lookup() IPv6 CIDR error = %v, want nil", err)
+ }
+ if !found {
+ t.Error("Container.Lookup() IPv6 CIDR found = false, want true")
+ }
+ if len(results) != 1 {
+ t.Errorf("Container.Lookup() IPv6 CIDR returned %d results, want 1", len(results))
+ }
+}
+
+func TestEntry_RemovePrefixComment(t *testing.T) {
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+
+ // Remove with comment (should still work)
+ err := entry.RemovePrefix("192.168.1.0/24 // comment")
+ if err != nil {
+ t.Errorf("Entry.RemovePrefix() with comment error = %v, want nil", err)
+ }
+}
+
+func TestEntry_GetIPv4SetBuildError(t *testing.T) {
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+
+ // Get the set
+ set, err := entry.GetIPv4Set()
+ if err != nil {
+ t.Errorf("Entry.GetIPv4Set() error = %v, want nil", err)
+ }
+ if set == nil {
+ t.Error("Entry.GetIPv4Set() returned nil set")
+ }
+
+ // Get it again (should use cached version)
+ set2, err := entry.GetIPv4Set()
+ if err != nil {
+ t.Errorf("Entry.GetIPv4Set() second call error = %v, want nil", err)
+ }
+ if set2 == nil {
+ t.Error("Entry.GetIPv4Set() second call returned nil set")
+ }
+}
+
+func TestEntry_GetIPv6SetBuildError(t *testing.T) {
+ entry := NewEntry("test")
+ entry.AddPrefix("2001:db8::/32")
+
+ // Get the set
+ set, err := entry.GetIPv6Set()
+ if err != nil {
+ t.Errorf("Entry.GetIPv6Set() error = %v, want nil", err)
+ }
+ if set == nil {
+ t.Error("Entry.GetIPv6Set() returned nil set")
+ }
+
+ // Get it again (should use cached version)
+ set2, err := entry.GetIPv6Set()
+ if err != nil {
+ t.Errorf("Entry.GetIPv6Set() second call error = %v, want nil", err)
+ }
+ if set2 == nil {
+ t.Error("Entry.GetIPv6Set() second call returned nil set")
+ }
+}
+
+func TestContainer_RemoveEntryWithIgnoreOptions_EdgeCases(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with only IPv4
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ container.Add(entry)
+
+ // Remove with CaseRemoveEntry and IgnoreIPv4 (should remove IPv6 builder, but there is none)
+ err := container.Remove(entry, CaseRemoveEntry, IgnoreIPv4)
+ if err != nil {
+ t.Errorf("Container.Remove() CaseRemoveEntry with IgnoreIPv4 error = %v, want nil", err)
+ }
+}
+
+func TestContainer_RemoveEntryWithIgnoreOptions_IPv6Only(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with only IPv6
+ entry := NewEntry("test")
+ entry.AddPrefix("2001:db8::/32")
+ container.Add(entry)
+
+ // Remove with CaseRemoveEntry and IgnoreIPv6 (should remove IPv4 builder, but there is none)
+ err := container.Remove(entry, CaseRemoveEntry, IgnoreIPv6)
+ if err != nil {
+ t.Errorf("Container.Remove() CaseRemoveEntry with IgnoreIPv6 error = %v, want nil", err)
+ }
+}
+
+func TestContainer_RemovePrefixWithIgnoreOptions_EdgeCases(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with only IPv4
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ container.Add(entry)
+
+ // Remove prefix with IgnoreIPv4 (should only remove IPv6, but there is none)
+ removeEntry := NewEntry("test")
+ removeEntry.AddPrefix("2001:db8::/32")
+ err := container.Remove(removeEntry, CaseRemovePrefix, IgnoreIPv4)
+ if err != nil {
+ t.Errorf("Container.Remove() CaseRemovePrefix with IgnoreIPv4 error = %v, want nil", err)
+ }
+}
+
+func TestContainer_RemovePrefixWithIgnoreOptions_IPv6Only(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with only IPv6
+ entry := NewEntry("test")
+ entry.AddPrefix("2001:db8::/32")
+ container.Add(entry)
+
+ // Remove prefix with IgnoreIPv6 (should only remove IPv4, but there is none)
+ removeEntry := NewEntry("test")
+ removeEntry.AddPrefix("192.168.1.0/24")
+ err := container.Remove(removeEntry, CaseRemovePrefix, IgnoreIPv6)
+ if err != nil {
+ t.Errorf("Container.Remove() CaseRemovePrefix with IgnoreIPv6 error = %v, want nil", err)
+ }
+}
+
+func TestContainer_AddExistingWithIgnoreOptionsAndNilBuilders(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with only IPv6
+ entry1 := NewEntry("test")
+ entry1.AddPrefix("2001:db8::/32")
+ container.Add(entry1)
+
+ // Add with IgnoreIPv6 (should add IPv4, but entry2 has none)
+ entry2 := NewEntry("test")
+ entry2.AddPrefix("192.168.1.0/24")
+ err := container.Add(entry2, IgnoreIPv6)
+ if err != nil {
+ t.Errorf("Container.Add() with IgnoreIPv6 error = %v, want nil", err)
+ }
+}
+
+func TestContainer_AddExistingWithIgnoreIPv4AndNilBuilders(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with only IPv4
+ entry1 := NewEntry("test")
+ entry1.AddPrefix("192.168.1.0/24")
+ container.Add(entry1)
+
+ // Add with IgnoreIPv4 (should add IPv6, but entry2 has none)
+ entry2 := NewEntry("test")
+ entry2.AddPrefix("2001:db8::/32")
+ err := container.Add(entry2, IgnoreIPv4)
+ if err != nil {
+ t.Errorf("Container.Add() with IgnoreIPv4 error = %v, want nil", err)
+ }
+}
diff --git a/lib/coverage_test.go b/lib/coverage_test.go
new file mode 100644
index 00000000..ced4df2f
--- /dev/null
+++ b/lib/coverage_test.go
@@ -0,0 +1,221 @@
+package lib
+
+import (
+ "testing"
+)
+
+// Additional tests for edge cases to improve coverage
+
+func TestContainer_InvalidContainer(t *testing.T) {
+ // Test with nil entries map
+ c := &container{entries: nil}
+
+ if c.isValid() {
+ t.Error("container with nil entries should not be valid")
+ }
+
+ if c.Len() != 0 {
+ t.Errorf("invalid container.Len() = %d, want 0", c.Len())
+ }
+
+ _, found := c.GetEntry("test")
+ if found {
+ t.Error("invalid container.GetEntry() should not find entries")
+ }
+}
+
+func TestEntry_BuildIPSetErrors(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Add IPv4 prefix
+ entry.AddPrefix("192.168.1.0/24")
+
+ // Build the set (this should succeed)
+ err := entry.buildIPSet()
+ if err != nil {
+ t.Errorf("Entry.buildIPSet() error = %v, want nil", err)
+ }
+
+ // Build again (should be cached and succeed)
+ err = entry.buildIPSet()
+ if err != nil {
+ t.Errorf("Entry.buildIPSet() second call error = %v, want nil", err)
+ }
+
+ // Test with IPv6
+ entry2 := NewEntry("test2")
+ entry2.AddPrefix("2001:db8::/32")
+
+ err = entry2.buildIPSet()
+ if err != nil {
+ t.Errorf("Entry.buildIPSet() for IPv6 error = %v, want nil", err)
+ }
+
+ // Build again for IPv6 (should be cached)
+ err = entry2.buildIPSet()
+ if err != nil {
+ t.Errorf("Entry.buildIPSet() for IPv6 second call error = %v, want nil", err)
+ }
+}
+
+func TestEntry_RemoveIPv6(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Add IPv6 prefix
+ entry.AddPrefix("2001:db8::/32")
+
+ // Remove it
+ err := entry.RemovePrefix("2001:db8::/32")
+ if err != nil {
+ t.Errorf("Entry.RemovePrefix() IPv6 error = %v, want nil", err)
+ }
+}
+
+func TestContainer_Add_ExistingEntryWithoutBuilders(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry without creating builders (empty entry)
+ entry1 := NewEntry("test")
+ container.Add(entry1)
+
+ // Add another entry with prefixes
+ entry2 := NewEntry("test")
+ entry2.AddPrefix("192.168.1.0/24")
+ entry2.AddPrefix("2001:db8::/32")
+
+ err := container.Add(entry2)
+ if err != nil {
+ t.Errorf("Container.Add() error = %v, want nil", err)
+ }
+}
+
+func TestWantedListExtended_EmptyJSON(t *testing.T) {
+ var w WantedListExtended
+ err := w.UnmarshalJSON([]byte(""))
+ if err != nil {
+ t.Errorf("WantedListExtended.UnmarshalJSON() with empty data error = %v, want nil", err)
+ }
+}
+
+func TestContainer_RemoveWithErrorInBuilder(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry with both IPv4 and IPv6
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+ entry.AddPrefix("2001:db8::/32")
+ container.Add(entry)
+
+ // Try removing with a malformed entry (empty, will cause issues building sets)
+ removeEntry := NewEntry("test")
+ // Don't add any prefixes, this should work but remove nothing
+ err := container.Remove(removeEntry, CaseRemovePrefix)
+ if err != nil {
+ t.Errorf("Container.Remove() with empty entry error = %v, want nil", err)
+ }
+}
+
+func TestEntry_ProcessPrefix_EdgeCases(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Test string with only comment marker and content
+ err := entry.AddPrefix("//")
+ if err != ErrInvalidIPType {
+ t.Errorf("Entry.AddPrefix('//') error = %v, want %v", err, ErrInvalidIPType)
+ }
+
+ err = entry.AddPrefix("#")
+ if err != ErrInvalidIPType {
+ t.Errorf("Entry.AddPrefix('#') error = %v, want %v", err, ErrInvalidIPType)
+ }
+
+ err = entry.AddPrefix("/*")
+ if err != ErrInvalidIPType {
+ t.Errorf("Entry.AddPrefix('/*') error = %v, want %v", err, ErrInvalidIPType)
+ }
+}
+
+func TestContainer_Lookup_EmptyResults(t *testing.T) {
+ container := NewContainer()
+
+ // Add entry
+ entry := NewEntry("entry1")
+ entry.AddPrefix("192.168.1.0/24")
+ container.Add(entry)
+
+ // Lookup with search list that doesn't match
+ results, found, err := container.Lookup("192.168.1.1", "nonexistent")
+ if err != nil {
+ t.Errorf("Container.Lookup() error = %v, want nil", err)
+ }
+ if found {
+ t.Error("Container.Lookup() found = true, want false")
+ }
+ if len(results) != 0 {
+ t.Errorf("Container.Lookup() returned %d results, want 0", len(results))
+ }
+}
+
+func TestEntry_MarshalPrefixOnlyIPv4(t *testing.T) {
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+
+ // Marshal with IgnoreIPv6 (should return IPv4 only)
+ prefixes, err := entry.MarshalPrefix(IgnoreIPv6)
+ if err != nil {
+ t.Errorf("Entry.MarshalPrefix(IgnoreIPv6) error = %v, want nil", err)
+ }
+ if len(prefixes) != 1 {
+ t.Errorf("Entry.MarshalPrefix(IgnoreIPv6) returned %d prefixes, want 1", len(prefixes))
+ }
+
+ // Test MarshalIPRange with only IPv4
+ ipranges, err := entry.MarshalIPRange(IgnoreIPv6)
+ if err != nil {
+ t.Errorf("Entry.MarshalIPRange(IgnoreIPv6) error = %v, want nil", err)
+ }
+ if len(ipranges) != 1 {
+ t.Errorf("Entry.MarshalIPRange(IgnoreIPv6) returned %d ranges, want 1", len(ipranges))
+ }
+
+ // Test MarshalText with only IPv4
+ cidrs, err := entry.MarshalText(IgnoreIPv6)
+ if err != nil {
+ t.Errorf("Entry.MarshalText(IgnoreIPv6) error = %v, want nil", err)
+ }
+ if len(cidrs) != 1 {
+ t.Errorf("Entry.MarshalText(IgnoreIPv6) returned %d CIDRs, want 1", len(cidrs))
+ }
+}
+
+func TestEntry_MarshalPrefixOnlyIPv6(t *testing.T) {
+ entry := NewEntry("test")
+ entry.AddPrefix("2001:db8::/32")
+
+ // Marshal with IgnoreIPv4 (should return IPv6 only)
+ prefixes, err := entry.MarshalPrefix(IgnoreIPv4)
+ if err != nil {
+ t.Errorf("Entry.MarshalPrefix(IgnoreIPv4) error = %v, want nil", err)
+ }
+ if len(prefixes) != 1 {
+ t.Errorf("Entry.MarshalPrefix(IgnoreIPv4) returned %d prefixes, want 1", len(prefixes))
+ }
+
+ // Test MarshalIPRange with only IPv6
+ ipranges, err := entry.MarshalIPRange(IgnoreIPv4)
+ if err != nil {
+ t.Errorf("Entry.MarshalIPRange(IgnoreIPv4) error = %v, want nil", err)
+ }
+ if len(ipranges) != 1 {
+ t.Errorf("Entry.MarshalIPRange(IgnoreIPv4) returned %d ranges, want 1", len(ipranges))
+ }
+
+ // Test MarshalText with only IPv6
+ cidrs, err := entry.MarshalText(IgnoreIPv4)
+ if err != nil {
+ t.Errorf("Entry.MarshalText(IgnoreIPv4) error = %v, want nil", err)
+ }
+ if len(cidrs) != 1 {
+ t.Errorf("Entry.MarshalText(IgnoreIPv4) returned %d CIDRs, want 1", len(cidrs))
+ }
+}
diff --git a/lib/entry_test.go b/lib/entry_test.go
new file mode 100644
index 00000000..516a0443
--- /dev/null
+++ b/lib/entry_test.go
@@ -0,0 +1,558 @@
+package lib
+
+import (
+ "net"
+ "net/netip"
+ "testing"
+)
+
+func TestNewEntry(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {"simple name", "test", "TEST"},
+ {"uppercase name", "TEST", "TEST"},
+ {"lowercase name", "test", "TEST"},
+ {"with spaces", " test ", "TEST"},
+ {"mixed case", "TeSt", "TEST"},
+ {"empty", "", ""},
+ {"spaces only", " ", ""},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ entry := NewEntry(tt.input)
+ if entry.GetName() != tt.expected {
+ t.Errorf("NewEntry(%q).GetName() = %q, want %q", tt.input, entry.GetName(), tt.expected)
+ }
+ })
+ }
+}
+
+func TestEntry_GetName(t *testing.T) {
+ entry := NewEntry("myentry")
+ if entry.GetName() != "MYENTRY" {
+ t.Errorf("Entry.GetName() = %q, want %q", entry.GetName(), "MYENTRY")
+ }
+}
+
+func TestEntry_AddPrefix_String(t *testing.T) {
+ tests := []struct {
+ name string
+ prefix string
+ wantErr bool
+ errType error
+ }{
+ // Valid IPv4
+ {"valid IPv4 CIDR", "192.168.1.0/24", false, nil},
+ {"valid IPv4 address", "192.168.1.1", false, nil},
+ {"valid IPv4 /32", "10.0.0.1/32", false, nil},
+
+ // Valid IPv6
+ {"valid IPv6 CIDR", "2001:db8::/32", false, nil},
+ {"valid IPv6 address", "2001:db8::1", false, nil},
+ {"valid IPv6 /128", "fe80::1/128", false, nil},
+
+ // Comment lines and prefixes with comments
+ {"IP with comment #", "192.168.1.0/24 # comment", false, nil},
+ {"IP with comment //", "192.168.1.0/24 // comment", false, nil},
+ {"IP with comment /*", "192.168.1.0/24 /* comment", false, nil},
+
+ // Invalid inputs
+ {"invalid CIDR", "192.168.1.0/33", true, ErrInvalidCIDR},
+ {"invalid IP", "invalid", true, ErrInvalidIP},
+ {"invalid format", "192.168.1", true, ErrInvalidIP},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ entry := NewEntry("test")
+ err := entry.AddPrefix(tt.prefix)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Entry.AddPrefix(%q) error = %v, wantErr %v", tt.prefix, err, tt.wantErr)
+ return
+ }
+ if tt.wantErr && tt.errType != nil && err != tt.errType {
+ t.Errorf("Entry.AddPrefix(%q) error = %v, want %v", tt.prefix, err, tt.errType)
+ }
+ })
+ }
+}
+
+// Separate test for comment-only lines since they have special handling
+func TestEntry_AddPrefix_CommentLines(t *testing.T) {
+ tests := []struct {
+ name string
+ prefix string
+ }{
+ {"comment with #", "# comment"},
+ {"comment with //", "// comment"},
+ {"comment with /*", "/* comment"},
+ {"only spaces", " "},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ entry := NewEntry("test")
+ err := entry.AddPrefix(tt.prefix)
+ // Comment lines should return ErrInvalidIPType because add() is called with nil prefix
+ if err != ErrInvalidIPType {
+ t.Errorf("Entry.AddPrefix(%q) error = %v, want %v", tt.prefix, err, ErrInvalidIPType)
+ }
+ })
+ }
+}
+
+func TestEntry_AddPrefix_NetIP(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Test with net.IP (IPv4)
+ ip := net.ParseIP("192.168.1.1")
+ err := entry.AddPrefix(ip)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(net.IP) error = %v, want nil", err)
+ }
+
+ // Test with *net.IPNet
+ _, ipnet, _ := net.ParseCIDR("10.0.0.0/8")
+ err = entry.AddPrefix(ipnet)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(*net.IPNet) error = %v, want nil", err)
+ }
+
+ // Test with netip.Addr (IPv4)
+ addr := netip.MustParseAddr("172.16.0.1")
+ err = entry.AddPrefix(addr)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(netip.Addr) error = %v, want nil", err)
+ }
+
+ // Test with *netip.Addr
+ addrPtr := netip.MustParseAddr("172.16.0.2")
+ err = entry.AddPrefix(&addrPtr)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(*netip.Addr) error = %v, want nil", err)
+ }
+
+ // Test with netip.Prefix
+ prefix := netip.MustParsePrefix("192.0.2.0/24")
+ err = entry.AddPrefix(prefix)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(netip.Prefix) error = %v, want nil", err)
+ }
+
+ // Test with *netip.Prefix
+ prefixPtr := netip.MustParsePrefix("198.51.100.0/24")
+ err = entry.AddPrefix(&prefixPtr)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(*netip.Prefix) error = %v, want nil", err)
+ }
+}
+
+func TestEntry_AddPrefix_IPv6(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Test with net.IP (IPv6)
+ ip := net.ParseIP("2001:db8::1")
+ err := entry.AddPrefix(ip)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(net.IP IPv6) error = %v, want nil", err)
+ }
+
+ // Test with netip.Addr (IPv6)
+ addr := netip.MustParseAddr("2001:db8::2")
+ err = entry.AddPrefix(addr)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(netip.Addr IPv6) error = %v, want nil", err)
+ }
+
+ // Test with *netip.Addr (IPv6)
+ addrPtr := netip.MustParseAddr("2001:db8::3")
+ err = entry.AddPrefix(&addrPtr)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(*netip.Addr IPv6) error = %v, want nil", err)
+ }
+
+ // Test with netip.Prefix (IPv6)
+ prefix := netip.MustParsePrefix("2001:db8::/32")
+ err = entry.AddPrefix(prefix)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(netip.Prefix IPv6) error = %v, want nil", err)
+ }
+
+ // Test with *netip.Prefix (IPv6)
+ prefixPtr := netip.MustParsePrefix("2001:db8:1::/48")
+ err = entry.AddPrefix(&prefixPtr)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(*netip.Prefix IPv6) error = %v, want nil", err)
+ }
+}
+
+func TestEntry_AddPrefix_IPv4In6(t *testing.T) {
+ entry := NewEntry("test")
+
+ // IPv4-mapped IPv6 address should be converted to IPv4
+ prefix := netip.MustParsePrefix("::ffff:192.168.1.0/120")
+ err := entry.AddPrefix(prefix)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(IPv4-in-IPv6) error = %v, want nil", err)
+ }
+
+ // Test with pointer
+ prefixPtr := netip.MustParsePrefix("::ffff:10.0.0.0/104")
+ err = entry.AddPrefix(&prefixPtr)
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(*IPv4-in-IPv6) error = %v, want nil", err)
+ }
+
+ // Invalid IPv4-in-IPv6 prefix (bits < 96)
+ invalidPrefix := netip.MustParsePrefix("::ffff:192.168.1.0/95")
+ err = entry.AddPrefix(invalidPrefix)
+ if err != ErrInvalidPrefix {
+ t.Errorf("Entry.AddPrefix(invalid IPv4-in-IPv6) error = %v, want %v", err, ErrInvalidPrefix)
+ }
+
+ // Test with pointer
+ err = entry.AddPrefix(&invalidPrefix)
+ if err != ErrInvalidPrefix {
+ t.Errorf("Entry.AddPrefix(*invalid IPv4-in-IPv6) error = %v, want %v", err, ErrInvalidPrefix)
+ }
+
+ // IPv4-mapped IPv6 CIDR string should NOT error - it gets converted
+ err = entry.AddPrefix("::ffff:192.168.1.0/120")
+ if err != nil {
+ t.Errorf("Entry.AddPrefix(IPv4-mapped IPv6 string) error = %v, want nil", err)
+ }
+}
+
+func TestEntry_AddPrefix_InvalidTypes(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Invalid type
+ err := entry.AddPrefix(123)
+ if err != ErrInvalidPrefixType {
+ t.Errorf("Entry.AddPrefix(int) error = %v, want %v", err, ErrInvalidPrefixType)
+ }
+
+ // Invalid net.IP
+ invalidIP := net.IP{}
+ err = entry.AddPrefix(invalidIP)
+ if err != ErrInvalidIP {
+ t.Errorf("Entry.AddPrefix(invalid net.IP) error = %v, want %v", err, ErrInvalidIP)
+ }
+}
+
+func TestEntry_RemovePrefix(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Add some prefixes first
+ entry.AddPrefix("192.168.1.0/24")
+ entry.AddPrefix("10.0.0.0/8")
+
+ // Remove a prefix
+ err := entry.RemovePrefix("192.168.1.0/24")
+ if err != nil {
+ t.Errorf("Entry.RemovePrefix() error = %v, want nil", err)
+ }
+
+ // Remove with comment
+ err = entry.RemovePrefix("10.0.0.0/8 # comment")
+ if err != nil {
+ t.Errorf("Entry.RemovePrefix() with comment error = %v, want nil", err)
+ }
+
+ // Remove invalid CIDR
+ err = entry.RemovePrefix("invalid")
+ if err != ErrInvalidIP {
+ t.Errorf("Entry.RemovePrefix(invalid) error = %v, want %v", err, ErrInvalidIP)
+ }
+}
+
+func TestEntry_GetIPv4Set(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Should error when no IPv4 set
+ _, err := entry.GetIPv4Set()
+ if err == nil {
+ t.Error("Entry.GetIPv4Set() on empty entry expected error, got nil")
+ }
+
+ // Add IPv4 prefix
+ entry.AddPrefix("192.168.1.0/24")
+
+ // Should succeed now
+ ipset, err := entry.GetIPv4Set()
+ if err != nil {
+ t.Errorf("Entry.GetIPv4Set() after adding prefix error = %v, want nil", err)
+ }
+ if ipset == nil {
+ t.Error("Entry.GetIPv4Set() returned nil IPSet")
+ }
+
+ // Verify the set contains our prefix
+ addr := netip.MustParseAddr("192.168.1.1")
+ if !ipset.Contains(addr) {
+ t.Errorf("IPv4 set doesn't contain expected address %v", addr)
+ }
+}
+
+func TestEntry_GetIPv6Set(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Should error when no IPv6 set
+ _, err := entry.GetIPv6Set()
+ if err == nil {
+ t.Error("Entry.GetIPv6Set() on empty entry expected error, got nil")
+ }
+
+ // Add IPv6 prefix
+ entry.AddPrefix("2001:db8::/32")
+
+ // Should succeed now
+ ipset, err := entry.GetIPv6Set()
+ if err != nil {
+ t.Errorf("Entry.GetIPv6Set() after adding prefix error = %v, want nil", err)
+ }
+ if ipset == nil {
+ t.Error("Entry.GetIPv6Set() returned nil IPSet")
+ }
+
+ // Verify the set contains our prefix
+ addr := netip.MustParseAddr("2001:db8::1")
+ if !ipset.Contains(addr) {
+ t.Errorf("IPv6 set doesn't contain expected address %v", addr)
+ }
+}
+
+func TestEntry_MarshalPrefix(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Should error when no prefixes
+ _, err := entry.MarshalPrefix()
+ if err == nil {
+ t.Error("Entry.MarshalPrefix() on empty entry expected error, got nil")
+ }
+
+ // Add IPv4 and IPv6 prefixes
+ entry.AddPrefix("192.168.1.0/24")
+ entry.AddPrefix("2001:db8::/32")
+
+ // Test without options
+ prefixes, err := entry.MarshalPrefix()
+ if err != nil {
+ t.Errorf("Entry.MarshalPrefix() error = %v, want nil", err)
+ }
+ if len(prefixes) != 2 {
+ t.Errorf("Entry.MarshalPrefix() returned %d prefixes, want 2", len(prefixes))
+ }
+
+ // Test with IgnoreIPv4
+ prefixes, err = entry.MarshalPrefix(IgnoreIPv4)
+ if err != nil {
+ t.Errorf("Entry.MarshalPrefix(IgnoreIPv4) error = %v, want nil", err)
+ }
+ if len(prefixes) != 1 {
+ t.Errorf("Entry.MarshalPrefix(IgnoreIPv4) returned %d prefixes, want 1", len(prefixes))
+ }
+ if !prefixes[0].Addr().Is6() {
+ t.Error("Entry.MarshalPrefix(IgnoreIPv4) should return only IPv6 prefixes")
+ }
+
+ // Test with IgnoreIPv6
+ prefixes, err = entry.MarshalPrefix(IgnoreIPv6)
+ if err != nil {
+ t.Errorf("Entry.MarshalPrefix(IgnoreIPv6) error = %v, want nil", err)
+ }
+ if len(prefixes) != 1 {
+ t.Errorf("Entry.MarshalPrefix(IgnoreIPv6) returned %d prefixes, want 1", len(prefixes))
+ }
+ if !prefixes[0].Addr().Is4() {
+ t.Error("Entry.MarshalPrefix(IgnoreIPv6) should return only IPv4 prefixes")
+ }
+}
+
+func TestEntry_MarshalIPRange(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Should error when no prefixes
+ _, err := entry.MarshalIPRange()
+ if err == nil {
+ t.Error("Entry.MarshalIPRange() on empty entry expected error, got nil")
+ }
+
+ // Add IPv4 and IPv6 prefixes
+ entry.AddPrefix("192.168.1.0/24")
+ entry.AddPrefix("2001:db8::/32")
+
+ // Test without options
+ ipranges, err := entry.MarshalIPRange()
+ if err != nil {
+ t.Errorf("Entry.MarshalIPRange() error = %v, want nil", err)
+ }
+ if len(ipranges) != 2 {
+ t.Errorf("Entry.MarshalIPRange() returned %d ranges, want 2", len(ipranges))
+ }
+
+ // Test with IgnoreIPv4
+ ipranges, err = entry.MarshalIPRange(IgnoreIPv4)
+ if err != nil {
+ t.Errorf("Entry.MarshalIPRange(IgnoreIPv4) error = %v, want nil", err)
+ }
+ if len(ipranges) != 1 {
+ t.Errorf("Entry.MarshalIPRange(IgnoreIPv4) returned %d ranges, want 1", len(ipranges))
+ }
+
+ // Test with IgnoreIPv6
+ ipranges, err = entry.MarshalIPRange(IgnoreIPv6)
+ if err != nil {
+ t.Errorf("Entry.MarshalIPRange(IgnoreIPv6) error = %v, want nil", err)
+ }
+ if len(ipranges) != 1 {
+ t.Errorf("Entry.MarshalIPRange(IgnoreIPv6) returned %d ranges, want 1", len(ipranges))
+ }
+}
+
+func TestEntry_MarshalText(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Should error when no prefixes
+ _, err := entry.MarshalText()
+ if err == nil {
+ t.Error("Entry.MarshalText() on empty entry expected error, got nil")
+ }
+
+ // Add IPv4 and IPv6 prefixes
+ entry.AddPrefix("192.168.1.0/24")
+ entry.AddPrefix("2001:db8::/32")
+
+ // Test without options
+ cidrs, err := entry.MarshalText()
+ if err != nil {
+ t.Errorf("Entry.MarshalText() error = %v, want nil", err)
+ }
+ if len(cidrs) != 2 {
+ t.Errorf("Entry.MarshalText() returned %d CIDRs, want 2", len(cidrs))
+ }
+
+ // Test with IgnoreIPv4
+ cidrs, err = entry.MarshalText(IgnoreIPv4)
+ if err != nil {
+ t.Errorf("Entry.MarshalText(IgnoreIPv4) error = %v, want nil", err)
+ }
+ if len(cidrs) != 1 {
+ t.Errorf("Entry.MarshalText(IgnoreIPv4) returned %d CIDRs, want 1", len(cidrs))
+ }
+
+ // Test with IgnoreIPv6
+ cidrs, err = entry.MarshalText(IgnoreIPv6)
+ if err != nil {
+ t.Errorf("Entry.MarshalText(IgnoreIPv6) error = %v, want nil", err)
+ }
+ if len(cidrs) != 1 {
+ t.Errorf("Entry.MarshalText(IgnoreIPv6) returned %d CIDRs, want 1", len(cidrs))
+ }
+
+ // Verify CIDRs are strings in correct format
+ for _, cidr := range cidrs {
+ _, err := netip.ParsePrefix(cidr)
+ if err != nil {
+ t.Errorf("Entry.MarshalText() returned invalid CIDR %q: %v", cidr, err)
+ }
+ }
+}
+
+func TestEntry_MultipleNilOptions(t *testing.T) {
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+
+ // Test with multiple nil options
+ prefixes, err := entry.MarshalPrefix(nil, nil, nil)
+ if err != nil {
+ t.Errorf("Entry.MarshalPrefix(nil, nil, nil) error = %v, want nil", err)
+ }
+ if len(prefixes) != 1 {
+ t.Errorf("Entry.MarshalPrefix(nil, nil, nil) returned %d prefixes, want 1", len(prefixes))
+ }
+}
+
+func TestEntry_InvalidPrefixInBuilder(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Test invalid IP type in add/remove
+ prefix := netip.MustParsePrefix("192.168.1.0/24")
+
+ err := entry.add(&prefix, IPType("invalid"))
+ if err != ErrInvalidIPType {
+ t.Errorf("Entry.add() with invalid IPType error = %v, want %v", err, ErrInvalidIPType)
+ }
+
+ err = entry.remove(&prefix, IPType("invalid"))
+ if err != ErrInvalidIPType {
+ t.Errorf("Entry.remove() with invalid IPType error = %v, want %v", err, ErrInvalidIPType)
+ }
+}
+
+func TestEntry_BuildIPSetError(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Add a valid prefix to create a builder
+ entry.AddPrefix("192.168.1.0/24")
+
+ // Get the IPv4 set (should succeed)
+ _, err := entry.GetIPv4Set()
+ if err != nil {
+ t.Errorf("Entry.GetIPv4Set() error = %v, want nil", err)
+ }
+
+ // Calling again should still work (cached)
+ _, err = entry.GetIPv4Set()
+ if err != nil {
+ t.Errorf("Entry.GetIPv4Set() second call error = %v, want nil", err)
+ }
+}
+
+func TestEntry_OnlyIPv4(t *testing.T) {
+ entry := NewEntry("test")
+ entry.AddPrefix("192.168.1.0/24")
+
+ // Should succeed
+ _, err := entry.GetIPv4Set()
+ if err != nil {
+ t.Errorf("Entry.GetIPv4Set() error = %v, want nil", err)
+ }
+
+ // IPv6 should fail
+ _, err = entry.GetIPv6Set()
+ if err == nil {
+ t.Error("Entry.GetIPv6Set() on IPv4-only entry expected error, got nil")
+ }
+}
+
+func TestEntry_OnlyIPv6(t *testing.T) {
+ entry := NewEntry("test")
+ entry.AddPrefix("2001:db8::/32")
+
+ // Should succeed
+ _, err := entry.GetIPv6Set()
+ if err != nil {
+ t.Errorf("Entry.GetIPv6Set() error = %v, want nil", err)
+ }
+
+ // IPv4 should fail
+ _, err = entry.GetIPv4Set()
+ if err == nil {
+ t.Error("Entry.GetIPv4Set() on IPv6-only entry expected error, got nil")
+ }
+}
+
+func TestEntry_RemoveFromEmptyBuilder(t *testing.T) {
+ entry := NewEntry("test")
+
+ // Remove from empty should not error
+ err := entry.RemovePrefix("192.168.1.0/24")
+ if err != nil {
+ t.Errorf("Entry.RemovePrefix() on empty entry error = %v, want nil", err)
+ }
+}
diff --git a/lib/error_test.go b/lib/error_test.go
new file mode 100644
index 00000000..bc6ab2fa
--- /dev/null
+++ b/lib/error_test.go
@@ -0,0 +1,62 @@
+package lib
+
+import (
+ "errors"
+ "testing"
+)
+
+func TestErrors(t *testing.T) {
+ tests := []struct {
+ name string
+ err error
+ expected 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 == nil {
+ t.Errorf("%s is nil", tt.name)
+ }
+ if tt.err.Error() != tt.expected {
+ t.Errorf("%s.Error() = %q, want %q", tt.name, tt.err.Error(), tt.expected)
+ }
+ })
+ }
+}
+
+func TestErrorsAreDistinct(t *testing.T) {
+ errorList := []error{
+ ErrDuplicatedConverter,
+ ErrUnknownAction,
+ ErrNotSupportedFormat,
+ ErrInvalidIPType,
+ ErrInvalidIP,
+ ErrInvalidIPLength,
+ ErrInvalidIPNet,
+ ErrInvalidCIDR,
+ ErrInvalidPrefix,
+ ErrInvalidPrefixType,
+ ErrCommentLine,
+ }
+
+ // Check that all errors are distinct
+ for i, err1 := range errorList {
+ for j, err2 := range errorList {
+ if i != j && errors.Is(err1, err2) {
+ t.Errorf("errors at index %d and %d are the same", i, j)
+ }
+ }
+ }
+}
diff --git a/lib/instance_test.go b/lib/instance_test.go
new file mode 100644
index 00000000..c6307814
--- /dev/null
+++ b/lib/instance_test.go
@@ -0,0 +1,609 @@
+package lib
+
+import (
+ "encoding/json"
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestNewInstance(t *testing.T) {
+ instance, err := NewInstance()
+ if err != nil {
+ t.Errorf("NewInstance() error = %v, want nil", err)
+ }
+ if instance == nil {
+ t.Fatal("NewInstance() returned nil")
+ }
+}
+
+func TestInstance_AddInput(t *testing.T) {
+ instance, _ := NewInstance()
+
+ converter := &mockInputConverter{
+ typ: "test",
+ action: ActionAdd,
+ description: "test",
+ }
+
+ instance.AddInput(converter)
+
+ // Verify by running (should not panic)
+ container := NewContainer()
+ err := instance.RunInput(container)
+ if err != nil {
+ t.Errorf("RunInput() after AddInput error = %v, want nil", err)
+ }
+}
+
+func TestInstance_AddOutput(t *testing.T) {
+ instance, _ := NewInstance()
+
+ converter := &mockOutputConverter{
+ typ: "test",
+ action: ActionOutput,
+ description: "test",
+ }
+
+ instance.AddOutput(converter)
+
+ // Verify by running (should not panic)
+ container := NewContainer()
+ err := instance.RunOutput(container)
+ if err != nil {
+ t.Errorf("RunOutput() after AddOutput error = %v, want nil", err)
+ }
+}
+
+func TestInstance_ResetInput(t *testing.T) {
+ instance, _ := NewInstance()
+
+ converter := &mockInputConverter{
+ typ: "test",
+ action: ActionAdd,
+ description: "test",
+ }
+
+ instance.AddInput(converter)
+ instance.ResetInput()
+
+ // After reset, Run should fail due to no input
+ instance.AddOutput(&mockOutputConverter{typ: "test", action: ActionOutput, description: "test"})
+ err := instance.Run()
+ if err == nil {
+ t.Error("Run() after ResetInput expected error, got nil")
+ }
+}
+
+func TestInstance_ResetOutput(t *testing.T) {
+ instance, _ := NewInstance()
+
+ converter := &mockOutputConverter{
+ typ: "test",
+ action: ActionOutput,
+ description: "test",
+ }
+
+ instance.AddOutput(converter)
+ instance.ResetOutput()
+
+ // After reset, Run should fail due to no output
+ instance.AddInput(&mockInputConverter{typ: "test", action: ActionAdd, description: "test"})
+ err := instance.Run()
+ if err == nil {
+ t.Error("Run() after ResetOutput expected error, got nil")
+ }
+}
+
+func TestInstance_RunInput(t *testing.T) {
+ instance, _ := NewInstance()
+ container := NewContainer()
+
+ // Add mock input converter
+ called := false
+ converter := &mockInputConverterWithCallback{
+ mockInputConverter: mockInputConverter{
+ typ: "test",
+ action: ActionAdd,
+ description: "test",
+ },
+ callback: func() { called = true },
+ }
+
+ instance.AddInput(converter)
+
+ err := instance.RunInput(container)
+ if err != nil {
+ t.Errorf("RunInput() error = %v, want nil", err)
+ }
+ if !called {
+ t.Error("RunInput() did not call input converter")
+ }
+}
+
+type mockInputConverterWithCallback struct {
+ mockInputConverter
+ callback func()
+}
+
+func (m *mockInputConverterWithCallback) Input(c Container) (Container, error) {
+ if m.callback != nil {
+ m.callback()
+ }
+ return c, nil
+}
+
+func TestInstance_RunInput_Error(t *testing.T) {
+ instance, _ := NewInstance()
+ container := NewContainer()
+
+ // Add mock input converter that returns error
+ converter := &mockInputConverterWithError{
+ mockInputConverter: mockInputConverter{
+ typ: "test",
+ action: ActionAdd,
+ description: "test",
+ },
+ err: errors.New("test error"),
+ }
+
+ instance.AddInput(converter)
+
+ err := instance.RunInput(container)
+ if err == nil {
+ t.Error("RunInput() expected error, got nil")
+ }
+ if err.Error() != "test error" {
+ t.Errorf("RunInput() error = %v, want 'test error'", err)
+ }
+}
+
+type mockInputConverterWithError struct {
+ mockInputConverter
+ err error
+}
+
+func (m *mockInputConverterWithError) Input(c Container) (Container, error) {
+ return nil, m.err
+}
+
+func TestInstance_RunOutput(t *testing.T) {
+ instance, _ := NewInstance()
+ container := NewContainer()
+
+ // Add mock output converter
+ called := false
+ converter := &mockOutputConverterWithCallback{
+ mockOutputConverter: mockOutputConverter{
+ typ: "test",
+ action: ActionOutput,
+ description: "test",
+ },
+ callback: func() { called = true },
+ }
+
+ instance.AddOutput(converter)
+
+ err := instance.RunOutput(container)
+ if err != nil {
+ t.Errorf("RunOutput() error = %v, want nil", err)
+ }
+ if !called {
+ t.Error("RunOutput() did not call output converter")
+ }
+}
+
+type mockOutputConverterWithCallback struct {
+ mockOutputConverter
+ callback func()
+}
+
+func (m *mockOutputConverterWithCallback) Output(c Container) error {
+ if m.callback != nil {
+ m.callback()
+ }
+ return nil
+}
+
+func TestInstance_RunOutput_Error(t *testing.T) {
+ instance, _ := NewInstance()
+ container := NewContainer()
+
+ // Add mock output converter that returns error
+ converter := &mockOutputConverterWithError{
+ mockOutputConverter: mockOutputConverter{
+ typ: "test",
+ action: ActionOutput,
+ description: "test",
+ },
+ err: errors.New("test error"),
+ }
+
+ instance.AddOutput(converter)
+
+ err := instance.RunOutput(container)
+ if err == nil {
+ t.Error("RunOutput() expected error, got nil")
+ }
+ if err.Error() != "test error" {
+ t.Errorf("RunOutput() error = %v, want 'test error'", err)
+ }
+}
+
+type mockOutputConverterWithError struct {
+ mockOutputConverter
+ err error
+}
+
+func (m *mockOutputConverterWithError) Output(c Container) error {
+ return m.err
+}
+
+func TestInstance_Run(t *testing.T) {
+ instance, _ := NewInstance()
+
+ instance.AddInput(&mockInputConverter{typ: "test", action: ActionAdd, description: "test"})
+ instance.AddOutput(&mockOutputConverter{typ: "test", action: ActionOutput, description: "test"})
+
+ err := instance.Run()
+ if err != nil {
+ t.Errorf("Run() error = %v, want nil", err)
+ }
+}
+
+func TestInstance_Run_NoInput(t *testing.T) {
+ instance, _ := NewInstance()
+
+ instance.AddOutput(&mockOutputConverter{typ: "test", action: ActionOutput, description: "test"})
+
+ err := instance.Run()
+ if err == nil {
+ t.Error("Run() without input expected error, got nil")
+ }
+ if err.Error() != "input type and output type must be specified" {
+ t.Errorf("Run() error = %v, want 'input type and output type must be specified'", err)
+ }
+}
+
+func TestInstance_Run_NoOutput(t *testing.T) {
+ instance, _ := NewInstance()
+
+ instance.AddInput(&mockInputConverter{typ: "test", action: ActionAdd, description: "test"})
+
+ err := instance.Run()
+ if err == nil {
+ t.Error("Run() without output expected error, got nil")
+ }
+ if err.Error() != "input type and output type must be specified" {
+ t.Errorf("Run() error = %v, want 'input type and output type must be specified'", err)
+ }
+}
+
+func TestInstance_Run_InputError(t *testing.T) {
+ instance, _ := NewInstance()
+
+ instance.AddInput(&mockInputConverterWithError{
+ mockInputConverter: mockInputConverter{typ: "test", action: ActionAdd, description: "test"},
+ err: errors.New("input error"),
+ })
+ instance.AddOutput(&mockOutputConverter{typ: "test", action: ActionOutput, description: "test"})
+
+ err := instance.Run()
+ if err == nil {
+ t.Error("Run() with input error expected error, got nil")
+ }
+ if err.Error() != "input error" {
+ t.Errorf("Run() error = %v, want 'input error'", err)
+ }
+}
+
+func TestInstance_Run_OutputError(t *testing.T) {
+ instance, _ := NewInstance()
+
+ instance.AddInput(&mockInputConverter{typ: "test", action: ActionAdd, description: "test"})
+ instance.AddOutput(&mockOutputConverterWithError{
+ mockOutputConverter: mockOutputConverter{typ: "test", action: ActionOutput, description: "test"},
+ err: errors.New("output error"),
+ })
+
+ err := instance.Run()
+ if err == nil {
+ t.Error("Run() with output error expected error, got nil")
+ }
+ if err.Error() != "output error" {
+ t.Errorf("Run() error = %v, want 'output error'", err)
+ }
+}
+
+func TestInstance_InitConfigFromBytes(t *testing.T) {
+ // Setup config creators
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ inputCreator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "testin", action: action, description: "test input"}, nil
+ }
+ outputCreator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "testout", action: action, description: "test output"}, nil
+ }
+
+ RegisterInputConfigCreator("testin", inputCreator)
+ RegisterOutputConfigCreator("testout", outputCreator)
+
+ instance, _ := NewInstance()
+
+ configJSON := `{
+ "input": [
+ {"type": "testin", "action": "add", "args": {}}
+ ],
+ "output": [
+ {"type": "testout", "action": "output", "args": {}}
+ ]
+ }`
+
+ err := instance.InitConfigFromBytes([]byte(configJSON))
+ if err != nil {
+ t.Errorf("InitConfigFromBytes() error = %v, want nil", err)
+ }
+
+ // Verify converters were added
+ err = instance.Run()
+ if err != nil {
+ t.Errorf("Run() after InitConfigFromBytes error = %v, want nil", err)
+ }
+}
+
+func TestInstance_InitConfigFromBytes_WithComments(t *testing.T) {
+ // Setup config creators
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ inputCreator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "testin", action: action, description: "test input"}, nil
+ }
+ outputCreator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "testout", action: action, description: "test output"}, nil
+ }
+
+ RegisterInputConfigCreator("testin", inputCreator)
+ RegisterOutputConfigCreator("testout", outputCreator)
+
+ instance, _ := NewInstance()
+
+ // Config with comments and trailing commas (hujson format)
+ configJSON := `{
+ // This is a comment
+ "input": [
+ {"type": "testin", "action": "add", "args": {}}, // trailing comma
+ ],
+ "output": [
+ {"type": "testout", "action": "output", "args": {}},
+ ], // trailing comma
+ }`
+
+ err := instance.InitConfigFromBytes([]byte(configJSON))
+ if err != nil {
+ t.Errorf("InitConfigFromBytes() with comments error = %v, want nil", err)
+ }
+
+ // Verify converters were added
+ err = instance.Run()
+ if err != nil {
+ t.Errorf("Run() after InitConfigFromBytes with comments error = %v, want nil", err)
+ }
+}
+
+func TestInstance_InitConfigFromBytes_InvalidJSON(t *testing.T) {
+ instance, _ := NewInstance()
+
+ configJSON := `{invalid json}`
+
+ err := instance.InitConfigFromBytes([]byte(configJSON))
+ if err == nil {
+ t.Error("InitConfigFromBytes() with invalid JSON expected error, got nil")
+ }
+}
+
+func TestInstance_InitConfig_LocalFile(t *testing.T) {
+ // Setup config creators
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ inputCreator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "testin", action: action, description: "test input"}, nil
+ }
+ outputCreator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "testout", action: action, description: "test output"}, nil
+ }
+
+ RegisterInputConfigCreator("testin", inputCreator)
+ RegisterOutputConfigCreator("testout", outputCreator)
+
+ // Create temporary config file
+ tmpDir := t.TempDir()
+ configFile := filepath.Join(tmpDir, "config.json")
+
+ configJSON := `{
+ "input": [
+ {"type": "testin", "action": "add", "args": {}}
+ ],
+ "output": [
+ {"type": "testout", "action": "output", "args": {}}
+ ]
+ }`
+
+ err := os.WriteFile(configFile, []byte(configJSON), 0644)
+ if err != nil {
+ t.Fatalf("Failed to create test config file: %v", err)
+ }
+
+ instance, _ := NewInstance()
+
+ err = instance.InitConfig(configFile)
+ if err != nil {
+ t.Errorf("InitConfig() error = %v, want nil", err)
+ }
+
+ // Verify converters were added
+ err = instance.Run()
+ if err != nil {
+ t.Errorf("Run() after InitConfig error = %v, want nil", err)
+ }
+}
+
+func TestInstance_InitConfig_RemoteURL(t *testing.T) {
+ // Setup config creators
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ inputCreator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "testin", action: action, description: "test input"}, nil
+ }
+ outputCreator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "testout", action: action, description: "test output"}, nil
+ }
+
+ RegisterInputConfigCreator("testin", inputCreator)
+ RegisterOutputConfigCreator("testout", outputCreator)
+
+ // Create test server
+ configJSON := `{
+ "input": [
+ {"type": "testin", "action": "add", "args": {}}
+ ],
+ "output": [
+ {"type": "testout", "action": "output", "args": {}}
+ ]
+ }`
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(configJSON))
+ }))
+ defer server.Close()
+
+ instance, _ := NewInstance()
+
+ err := instance.InitConfig(server.URL)
+ if err != nil {
+ t.Errorf("InitConfig() with URL error = %v, want nil", err)
+ }
+
+ // Verify converters were added
+ err = instance.Run()
+ if err != nil {
+ t.Errorf("Run() after InitConfig with URL error = %v, want nil", err)
+ }
+}
+
+func TestInstance_InitConfig_RemoteURL_HTTPS(t *testing.T) {
+ // Setup config creators
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ inputCreator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "testin", action: action, description: "test input"}, nil
+ }
+ outputCreator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "testout", action: action, description: "test output"}, nil
+ }
+
+ RegisterInputConfigCreator("testin", inputCreator)
+ RegisterOutputConfigCreator("testout", outputCreator)
+
+ // Create test server
+ configJSON := `{
+ "input": [
+ {"type": "testin", "action": "add", "args": {}}
+ ],
+ "output": [
+ {"type": "testout", "action": "output", "args": {}}
+ ]
+ }`
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ w.Write([]byte(configJSON))
+ }))
+ defer server.Close()
+
+ instance, _ := NewInstance()
+
+ // Replace http:// with https:// in URL (will fail but tests the code path)
+ httpsURL := "https" + server.URL[4:]
+ err := instance.InitConfig(httpsURL)
+ // This will fail because it's not a real HTTPS server, but it tests the code path
+ if err == nil {
+ // If it somehow succeeds, that's also fine
+ t.Log("InitConfig() with HTTPS URL succeeded unexpectedly")
+ }
+}
+
+func TestInstance_InitConfig_FileNotFound(t *testing.T) {
+ instance, _ := NewInstance()
+
+ err := instance.InitConfig("/nonexistent/config.json")
+ if err == nil {
+ t.Error("InitConfig() with non-existent file expected error, got nil")
+ }
+}
+
+func TestInstance_InitConfig_WithSpaces(t *testing.T) {
+ // Setup config creators
+ inputConfigCreatorCache = make(map[string]inputConfigCreator)
+ outputConfigCreatorCache = make(map[string]outputConfigCreator)
+
+ inputCreator := func(action Action, data json.RawMessage) (InputConverter, error) {
+ return &mockInputConverter{typ: "testin", action: action, description: "test input"}, nil
+ }
+ outputCreator := func(action Action, data json.RawMessage) (OutputConverter, error) {
+ return &mockOutputConverter{typ: "testout", action: action, description: "test output"}, nil
+ }
+
+ RegisterInputConfigCreator("testin", inputCreator)
+ RegisterOutputConfigCreator("testout", outputCreator)
+
+ // Create temporary config file
+ tmpDir := t.TempDir()
+ configFile := filepath.Join(tmpDir, "config.json")
+
+ configJSON := `{
+ "input": [
+ {"type": "testin", "action": "add", "args": {}}
+ ],
+ "output": [
+ {"type": "testout", "action": "output", "args": {}}
+ ]
+ }`
+
+ err := os.WriteFile(configFile, []byte(configJSON), 0644)
+ if err != nil {
+ t.Fatalf("Failed to create test config file: %v", err)
+ }
+
+ instance, _ := NewInstance()
+
+ // Test with spaces around the path
+ err = instance.InitConfig(" " + configFile + " ")
+ if err != nil {
+ t.Errorf("InitConfig() with spaces error = %v, want nil", err)
+ }
+}
+
+func TestInstance_MultipleInputOutput(t *testing.T) {
+ instance, _ := NewInstance()
+
+ // Add multiple inputs and outputs
+ instance.AddInput(&mockInputConverter{typ: "test1", action: ActionAdd, description: "test1"})
+ instance.AddInput(&mockInputConverter{typ: "test2", action: ActionAdd, description: "test2"})
+ instance.AddOutput(&mockOutputConverter{typ: "test1", action: ActionOutput, description: "test1"})
+ instance.AddOutput(&mockOutputConverter{typ: "test2", action: ActionOutput, description: "test2"})
+
+ err := instance.Run()
+ if err != nil {
+ t.Errorf("Run() with multiple converters error = %v, want nil", err)
+ }
+}
diff --git a/lib/lib_test.go b/lib/lib_test.go
new file mode 100644
index 00000000..0310941d
--- /dev/null
+++ b/lib/lib_test.go
@@ -0,0 +1,86 @@
+package lib
+
+import (
+ "testing"
+)
+
+func TestConstants(t *testing.T) {
+ // Test Action constants
+ if ActionAdd != "add" {
+ t.Errorf("ActionAdd = %q, want %q", ActionAdd, "add")
+ }
+ if ActionRemove != "remove" {
+ t.Errorf("ActionRemove = %q, want %q", ActionRemove, "remove")
+ }
+ if ActionOutput != "output" {
+ t.Errorf("ActionOutput = %q, want %q", ActionOutput, "output")
+ }
+
+ // Test IPType constants
+ if IPv4 != "ipv4" {
+ t.Errorf("IPv4 = %q, want %q", IPv4, "ipv4")
+ }
+ if IPv6 != "ipv6" {
+ t.Errorf("IPv6 = %q, want %q", IPv6, "ipv6")
+ }
+
+ // Test CaseRemove constants
+ if CaseRemovePrefix != 0 {
+ t.Errorf("CaseRemovePrefix = %d, want %d", CaseRemovePrefix, 0)
+ }
+ if CaseRemoveEntry != 1 {
+ t.Errorf("CaseRemoveEntry = %d, want %d", CaseRemoveEntry, 1)
+ }
+}
+
+func TestActionsRegistry(t *testing.T) {
+ tests := []struct {
+ action Action
+ expected bool
+ }{
+ {ActionAdd, true},
+ {ActionRemove, true},
+ {ActionOutput, true},
+ {Action("invalid"), false},
+ {Action(""), false},
+ }
+
+ for _, tt := range tests {
+ t.Run(string(tt.action), func(t *testing.T) {
+ got := ActionsRegistry[tt.action]
+ if got != tt.expected {
+ t.Errorf("ActionsRegistry[%q] = %v, want %v", tt.action, got, tt.expected)
+ }
+ })
+ }
+}
+
+func TestIgnoreIPv4(t *testing.T) {
+ result := IgnoreIPv4()
+ if result != IPv4 {
+ t.Errorf("IgnoreIPv4() = %q, want %q", result, IPv4)
+ }
+}
+
+func TestIgnoreIPv6(t *testing.T) {
+ result := IgnoreIPv6()
+ if result != IPv6 {
+ t.Errorf("IgnoreIPv6() = %q, want %q", result, IPv6)
+ }
+}
+
+func TestIgnoreIPOption(t *testing.T) {
+ // Test that IgnoreIPv4 returns correct IPType when called
+ opt := IgnoreIPv4
+ result := opt()
+ if result != IPv4 {
+ t.Errorf("IgnoreIPv4() = %q, want %q", result, IPv4)
+ }
+
+ // Test that IgnoreIPv6 returns correct IPType when called
+ opt2 := IgnoreIPv6
+ result2 := opt2()
+ if result2 != IPv6 {
+ t.Errorf("IgnoreIPv6() = %q, want %q", result2, IPv6)
+ }
+}