Skip to content

Commit 694ef31

Browse files
committed
feat: Enhance mock server 2
1 parent a5ad232 commit 694ef31

File tree

3 files changed

+252
-37
lines changed

3 files changed

+252
-37
lines changed

cmd/mockserver/run.go

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import (
1010
)
1111

1212
type runConfig struct {
13-
host string
14-
port int
15-
delay int
13+
host string
14+
port int
15+
delay int
16+
defaultResponseCode string
17+
defaultResponseType string
1618
}
1719

1820
// Command-specific flags for the run command
@@ -34,9 +36,11 @@ func newRunCommand() *cobra.Command {
3436

3537
return mockserver.Run(
3638
mockserver2.RunOptions{
37-
Host: runCfg.host,
38-
Port: runCfg.port,
39-
Delay: runCfg.delay,
39+
Host: runCfg.host,
40+
Port: runCfg.port,
41+
Delay: runCfg.delay,
42+
DefaultResponseCode: runCfg.defaultResponseCode,
43+
DefaultResponseType: runCfg.defaultResponseType,
4044
},
4145
)
4246
},
@@ -45,6 +49,20 @@ func newRunCommand() *cobra.Command {
4549
cmd.Flags().StringVarP(&runCfg.host, "host", "", "127.0.0.1", "Host to run the mock server on")
4650
cmd.Flags().IntVarP(&runCfg.port, "port", "p", 8080, "Port to run the mock server on")
4751
cmd.Flags().IntVarP(&runCfg.delay, "delay", "d", 0, "Delay in milliseconds to simulate network latency")
52+
cmd.Flags().StringVarP(
53+
&runCfg.defaultResponseCode,
54+
"default-response-code",
55+
"",
56+
"200",
57+
"Default response code to use",
58+
)
59+
cmd.Flags().StringVarP(
60+
&runCfg.defaultResponseType,
61+
"default-response-type",
62+
"",
63+
"json",
64+
"Default response type to use",
65+
)
4866

4967
return cmd
5068
}

internal/mockserver/mockserver.go

Lines changed: 128 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,27 @@ import (
1111
"github.com/gin-gonic/gin"
1212
)
1313

14+
const (
15+
responseCodeHeaderName = "X-Mock-Response-Code"
16+
responseCodeQueryParamName = "x-response-code"
17+
responseTypeQueryParamName = "x-response-type"
18+
availableResponsesHeaderName = "X-Available-Responses"
19+
)
20+
1421
type RunOptions struct {
15-
Host string
16-
Port int
17-
Delay int
22+
Host string
23+
Port int
24+
Delay int
25+
DefaultResponseCode string
26+
DefaultResponseType string
27+
}
28+
29+
// XMLNode represents a generic XML node
30+
type XMLNode struct {
31+
XMLName xml.Name
32+
Attrs []xml.Attr `xml:"attr,omitempty"`
33+
Value string `xml:",chardata"`
34+
Children []*XMLNode `xml:",any"`
1835
}
1936

