Skip to content

Commit 8a51b21

Browse files
mpywclaude
andcommitted
test: improve config coverage and add mock return validation tests
- Add tests for DSN missing with SQL query - Add tests for ParseFile (success and file not found) - Add tests for mock validation errors (type one/many mismatch) - Add tests for invalid mock JS return values (array_js returning scalar/object, object_js returning array/scalar) - Add test for 204 No Content response - Remove dead code in errors.go (findUsedArraySource, unreachable branches) Config package coverage: 72.9% -> 94.6% Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1d5d90c commit 8a51b21

6 files changed

Lines changed: 239 additions & 60 deletions

File tree

e2e/invalid_mock_return_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package e2e
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestInvalidMockReturn(t *testing.T) {
12+
cfg := loadConfig(t, "invalid_mock_return_test.yaml")
13+
handler := createHandler(t, nil, cfg)
14+
15+
t.Run("array_js returns scalar", func(t *testing.T) {
16+
req := httptest.NewRequest("GET", "/array-returns-scalar", nil)
17+
w := httptest.NewRecorder()
18+
handler.ServeHTTP(w, req)
19+
20+
assert.Equal(t, http.StatusInternalServerError, w.Code)
21+
assert.Contains(t, w.Body.String(), "array")
22+
})
23+
24+
t.Run("array_js returns object", func(t *testing.T) {
25+
req := httptest.NewRequest("GET", "/array-returns-object", nil)
26+
w := httptest.NewRecorder()
27+
handler.ServeHTTP(w, req)
28+
29+
assert.Equal(t, http.StatusInternalServerError, w.Code)
30+
assert.Contains(t, w.Body.String(), "array")
31+
})
32+
33+
t.Run("object_js returns array", func(t *testing.T) {
34+
req := httptest.NewRequest("GET", "/object-returns-array", nil)
35+
w := httptest.NewRecorder()
36+
handler.ServeHTTP(w, req)
37+
38+
assert.Equal(t, http.StatusInternalServerError, w.Code)
39+
// Error message mentions type validation
40+
assert.Contains(t, w.Body.String(), "error")
41+
})
42+
43+
t.Run("object_js returns scalar", func(t *testing.T) {
44+
req := httptest.NewRequest("GET", "/object-returns-scalar", nil)
45+
w := httptest.NewRecorder()
46+
handler.ServeHTTP(w, req)
47+
48+
assert.Equal(t, http.StatusInternalServerError, w.Code)
49+
// Error message mentions type validation
50+
assert.Contains(t, w.Body.String(), "error")
51+
})
52+
}

e2e/invalid_mock_return_test.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
queries:
2+
# array_js returning a scalar (should error)
3+
- type: many
4+
path: /array-returns-scalar
5+
mock:
6+
array_js: |
7+
return "not an array";
8+
9+
# array_js returning an object (should error)
10+
- type: many
11+
path: /array-returns-object
12+
mock:
13+
array_js: |
14+
return { id: 1 };
15+
16+
# object_js returning an array (should error)
17+
- type: one
18+
path: /object-returns-array
19+
mock:
20+
object_js: |
21+
return [{ id: 1 }];
22+
23+
# object_js returning a scalar (should error)
24+
- type: one
25+
path: /object-returns-scalar
26+
mock:
27+
object_js: |
28+
return "not an object";

