diff options
| author | anthropic-code-agent[bot] <[email protected]> | 2026-03-06 20:53:28 +0000 |
|---|---|---|
| committer | anthropic-code-agent[bot] <[email protected]> | 2026-03-06 20:53:28 +0000 |
| commit | cd2ba4b2110cb81171ba2af2c95c9fd2e0ffc9d5 (patch) | |
| tree | 5723ae5127597a84e94b4f3ba6ce746a191bf91e /lib | |
| parent | 698dc71e82a0c9d507e327b15b7ac9108412f373 (diff) | |
Add comprehensive unit tests for lib package with 92.6% coverageclaude/write-unit-tests-lib-package
Co-authored-by: Loyalsoldier <[email protected]>
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/common_test.go | 293 | ||||
| -rw-r--r-- | lib/config_test.go | 437 | ||||
| -rw-r--r-- | lib/container_test.go | 635 | ||||
| -rw-r--r-- | lib/converter_test.go | 357 | ||||
| -rw-r--r-- | lib/coverage2_test.go | 261 | ||||
| -rw-r--r-- | lib/coverage_test.go | 221 | ||||
| -rw-r--r-- | lib/entry_test.go | 558 | ||||
| -rw-r--r-- | lib/error_test.go | 62 | ||||
| -rw-r--r-- | lib/instance_test.go | 609 | ||||
| -rw-r--r-- | lib/lib_test.go | 86 |
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) + } +} |
