Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 59 additions & 13 deletions accessor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ func TestLocationString(t *testing.T) {
loc: Location{Segment: "NK", Field: 1},
want: "NK-1",
},
{
name: "component zero omitted",
loc: Location{Segment: "PID", Field: 3, Component: 0},
want: "PID-3",
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -318,26 +323,40 @@ func TestParseLocationEdgeCases(t *testing.T) {
name string
input string
wantErr bool
wantLoc Location // asserted when non-zero (Segment != "")
}{
{"EmptySegmentIndex", "OBX()-5", true},
{"SingleCharSegment", "X-1", true},
{"BadRepIndex", "PID-3[abc]", true},
{"BadComponentNum", "PID-3.abc", true},
{"BadSubcomponentNum", "PID-3.1.abc", true},
{"EmptyBrackets", "PID-3[]", true},
{"EmptyFieldPart", "PID-", true},
{"TrailingGarbageAfterRep", "PID-3[1]x", true},
{"TrailingGarbageAfterSegIndex", "OBX(1)x-5", true},
{"EmptySegmentIndex", "OBX()-5", true, Location{}},
{"SingleCharSegment", "X-1", true, Location{}},
{"BadRepIndex", "PID-3[abc]", true, Location{}},
{"BadComponentNum", "PID-3.abc", true, Location{}},
{"BadSubcomponentNum", "PID-3.1.abc", true, Location{}},
{"EmptyBrackets", "PID-3[]", true, Location{}},
{"EmptyFieldPart", "PID-", true, Location{}},
{"TrailingGarbageAfterRep", "PID-3[1]x", true, Location{}},
{"TrailingGarbageAfterSegIndex", "OBX(1)x-5", true, Location{}},
// ZeroComponent documents that ".0" is accepted and treated as
// "component not specified" (equivalent to omitting the component).
{"ZeroComponent", "PID-3.0", false},
{"ZeroComponent", "PID-3.0", false, Location{Segment: "PID", Field: 3, Component: 0}},
// Trailing dot after field: accepted, Component stays 0.
{"TrailingDotAfterField", "PID-3.", false, Location{Segment: "PID", Field: 3, Component: 0}},
// Trailing dot after repetition: accepted, Component stays 0.
{"TrailingDotAfterRepetition", "PID-3[1].", false, Location{Segment: "PID", Field: 3, Repetition: 1, Component: 0}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseLocation(tt.input)
if tt.wantErr && err == nil {
t.Errorf("expected error for %q", tt.input)
got, err := ParseLocation(tt.input)
if tt.wantErr {
if err == nil {
t.Errorf("expected error for %q, got %+v", tt.input, got)
}
return
}
if err != nil {
t.Fatalf("unexpected error for %q: %v", tt.input, err)
}
if tt.wantLoc.Segment != "" && got != tt.wantLoc {
t.Errorf("ParseLocation(%q) = %+v, want %+v", tt.input, got, tt.wantLoc)
}
})
}
Expand Down Expand Up @@ -378,6 +397,33 @@ func TestGetSubcomponentEmpty(t *testing.T) {
}
}

func TestMessageGetPresentButEmptyField(t *testing.T) {
// PID-2 is between two consecutive field separators (||) — present but empty.
raw := []byte("MSH|^~\\&|S|F|R|RF|20240101||ADT^A01|1|P|2.5.1\rPID|1||123")
msg, err := ParseMessage(raw)
if err != nil {
t.Fatalf("ParseMessage: %v", err)
}

v := msg.Get("PID-2")
if !v.IsEmpty() {
t.Errorf("Get(PID-2).IsEmpty() = false, want true")
}
if v.String() != "" {
t.Errorf("Get(PID-2).String() = %q, want empty", v.String())
}
// A present-but-empty field returns a zero Value (nil raw): the early-return
// in getByLocation when field.IsEmpty() is true yields a zero Value.
if v.Raw() != nil {
t.Errorf("Get(PID-2).Raw() = %v, want nil", v.Raw())
}

// Contrast: PID-3 has a value.
if got := msg.Get("PID-3").String(); got != "123" {
t.Errorf("Get(PID-3).String() = %q, want 123", got)
}
}

func BenchmarkParseLocation(b *testing.B) {
inputs := []struct {
name string
Expand Down
60 changes: 60 additions & 0 deletions ack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,66 @@ func TestAckWithErrors(t *testing.T) {
t.Errorf("ERR-2.5 = %q, want 2", got)
}
})

