diff --git a/coderd/coderdtest/swagger_test.go b/coderd/coderdtest/swagger_test.go index ef8dd88f79c99..07fb19ad64b85 100644 --- a/coderd/coderdtest/swagger_test.go +++ b/coderd/coderdtest/swagger_test.go @@ -18,6 +18,7 @@ func TestEndpointsDocumented(t *testing.T) { swaggerComments, err := coderdtest.ParseSwaggerComments("..") require.NoError(t, err, "can't parse swagger comments") + require.NotEmpty(t, swaggerComments, "swagger comments must be present") _, _, api := coderdtest.NewWithAPI(t, nil) coderdtest.VerifySwaggerDefinitions(t, api.APIHandler, swaggerComments) diff --git a/coderd/coderdtest/swaggerparser.go b/coderd/coderdtest/swaggerparser.go index ccdb082d4a328..c5d19cf088966 100644 --- a/coderd/coderdtest/swaggerparser.go +++ b/coderd/coderdtest/swaggerparser.go @@ -149,6 +149,7 @@ func parseSwaggerComment(commentGroup *ast.CommentGroup) SwaggerComment { func VerifySwaggerDefinitions(t *testing.T, router chi.Router, swaggerComments []SwaggerComment) { assertUniqueRoutes(t, swaggerComments) + assertSingleAnnotations(t, swaggerComments) err := chi.Walk(router, func(method, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { method = strings.ToLower(method) @@ -192,6 +193,36 @@ func assertUniqueRoutes(t *testing.T, comments []SwaggerComment) { } } +var uniqueAnnotations = []string{"@ID", "@Summary", "@Tags", "@Router"} + +func assertSingleAnnotations(t *testing.T, comments []SwaggerComment) { + for _, comment := range comments { + counters := map[string]int{} + + for _, line := range comment.raw { + splitN := strings.SplitN(strings.TrimSpace(line.Text), " ", 3) + if len(splitN) < 2 { + continue // comment prefix without any content + } + + if !strings.HasPrefix(splitN[1], "@") { + continue // not a swagger annotation + } + + annotation := splitN[1] + if _, ok := counters[annotation]; !ok { + counters[annotation] = 0 + } + counters[annotation]++ + } + + for _, annotation := range uniqueAnnotations { + v := counters[annotation] + assert.Equal(t, 1, v, "%s annotation for route %s must be defined only once", annotation, comment.router) + } + } +} + func findSwaggerCommentByMethodAndRoute(comments []SwaggerComment, method, route string) *SwaggerComment { for _, c := range comments { if c.method == method && c.router == route { @@ -219,6 +250,7 @@ func assertRequiredAnnotations(t *testing.T, comment SwaggerComment) { assert.NotEmpty(t, comment.id, "@ID must be defined") assert.NotEmpty(t, comment.summary, "@Summary must be defined") assert.NotEmpty(t, comment.tags, "@Tags must be defined") + assert.NotEmpty(t, comment.router, "@Router must be defined") } func assertGoCommentFirst(t *testing.T, comment SwaggerComment) { @@ -295,6 +327,8 @@ func assertAccept(t *testing.T, comment SwaggerComment) { } } +var allowedProduceTypes = []string{"json", "text/event-stream"} + func assertProduce(t *testing.T, comment SwaggerComment) { var hasResponseModel bool for _, r := range comment.successes { @@ -306,6 +340,7 @@ func assertProduce(t *testing.T, comment SwaggerComment) { if hasResponseModel { assert.True(t, comment.produce != "", "Route must have @Produce annotation as it responds with a model structure") + assert.Contains(t, allowedProduceTypes, comment.produce, "@Produce value is limited to specific types: %s", strings.Join(allowedProduceTypes, ",")) } else { if (comment.router == "/workspaceagents/me/app-health" && comment.method == "post") || (comment.router == "/workspaceagents/me/version" && comment.method == "post") || diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index e05c3d19a2000..58851458f52e5 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -826,7 +826,7 @@ func convertWorkspaceAgent(derpMap *tailcfg.DERPMap, coordinator tailnet.Coordin // @ID submit-workspace-agent-stats // @Security CoderSessionToken // @Accept json -// @Produce application/json +// @Produce json // @Tags Agents // @Param request body codersdk.AgentStats true "Stats request" // @Success 200 {object} codersdk.AgentStatsResponse @@ -904,7 +904,7 @@ func (api *API) workspaceAgentReportStats(rw http.ResponseWriter, r *http.Reques // @ID submit-workspace-agent-application-health // @Security CoderSessionToken // @Accept json -// @Produce application/json +// @Produce json // @Tags Agents // @Param request body codersdk.PostWorkspaceAppHealthsRequest true "Application health request" // @Success 200 diff --git a/enterprise/coderd/coderdenttest/swagger_test.go b/enterprise/coderd/coderdenttest/swagger_test.go index c4ad0a8dd5810..2d3e16aa9929b 100644 --- a/enterprise/coderd/coderdenttest/swagger_test.go +++ b/enterprise/coderd/coderdenttest/swagger_test.go @@ -14,6 +14,7 @@ func TestEnterpriseEndpointsDocumented(t *testing.T) { swaggerComments, err := coderdtest.ParseSwaggerComments("..", "../../../coderd") require.NoError(t, err, "can't parse swagger comments") + require.NotEmpty(t, swaggerComments, "swagger comments must be present") _, _, api := coderdenttest.NewWithAPI(t, nil) coderdtest.VerifySwaggerDefinitions(t, api.AGPL.APIHandler, swaggerComments)