2037
type MockServer struct {
@@ -28,8 +45,11 @@ func NewMockServer(doc *openapi3.T) *MockServer {
2845
}
2946

3047
func (m *MockServer) Run(options RunOptions) error {
48+
gin.SetMode(gin.ReleaseMode)
3149
app := gin.Default()
3250

51+
app.Use(m.availableResponsesMiddleware())
52+
3353
for _, path := range m.doc.Paths.InMatchingOrder() {
3454
for method, operation := range m.doc.Paths.Find(path).Operations() {
3555
app.Handle(method, convertPathToGinFormat(path), m.registerHandler(operation, options))
@@ -41,90 +61,167 @@ func (m *MockServer) Run(options RunOptions) error {
4161
"/", func(c *gin.Context) {
4262
var routes []gin.H
4363
for _, route := range app.Routes() {
64+
if route.Path == "/" {
65+
continue
66+
}
67+
68+
path := m.doc.Paths.Find(convertGinPathToOpenAPI(route.Path))
69+
responseCodeToContentType := make(map[string]string)
70+
if op := path.GetOperation(route.Method); op != nil {
71+
for code, resp := range op.Responses.Map() {
72+
responseCodeToContentType[code] = "application/json"
73+
for contentType := range resp.Value.Content {
74+
responseCodeToContentType[code] = contentType
75+
}
76+
}
77+
}
78+
4479
routes = append(
4580
routes, gin.H{
46-
"method": route.Method,
47-
"path": route.Path,
81+
"method": route.Method,
82+
"path": route.Path,
83+
"availableResponses": responseCodeToContentType,
4884
},
4985
)
5086
}
51-
c.JSON(http.StatusOK, routes)
87+
88+
c.JSON(
89+
http.StatusOK, gin.H{
90+
"routes": routes,
91+
"usage": gin.H{
92+
"responseCode": gin.H{
93+
"queryParam": fmt.Sprintf("?%s=<status_code>", responseCodeQueryParamName),
94+
"header": fmt.Sprintf("%s: <status_code>", responseCodeHeaderName),
95+
"availableCodes": fmt.Sprintf("%s header in response", availableResponsesHeaderName),
96+
},
97+
"responseType": gin.H{
98+
"queryParam": fmt.Sprintf("?%s=<response_type>", responseTypeQueryParamName),
99+
},
100+
},
101+
},
102+
)
52103
},
53104
)
54105

106+
fmt.Printf("Mock server listening on http://%s:%d\n", options.Host, options.Port)
55107
return app.Run(fmt.Sprintf("%s:%d", options.Host, options.Port))
56108
}
57109

110+
func (m *MockServer) availableResponsesMiddleware() gin.HandlerFunc {
111+
return func(c *gin.Context) {
112+
c.Next()
113+
114+
// After handler execution, add header with available response codes
115+
path := m.doc.Paths.Find(convertGinPathToOpenAPI(c.FullPath()))
116+
if path != nil {
117+
if op := path.GetOperation(c.Request.Method); op != nil {
118+
var codes []string
119+
for code := range op.Responses.Map() {
120+
codes = append(codes, code)
121+
}
122+
if len(codes) > 0 {
123+
c.Header(availableResponsesHeaderName, strings.Join(codes, ","))
124+
}
125+
}
126+
}
127+
}
128+
}
129+
58130
func (m *MockServer) registerHandler(op *openapi3.Operation, options RunOptions) gin.HandlerFunc {
59131
return func(c *gin.Context) {
60132
// If Delay is set in options, apply it to simulate latency
61133
if options.Delay > 0 {
62134
time.Sleep(time.Duration(options.Delay) * time.Millisecond)
63135
}
64136

65-
status, response := m.findResponse(op)
137+
// Get desired response code from query param or header
138+
desiredCode := c.Query(responseCodeQueryParamName)
139+
if desiredCode == "" {
140+
desiredCode = c.GetHeader(responseCodeHeaderName)
141+
}
142+
143+
// If no code specified, use default
144+
if desiredCode == "" {
145+
desiredCode = options.DefaultResponseCode
146+
}
147+
148+
// Get desired response type from query param
149+
desiredType := c.Query(responseTypeQueryParamName)
150+
if desiredType == "" {
151+
desiredType = options.DefaultResponseType
152+
}
153+
154+
response := m.findSpecificResponse(op, desiredCode)
66155
if response == nil {
67-
c.JSON(http.StatusInternalServerError, gin.H{"error": "No response defined"})
156+
body := gin.H{
157+
"error": fmt.Sprintf("No response defined for status code %s", desiredCode),
158+
"availableCodes": m.getAvailableResponseCodes(op),
159+
}
160+
if desiredType == "xml" {
161+
c.XML(http.StatusBadRequest, body)
162+
return
163+
} else {
164+
c.JSON(http.StatusBadRequest, body)
165+
}
68166
return
69167
}
70168

71-
// Get accepted content types from Accept header
169+
status := parseStatusCode(desiredCode)
72170
acceptHeader := c.GetHeader("Accept")
73171
acceptedTypes := parseAcceptHeader(acceptHeader)
74172

75-
// Find matching content type and schema
76173
var contentType string
77174
var schema *openapi3.Schema
78175
for mediaType, content := range response.Content {
79176
for _, acceptedType := range acceptedTypes {
80-
if strings.HasPrefix(mediaType, acceptedType) {
177+
if desiredType != "" {
178+
if strings.HasSuffix(mediaType, desiredType) {
179+
contentType = mediaType
180+
schema = content.Schema.Value
181+
break
182+
}
183+
184+
} else if strings.HasPrefix(mediaType, acceptedType) {
81185
contentType = mediaType
82186
schema = content.Schema.Value
83187
break
84188
}
85189
}
190+
86191
if schema != nil {
87192
break
88193
}
89194
}
90195

91-
// If no matching content type found, default to JSON
92196
if schema == nil {
93197
contentType = "application/json"
94198
if jsonContent, ok := response.Content["application/json"]; ok {
95199
schema = jsonContent.Schema.Value
96200
}
97201
}
98202

99-
// Generate mock data based on schema
100203
mockData := generateMockData(schema)
101204

102-
// Send response based on content type
103205
switch {
104206
case strings.Contains(contentType, "application/xml"):
105-
c.Header("Content-Type", "application/xml")
106-
xmlData, err := xml.Marshal(mockData)
107-
if err != nil {
108-
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate XML"})
109-
return
110-
}
111-
c.String(status, string(xmlData))
207+
c.XML(status, mapToXML(mockData, schema, "root"))
112208
default:
113209
c.JSON(status, mockData)
114210
}
115211
}
116212
}
117213

118-
func (m *MockServer) findResponse(op *openapi3.Operation) (int, *openapi3.Response) {
119-
for statusCode, responseRef := range op.Responses.Map() {
120-
status := parseStatusCode(statusCode)
121-
if status >= 200 && status < 300 {
122-
return status, responseRef.Value
123-
}
214+
func (m *MockServer) findSpecificResponse(op *openapi3.Operation, code string) *openapi3.Response {
215+
if responseRef, ok := op.Responses.Map()[code]; ok {
216+
return responseRef.Value
124217
}
125-
// Default to 200 if no successful response found
126-
if defaultResponse := op.Responses.Default(); defaultResponse != nil {
127-
return 200, defaultResponse.Value
218+
return nil
219+
}
220+
221+
func (m *MockServer) getAvailableResponseCodes(op *openapi3.Operation) []string {
222+
var codes []string
223+
for code := range op.Responses.Map() {
224+
codes = append(codes, code)
128225
}
129-
return 200, nil
226+
return codes
130227
}

0 commit comments

Comments
 (0)