t.Run("EmptyLocation", func(t *testing.T) {
// appendERL("", d) hits the len < 2 guard and returns buf unchanged.
// ERR-2 should be empty while ERR-5 (code) and ERR-7 (description) are set.
msg := parseTestMsg(t, baseMSH)
issues := []Issue{
{Severity: SeverityError, Location: "", Code: "CUSTOM", Description: "desc"},
}

ack, err := msg.Ack(AE, "ACK-X", WithTimestamp(fixedTime), WithErrors(issues))
if err != nil {
t.Fatalf("Ack: %v", err)
}

ackMsg, err := ParseMessage(ack)
if err != nil {
t.Fatalf("ParseMessage(ack): %v", err)
}

errSeg := &ackMsg.Segments()[2]
// ERR-2.1 should be empty (no ERL constructed for empty location).
if got := errSeg.Field(2).Rep(0).Component(1).String(); got != "" {
t.Errorf("ERR-2.1 = %q, want empty", got)
}
// ERR-5.1 should carry the error code.
if got := errSeg.Field(5).Rep(0).Component(1).String(); got != "CUSTOM" {
t.Errorf("ERR-5.1 = %q, want CUSTOM", got)
}
// ERR-7 should carry the description.
if got := errSeg.Field(7).String(); got != "desc" {
t.Errorf("ERR-7 = %q, want desc", got)
}
})
}

func TestAckTruncatedMSH(t *testing.T) {
fixedTime := time.Date(2024, 1, 15, 12, 30, 0, 0, time.UTC)
// MSH with only 9 fields — MSH-10 through MSH-12 are absent.
// Missing fields return zero Values; Ack() must not panic or error.
msg := parseTestMsg(t,
"MSH|^~\\&|S|F|R|RF|20240115||ADT^A01")

ack, err := msg.Ack(AA, "REPLY001", WithTimestamp(fixedTime))
if err != nil {
t.Fatalf("Ack: %v", err)
}

ackMsg, err := ParseMessage(ack)
if err != nil {
t.Fatalf("ParseMessage(ack): %v", err)
}

// MSA-2 holds the original message control ID (MSH-10), which was absent.
if got := ackMsg.Get("MSA-2").String(); got != "" {
t.Errorf("MSA-2 = %q, want empty (original MSH-10 absent)", got)
}
// The supplied controlID must appear in the ACK MSH-10.
if got := ackMsg.Get("MSH-10").String(); got != "REPLY001" {
t.Errorf("MSH-10 = %q, want REPLY001", got)
}
}

func TestAckNoMSH(t *testing.T) {
Expand Down
105 changes: 105 additions & 0 deletions batch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,111 @@ func TestBatchBuilderBuildIsReusable(t *testing.T) {
}
}

func TestBatchBuilderSetHeaderReservedFieldNums(t *testing.T) {
// SetHeader with keys 0, 1, 2 are silently ignored by Build() (loop starts at i=3).
bb := NewBatchBuilder()
bb.SetHeader(0, "ZERO")
bb.SetHeader(1, "SEP")
bb.SetHeader(2, "ENC")
data, err := bb.Build()
if err != nil {
t.Fatalf("Build: %v", err)
}
batch, err := ParseBatch(data)
if err != nil {
t.Fatalf("ParseBatch: %v", err)
}
if batch.Header == nil {
t.Fatal("expected BHS header")
}
// Field(1) must still be the literal field separator.
if got := batch.Header.Field(1).String(); got != "|" {
t.Errorf("BHS-1 = %q, want |", got)
}
// Field(2) must still be the encoding chars.
if got := batch.Header.Field(2).String(); got != "^~\\&" {
t.Errorf("BHS-2 = %q, want ^~\\&", got)
}
// Field(3) must be empty — "ZERO" should not have leaked in.
if got := batch.Header.Field(3); !got.IsEmpty() {
t.Errorf("BHS-3 = %q, want empty (reserved field 0 must not bleed through)", got.String())
}
}

