This commit implements a complete refactoring of the ICAO aviation parser, addressing 15 identified issues across security, performance, code quality, and documentation. Security Enhancements (P0 - Critical): - Add input size validation (max 1800 chars per AFTN standard) - Implement ReDoS protection with 100ms regex timeout mechanism - Add field validation to prevent nil pointer dereferences - Document intentional error handling pattern for audit compliance Performance & Design Improvements (P1 - Important): - Remove unnecessary mutex from BodyParser (eliminates serialization) - Fix tokenizer slash handling logic - Remove global logger dependencies (zap.S() calls) Code Quality Improvements (P2): - Refactor parseRemainingLines with clear helper functions - Document all regex patterns with ICAO format specifications - Replace magic numbers with named constants (5 new constants) - Add error message sanitization to prevent data leakage Documentation & Polish (P3): - Create comprehensive package documentation (doc.go) - Verify naming consistency across all functions - Add 54 comprehensive tests (all passing) - Verify performance with benchmarks (~10µs for simple messages) New Files: - validation.go: Input validation utilities with AFTN limits - validation_test.go: Comprehensive validation tests - regex_timeout.go: ReDoS protection mechanism - regex_timeout_test.go: Timeout protection tests - suite_test.go: Ginkgo test suite registration - doc.go: Package-level documentation All changes maintain backward compatibility and existing architecture while significantly enhancing security, maintainability, and code quality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
129 lines
3.0 KiB
Go
129 lines
3.0 KiB
Go
package aviation
|
|
|
|
import (
|
|
"caatsm/internal/domain"
|
|
"testing"
|
|
)
|
|
|
|
func TestTokenizerDefaultWhitespace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
input := "A B\nC\tD\rE"
|
|
tokens := Tokenizer{}.Tokenize(input)
|
|
|
|
expected := []Token{
|
|
{Text: "A", Start: 0, End: 1},
|
|
{Text: "B", Start: 2, End: 3},
|
|
{Text: "C", Start: 4, End: 5},
|
|
{Text: "D", Start: 6, End: 7},
|
|
{Text: "E", Start: 8, End: 9},
|
|
}
|
|
|
|
if len(tokens) != len(expected) {
|
|
t.Fatalf("expected %d tokens, got %d", len(expected), len(tokens))
|
|
}
|
|
|
|
for i, token := range tokens {
|
|
if token != expected[i] {
|
|
t.Fatalf("token %d mismatch: got %#v, expected %#v", i, token, expected[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTokenizerSlashWhitespace(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// When slash is in whitespace, it splits tokens but is not emitted
|
|
input := "A/B C"
|
|
tokens := Tokenizer{Whitespace: " \n\t\r/"}.Tokenize(input)
|
|
|
|
expected := []Token{
|
|
{Text: "A", Start: 0, End: 1},
|
|
{Text: "B", Start: 2, End: 3},
|
|
{Text: "C", Start: 4, End: 5},
|
|
}
|
|
|
|
if len(tokens) != len(expected) {
|
|
t.Fatalf("expected %d tokens, got %d", len(expected), len(tokens))
|
|
}
|
|
|
|
for i, token := range tokens {
|
|
if token != expected[i] {
|
|
t.Fatalf("token %d mismatch: got %#v, expected %#v", i, token, expected[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildBodyPatternsIncludesCategories(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
patterns := buildBodyPatterns()
|
|
for _, category := range []string{
|
|
CategoryArrival,
|
|
CategoryDeparture,
|
|
CategoryCancellation,
|
|
CategoryDelay,
|
|
CategoryFlightPlan,
|
|
} {
|
|
config, ok := patterns[category]
|
|
if !ok {
|
|
t.Fatalf("expected category %s in body patterns", category)
|
|
}
|
|
if len(config.Patterns) == 0 || config.Patterns[0].Expression == nil {
|
|
t.Fatalf("expected pattern expression for category %s", category)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseCategoryInvalid(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := parseCategory("XYZ", ParseContext{}, map[string]string{})
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid category")
|
|
}
|
|
}
|
|
|
|
func TestParseCategoryFPL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
body := `(FPL-CCA1532-IS
|
|
-A332/H
|
|
-SDE3FGHIJ4J5M1RWY/LB101
|
|
-ZSSS2035
|
|
-K0859S1040 PIAKS G330 PIMOL A539 BTO W82 DOGAR
|
|
-ZBAA0153 ZBYN
|
|
-PBN/A1B2B3B4B5D1L1 NAV/ABAS REG/B6513 EET/ZBPE0112 SEL/KMAL PER/C RIF/FRT N640 ZBYN RMK/TCAS EQUIPPED)`
|
|
|
|
data := extract(body, FplPatternExpression)
|
|
if data == nil {
|
|
t.Fatal("expected FPL pattern to match")
|
|
}
|
|
|
|
parsed, err := parseCategory(CategoryFlightPlan, ParseContext{
|
|
Body: body,
|
|
Tokens: Tokenizer{}.Tokenize(body),
|
|
}, data)
|
|
if err != nil {
|
|
t.Fatalf("unexpected parse error: %v", err)
|
|
}
|
|
|
|
fpl, ok := parsed.(*domain.FPL)
|
|
if !ok {
|
|
t.Fatalf("expected *domain.FPL, got %T", parsed)
|
|
}
|
|
|
|
if fpl.FlightNumber != "CCA1532" {
|
|
t.Fatalf("expected flight number CCA1532, got %s", fpl.FlightNumber)
|
|
}
|
|
if fpl.PBN != "A1B2B3B4B5D1L1" {
|
|
t.Fatalf("expected PBN A1B2B3B4B5D1L1, got %s", fpl.PBN)
|
|
}
|
|
if fpl.RerouteInformation != "FRT N640 ZBYN" {
|
|
t.Fatalf("expected reroute information, got %s", fpl.RerouteInformation)
|
|
}
|
|
if fpl.Remarks != "TCAS EQUIPPED" {
|
|
t.Fatalf("expected remarks TCAS EQUIPPED, got %s", fpl.Remarks)
|
|
}
|
|
}
|