snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 1s
This commit is contained in:
349
tools/mattermost-govet/openApiSync/openApiSync.go
Normal file
349
tools/mattermost-govet/openApiSync/openApiSync.go
Normal file
@@ -0,0 +1,349 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package openApiSync
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pb33f/libopenapi"
|
||||
v3high "github.com/pb33f/libopenapi/datamodel/high/v3"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sajari/fuzzy"
|
||||
"golang.org/x/tools/go/analysis"
|
||||
)
|
||||
|
||||
var (
|
||||
Analyzer = &analysis.Analyzer{
|
||||
Name: "openApiSync",
|
||||
Doc: "check for inconsistencies between OpenAPI spec and the source code",
|
||||
Run: run,
|
||||
}
|
||||
|
||||
specFile string
|
||||
groupSplitRegexp = regexp.MustCompile(`{([a-z_]*):([a-z_]*)\|([a-z_]*)}`)
|
||||
// Excludes | so group-alternation patterns like {type:a|b} are left intact for splitHandlerByGroup.
|
||||
pathParamConstraintRegex = regexp.MustCompile(`\{([^:}]+):[^|}]+\}`)
|
||||
openAPIParamRegex = regexp.MustCompile(`\{[^}]+\}`)
|
||||
IgnoredCases = []string{"{websocket}", "api/v4/remotecluster"}
|
||||
)
|
||||
|
||||
func init() {
|
||||
Analyzer.Flags.StringVar(&specFile, "spec", "", "Path to the OpenAPI 3 YAML spec file")
|
||||
}
|
||||
|
||||
// formatNode converts AST node to string
|
||||
func formatNode(fset *token.FileSet, node any) string {
|
||||
var typeNameBuf bytes.Buffer
|
||||
_ = printer.Fprint(&typeNameBuf, fset, node)
|
||||
return typeNameBuf.String()
|
||||
}
|
||||
|
||||
// stringInSlice checks presence of a string in a slice
|
||||
func stringInSlice(str string, slice []string, partial bool) bool {
|
||||
for _, s := range slice {
|
||||
if s == str {
|
||||
return true
|
||||
}
|
||||
if partial && strings.Contains(str, s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// cleanRegexp strips regex constraints from Go router path parameters so they
|
||||
// can be matched against OpenAPI path templates.
|
||||
// e.g., "/users/{user_id:[0-9]+}" → "/users/{user_id}"
|
||||
func cleanRegexp(s string) string {
|
||||
return pathParamConstraintRegex.ReplaceAllString(s, "{$1}")
|
||||
}
|
||||
|
||||
// matchesTemplate returns true if the OpenAPI specPath template matches handlerPath.
|
||||
// Spec path parameters (e.g., {foo}) match any single non-slash segment, mirroring
|
||||
// the behavior of kin-openapi's Paths.Find().
|
||||
func matchesTemplate(specPath, handlerPath string) bool {
|
||||
specParts := strings.Split(strings.Trim(specPath, "/"), "/")
|
||||
handlerParts := strings.Split(strings.Trim(handlerPath, "/"), "/")
|
||||
if len(specParts) != len(handlerParts) {
|
||||
return false
|
||||
}
|
||||
for i, specPart := range specParts {
|
||||
if openAPIParamRegex.MatchString(specPart) {
|
||||
continue
|
||||
}
|
||||
if specPart != handlerParts[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// findPathItem returns the path item matching handlerPath in the spec.
|
||||
// It first tries exact key lookup, then falls back to template matching.
|
||||
func findPathItem(paths *v3high.Paths, handlerPath string) *v3high.PathItem {
|
||||
if item := paths.PathItems.GetOrZero(handlerPath); item != nil {
|
||||
return item
|
||||
}
|
||||
for specPath, item := range paths.PathItems.FromOldest() {
|
||||
if matchesTemplate(specPath, handlerPath) {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// splitHandlerByGroup checks if URL path regexp contains named groups, and splits them in separate paths to be compatible with OpenAPI paths
|
||||
func splitHandlerByGroup(str string) []string {
|
||||
matches := groupSplitRegexp.FindAllStringSubmatch(str, -1)
|
||||
if len(matches) != 1 {
|
||||
return []string{str}
|
||||
}
|
||||
group := matches[0][0]
|
||||
part1 := matches[0][2]
|
||||
part2 := matches[0][3]
|
||||
return []string{strings.Replace(str, group, part1, 1), strings.Replace(str, group, part2, 1)}
|
||||
}
|
||||
|
||||
// getOperation returns the operation for the given HTTP method on a path item, or nil if not defined.
|
||||
func getOperation(pathItem *v3high.PathItem, method string) *v3high.Operation {
|
||||
switch strings.ToUpper(method) {
|
||||
case http.MethodGet:
|
||||
return pathItem.Get
|
||||
case http.MethodPost:
|
||||
return pathItem.Post
|
||||
case http.MethodPut:
|
||||
return pathItem.Put
|
||||
case http.MethodDelete:
|
||||
return pathItem.Delete
|
||||
case http.MethodPatch:
|
||||
return pathItem.Patch
|
||||
case http.MethodHead:
|
||||
return pathItem.Head
|
||||
case http.MethodOptions:
|
||||
return pathItem.Options
|
||||
case http.MethodTrace:
|
||||
return pathItem.Trace
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processRouterInit checks that all Init functions defined in `names` are properly documented
|
||||
func processRouterInit(pass *analysis.Pass, names []string, routerPrefixes map[string]string, paths *v3high.Paths, cm *fuzzy.Model) {
|
||||
for _, file := range pass.Files {
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
decl, ok := n.(*ast.FuncDecl)
|
||||
if !ok || !stringInSlice(decl.Name.Name, names, false) {
|
||||
return true
|
||||
}
|
||||
for _, stmt := range decl.Body.List {
|
||||
expr, ok := stmt.(*ast.ExprStmt)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
aexpr, ok := expr.X.(*ast.CallExpr).Fun.(*ast.SelectorExpr).X.(*ast.CallExpr)
|
||||
if !ok || len(aexpr.Args) != 2 {
|
||||
continue
|
||||
}
|
||||
prefix, _ := strconv.Unquote(aexpr.Args[0].(*ast.BasicLit).Value)
|
||||
method := getMethodFromExpr(expr)
|
||||
|
||||
name := aexpr.Fun.(*ast.SelectorExpr).X.(*ast.SelectorExpr).Sel.Name
|
||||
handler := cleanRegexp(routerPrefixes[name]) + cleanRegexp(prefix)
|
||||
if stringInSlice(handler, IgnoredCases, true) { // ignore special cases
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(handler, "/") {
|
||||
handler = "/" + handler
|
||||
}
|
||||
for _, h := range splitHandlerByGroup(handler) {
|
||||
pathItem := findPathItem(paths, h)
|
||||
if pathItem == nil {
|
||||
suffix := ""
|
||||
if suggestions := cm.Suggestions(h, false); len(suggestions) > 0 {
|
||||
suffix = fmt.Sprintf(" (maybe you meant: %v)", suggestions)
|
||||
}
|
||||
pass.Reportf(aexpr.Pos(), "Cannot find %v method: %v in OpenAPI 3 spec.%s", h, method, suffix)
|
||||
} else if getOperation(pathItem, method) == nil {
|
||||
pass.Reportf(aexpr.Pos(), "Handler %v is defined with method %s, but it's not in the spec", h, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// parseRoutesStruct scans Routes struct for mux.Router fields and scans their comments
|
||||
func parseRoutesStruct(pass *analysis.Pass, decl *ast.GenDecl, routerPrefixes map[string]string) {
|
||||
spec, ok := decl.Specs[0].(*ast.TypeSpec)
|
||||
if !ok || spec.Name.String() != "Routes" {
|
||||
return
|
||||
}
|
||||
for _, f := range spec.Type.(*ast.StructType).Fields.List {
|
||||
typeName := formatNode(pass.Fset, f.Type)
|
||||
if typeName != "*mux.Router" {
|
||||
continue
|
||||
}
|
||||
routerName := f.Names[0].Name
|
||||
if routerName == "ApiRoot" || routerName == "Root" {
|
||||
continue
|
||||
}
|
||||
if f.Comment != nil && len(f.Comment.List) > 0 {
|
||||
comment := f.Comment.List[0].Text
|
||||
if strings.HasPrefix(comment, "// '") && strings.HasSuffix(comment, "'") {
|
||||
routerPrefixes[routerName] = comment[4 : len(comment)-1]
|
||||
} else {
|
||||
pass.Reportf(f.Comment.List[0].Pos(), "Comment for field %s is not formatted correctly\n", routerName)
|
||||
}
|
||||
} else {
|
||||
pass.Reportf(f.Comment.Pos(), "Router field %s in Router struct is not commented properly\n", routerName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseInitFunction checks a specific Init function for proper documentation of registered API handlers
|
||||
func parseInitFunction(pass *analysis.Pass, decl *ast.FuncDecl, routerPrefixes map[string]string, initFunctions []string) []string {
|
||||
for _, stmt := range decl.Body.List {
|
||||
switch node := stmt.(type) {
|
||||
case *ast.ExprStmt:
|
||||
call, ok := node.X.(*ast.CallExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
selector, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ident, ok := selector.X.(*ast.Ident)
|
||||
if ok && ident.Name == "api" {
|
||||
initFunctions = append(initFunctions, selector.Sel.Name)
|
||||
}
|
||||
case *ast.AssignStmt:
|
||||
if len(node.Lhs) != 1 || !strings.HasPrefix(formatNode(pass.Fset, node.Lhs[0]), "api.BaseRoutes") {
|
||||
continue
|
||||
}
|
||||
subRouterName := formatNode(pass.Fset, node.Lhs[0])[15:]
|
||||
if subRouterName == "APIRoot" || subRouterName == "APIRoot5" || subRouterName == "Root" {
|
||||
continue
|
||||
}
|
||||
|
||||
rhs := formatNode(pass.Fset, node.Rhs[0])[15:]
|
||||
router := rhs[:strings.Index(rhs, ".")]
|
||||
path := rhs[strings.Index(rhs, ".")+13 : strings.LastIndex(rhs, ".")-2]
|
||||
prefix := ""
|
||||
switch router {
|
||||
case "APIRoot":
|
||||
prefix = "api/v4"
|
||||
case "Root":
|
||||
prefix = ""
|
||||
default:
|
||||
if s, ok := routerPrefixes[router]; ok {
|
||||
prefix = s
|
||||
} else {
|
||||
pass.Reportf(node.Rhs[0].Pos(), "cannot find prefix for %s\n", router)
|
||||
}
|
||||
}
|
||||
s := fmt.Sprintf("%v%v", prefix, path)
|
||||
s2 := routerPrefixes[subRouterName]
|
||||
if s2 != s {
|
||||
pass.Reportf(node.Rhs[0].Pos(), "PathPrefix doesn't match field comment for field '%s': '%s' vs '%s'\n", subRouterName, s, s2)
|
||||
}
|
||||
}
|
||||
}
|
||||
return initFunctions
|
||||
}
|
||||
|
||||
func validateComments(pass *analysis.Pass) ([]string, map[string]string) {
|
||||
initFunctions := []string{}
|
||||
routerPrefixes := map[string]string{}
|
||||
for _, file := range pass.Files {
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
if decl, ok := n.(*ast.GenDecl); ok && len(decl.Specs) == 1 {
|
||||
parseRoutesStruct(pass, decl, routerPrefixes)
|
||||
} else if decl, ok := n.(*ast.FuncDecl); ok && decl.Name.Name == "Init" {
|
||||
initFunctions = parseInitFunction(pass, decl, routerPrefixes, initFunctions)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return initFunctions, routerPrefixes
|
||||
}
|
||||
|
||||
func run(pass *analysis.Pass) (any, error) {
|
||||
if specFile == "" {
|
||||
return nil, errors.New("Please supply a path to OpenAPI spec yaml file via -openApiSync.spec")
|
||||
}
|
||||
if _, err := os.Stat(specFile); err != nil {
|
||||
return nil, errors.Wrapf(err, "spec file does not exist")
|
||||
}
|
||||
data, err := os.ReadFile(specFile)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to read spec file")
|
||||
}
|
||||
doc, err := libopenapi.NewDocument(data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to parse spec file. Expected OpenAPI3 format.")
|
||||
}
|
||||
model, err := doc.BuildV3Model()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to build OpenAPI3 model")
|
||||
}
|
||||
|
||||
initFunctions, routerPrefixes := validateComments(pass)
|
||||
|
||||
var swaggerPaths []string
|
||||
for p := range model.Model.Paths.PathItems.KeysFromOldest() {
|
||||
swaggerPaths = append(swaggerPaths, p)
|
||||
}
|
||||
fuzzyModel := fuzzy.NewModel()
|
||||
fuzzyModel.Train(swaggerPaths)
|
||||
|
||||
processRouterInit(pass, initFunctions, routerPrefixes, model.Model.Paths, fuzzyModel)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func getMethodFromExpr(expr *ast.ExprStmt) string {
|
||||
var method string
|
||||
methodArg := expr.X.(*ast.CallExpr).Args[0]
|
||||
|
||||
if methodSelectorExpr, ok := methodArg.(*ast.SelectorExpr); ok {
|
||||
if methodSelectorExpr.X.(*ast.Ident).Name == "http" {
|
||||
switch methodSelectorExpr.Sel.Name {
|
||||
case "MethodGet":
|
||||
method = http.MethodGet
|
||||
case "MethodHead":
|
||||
method = http.MethodHead
|
||||
case "MethodPost":
|
||||
method = http.MethodPost
|
||||
case "MethodPut":
|
||||
method = http.MethodPut
|
||||
case "MethodPatch":
|
||||
method = http.MethodPatch
|
||||
case "MethodDelete":
|
||||
method = http.MethodDelete
|
||||
case "MethodConnect":
|
||||
method = http.MethodConnect
|
||||
case "MethodOptions":
|
||||
method = http.MethodOptions
|
||||
case "MethodTrace":
|
||||
method = http.MethodTrace
|
||||
}
|
||||
}
|
||||
} else if methodBasicLit, ok := methodArg.(*ast.BasicLit); ok {
|
||||
method, _ = strconv.Unquote(methodBasicLit.Value)
|
||||
}
|
||||
|
||||
return method
|
||||
}
|
||||
148
tools/mattermost-govet/openApiSync/openApiSync_test.go
Normal file
148
tools/mattermost-govet/openApiSync/openApiSync_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package openApiSync
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
v3high "github.com/pb33f/libopenapi/datamodel/high/v3"
|
||||
"golang.org/x/tools/go/analysis/analysistest"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
testdata := analysistest.TestData()
|
||||
specFile = analysistest.TestData() + "/spec.yaml"
|
||||
analysistest.Run(t, testdata, Analyzer, "api")
|
||||
}
|
||||
|
||||
func TestCleanRegexp(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"/api/v4/users/{user_id:[0-9]+}", "/api/v4/users/{user_id}"},
|
||||
{"/api/v4/users/{username:[A-Za-z0-9\\_\\-\\.]+}", "/api/v4/users/{username}"},
|
||||
{"/api/v4/jobs/type/{job_type:[A-Za-z0-9_-]+}", "/api/v4/jobs/type/{job_type}"},
|
||||
// Literal-value constraints (e.g., {websocket:websocket}) are stripped like any other.
|
||||
{"/api/v4/{websocket:websocket}", "/api/v4/{websocket}"},
|
||||
// Group-alternation patterns must be left intact for splitHandlerByGroup.
|
||||
{"/api/v4/groups/{group_id}/{syncable_type:teams|channels}/{syncable_id}/link", "/api/v4/groups/{group_id}/{syncable_type:teams|channels}/{syncable_id}/link"},
|
||||
// No constraint — unchanged.
|
||||
{"/api/v4/users/{user_id}/posts", "/api/v4/users/{user_id}/posts"},
|
||||
// Multiple constrained params in one path.
|
||||
{"/api/v4/users/{user_id:[0-9]+}/posts/{post_id:[0-9]+}", "/api/v4/users/{user_id}/posts/{post_id}"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
if got := cleanRegexp(tc.input); got != tc.want {
|
||||
t.Errorf("cleanRegexp(%q) = %q, want %q", tc.input, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchesTemplate(t *testing.T) {
|
||||
tests := []struct {
|
||||
specPath string
|
||||
handlerPath string
|
||||
want bool
|
||||
}{
|
||||
// Exact match.
|
||||
{
|
||||
"/api/v4/groups/{group_id}/teams/{syncable_id}/link",
|
||||
"/api/v4/groups/{group_id}/teams/{syncable_id}/link",
|
||||
true,
|
||||
},
|
||||
// Spec param matches handler literal segment.
|
||||
{
|
||||
"/api/v4/groups/{group_id}/{syncable_type}/{syncable_id}/link",
|
||||
"/api/v4/groups/{group_id}/channels/{syncable_id}/link",
|
||||
true,
|
||||
},
|
||||
// Spec literal does not match a different handler literal.
|
||||
{
|
||||
"/api/v4/groups/{group_id}/teams/{syncable_id}/link",
|
||||
"/api/v4/groups/{group_id}/channels/{syncable_id}/link",
|
||||
false,
|
||||
},
|
||||
// Spec param matches handler param name.
|
||||
{
|
||||
"/api/v4/users/{user_id}",
|
||||
"/api/v4/users/{user_id}",
|
||||
true,
|
||||
},
|
||||
// Spec param matches handler literal "me".
|
||||
{
|
||||
"/api/v4/users/{user_id}",
|
||||
"/api/v4/users/me",
|
||||
true,
|
||||
},
|
||||
// Different path lengths.
|
||||
{
|
||||
"/api/v4/users/{user_id}/posts",
|
||||
"/api/v4/users/{user_id}",
|
||||
false,
|
||||
},
|
||||
// Completely different paths.
|
||||
{
|
||||
"/api/v4/teams/{team_id}",
|
||||
"/api/v4/users/{user_id}",
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.specPath+"~"+tc.handlerPath, func(t *testing.T) {
|
||||
if got := matchesTemplate(tc.specPath, tc.handlerPath); got != tc.want {
|
||||
t.Errorf("matchesTemplate(%q, %q) = %v, want %v", tc.specPath, tc.handlerPath, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOperation(t *testing.T) {
|
||||
get := &v3high.Operation{}
|
||||
post := &v3high.Operation{}
|
||||
put := &v3high.Operation{}
|
||||
patch := &v3high.Operation{}
|
||||
del := &v3high.Operation{}
|
||||
head := &v3high.Operation{}
|
||||
opts := &v3high.Operation{}
|
||||
trace := &v3high.Operation{}
|
||||
|
||||
pathItem := &v3high.PathItem{
|
||||
Get: get,
|
||||
Post: post,
|
||||
Put: put,
|
||||
Patch: patch,
|
||||
Delete: del,
|
||||
Head: head,
|
||||
Options: opts,
|
||||
Trace: trace,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
method string
|
||||
want *v3high.Operation
|
||||
}{
|
||||
{http.MethodGet, get},
|
||||
{http.MethodPost, post},
|
||||
{http.MethodPut, put},
|
||||
{http.MethodPatch, patch},
|
||||
{http.MethodDelete, del},
|
||||
{http.MethodHead, head},
|
||||
{http.MethodOptions, opts},
|
||||
{http.MethodTrace, trace},
|
||||
{"get", get}, // case-insensitive
|
||||
{"post", post}, // case-insensitive
|
||||
{"UNKNOWN", nil},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.method, func(t *testing.T) {
|
||||
if got := getOperation(pathItem, tc.method); got != tc.want {
|
||||
t.Errorf("getOperation(pathItem, %q) = %v, want %v", tc.method, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
17
tools/mattermost-govet/openApiSync/testdata/spec.yaml
vendored
Normal file
17
tools/mattermost-govet/openApiSync/testdata/spec.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
openapi: "3.0.0"
|
||||
info:
|
||||
title: Test API
|
||||
version: "1.0.0"
|
||||
paths:
|
||||
/api/v4/users/ids:
|
||||
post:
|
||||
summary: Get users by IDs
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
/api/v4/groups/{group_id}/teams/{syncable_id}/link:
|
||||
post:
|
||||
summary: Link a group syncable
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
43
tools/mattermost-govet/openApiSync/testdata/src/api/api.go
vendored
Normal file
43
tools/mattermost-govet/openApiSync/testdata/src/api/api.go
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type Routes struct {
|
||||
Root *mux.Router // ''
|
||||
APIRoot *mux.Router // 'api/v4'
|
||||
|
||||
Users *mux.Router // 'api/v4/userzs'
|
||||
Groups *mux.Router // 'api/v4/groups'
|
||||
}
|
||||
|
||||
type API struct {
|
||||
BaseRoutes *Routes
|
||||
}
|
||||
|
||||
func (*API) ApiSessionRequired(h func(*context.Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return nil
|
||||
}
|
||||
func Init(root *mux.Router) *API {
|
||||
api := &API{
|
||||
BaseRoutes: &Routes{},
|
||||
}
|
||||
api.BaseRoutes.Root = root
|
||||
api.BaseRoutes.APIRoot = root.PathPrefix("api/v4").Subrouter()
|
||||
|
||||
api.BaseRoutes.Users = api.BaseRoutes.APIRoot.PathPrefix("/users").Subrouter() // want "PathPrefix doesn't match field comment for field 'Users': 'api/v4/users' vs 'api/v4/userzs'"
|
||||
api.BaseRoutes.Groups = api.BaseRoutes.APIRoot.PathPrefix("/gruops").Subrouter() // want "PathPrefix doesn't match field comment for field 'Groups': 'api/v4/gruops' vs 'api/v4/groups'"
|
||||
api.InitUsers()
|
||||
return api
|
||||
}
|
||||
|
||||
func _() {
|
||||
_ = Init(nil)
|
||||
}
|
||||
17
tools/mattermost-govet/openApiSync/testdata/src/api/users.go
vendored
Normal file
17
tools/mattermost-govet/openApiSync/testdata/src/api/users.go
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (a *API) InitUsers() {
|
||||
a.BaseRoutes.Users.Handle("/ids", a.ApiSessionRequired(getUsersByIds)).Methods(http.MethodPost) // want `Cannot find /api/v4/userzs/ids method: POST in OpenAPI 3 spec. \(maybe you meant: \[/api/v4/users/ids\]\)`
|
||||
a.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channelz}/{syncable_id:[A-Za-z0-9]+}/link", a.ApiSessionRequired(getUsersByIds)).Methods("POST") // want `Cannot find /api/v4/groups/{group_id}/channelz/{syncable_id}/link method: POST in OpenAPI 3 spec.`
|
||||
|
||||
}
|
||||
func getUsersByIds(c *context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
22
tools/mattermost-govet/openApiSync/testdata/src/github.com/gorilla/mux/router.go
vendored
Normal file
22
tools/mattermost-govet/openApiSync/testdata/src/github.com/gorilla/mux/router.go
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package mux
|
||||
|
||||
import "net/http"
|
||||
|
||||
type Router struct {
|
||||
}
|
||||
|
||||
func (*Router) PathPrefix(_ string) *Router {
|
||||
return &Router{}
|
||||
}
|
||||
func (*Router) Subrouter() *Router {
|
||||
return &Router{}
|
||||
}
|
||||
func (*Router) Handle(_ string, h http.Handler) *Router {
|
||||
return &Router{}
|
||||
}
|
||||
func (*Router) Methods(_ ...string) *Router {
|
||||
return &Router{}
|
||||
}
|
||||
Reference in New Issue
Block a user