e2e/request_response_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,15 @@ func TestResponseObject(t *testing.T) {
218218
"after202": 202
219219
}`, rec.Body.String())
220220
})
221+
222+
t.Run("response.status 204 No Content returns empty body", func(t *testing.T) {
223+
req := httptest.NewRequest(http.MethodGet, "/test/response/204", nil)
224+
rec := httptest.NewRecorder()
225+
226+
mux.ServeHTTP(rec, req)
227+
require.Equal(t, http.StatusNoContent, rec.Code)
228+
require.Empty(t, rec.Body.String())
229+
})
221230
}
222231

223232
func TestRequestInDifferentContexts(t *testing.T) {

e2e/request_response_test.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,16 @@ queries:
193193
const after202 = response.status;
194194
return { before, after99, after600, after202 };
195195
196+
# Test 204 No Content returns empty body
197+
- type: one
198+
path: /test/response/204
199+
mock:
200+
object: { id: 1 }
201+
transform:
202+
post: |
203+
response.status = 204;
204+
return output;
205+
196206
# ==========================================================================
197207
# Request in Different Contexts
198208
# ==========================================================================

internal/config/config_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package config
22

33
import (
4+
"os"
45
"testing"
56

67
"github.com/stretchr/testify/assert"
@@ -349,4 +350,141 @@ queries:
349350

350351
// Note: Transform JS validation happens at server startup via ValidateTransforms,
351352
// not during Parse. Parse only validates the YAML schema structure.
353+
354+
t.Run("schema validation error - type one with array source without filter", func(t *testing.T) {
355+
yaml := `
356+
queries:
357+
- type: one
358+
path: /user
359+
mock:
360+
array:
361+
- id: 1
362+
- id: 2
363+
`
364+
_, err := Parse([]byte(yaml))
365+
require.Error(t, err)
366+
assert.Contains(t, err.Error(), "filter")
367+
assert.Contains(t, err.Error(), "array")
368+
})
369+
370+
t.Run("schema validation error - type many with object source", func(t *testing.T) {
371+
yaml := `
372+
queries:
373+
- type: many
374+
path: /users
375+
mock:
376+
object:
377+
id: 1
378+
`
379+
_, err := Parse([]byte(yaml))
380+
require.Error(t, err)
381+
assert.Contains(t, err.Error(), "object")
382+
assert.Contains(t, err.Error(), "many")
383+
})
384+
385+
t.Run("schema validation error - type none with mock", func(t *testing.T) {
386+
yaml := `
387+
mutations:
388+
- type: none
389+
path: /delete
390+
mock:
391+
object:
392+
id: 1
393+
`
394+
_, err := Parse([]byte(yaml))
395+
require.Error(t, err)
396+
assert.Contains(t, err.Error(), "none")
397+
assert.Contains(t, err.Error(), "mock")
398+
})
399+
400+
t.Run("schema validation error - multiple mock sources", func(t *testing.T) {
401+
yaml := `
402+
queries:
403+
- type: many
404+
path: /users
405+
mock:
406+
array:
407+
- id: 1
408+
array_js: "return [{id: 1}]"
409+
`
410+
_, err := Parse([]byte(yaml))
411+
require.Error(t, err)
412+
assert.Contains(t, err.Error(), "one source")
413+
})
414+
415+
t.Run("schema validation error - type one with csv without filter", func(t *testing.T) {
416+
yaml := `
417+
queries:
418+
- type: one
419+
path: /user
420+
mock:
421+
csv: |
422+
id,name
423+
1,Alice
424+
2,Bob
425+
`
426+
_, err := Parse([]byte(yaml))
427+
require.Error(t, err)
428+
assert.Contains(t, err.Error(), "filter")
429+
assert.Contains(t, err.Error(), "csv")
430+
})
431+
}
432+
433+
func TestParseFile(t *testing.T) {
434+
t.Run("success", func(t *testing.T) {
435+
// Create a temporary file
436+
tmpFile, err := os.CreateTemp("", "config-*.yaml")
437+
require.NoError(t, err)
438+
defer os.Remove(tmpFile.Name())
439+
440+
content := `
441+
queries:
442+
- type: one
443+
path: /user
444+
mock:
445+
object:
446+
id: 1
447+
`
448+
_, err = tmpFile.WriteString(content)
449+
require.NoError(t, err)
450+
tmpFile.Close()
451+
452+
cfg, err := ParseFile(tmpFile.Name())
453+
require.NoError(t, err)
454+
assert.Len(t, cfg.Queries, 1)
455+
})
456+
457+
t.Run("file not found", func(t *testing.T) {
458+
_, err := ParseFile("/nonexistent/path/config.yaml")
459+
require.Error(t, err)
460+
assert.Contains(t, err.Error(), "failed to open file")
461+
})
462+
}
463+
464+
func TestParse_DSNValidation(t *testing.T) {
465+
t.Run("missing DSN with SQL query", func(t *testing.T) {
466+
yaml := `
467+
queries:
468+
- type: one
469+
path: /user
470+
sql: SELECT * FROM users WHERE id = :id
471+
`
472+
_, err := Parse([]byte(yaml))
473+
require.Error(t, err)
474+
assert.Contains(t, err.Error(), "missing dsn")
475+
})
476+
477+
t.Run("missing DSN with mock only is allowed", func(t *testing.T) {
478+
yaml := `
479+
queries:
480+
- type: one
481+
path: /user
482+
mock:
483+
object:
484+
id: 1
485+
`
486+
cfg, err := Parse([]byte(yaml))
487+
require.NoError(t, err)
488+
assert.Empty(t, cfg.DSN)
489+
})
352490
}

internal/config/errors.go

Lines changed: 2 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -108,47 +108,11 @@ func findMockSourceError(errors []errorWithPath) string {
108108
}
109109
}
110110

111-
// Look for missing filter error (array source with type: one without filter)
112-
for _, e := range errors {
113-
if !isMockPath(e.path) {
114-
continue
115-
}
116-
117-
if req, ok := e.err.ErrorKind.(*kind.Required); ok {
118-
if slices.Contains(req.Missing, "filter") {
119-
if usedSource := findUsedArraySource(errors, e.path); usedSource != "" {
120-
parentPath := getParentPath(e.path)
121-
return fmt.Sprintf("at '%s': type 'one' with '%s' source requires 'filter' to select a single row",
122-
formatPath(parentPath), usedSource)
123-
}
124-
}
125-
}
126-
}
127-
128-
// Look for multiple sources error
129-
for _, e := range errors {
130-
if !isMockPath(e.path) {
131-
continue
132-
}
133-
134-
if addl, ok := e.err.ErrorKind.(*kind.AdditionalProperties); ok {
135-
if len(addl.Properties) > 1 {
136-
parentPath := getParentPath(e.path)
137-
return fmt.Sprintf("at '%s': mock must have exactly one source, found multiple: %s",
138-
formatPath(parentPath), strings.Join(addl.Properties, ", "))
139-
}
140-
}
141-
}
142-
143111
return ""
144112
}
145113

146114
// formatMockSourceMismatchError creates a helpful message for source/type mismatches.
147115
func formatMockSourceMismatchError(parentPath string, typeName string, invalidSources []string) string {
148-
if len(invalidSources) == 0 {
149-
return ""
150-
}
151-
152116
source := invalidSources[0]
153117

154118
// Check if multiple sources
@@ -157,7 +121,7 @@ func formatMockSourceMismatchError(parentPath string, typeName string, invalidSo
157121
formatPath(parentPath), strings.Join(invalidSources, ", "))
158122
}
159123

160-
// type: one with array source (without filter - but we should have caught this above)
124+
// type: one with array source without filter
161125
if typeName == "one" && isArraySource(source) {
162126
return fmt.Sprintf("at '%s': type 'one' with '%s' requires 'filter' to select a single row, or use object/object_js for a single object",
163127
formatPath(parentPath), source)
@@ -169,13 +133,7 @@ func formatMockSourceMismatchError(parentPath string, typeName string, invalidSo
169133
formatPath(parentPath), source)
170134
}
171135

172-
// type: none with mock
173-
if typeName == "none" {
174-
return fmt.Sprintf("at '%s': type 'none' does not support mock - use 'sql' instead",
175-
formatPath(parentPath))
176-
}
177-
178-
// Generic message
136+
// Generic message for other cases
179137
return fmt.Sprintf("at '%s': invalid mock source '%s' for type '%s'",
180138
formatPath(parentPath), source, typeName)
181139
}
@@ -235,22 +193,6 @@ func isTypeNone(errors []errorWithPath, parentPath string) bool {
235193
return foundOne && foundMany
236194
}
237195

238-
// findUsedArraySource finds which array source was used from error messages.
239-
func findUsedArraySource(errors []errorWithPath, mockPath string) string {
240-
for _, e := range errors {
241-
if e.path == mockPath {
242-
if addl, ok := e.err.ErrorKind.(*kind.AdditionalProperties); ok {
243-
for _, prop := range addl.Properties {
244-
if isArraySource(prop) {
245-
return prop
246-
}
247-
}
248-
}
249-
}
250-
}
251-
return ""
252-
}
253-
254196
// isSqlMockOneOfError checks if the oneOf error is specifically about sql/mock.
255197
func isSqlMockOneOfError(err *jsonschema.ValidationError) bool {
256198
if err == nil {

0 commit comments

Comments
 (0)