func TestBatchBuilderMessageWithoutTrailingCR(t *testing.T) {
// ParseMessage never appends a trailing \r. Build() detects this and adds one.
raw := []byte("MSH|^~\\&|APP|FAC|||20240101||ADT^A01|001|P|2.5.1")
msg, err := ParseMessage(raw)
if err != nil {
t.Fatalf("ParseMessage: %v", err)
}
// Confirm the precondition: no trailing \r.
msgRaw := msg.Raw()
if len(msgRaw) > 0 && msgRaw[len(msgRaw)-1] == '\r' {
t.Skip("message unexpectedly ends with \\r; precondition not met")
}

bb := NewBatchBuilder()
bb.Add(msg)
data, err := bb.Build()
if err != nil {
t.Fatalf("Build: %v", err)
}
batch, err := ParseBatch(data)
if err != nil {
t.Fatalf("ParseBatch round-trip: %v", err)
}
if len(batch.Messages) != 1 {
t.Fatalf("expected 1 message, got %d", len(batch.Messages))
}
if got := batch.Messages[0].Get("MSH-10").String(); got != "001" {
t.Errorf("MSH-10 = %q, want 001", got)
}
}

func TestBatchBuilderSetHeaderEmptyValue(t *testing.T) {
// Empty string stored and passed through Escape fast-path; BHS-3 should be empty.
bb := NewBatchBuilder()
bb.SetHeader(3, "")
data, err := bb.Build()
if err != nil {
t.Fatalf("Build: %v", err)
}
batch, err := ParseBatch(data)
if err != nil {
t.Fatalf("ParseBatch: %v", err)
}
if batch.Header == nil {
t.Fatal("expected BHS header")
}
if got := batch.Header.Field(3); !got.IsEmpty() {
t.Errorf("BHS-3 = %q, want empty", got.String())
}
}

func TestBatchBuilderBHS7RefreshAfterReset(t *testing.T) {
// After Reset(), Build() calls time.Now() again, so BHS-7 is never stale.
bb := NewBatchBuilder()
before := time.Now().Format("20060102150405")
bb.Reset()
data, err := bb.Build()
after := time.Now().Format("20060102150405")
if err != nil {
t.Fatalf("Build: %v", err)
}
batch, err := ParseBatch(data)
if err != nil {
t.Fatalf("ParseBatch: %v", err)
}
if batch.Header == nil {
t.Fatal("expected BHS header")
}
got := batch.Header.Field(7).String()
if got < before || got > after {
t.Errorf("BHS-7 = %q after Reset, want between %q and %q", got, before, after)
}
}

func BenchmarkBatchBuilder1Message(b *testing.B) {
msg, _ := ParseMessage([]byte("MSH|^~\\&|APP|FAC|REC|RFAC|20240101||ADT^A01|001|P|2.5.1\rPID|1||123^^^MRN||Smith^John|||M"))
bb := NewBatchBuilder()
Expand Down
8 changes: 8 additions & 0 deletions escape_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,12 @@ func TestUnescapeEdgeCases(t *testing.T) {
t.Errorf("got %q, want %q", got, `\X\`)
}
})

t.Run("HexThreeNibbles", func(t *testing.T) {
// \X41F\ — three hex chars: pair "41" decodes to 'A'; trailing 'F' is dropped.
got := Unescape([]byte(`\X41F\`), d)
if string(got) != "A" {
t.Errorf("got %q, want %q", got, "A")
}
})
}
20 changes: 20 additions & 0 deletions message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,26 @@ func TestParseMessage(t *testing.T) {
}
})

t.Run("exactly minMSHLength", func(t *testing.T) {
// "MSH|^~\&" is exactly 8 bytes (minMSHLength) — the smallest valid message.
msg, err := ParseMessage([]byte("MSH|^~\\&"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(msg.Segments()) != 1 {
t.Errorf("len(Segments()) = %d, want 1", len(msg.Segments()))
}
if got := msg.Segments()[0].Field(1).String(); got != "|" {
t.Errorf("Field(1) = %q, want |", got)
}
if got := msg.Segments()[0].Field(2).String(); got != "^~\\&" {
t.Errorf("Field(2) = %q, want ^~\\&", got)
}
if got := msg.Segments()[0].Field(3); !got.IsEmpty() {
t.Errorf("Field(3) should be empty, got %q", got.String())
}
})

t.Run("too short", func(t *testing.T) {
_, err := ParseMessage([]byte("MSH"))
if err != ErrMessageTooShort {
Expand Down
Loading
Loading