package xmlparser import ( "errors" "strings" "testing" ) func TestValidationError(t *testing.T) { tests := []struct { name string field string message string value string expected string }{ { name: "Error with value", field: "name", message: "is required", value: "empty", expected: "validation error in field 'name': is required (value: empty)", }, { name: "Error without value", field: "id", message: "must be unique", value: "", expected: "validation error in field 'id': must be unique", }, { name: "Complex error message", field: "email", message: "must be a valid email address", value: "invalid-email", expected: "validation error in field 'email': must be a valid email address (value: invalid-email)", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidationError{ Field: tt.field, Message: tt.message, Value: tt.value, } if err.Error() != tt.expected { t.Errorf("Expected error message '%s', got '%s'", tt.expected, err.Error()) } }) } } func TestValidationErrors(t *testing.T) { tests := []struct { name string errors ValidationErrors expected string }{ { name: "Empty validation errors", errors: ValidationErrors{}, expected: "no validation errors", }, { name: "Single validation error", errors: ValidationErrors{ {Field: "name", Message: "is required"}, }, expected: "validation failed with 1 error(s): validation error in field 'name': is required", }, { name: "Multiple validation errors", errors: ValidationErrors{ {Field: "name", Message: "is required"}, {Field: "email", Message: "must be valid", Value: "invalid"}, }, expected: "validation failed with 2 error(s): validation error in field 'name': is required; validation error in field 'email': must be valid (value: invalid)", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.errors.Error() != tt.expected { t.Errorf("Expected error message '%s', got '%s'", tt.expected, tt.errors.Error()) } }) } } func TestValidationErrorsIsEmpty(t *testing.T) { tests := []struct { name string errors ValidationErrors expected bool }{ { name: "Empty errors", errors: ValidationErrors{}, expected: true, }, { name: "Nil errors", errors: nil, expected: true, }, { name: "Non-empty errors", errors: ValidationErrors{ {Field: "test", Message: "test error"}, }, expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.errors.IsEmpty() != tt.expected { t.Errorf("Expected IsEmpty() to return %v, got %v", tt.expected, tt.errors.IsEmpty()) } }) } } func TestValidationErrorsAdd(t *testing.T) { var errors ValidationErrors // Test adding errors errors.Add("field1", "message1", "value1") errors.Add("field2", "message2", "") if len(errors) != 2 { t.Errorf("Expected 2 errors, got %d", len(errors)) } // Test first error if errors[0].Field != "field1" { t.Errorf("Expected first error field 'field1', got '%s'", errors[0].Field) } if errors[0].Message != "message1" { t.Errorf("Expected first error message 'message1', got '%s'", errors[0].Message) } if errors[0].Value != "value1" { t.Errorf("Expected first error value 'value1', got '%s'", errors[0].Value) } // Test second error if errors[1].Field != "field2" { t.Errorf("Expected second error field 'field2', got '%s'", errors[1].Field) } if errors[1].Message != "message2" { t.Errorf("Expected second error message 'message2', got '%s'", errors[1].Message) } if errors[1].Value != "" { t.Errorf("Expected second error value to be empty, got '%s'", errors[1].Value) } } func TestXMLParseError(t *testing.T) { tests := []struct { name string error XMLParseError expected string }{ { name: "Complete error information", error: XMLParseError{ Line: 10, Column: 5, Element: "TestElement", Message: "unexpected end of element", Original: errors.New("original error"), }, expected: "XML parse error (element 'TestElement', line 10, column 5): unexpected end of element", }, { name: "Error with line only", error: XMLParseError{ Line: 15, Message: "syntax error", }, expected: "XML parse error (line 15): syntax error", }, { name: "Error with element only", error: XMLParseError{ Element: "BadElement", Message: "invalid element", }, expected: "XML parse error (element 'BadElement'): invalid element", }, { name: "Minimal error", error: XMLParseError{ Message: "general parse error", }, expected: "XML parse error: general parse error", }, { name: "Error with line and column", error: XMLParseError{ Line: 20, Column: 8, Message: "missing closing tag", }, expected: "XML parse error (line 20, column 8): missing closing tag", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.error.Error() != tt.expected { t.Errorf("Expected error message '%s', got '%s'", tt.expected, tt.error.Error()) } }) } } func TestXMLParseErrorUnwrap(t *testing.T) { originalErr := errors.New("original error") xmlErr := XMLParseError{ Message: "wrapper error", Original: originalErr, } unwrapped := xmlErr.Unwrap() if unwrapped != originalErr { t.Errorf("Expected unwrapped error to be original error, got %v", unwrapped) } // Test with no original error xmlErrNoOriginal := XMLParseError{ Message: "standalone error", } unwrappedNil := xmlErrNoOriginal.Unwrap() if unwrappedNil != nil { t.Errorf("Expected unwrapped error to be nil, got %v", unwrappedNil) } } func TestSanitizeXMLString(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "Clean string", input: "Hello World", expected: "Hello World", }, { name: "String with valid whitespace", input: "Hello\tWorld\nNew Line\rCarriage Return", expected: "Hello\tWorld\nNew Line\rCarriage Return", }, { name: "String with control characters", input: "Hello\x00\x01\x02World", expected: "HelloWorld", }, { name: "String with high Unicode", input: "Hello δΈ–η•Œ 🌍", expected: "Hello δΈ–η•Œ 🌍", }, { name: "Empty string", input: "", expected: "", }, { name: "String with only control characters", input: "\x00\x01\x02\x03", expected: "", }, { name: "Mixed valid and invalid characters", input: "Valid\x00Invalid\tTab\x01More\nNewline", expected: "ValidInvalid\tTabMore\nNewline", }, { name: "XML special characters", input: "<>&\"'", expected: "<>&\"'", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := SanitizeXMLString(tt.input) if result != tt.expected { t.Errorf("Expected '%s', got '%s'", tt.expected, result) } }) } } func TestValidateXMLName(t *testing.T) { tests := []struct { name string xmlName string expected bool }{ // Valid names { name: "Simple name", xmlName: "element", expected: true, }, { name: "Name with underscore", xmlName: "_element", expected: true, }, { name: "Name with uppercase", xmlName: "Element", expected: true, }, { name: "Name with numbers", xmlName: "element123", expected: true, }, { name: "Name with hyphen", xmlName: "my-element", expected: true, }, { name: "Name with period", xmlName: "my.element", expected: true, }, { name: "Complex valid name", xmlName: "My_Element-123.test", expected: true, }, // Invalid names { name: "Empty name", xmlName: "", expected: false, }, { name: "Name starting with number", xmlName: "123element", expected: false, }, { name: "Name starting with hyphen", xmlName: "-element", expected: false, }, { name: "Name starting with period", xmlName: ".element", expected: false, }, { name: "Name with space", xmlName: "my element", expected: false, }, { name: "Name with special characters", xmlName: "element@test", expected: false, }, { name: "Name with colon", xmlName: "element:test", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ValidateXMLName(tt.xmlName) if result != tt.expected { t.Errorf("Expected ValidateXMLName('%s') to return %v, got %v", tt.xmlName, tt.expected, result) } }) } } func TestIsValidNamespace(t *testing.T) { tests := []struct { name string namespace string expected bool }{ // Valid namespaces { name: "Empty namespace", namespace: "", expected: true, }, { name: "HTTP namespace", namespace: "http://example.com", expected: true, }, { name: "HTTPS namespace", namespace: "https://example.com", expected: true, }, { name: "URN namespace", namespace: "urn:example:namespace", expected: true, }, { name: "Complex HTTP namespace", namespace: "http://www.example.com/schema/2023", expected: true, }, { name: "Complex HTTPS namespace", namespace: "https://schemas.example.org/xml/namespace", expected: true, }, { name: "Complex URN namespace", namespace: "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2", expected: true, }, // Invalid namespaces { name: "Invalid protocol", namespace: "ftp://example.com", expected: false, }, { name: "No protocol", namespace: "example.com", expected: false, }, { name: "Invalid format", namespace: "not-a-namespace", expected: false, }, { name: "Partial HTTP", namespace: "http:", expected: false, }, { name: "Partial URN", namespace: "urn", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsValidNamespace(tt.namespace) if result != tt.expected { t.Errorf("Expected IsValidNamespace('%s') to return %v, got %v", tt.namespace, tt.expected, result) } }) } } func TestNormalizeWhitespace(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "Single spaces", input: "hello world", expected: "hello world", }, { name: "Multiple spaces", input: "hello world", expected: "hello world", }, { name: "Mixed whitespace", input: "hello\t\n\r world", expected: "hello world", }, { name: "Leading whitespace", input: " hello world", expected: "hello world", }, { name: "Trailing whitespace", input: "hello world ", expected: "hello world", }, { name: "Leading and trailing whitespace", input: " hello world ", expected: "hello world", }, { name: "Multiple words with mixed whitespace", input: "one\ttwo\n\nthree\r\r\rfour five", expected: "one two three four five", }, { name: "Empty string", input: "", expected: "", }, { name: "Only whitespace", input: " \t\n\r ", expected: "", }, { name: "Single word", input: "hello", expected: "hello", }, { name: "Single word with whitespace", input: " hello ", expected: "hello", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := NormalizeWhitespace(tt.input) if result != tt.expected { t.Errorf("Expected '%s', got '%s'", tt.expected, result) } }) } } func TestTruncateString(t *testing.T) { tests := []struct { name string input string maxLength int expected string }{ { name: "Short string", input: "hello", maxLength: 10, expected: "hello", }, { name: "Exact length", input: "hello", maxLength: 5, expected: "hello", }, { name: "Truncate with ellipsis", input: "hello world", maxLength: 8, expected: "hello...", }, { name: "Very short max length", input: "hello", maxLength: 3, expected: "hel", }, { name: "Max length 0", input: "hello", maxLength: 0, expected: "", }, { name: "Max length 1", input: "hello", maxLength: 1, expected: "h", }, { name: "Max length 2", input: "hello", maxLength: 2, expected: "he", }, { name: "Empty string", input: "", maxLength: 5, expected: "", }, { name: "Long string truncation", input: "This is a very long string that needs to be truncated", maxLength: 20, expected: "This is a very lo...", }, { name: "Unicode string", input: "Hello δΈ–η•Œ", maxLength: 8, expected: "Hello...", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := TruncateString(tt.input, tt.maxLength) if result != tt.expected { t.Errorf("Expected '%s', got '%s'", tt.expected, result) } // Verify result length doesn't exceed maxLength if len(result) > tt.maxLength { t.Errorf("Result length %d exceeds maxLength %d", len(result), tt.maxLength) } }) } } func TestUtilityFunctionsWithRealWorldData(t *testing.T) { // Test with real-world XML-like data t.Run("Real world XML sanitization", func(t *testing.T) { input := "Contract Notice\x00 for\x01 Public\x02 Procurement\t\nProject" expected := "Contract Notice for Public Procurement\t\nProject" result := SanitizeXMLString(input) if result != expected { t.Errorf("Expected '%s', got '%s'", expected, result) } }) t.Run("Real world XML name validation", func(t *testing.T) { validNames := []string{ "ContractNotice", "ProcurementProject", "TenderingTerms", "_privateElement", "xml-element", "element.version", } for _, name := range validNames { if !ValidateXMLName(name) { t.Errorf("Expected '%s' to be valid XML name", name) } } invalidNames := []string{ "123Contract", "-element", ".element", "element with spaces", "element@symbol", } for _, name := range invalidNames { if ValidateXMLName(name) { t.Errorf("Expected '%s' to be invalid XML name", name) } } }) t.Run("Real world namespace validation", func(t *testing.T) { validNamespaces := []string{ "", "http://www.w3.org/2001/XMLSchema", "https://ted.europa.eu/schema/eforms", "urn:oasis:names:specification:ubl:schema:xsd:ContractNotice-2", } for _, ns := range validNamespaces { if !IsValidNamespace(ns) { t.Errorf("Expected '%s' to be valid namespace", ns) } } }) t.Run("Real world whitespace normalization", func(t *testing.T) { input := " Contract\t\tNotice\n\n\nfor\r\r\rPublic Procurement " expected := "Contract Notice for Public Procurement" result := NormalizeWhitespace(input) if result != expected { t.Errorf("Expected '%s', got '%s'", expected, result) } }) t.Run("Real world string truncation", func(t *testing.T) { longDescription := "This is a very long contract description that contains detailed information about the procurement project including all requirements, specifications, and conditions that need to be met by potential contractors." truncated := TruncateString(longDescription, 50) if len(truncated) > 50 { t.Errorf("Truncated string length %d exceeds limit 50", len(truncated)) } if !strings.HasSuffix(truncated, "...") && len(longDescription) > 50 { t.Error("Expected truncated string to end with '...'") } }) } func TestErrorTypes(t *testing.T) { // Test that our error types implement the error interface var _ error = ValidationError{} var _ error = ValidationErrors{} var _ error = XMLParseError{} // Test error interface methods ve := ValidationError{Field: "test", Message: "test error"} if ve.Error() == "" { t.Error("ValidationError.Error() should return non-empty string") } ves := ValidationErrors{ve} if ves.Error() == "" { t.Error("ValidationErrors.Error() should return non-empty string") } xpe := XMLParseError{Message: "test parse error"} if xpe.Error() == "" { t.Error("XMLParseError.Error() should return non-empty string") } } func TestValidationErrorsChaining(t *testing.T) { var errors ValidationErrors // Build up errors errors.Add("field1", "error1", "value1") errors.Add("field2", "error2", "") errors.Add("field3", "error3", "value3") if len(errors) != 3 { t.Errorf("Expected 3 errors, got %d", len(errors)) } if errors.IsEmpty() { t.Error("Expected errors to not be empty") } errorMessage := errors.Error() if !strings.Contains(errorMessage, "validation failed with 3 error(s)") { t.Errorf("Error message should contain count, got: %s", errorMessage) } if !strings.Contains(errorMessage, "field1") { t.Error("Error message should contain field1") } if !strings.Contains(errorMessage, "field2") { t.Error("Error message should contain field2") } if !strings.Contains(errorMessage, "field3") { t.Error("Error message should contain field3") } }