1package magna
2
3import (
4 "errors"
5 "testing"
6
7 "github.com/stretchr/testify/assert"
8 "github.com/stretchr/testify/require"
9)
10
11func TestQuestionDecode(t *testing.T) {
12 tests := []struct {
13 name string
14 input []byte
15 expectedOffset int
16 expected Question
17 expectedErr error
18 wantErrMsg string
19 }{
20 {
21 name: "Valid question - example.com A IN",
22 input: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 0, 1, 0, 1},
23 expectedOffset: 17,
24 expected: Question{
25 QName: "example.com",
26 QType: DNSType(1),
27 QClass: DNSClass(1),
28 },
29 expectedErr: nil,
30 },
31 {
32 name: "Valid question - example.com MX CH",
33 input: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 0, 15, 0, 3},
34 expectedOffset: 17,
35 expected: Question{
36 QName: "example.com",
37 QType: DNSType(15),
38 QClass: DNSClass(3),
39 },
40 expectedErr: nil,
41 },
42 {
43 name: "Invalid domain name - label too long",
44 input: []byte{64, 'i', 'n', 'v', 'a', 'l', 'i', 'd', 0, 0, 1, 0, 1},
45 expectedOffset: 13,
46 expected: Question{},
47 expectedErr: &InvalidLabelError{},
48 wantErrMsg: "question decode: failed to decode QName: invalid domain label: length 64 exceeds maximum 63",
49 },
50 {
51 name: "Invalid domain name - compression loop",
52 input: []byte{0xC0, 0x00, 0, 1, 0, 1},
53 expectedOffset: 6,
54 expected: Question{},
55 expectedErr: &DomainCompressionError{},
56 wantErrMsg: "question decode: failed to decode QName: invalid domain compression: pointer loop detected",
57 },
58 {
59 name: "Insufficient buffer for QType",
60 input: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 0},
61 expectedOffset: 14,
62 expected: Question{QName: "example.com"},
63 expectedErr: &BufferOverflowError{},
64 wantErrMsg: "question decode: failed to decode QType for example.com: buffer overflow",
65 },
66 {
67 name: "Insufficient buffer for QClass",
68 input: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 0, 1, 0},
69 expectedOffset: 16,
70 expected: Question{QName: "example.com", QType: DNSType(1)},
71 expectedErr: &BufferOverflowError{},
72 wantErrMsg: "question decode: failed to decode QClass for example.com: buffer overflow",
73 },
74 }
75
76 for _, tt := range tests {
77 t.Run(tt.name, func(t *testing.T) {
78 q := &Question{}
79 offset, err := q.Decode(tt.input, 0)
80
81 if tt.expectedErr != nil {
82 assert.Error(t, err, "Expected an error but got nil")
83 assert.True(t, errors.Is(err, tt.expectedErr), "Error type mismatch. Got %T, expected %T", err, tt.expectedErr)
84 if tt.wantErrMsg != "" {
85 assert.ErrorContains(t, err, tt.wantErrMsg, "Wrapped error message mismatch")
86 }
87 assert.Equal(t, tt.expectedOffset, offset, "Offset mismatch on error")
88 } else {
89 assert.NoError(t, err, "Expected no error but got one")
90 assert.Equal(t, tt.expected, *q, "Decoded question mismatch")
91 assert.Equal(t, tt.expectedOffset, offset, "Offset mismatch on success")
92 }
93 })
94 }
95}
96
97func TestQuestionEncode(t *testing.T) {
98 tests := []struct {
99 name string
100 question Question
101 initialBuf []byte
102 offsets map[string]uint16
103 expected []byte
104 expectedErr error
105 wantErrMsg string
106 newOffsets map[string]uint16
107 }{
108 {
109 name: "Simple domain - example.com A IN",
110 question: Question{
111 QName: "example.com",
112 QType: DNSType(1),
113 QClass: DNSClass(1),
114 },
115 initialBuf: nil,
116 offsets: make(map[string]uint16),
117 expected: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 0, 1, 0, 1},
118 newOffsets: map[string]uint16{"example.com": 0, "com": 8},
119 },
120 {
121 name: "Subdomain - subdomain.example.com AAAA IN",
122 question: Question{
123 QName: "subdomain.example.com",
124 QType: DNSType(28),
125 QClass: DNSClass(1),
126 },
127 initialBuf: nil,
128 offsets: make(map[string]uint16),
129 expected: []byte{9, 's', 'u', 'b', 'd', 'o', 'm', 'a', 'i', 'n', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 0, 28, 0, 1},
130 newOffsets: map[string]uint16{"subdomain.example.com": 0, "example.com": 10, "com": 18},
131 },
132 {
133 name: "Different class - example.com MX CH",
134 question: Question{
135 QName: "example.com",
136 QType: DNSType(15),
137 QClass: DNSClass(3),
138 },
139 initialBuf: nil,
140 offsets: make(map[string]uint16),
141 expected: []byte{7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 0, 15, 0, 3},
142 newOffsets: map[string]uint16{"example.com": 0, "com": 8},
143 },
144 {
145 name: "Domain compression - example.com after subdomain.example.com",
146 question: Question{
147 QName: "example.com",
148 QType: DNSType(1),
149 QClass: DNSClass(1),
150 },
151 initialBuf: nil,
152 offsets: map[string]uint16{
153 "subdomain.example.com": 0,
154 "example.com": 10,
155 "com": 18,
156 },
157 expected: []byte{0xC0, 0x0a, 0x00, 0x01, 0x00, 0x01},
158 newOffsets: map[string]uint16{
159 "subdomain.example.com": 0,
160 "example.com": 10,
161 "com": 18,
162 },
163 },
164 {
165 name: "Encode with initial buffer",
166 question: Question{
167 QName: "test.org",
168 QType: AType,
169 QClass: IN,
170 },
171 initialBuf: []byte{0xAA, 0xBB},
172 offsets: make(map[string]uint16),
173 expected: []byte{0xAA, 0xBB, 4, 't', 'e', 's', 't', 3, 'o', 'r', 'g', 0, 0, 1, 0, 1},
174 newOffsets: map[string]uint16{"test.org": 2, "org": 7},
175 },
176 {
177 name: "Encode invalid domain - label too long",
178 question: Question{
179 QName: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com",
180 QType: AType,
181 QClass: IN,
182 },
183 initialBuf: nil,
184 offsets: make(map[string]uint16),
185 expected: nil,
186 expectedErr: &InvalidLabelError{},
187 wantErrMsg: "question encode: failed to encode QName",
188 newOffsets: map[string]uint16{},
189 },
190 }
191
192 for _, tt := range tests {
193 t.Run(tt.name, func(t *testing.T) {
194 currentOffsets := make(map[string]uint16)
195 for k, v := range tt.offsets {
196 currentOffsets[k] = v
197 }
198
199 result, err := tt.question.Encode(tt.initialBuf, ¤tOffsets)
200
201 if tt.expectedErr != nil {
202 assert.Error(t, err, "Expected an error but got nil")
203 assert.True(t, errors.Is(err, tt.expectedErr), "Error type mismatch. Got %T, expected %T", err, tt.expectedErr)
204 if tt.wantErrMsg != "" {
205 assert.ErrorContains(t, err, tt.wantErrMsg, "Wrapped error message mismatch")
206 }
207 } else {
208 assert.NoError(t, err, "Expected no error but got one")
209 assert.Equal(t, tt.expected, result, "Encoded question mismatch")
210 assert.Equal(t, tt.newOffsets, currentOffsets, "Final offsets mismatch")
211 }
212 })
213 }
214}
215
216func TestQuestionEncodeDecodeRoundTrip(t *testing.T) {
217 tests := []struct {
218 name string
219 question Question
220 }{
221 {
222 name: "Simple domain - example.com A IN",
223 question: Question{
224 QName: "example.com",
225 QType: DNSType(1),
226 QClass: DNSClass(1),
227 },
228 },
229 {
230 name: "Subdomain - subdomain.example.com AAAA IN",
231 question: Question{
232 QName: "subdomain.example.com",
233 QType: DNSType(28),
234 QClass: DNSClass(1),
235 },
236 },
237 {
238 name: "Different class - example.com MX CH",
239 question: Question{
240 QName: "example.com",
241 QType: DNSType(15),
242 QClass: DNSClass(3),
243 },
244 },
245 }
246
247 for _, tt := range tests {
248 t.Run(tt.name, func(t *testing.T) {
249 offsets := make(map[string]uint16)
250 encoded, err := tt.question.Encode(nil, &offsets)
251 require.NoError(t, err, "Encoding failed")
252
253 decodedQuestion := &Question{}
254 offset, err := decodedQuestion.Decode(encoded, 0)
255
256 assert.NoError(t, err, "Decoding failed")
257 assert.Equal(t, len(encoded), offset, "Offset after decoding should match encoded length")
258 assert.Equal(t, tt.question, *decodedQuestion, "Decoded question does not match original")
259 })
260 }
261}