Serve OpenAPI 3.0 spec at /openapi.v1.json (#37038)

Add a build-time conversion step that transforms the existing Swagger
2.0 spec into an OpenAPI 3.0 spec. The OAS3 spec is served alongside the
existing Swagger 2.0 spec, enabling API clients that require OAS3 to
generate code directly from Gitea's API.

This is not to be an answer to how gitea handles OAS3 long term,
but a way to use what we have to move a step forward.

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Myers Carpenter
2026-04-29 08:47:52 -04:00
committed by GitHub
parent 18762c7748
commit 9e031eb3df
39 changed files with 34700 additions and 99 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ indent_style = tab
[templates/custom/*.tmpl]
insert_final_newline = false
[templates/swagger/v1_json.tmpl]
[templates/swagger/*_json.tmpl]
indent_style = space
insert_final_newline = false
+18 -2
View File
@@ -151,6 +151,7 @@ ESLINT_CONCURRENCY ?= 2
SWAGGER_SPEC := templates/swagger/v1_json.tmpl
SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json
SWAGGER_EXCLUDE := code.gitea.io/sdk
OPENAPI3_SPEC := templates/swagger/v1_openapi3_json.tmpl
TEST_MYSQL_HOST ?= mysql:3306
TEST_MYSQL_DBNAME ?= testgitea
@@ -233,7 +234,7 @@ TAGS_PREREQ := $(TAGS_EVIDENCE)
endif
.PHONY: generate-swagger
generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments
generate-swagger: $(SWAGGER_SPEC) $(OPENAPI3_SPEC) ## generate the swagger spec from code comments
$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT)
$(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)'
@@ -255,6 +256,21 @@ swagger-validate: ## check if the swagger spec is valid
$(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)'
@$(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)' # remove the prefix slash from basePath
.PHONY: generate-openapi3
generate-openapi3: $(OPENAPI3_SPEC) ## generate the OpenAPI 3.0 spec from the Swagger 2.0 spec
$(OPENAPI3_SPEC): $(SWAGGER_SPEC) build/generate-openapi.go $(wildcard build/openapi3gen/*.go)
$(GO) run build/generate-openapi.go
.PHONY: openapi3-check
openapi3-check: generate-openapi3
@diff=$$(git diff --color=always '$(OPENAPI3_SPEC)'); \
if [ -n "$$diff" ]; then \
echo "Please run 'make generate-openapi3' and commit the result:"; \
printf "%s" "$${diff}"; \
exit 1; \
fi
.PHONY: checks
checks: checks-frontend checks-backend ## run various consistency checks
@@ -262,7 +278,7 @@ checks: checks-frontend checks-backend ## run various consistency checks
checks-frontend: lockfile-check svg-check ## check frontend files
.PHONY: checks-backend
checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check ## check backend files
checks-backend: tidy-check swagger-check openapi3-check fmt-check swagger-validate security-check ## check backend files
.PHONY: lint
lint: lint-frontend lint-backend lint-spell ## lint everything
+40
View File
File diff suppressed because one or more lines are too long
+97
View File
@@ -0,0 +1,97 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// generate-openapi converts Gitea's Swagger 2.0 spec into an OpenAPI 3.0 spec.
//
// Gitea generates a Swagger 2.0 spec from code annotations (make generate-swagger).
// This tool converts it to OAS3 so that SDK generators and tools that require
// OAS3 (e.g. progenitor for Rust) can consume it directly. The conversion also
// deduplicates inline enum definitions into named schema components, producing
// cleaner SDK output with proper enum types instead of anonymous strings.
//
// Run: go run build/generate-openapi.go
// Output: templates/swagger/v1_openapi3_json.tmpl
//go:build ignore
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"regexp"
"sort"
"strings"
"code.gitea.io/gitea/build/openapi3gen"
"github.com/getkin/kin-openapi/openapi3"
)
const (
swaggerSpecPath = "templates/swagger/v1_json.tmpl"
openapi3OutPath = "templates/swagger/v1_openapi3_json.tmpl"
appSubUrlVar = "{{.SwaggerAppSubUrl}}"
appVerVar = "{{.SwaggerAppVer}}"
appSubUrlPlaceholder = "GITEA_APP_SUB_URL_PLACEHOLDER"
appVerPlaceholder = "0.0.0-gitea-placeholder"
)
var (
appSubUrlRe = regexp.MustCompile(regexp.QuoteMeta(appSubUrlVar))
appVerRe = regexp.MustCompile(regexp.QuoteMeta(appVerVar))
enumScanDirs = []string{
"modules/structs",
"modules/commitstatus",
}
)
func main() {
astEnumMap, err := openapi3gen.ScanSwaggerEnumTypes(enumScanDirs)
if err != nil {
log.Fatalf("scanning swagger:enum annotations: %v", err)
}
names := make([]string, 0, len(astEnumMap))
for _, n := range astEnumMap {
names = append(names, n)
}
sort.Strings(names)
fmt.Fprintf(os.Stderr, "discovered %d swagger:enum types: %s\n", len(names), strings.Join(names, ", "))
data, err := os.ReadFile(swaggerSpecPath)
if err != nil {
log.Fatalf("reading swagger spec: %v", err)
}
cleaned := appSubUrlRe.ReplaceAll(data, []byte(appSubUrlPlaceholder))
cleaned = appVerRe.ReplaceAll(cleaned, []byte(appVerPlaceholder))
oas3, err := openapi3gen.Convert(cleaned, astEnumMap)
if err != nil {
log.Fatalf("converting to openapi 3.0: %v", err)
}
oas3.Servers = openapi3.Servers{
{URL: appSubUrlPlaceholder + "/api/v1"},
}
out, err := json.MarshalIndent(oas3, "", " ")
if err != nil {
log.Fatalf("marshaling openapi 3.0: %v", err)
}
result := strings.ReplaceAll(string(out), appSubUrlPlaceholder, appSubUrlVar)
result = strings.ReplaceAll(result, appVerPlaceholder, appVerVar)
result = strings.TrimSpace(result)
if err := os.WriteFile(openapi3OutPath, []byte(result), 0o644); err != nil {
log.Fatalf("writing openapi 3.0 spec: %v", err)
}
fmt.Printf("Generated %s\n", openapi3OutPath)
}
+281
View File
@@ -0,0 +1,281 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openapi3gen
import (
"fmt"
"regexp"
"strings"
"code.gitea.io/gitea/modules/json"
"github.com/getkin/kin-openapi/openapi2"
"github.com/getkin/kin-openapi/openapi2conv"
"github.com/getkin/kin-openapi/openapi3"
)
// rxDeprecated matches "deprecated" as a word at the start of a description
// or preceded by whitespace/punctuation that indicates a leading marker (e.g.
// "Deprecated: true", "deprecated (use X instead)"). Rejects negated phrases
// like "not deprecated" or "previously deprecated, now supported".
var rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`)
// Convert parses a Swagger 2.0 spec and returns an OAS3 spec, applying
// Gitea-specific post-processing: file-schema fixups, URI formats,
// deprecated flags, and shared-enum extraction.
//
// astEnumMap is a value-set-key → Go-type-name map (built by
// ScanSwaggerEnumTypes). If a shared enum in the spec has no entry in the
// map, Convert returns an error — no fallback naming.
func Convert(swaggerJSON []byte, astEnumMap map[string]string) (*openapi3.T, error) {
var swagger2 openapi2.T
if err := json.Unmarshal(swaggerJSON, &swagger2); err != nil {
return nil, fmt.Errorf("parsing swagger 2.0: %w", err)
}
oas3, err := openapi2conv.ToV3(&swagger2)
if err != nil {
return nil, fmt.Errorf("converting to openapi 3.0: %w", err)
}
fixFileSchemas(oas3)
addURIFormats(oas3)
addDeprecatedFlags(oas3)
if err := extractSharedEnums(oas3, astEnumMap); err != nil {
return nil, err
}
return oas3, nil
}
func fixFileSchemas(doc *openapi3.T) {
for _, pathItem := range doc.Paths.Map() {
for _, op := range []*openapi3.Operation{
pathItem.Get, pathItem.Post, pathItem.Put, pathItem.Patch,
pathItem.Delete, pathItem.Head, pathItem.Options, pathItem.Trace,
} {
if op == nil {
continue
}
for _, resp := range op.Responses.Map() {
if resp.Value == nil {
continue
}
for _, mediaType := range resp.Value.Content {
fixSchema(mediaType.Schema)
}
}
if op.RequestBody != nil && op.RequestBody.Value != nil {
for _, mediaType := range op.RequestBody.Value.Content {
fixSchema(mediaType.Schema)
}
}
}
}
}
// fixSchema rewrites any "type: file" schemas to the OAS3 equivalent
// (type: string, format: binary), recursing into Properties, Items, and
// AllOf/OneOf/AnyOf/Not branches. $ref nodes are skipped so shared schemas
// are rewritten exactly once when visited through their declaration.
func fixSchema(ref *openapi3.SchemaRef) {
if ref == nil || ref.Value == nil || ref.Ref != "" {
return
}
s := ref.Value
if s.Type.Is("file") {
s.Type = &openapi3.Types{"string"}
s.Format = "binary"
}
for _, p := range s.Properties {
fixSchema(p)
}
fixSchema(s.Items)
for _, sub := range s.AllOf {
fixSchema(sub)
}
for _, sub := range s.OneOf {
fixSchema(sub)
}
for _, sub := range s.AnyOf {
fixSchema(sub)
}
fixSchema(s.Not)
}
// addURIFormats sets format: uri on string properties whose names indicate
// they hold URLs. This information is lost in Swagger 2.0 but is valuable
// for code generators.
func addURIFormats(doc *openapi3.T) {
if doc.Components == nil {
return
}
for _, schemaRef := range doc.Components.Schemas {
if schemaRef.Value == nil {
continue
}
for propName, propRef := range schemaRef.Value.Properties {
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
continue
}
prop := propRef.Value
if !prop.Type.Is("string") || prop.Format != "" {
continue
}
if isURLProperty(propName) {
prop.Format = "uri"
}
}
}
}
func isURLProperty(name string) bool {
if strings.HasSuffix(name, "_url") {
return true
}
switch name {
case "url", "html_url", "clone_url":
return true
}
return false
}
// addDeprecatedFlags sets deprecated: true on schema properties whose
// description starts with a "deprecated" marker (e.g. "Deprecated: true"
// or "deprecated (use X instead)"). Does not match negated phrases.
func addDeprecatedFlags(doc *openapi3.T) {
if doc.Components == nil {
return
}
for _, schemaRef := range doc.Components.Schemas {
if schemaRef.Value == nil {
continue
}
for _, propRef := range schemaRef.Value.Properties {
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
continue
}
if rxDeprecated.MatchString(propRef.Value.Description) {
propRef.Value.Deprecated = true
}
}
}
}
type enumUsage struct {
schemaName string
propName string
propRef *openapi3.SchemaRef
inItems bool
}
// extractSharedEnums finds identical enum arrays used by multiple schema
// properties, creates a standalone named schema for each, and replaces
// the inline enums with $ref pointers.
//
// If the derived enum name collides with an existing component schema, or
// no // swagger:enum annotation matches the value set, generation aborts
// with an actionable error — there are no silent fallbacks.
func extractSharedEnums(doc *openapi3.T, astEnumMap map[string]string) error {
if doc.Components == nil {
return nil
}
enumGroups := map[string][]enumUsage{}
for schemaName, schemaRef := range doc.Components.Schemas {
if schemaRef.Value == nil {
continue
}
for propName, propRef := range schemaRef.Value.Properties {
if propRef == nil || propRef.Value == nil || propRef.Ref != "" {
continue
}
if len(propRef.Value.Enum) > 1 && propRef.Value.Type.Is("string") {
key := EnumKey(propRef.Value.Enum)
enumGroups[key] = append(enumGroups[key], enumUsage{schemaName, propName, propRef, false})
}
if propRef.Value.Type.Is("array") && propRef.Value.Items != nil &&
propRef.Value.Items.Value != nil && propRef.Value.Items.Ref == "" &&
len(propRef.Value.Items.Value.Enum) > 1 && propRef.Value.Items.Value.Type.Is("string") {
key := EnumKey(propRef.Value.Items.Value.Enum)
enumGroups[key] = append(enumGroups[key], enumUsage{schemaName, propName, propRef, true})
}
}
}
for key, usages := range enumGroups {
if len(usages) < 2 {
continue
}
enumName, err := deriveEnumName(key, usages, astEnumMap)
if err != nil {
return err
}
if _, exists := doc.Components.Schemas[enumName]; exists {
return fmt.Errorf("enum name collision: %s already exists as a component schema", enumName)
}
var enumValues []any
if usages[0].inItems {
enumValues = usages[0].propRef.Value.Items.Value.Enum
} else {
enumValues = usages[0].propRef.Value.Enum
}
doc.Components.Schemas[enumName] = &openapi3.SchemaRef{
Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: enumValues,
},
}
ref := "#/components/schemas/" + enumName
for _, usage := range usages {
if usage.inItems {
usage.propRef.Value.Items = &openapi3.SchemaRef{Ref: ref}
} else {
old := usage.propRef.Value
if old.Description == "" && !old.Deprecated && old.Format == "" {
usage.propRef.Ref = ref
usage.propRef.Value = nil
} else {
usage.propRef.Value = &openapi3.Schema{
AllOf: openapi3.SchemaRefs{
{Ref: ref},
},
Description: old.Description,
Deprecated: old.Deprecated,
Format: old.Format,
}
}
}
}
}
return nil
}
// deriveEnumName looks up a shared enum's Go type name from astEnumMap by
// value-set key. If no annotation matches, returns an error identifying the
// offending properties and the fix.
func deriveEnumName(key string, usages []enumUsage, astEnumMap map[string]string) (string, error) {
if name, ok := astEnumMap[key]; ok {
return name, nil
}
props := map[string]bool{}
for _, u := range usages {
props[fmt.Sprintf("%s.%s", u.schemaName, u.propName)] = true
}
propList := make([]string, 0, len(props))
for p := range props {
propList = append(propList, p)
}
return "", fmt.Errorf(
"no swagger:enum annotation matches value-set %q used by %d properties: %v; "+
"fix by adding a named string type with // swagger:enum to modules/structs or modules/commitstatus",
key, len(usages), propList,
)
}
+170
View File
@@ -0,0 +1,170 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openapi3gen
import (
"strings"
"testing"
"github.com/getkin/kin-openapi/openapi3"
)
func TestDeriveEnumName_hit(t *testing.T) {
key := EnumKey([]any{"red", "green", "blue"})
astMap := map[string]string{key: "Color"}
usages := []enumUsage{{schemaName: "Paint", propName: "color"}}
got, err := deriveEnumName(key, usages, astMap)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != "Color" {
t.Fatalf("got %q, want %q", got, "Color")
}
}
func TestDeriveEnumName_miss(t *testing.T) {
key := EnumKey([]any{"x", "y"})
usages := []enumUsage{{schemaName: "Thing", propName: "kind"}}
_, err := deriveEnumName(key, usages, map[string]string{})
if err == nil {
t.Fatal("expected miss error, got nil")
}
msg := err.Error()
if !strings.Contains(msg, "Thing.kind") {
t.Fatalf("error %q should list the missing usage", msg)
}
if !strings.Contains(msg, "swagger:enum") {
t.Fatalf("error %q should hint at the fix", msg)
}
}
func TestExtractSharedEnums_usesASTMap(t *testing.T) {
doc := &openapi3.T{
Components: &openapi3.Components{
Schemas: openapi3.Schemas{
"A": {Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green", "blue"},
}},
},
}},
"B": {Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green", "blue"},
}},
},
}},
},
},
}
astMap := map[string]string{EnumKey([]any{"red", "green", "blue"}): "Color"}
if err := extractSharedEnums(doc, astMap); err != nil {
t.Fatalf("extractSharedEnums: %v", err)
}
if _, ok := doc.Components.Schemas["Color"]; !ok {
t.Fatalf("expected Color schema to be extracted")
}
}
func TestFixFileSchemas_recursesIntoNested(t *testing.T) {
fileType := func() *openapi3.SchemaRef {
return &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"file"}}}
}
doc := &openapi3.T{
Paths: openapi3.NewPaths(),
}
doc.Paths.Set("/upload", &openapi3.PathItem{
Post: &openapi3.Operation{
RequestBody: &openapi3.RequestBodyRef{
Value: &openapi3.RequestBody{
Content: openapi3.Content{
"multipart/form-data": {
Schema: &openapi3.SchemaRef{Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"attachment": fileType(),
"items": {Value: &openapi3.Schema{
Type: &openapi3.Types{"array"},
Items: fileType(),
}},
"alt": {Value: &openapi3.Schema{
AllOf: openapi3.SchemaRefs{fileType()},
}},
"one": {Value: &openapi3.Schema{
OneOf: openapi3.SchemaRefs{fileType()},
}},
"any": {Value: &openapi3.Schema{
AnyOf: openapi3.SchemaRefs{fileType()},
}},
"not": {Value: &openapi3.Schema{
Not: fileType(),
}},
},
}},
},
},
},
},
Responses: openapi3.NewResponses(),
},
})
fixFileSchemas(doc)
props := doc.Paths.Value("/upload").Post.RequestBody.Value.Content["multipart/form-data"].Schema.Value.Properties
if !props["attachment"].Value.Type.Is("string") || props["attachment"].Value.Format != "binary" {
t.Errorf("nested property not fixed: %+v", props["attachment"].Value)
}
if !props["items"].Value.Items.Value.Type.Is("string") || props["items"].Value.Items.Value.Format != "binary" {
t.Errorf("array items not fixed: %+v", props["items"].Value.Items.Value)
}
if !props["alt"].Value.AllOf[0].Value.Type.Is("string") || props["alt"].Value.AllOf[0].Value.Format != "binary" {
t.Errorf("allOf branch not fixed: %+v", props["alt"].Value.AllOf[0].Value)
}
if !props["one"].Value.OneOf[0].Value.Type.Is("string") || props["one"].Value.OneOf[0].Value.Format != "binary" {
t.Errorf("oneOf branch not fixed: %+v", props["one"].Value.OneOf[0].Value)
}
if !props["any"].Value.AnyOf[0].Value.Type.Is("string") || props["any"].Value.AnyOf[0].Value.Format != "binary" {
t.Errorf("anyOf branch not fixed: %+v", props["any"].Value.AnyOf[0].Value)
}
if !props["not"].Value.Not.Value.Type.Is("string") || props["not"].Value.Not.Value.Format != "binary" {
t.Errorf("not branch not fixed: %+v", props["not"].Value.Not.Value)
}
}
func TestExtractSharedEnums_missReturnsError(t *testing.T) {
doc := &openapi3.T{
Components: &openapi3.Components{
Schemas: openapi3.Schemas{
"A": {Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green"},
}},
},
}},
"B": {Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Properties: openapi3.Schemas{
"color": {Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Enum: []any{"red", "green"},
}},
},
}},
},
},
}
if err := extractSharedEnums(doc, map[string]string{}); err == nil {
t.Fatal("expected miss error")
}
}
+188
View File
@@ -0,0 +1,188 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package openapi3gen converts Gitea's Swagger 2.0 spec to an OpenAPI 3.0
// spec. It discovers Go enum type names by scanning swagger:enum annotations
// in the source tree, then names extracted shared-enum schemas accordingly.
package openapi3gen
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
)
// EnumKey returns a canonical key for a set of enum values: values are
// stringified, sorted, and joined with "|". Used to match enum value sets
// across spec properties and scanned Go type declarations.
func EnumKey(values []any) string {
strs := make([]string, len(values))
for i, v := range values {
strs[i] = fmt.Sprintf("%v", v)
}
sort.Strings(strs)
return strings.Join(strs, "|")
}
var rxSwaggerEnum = regexp.MustCompile(`swagger:enum\s+(\w+)`)
// ScanSwaggerEnumTypes walks .go files under each dir and returns a map from
// a canonical value-set key (see EnumKey) to the Go type name declared with
// // swagger:enum TypeName.
//
// Returns an error on parse failure, on an annotation for a type whose
// constants can't be extracted, or on value-set collisions between two
// different enum types.
func ScanSwaggerEnumTypes(dirs []string) (map[string]string, error) {
fset := token.NewFileSet()
parsed := []*ast.File{}
for _, dir := range dirs {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", dir, err)
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") {
continue
}
if strings.HasSuffix(entry.Name(), "_test.go") {
continue
}
path := filepath.Join(dir, entry.Name())
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
parsed = append(parsed, file)
}
}
enumTypes := map[string]string{} // typeName → "" (presence marker)
enumValues := map[string][]any{} // typeName → values
// Pass 1: collect every // swagger:enum TypeName declaration.
for _, file := range parsed {
for _, decl := range file.Decls {
gd, ok := decl.(*ast.GenDecl)
if !ok || gd.Tok != token.TYPE {
continue
}
if err := collectEnumType(gd, enumTypes); err != nil {
return nil, fmt.Errorf("%s: %w", fset.Position(gd.Pos()).Filename, err)
}
}
}
// Pass 2: collect const values; now every annotated type is visible.
for _, file := range parsed {
for _, decl := range file.Decls {
gd, ok := decl.(*ast.GenDecl)
if !ok || gd.Tok != token.CONST {
continue
}
collectEnumValues(gd, enumTypes, enumValues)
}
}
result := map[string]string{}
for typeName := range enumTypes {
values, ok := enumValues[typeName]
if !ok || len(values) == 0 {
return nil, fmt.Errorf("swagger:enum %s has no const block with typed string values", typeName)
}
key := EnumKey(values)
if existing, ok := result[key]; ok && existing != typeName {
return nil, fmt.Errorf("swagger:enum value-set collision: %s and %s both use %q", existing, typeName, key)
}
result[key] = typeName
}
return result, nil
}
// collectEnumType scans a `type` GenDecl for // swagger:enum annotations,
// handling both the lone form (`// swagger:enum Foo\n type Foo string`)
// where the comment group is attached to the GenDecl, and the grouped form:
//
// type (
// // swagger:enum Foo
// Foo string
// )
//
// where the comment group is attached to each TypeSpec. Caveat: Go's parser
// only attaches a CommentGroup when it is immediately adjacent to the decl.
// A blank line (not a `//` continuation line) between the comment and the
// declaration drops the Doc, so annotations MUST sit directly above their
// type. All current annotated files obey this — the rule is noted here so
// a future edit that inserts a blank line fails fast rather than silently.
func collectEnumType(gd *ast.GenDecl, enumTypes map[string]string) error {
if err := registerEnumAnnotation(gd.Doc, gd.Specs, enumTypes); err != nil {
return err
}
for _, spec := range gd.Specs {
ts, ok := spec.(*ast.TypeSpec)
if !ok || ts.Doc == nil {
continue
}
if err := registerEnumAnnotation(ts.Doc, []ast.Spec{ts}, enumTypes); err != nil {
return err
}
}
return nil
}
func registerEnumAnnotation(doc *ast.CommentGroup, specs []ast.Spec, enumTypes map[string]string) error {
if doc == nil {
return nil
}
matches := rxSwaggerEnum.FindStringSubmatch(doc.Text())
if len(matches) < 2 {
return nil
}
annotated := matches[1]
for _, spec := range specs {
ts, ok := spec.(*ast.TypeSpec)
if !ok {
continue
}
if ts.Name.Name == annotated {
enumTypes[annotated] = ""
return nil
}
}
return fmt.Errorf("swagger:enum %s: no type declaration with that name in the same decl group; check for a typo", annotated)
}
func collectEnumValues(gd *ast.GenDecl, enumTypes map[string]string, enumValues map[string][]any) {
for _, spec := range gd.Specs {
vs, ok := spec.(*ast.ValueSpec)
if !ok || vs.Type == nil {
continue
}
ident, ok := vs.Type.(*ast.Ident)
if !ok {
continue
}
if _, isEnum := enumTypes[ident.Name]; !isEnum {
continue
}
for _, val := range vs.Values {
lit, ok := val.(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
continue
}
unquoted, err := strconv.Unquote(lit.Value)
if err != nil {
continue
}
enumValues[ident.Name] = append(enumValues[ident.Name], unquoted)
}
}
}
+239
View File
@@ -0,0 +1,239 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openapi3gen
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestEnumKey_sortsAndJoins(t *testing.T) {
key := EnumKey([]any{"b", "a", "c"})
if key != "a|b|c" {
t.Fatalf("EnumKey = %q, want %q", key, "a|b|c")
}
}
func TestEnumKey_handlesNonStringValues(t *testing.T) {
key := EnumKey([]any{2, 1, 3})
if key != "1|2|3" {
t.Fatalf("EnumKey = %q, want %q", key, "1|2|3")
}
}
func TestScanSwaggerEnumTypes_basic(t *testing.T) {
dir := t.TempDir()
src := `package fixture
// Color is a primary color.
// swagger:enum Color
type Color string
const (
ColorRed Color = "red"
ColorGreen Color = "green"
ColorBlue Color = "blue"
)
`
if err := os.WriteFile(filepath.Join(dir, "color.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
got, err := ScanSwaggerEnumTypes([]string{dir})
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
wantKey := EnumKey([]any{"red", "green", "blue"})
if got[wantKey] != "Color" {
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Color")
}
}
func TestScanSwaggerEnumTypes_orphanAnnotation(t *testing.T) {
dir := t.TempDir()
src := `package fixture
// swagger:enum Sttype
type StateType string
const (
StateOpen StateType = "open"
)
`
if err := os.WriteFile(filepath.Join(dir, "typo.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
_, err := ScanSwaggerEnumTypes([]string{dir})
if err == nil {
t.Fatal("expected error for annotation referencing a non-matching type name")
}
if !strings.Contains(err.Error(), "Sttype") {
t.Fatalf("error %q should mention the typo'd name Sttype", err.Error())
}
}
func TestScanSwaggerEnumTypes_collision(t *testing.T) {
dir := t.TempDir()
src := `package fixture
// swagger:enum Alpha
type Alpha string
const (
AlphaX Alpha = "x"
AlphaY Alpha = "y"
)
// swagger:enum Beta
type Beta string
const (
BetaX Beta = "x"
BetaY Beta = "y"
)
`
if err := os.WriteFile(filepath.Join(dir, "dup.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
_, err := ScanSwaggerEnumTypes([]string{dir})
if err == nil {
t.Fatal("expected collision error, got nil")
}
msg := err.Error()
if !strings.Contains(msg, "Alpha") || !strings.Contains(msg, "Beta") {
t.Fatalf("error %q should mention both Alpha and Beta", msg)
}
}
func TestScanSwaggerEnumTypes_parseFailure(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "bad.go"), []byte("package fixture\nfunc Foo() {"), 0o644); err != nil {
t.Fatal(err)
}
_, err := ScanSwaggerEnumTypes([]string{dir})
if err == nil {
t.Fatal("expected parse error, got nil")
}
}
func TestScanSwaggerEnumTypes_annotationWithoutConsts(t *testing.T) {
dir := t.TempDir()
src := `package fixture
// swagger:enum Lonely
type Lonely string
`
if err := os.WriteFile(filepath.Join(dir, "lonely.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
_, err := ScanSwaggerEnumTypes([]string{dir})
if err == nil {
t.Fatal("expected error for annotation without consts")
}
if !strings.Contains(err.Error(), "Lonely") {
t.Fatalf("error %q should mention Lonely", err.Error())
}
}
func TestScanSwaggerEnumTypes_constsAndTypeInDifferentFiles(t *testing.T) {
dir := t.TempDir()
// Name ordering: `a_consts.go` < `b_type.go`, so readdir returns consts first.
// Old single-pass scanner would miss the values; two-pass must not.
constsSrc := `package fixture
const (
HueA Hue = "a"
HueB Hue = "b"
)
`
typeSrc := `package fixture
// swagger:enum Hue
type Hue string
`
if err := os.WriteFile(filepath.Join(dir, "a_consts.go"), []byte(constsSrc), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "b_type.go"), []byte(typeSrc), 0o644); err != nil {
t.Fatal(err)
}
got, err := ScanSwaggerEnumTypes([]string{dir})
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
wantKey := EnumKey([]any{"a", "b"})
if got[wantKey] != "Hue" {
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Hue")
}
}
func TestScanSwaggerEnumTypes_constsBeforeType(t *testing.T) {
dir := t.TempDir()
src := `package fixture
const (
ShadeDark Shade = "dark"
ShadeLight Shade = "light"
)
// swagger:enum Shade
type Shade string
`
if err := os.WriteFile(filepath.Join(dir, "shade.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
got, err := ScanSwaggerEnumTypes([]string{dir})
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
wantKey := EnumKey([]any{"dark", "light"})
if got[wantKey] != "Shade" {
t.Fatalf("map[%q] = %q, want %q", wantKey, got[wantKey], "Shade")
}
}
func TestScanSwaggerEnumTypes_groupedTypeDecl(t *testing.T) {
dir := t.TempDir()
src := `package fixture
type (
// swagger:enum Color
Color string
// swagger:enum Shade
Shade string
)
const (
ColorRed Color = "red"
ColorBlue Color = "blue"
)
const (
ShadeDark Shade = "dark"
ShadeLight Shade = "light"
)
`
if err := os.WriteFile(filepath.Join(dir, "grouped.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}
got, err := ScanSwaggerEnumTypes([]string{dir})
if err != nil {
t.Fatalf("ScanSwaggerEnumTypes: %v", err)
}
colorKey := EnumKey([]any{"red", "blue"})
shadeKey := EnumKey([]any{"dark", "light"})
if got[colorKey] != "Color" {
t.Fatalf("Color: map[%q] = %q, want %q", colorKey, got[colorKey], "Color")
}
if got[shadeKey] != "Shade" {
t.Fatalf("Shade: map[%q] = %q, want %q", shadeKey, got[shadeKey], "Shade")
}
}
+8
View File
@@ -46,6 +46,7 @@ require (
github.com/ethantkoenig/rupture v1.0.1
github.com/felixge/fgprof v0.9.5
github.com/fsnotify/fsnotify v1.9.0
github.com/getkin/kin-openapi v0.134.0
github.com/gliderlabs/ssh v0.3.8
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/cors v1.2.2
@@ -192,6 +193,8 @@ require (
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/go-webauthn/x v0.2.3 // indirect
github.com/goccy/go-json v0.10.6 // indirect
@@ -232,15 +235,19 @@ require (
github.com/minio/minlz v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c // indirect
github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.8 // indirect
github.com/olekukonko/tablewriter v1.1.4 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
@@ -260,6 +267,7 @@ require (
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/unknwon/com v1.0.1 // indirect
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
+18
View File
@@ -275,6 +275,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/getkin/kin-openapi v0.134.0 h1:/L5+1+kfe6dXh8Ot/wqiTgUkjOIEJiC0bbYVziHB8rU=
github.com/getkin/kin-openapi v0.134.0/go.mod h1:wK6ZLG/VgoETO9pcLJ/VmAtIcl/DNlMayNTb716EUxE=
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0=
github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -310,6 +312,10 @@ github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZR
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI=
@@ -547,6 +553,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
@@ -565,6 +573,10 @@ github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsR
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c h1:7ACFcSaQsrWtrH4WHHfUqE1C+f8r2uv8KGaW0jTNjus=
github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c/go.mod h1:JKox4Gszkxt57kj27u7rvi7IFoIULvCZHUsBTUmQM/s=
github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b h1:vivRhVUAa9t1q0Db4ZmezBP8pWQWnXHFokZj0AOea2g=
github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
@@ -591,6 +603,8 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
@@ -700,6 +714,8 @@ github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77ro
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
@@ -712,6 +728,8 @@ github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZ
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+1
View File
@@ -615,6 +615,7 @@ var (
"sitemap.xml", // search engine sitemap
"ssh_info", // agit info
"swagger.v1.json",
"openapi3.v1.json",
"ghost", // reserved name for deleted users (id: -1)
"gitea-actions", // gitea builtin user (id: -2)
+2 -2
View File
@@ -30,7 +30,7 @@ type CreateUserOption struct {
// Whether the user has restricted access privileges
Restricted *bool `json:"restricted"`
// User visibility level: public, limited, or private
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"`
// For explicitly setting the user creation timestamp. Useful when users are
// migrated from other systems. When omitted, the user's creation timestamp
@@ -79,5 +79,5 @@ type EditUserOption struct {
// Whether the user has restricted access privileges
Restricted *bool `json:"restricted"`
// User visibility level: public, limited, or private
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"`
}
+3 -5
View File
@@ -22,7 +22,7 @@ type Organization struct {
// The location of the organization
Location string `json:"location"`
// The visibility level of the organization (public, limited, private)
Visibility string `json:"visibility"`
Visibility UserVisibility `json:"visibility"`
// Whether repository administrators can change team access
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
// username of the organization
@@ -60,8 +60,7 @@ type CreateOrgOption struct {
// The location of the organization
Location string `json:"location" binding:"MaxSize(50)"`
// possible values are `public` (default), `limited` or `private`
// enum: ["public","limited","private"]
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
Visibility UserVisibility `json:"visibility" binding:"In(,public,limited,private)"`
// Whether repository administrators can change team access
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
}
@@ -79,8 +78,7 @@ type EditOrgOption struct {
// The location of the organization
Location *string `json:"location" binding:"MaxSize(50)"`
// possible values are `public`, `limited` or `private`
// enum: ["public","limited","private"]
Visibility *string `json:"visibility" binding:"In(,public,limited,private)"`
Visibility *UserVisibility `json:"visibility" binding:"In(,public,limited,private)"`
// Whether repository administrators can change team access
RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"`
}
+6 -9
View File
@@ -15,9 +15,8 @@ type Team struct {
// The organization that the team belongs to
Organization *Organization `json:"organization"`
// Whether the team has access to all repositories in the organization
IncludesAllRepositories bool `json:"includes_all_repositories"`
// enum: ["none","read","write","admin","owner"]
Permission string `json:"permission"`
IncludesAllRepositories bool `json:"includes_all_repositories"`
Permission AccessLevelName `json:"permission"`
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
Units []string `json:"units"`
@@ -34,9 +33,8 @@ type CreateTeamOption struct {
// The description of the team
Description string `json:"description" binding:"MaxSize(255)"`
// Whether the team has access to all repositories in the organization
IncludesAllRepositories bool `json:"includes_all_repositories"`
// enum: ["read","write","admin"]
Permission string `json:"permission"`
IncludesAllRepositories bool `json:"includes_all_repositories"`
Permission RepoWritePermission `json:"permission"`
// example: ["repo.actions","repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.ext_wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
Units []string `json:"units"`
@@ -53,9 +51,8 @@ type EditTeamOption struct {
// The description of the team
Description *string `json:"description" binding:"MaxSize(255)"`
// Whether the team has access to all repositories in the organization
IncludesAllRepositories *bool `json:"includes_all_repositories"`
// enum: ["read","write","admin"]
Permission string `json:"permission"`
IncludesAllRepositories *bool `json:"includes_all_repositories"`
Permission RepoWritePermission `json:"permission"`
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
Units []string `json:"units"`
+11 -4
View File
@@ -8,6 +8,15 @@ import (
"time"
)
// ObjectFormatName is the git hash algorithm used by a repository.
// swagger:enum ObjectFormatName
type ObjectFormatName string
const (
ObjectFormatSHA1 ObjectFormatName = "sha1"
ObjectFormatSHA256 ObjectFormatName = "sha256"
)
// Permission represents a set of permissions
type Permission struct {
Admin bool `json:"admin"` // Admin indicates if the user is an administrator of the repository.
@@ -114,8 +123,7 @@ type Repository struct {
Internal bool `json:"internal"`
MirrorInterval string `json:"mirror_interval"`
// ObjectFormatName of the underlying git repository
// enum: ["sha1","sha256"]
ObjectFormatName string `json:"object_format_name"`
ObjectFormatName ObjectFormatName `json:"object_format_name"`
// swagger:strfmt date-time
MirrorUpdated time.Time `json:"mirror_updated"`
RepoTransfer *RepoTransfer `json:"repo_transfer,omitempty"`
@@ -153,8 +161,7 @@ type CreateRepoOption struct {
// enum: ["default","collaborator","committer","collaboratorcommitter"]
TrustModel string `json:"trust_model"`
// ObjectFormatName of the underlying git repository, empty string for default (sha1)
// enum: ["sha1","sha256"]
ObjectFormatName string `json:"object_format_name" binding:"MaxSize(6)"`
ObjectFormatName ObjectFormatName `json:"object_format_name" binding:"MaxSize(6)"`
}
// EditRepoOption options when editing a repository's properties
+27 -3
View File
@@ -3,17 +3,41 @@
package structs
// RepoWritePermission is a permission level callers may grant to a team or
// collaborator on input. Output fields use AccessLevelName instead.
// swagger:enum RepoWritePermission
type RepoWritePermission string
const (
RepoWritePermissionRead RepoWritePermission = "read"
RepoWritePermissionWrite RepoWritePermission = "write"
RepoWritePermissionAdmin RepoWritePermission = "admin"
)
// AccessLevelName is the string rendering of a perm.AccessMode produced on
// API responses. Callers must not send these values; use RepoWritePermission
// on input.
// swagger:enum AccessLevelName
type AccessLevelName string
const (
AccessLevelNameNone AccessLevelName = "none"
AccessLevelNameRead AccessLevelName = "read"
AccessLevelNameWrite AccessLevelName = "write"
AccessLevelNameAdmin AccessLevelName = "admin"
AccessLevelNameOwner AccessLevelName = "owner"
)
// AddCollaboratorOption options when adding a user as a collaborator of a repository
type AddCollaboratorOption struct {
// enum: ["read","write","admin"]
// Permission level to grant the collaborator
Permission *string `json:"permission"`
Permission *RepoWritePermission `json:"permission"`
}
// RepoCollaboratorPermission to get repository permission for a collaborator
type RepoCollaboratorPermission struct {
// Permission level of the collaborator
Permission string `json:"permission"`
Permission AccessLevelName `json:"permission"`
// RoleName is the name of the permission role
RoleName string `json:"role_name"`
// User information of the collaborator
+1 -1
View File
@@ -51,7 +51,7 @@ type User struct {
// the user's description
Description string `json:"description"`
// User visibility level option: public, limited, private
Visibility string `json:"visibility"`
Visibility UserVisibility `json:"visibility"`
// user counts
Followers int `json:"followers_count"`
+11
View File
@@ -56,3 +56,14 @@ func ExtractKeysFromMapString(in map[string]VisibleType) (keys []string) {
}
return keys
}
// UserVisibility defines the visibility level of a user or organization as
// rendered in API payloads. The DB representation is VisibleType (int).
// swagger:enum UserVisibility
type UserVisibility string
const (
UserVisibilityPublic UserVisibility = "public"
UserVisibilityLimited UserVisibility = "limited"
UserVisibilityPrivate UserVisibility = "private"
)
+1 -1
View File
@@ -48,7 +48,7 @@ func CreateOrg(ctx *context.APIContext) {
visibility := api.VisibleTypePublic
if form.Visibility != "" {
visibility = api.VisibilityModes[form.Visibility]
visibility = api.VisibilityModes[string(form.Visibility)]
}
org := &organization.Organization{
+2 -2
View File
@@ -123,7 +123,7 @@ func CreateUser(ctx *context.APIContext) {
}
if form.Visibility != "" {
visibility := api.VisibilityModes[form.Visibility]
visibility := api.VisibilityModes[string(form.Visibility)]
overwriteDefault.Visibility = &visibility
}
@@ -239,7 +239,7 @@ func EditUser(ctx *context.APIContext) {
Description: optional.FromPtr(form.Description),
IsActive: optional.FromPtr(form.Active),
IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin),
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
Visibility: optional.FromMapLookup(api.VisibilityModes, string(form.Visibility)),
AllowGitHook: optional.FromPtr(form.AllowGitHook),
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
+2 -2
View File
@@ -258,7 +258,7 @@ func Create(ctx *context.APIContext) {
visibility := api.VisibleTypePublic
if form.Visibility != "" {
visibility = api.VisibilityModes[form.Visibility]
visibility = api.VisibilityModes[string(form.Visibility)]
}
org := &organization.Organization{
@@ -402,7 +402,7 @@ func Edit(ctx *context.APIContext) {
Description: optional.FromPtr(form.Description),
Website: optional.FromPtr(form.Website),
Location: optional.FromPtr(form.Location),
Visibility: optional.FromMapLookup(api.VisibilityModes, optional.FromPtr(form.Visibility).Value()),
Visibility: optional.FromMapLookup(api.VisibilityModes, string(optional.FromPtr(form.Visibility).Value())),
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
}
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
+4 -4
View File
@@ -210,7 +210,7 @@ func CreateTeam(ctx *context.APIContext) {
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.CreateTeamOption)
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin)
team := &organization.Team{
OrgID: ctx.Org.Organization.ID,
Name: form.Name,
@@ -224,7 +224,7 @@ func CreateTeam(ctx *context.APIContext) {
if len(form.UnitsMap) > 0 {
attachTeamUnitsMap(team, form.UnitsMap)
} else if len(form.Units) > 0 {
unitPerm := perm.ParseAccessMode(form.Permission, perm.AccessModeRead, perm.AccessModeWrite)
unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite)
attachTeamUnits(team, unitPerm, form.Units)
} else {
ctx.APIErrorInternal(errors.New("units permission should not be empty"))
@@ -298,7 +298,7 @@ func EditTeam(ctx *context.APIContext) {
isAuthChanged := false
isIncludeAllChanged := false
if !team.IsOwnerTeam() && len(form.Permission) != 0 {
teamPermission := perm.ParseAccessMode(form.Permission, perm.AccessModeNone, perm.AccessModeAdmin)
teamPermission := perm.ParseAccessMode(string(form.Permission), perm.AccessModeNone, perm.AccessModeAdmin)
if team.AccessMode != teamPermission {
isAuthChanged = true
team.AccessMode = teamPermission
@@ -314,7 +314,7 @@ func EditTeam(ctx *context.APIContext) {
if len(form.UnitsMap) > 0 {
attachTeamUnitsMap(team, form.UnitsMap)
} else if len(form.Units) > 0 {
unitPerm := perm.ParseAccessMode(form.Permission, perm.AccessModeRead, perm.AccessModeWrite)
unitPerm := perm.ParseAccessMode(string(form.Permission), perm.AccessModeRead, perm.AccessModeWrite)
attachTeamUnits(team, unitPerm, form.Units)
}
} else {
+1 -1
View File
@@ -181,7 +181,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) {
p := perm.AccessModeWrite
if form.Permission != nil {
p = perm.ParseAccessMode(*form.Permission, perm.AccessModeRead, perm.AccessModeWrite, perm.AccessModeAdmin)
p = perm.ParseAccessMode(string(*form.Permission), perm.AccessModeRead, perm.AccessModeWrite, perm.AccessModeAdmin)
}
if err := repo_service.AddOrUpdateCollaborator(ctx, ctx.Repo.Repository, collaborator, p); err != nil {
+1 -1
View File
@@ -262,7 +262,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
DefaultBranch: opt.DefaultBranch,
TrustModel: repo_model.ToTrustModel(opt.TrustModel),
IsTemplate: opt.Template,
ObjectFormatName: opt.ObjectFormatName,
ObjectFormatName: string(opt.ObjectFormatName),
})
if err != nil {
if repo_model.IsErrRepoAlreadyExist(err) {
+7
View File
@@ -16,3 +16,10 @@ func SwaggerV1Json(ctx *context.Context) {
ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe
ctx.JSONTemplate("swagger/v1_json")
}
// OpenAPI3Json render OpenAPI 3.0 json (auto-converted from Swagger 2.0)
func OpenAPI3Json(ctx *context.Context) {
ctx.Data["SwaggerAppVer"] = template.HTML(template.JSEscapeString(setting.AppVer))
ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe
ctx.JSONTemplate("swagger/v1_openapi3_json")
}
+1
View File
@@ -1751,6 +1751,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
if setting.API.EnableSwagger {
m.Get("/swagger.v1.json", SwaggerV1Json)
m.Get("/openapi3.v1.json", OpenAPI3Json)
}
if !setting.IsProd || setting.IsInE2eTesting() {
+2 -2
View File
@@ -711,7 +711,7 @@ func ToOrganization(ctx context.Context, org *organization.Organization) *api.Or
Description: org.Description,
Website: org.Website,
Location: org.Location,
Visibility: org.Visibility.String(),
Visibility: api.UserVisibility(org.Visibility.String()),
RepoAdminChangeTeamAccess: org.RepoAdminChangeTeamAccess,
}
}
@@ -740,7 +740,7 @@ func ToTeams(ctx context.Context, teams []*organization.Team, loadOrgs bool) ([]
Description: t.Description,
IncludesAllRepositories: t.IncludesAllRepositories,
CanCreateOrgRepo: t.CanCreateOrgRepo,
Permission: t.AccessMode.ToString(),
Permission: api.AccessLevelName(t.AccessMode.ToString()),
Units: t.GetUnitNames(),
UnitsMap: t.GetUnitsMap(),
}
+1 -1
View File
@@ -251,7 +251,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
MirrorUpdated: mirrorUpdated,
RepoTransfer: transfer,
Topics: util.SliceNilAsEmpty(repo.Topics),
ObjectFormatName: repo.ObjectFormatName,
ObjectFormatName: api.ObjectFormatName(repo.ObjectFormatName),
Licenses: util.SliceNilAsEmpty(repoLicenses.StringList()),
}
}
+2 -2
View File
@@ -65,7 +65,7 @@ func toUser(ctx context.Context, user *user_model.User, signed, authed bool) *ap
StarredRepos: user.NumStars,
}
result.Visibility = user.Visibility.String()
result.Visibility = api.UserVisibility(user.Visibility.String())
// hide primary email if API caller is anonymous or user keep email private
if signed && (!user.KeepEmailPrivate || authed) {
@@ -104,7 +104,7 @@ func User2UserSettings(user *user_model.User) api.UserSettings {
func ToUserAndPermission(ctx context.Context, user, doer *user_model.User, accessMode perm.AccessMode) api.RepoCollaboratorPermission {
return api.RepoCollaboratorPermission{
User: ToUser(ctx, user, doer),
Permission: accessMode.ToString(),
Permission: api.AccessLevelName(accessMode.ToString()),
RoleName: accessMode.ToString(),
}
}
+2 -2
View File
@@ -29,11 +29,11 @@ func TestUser_ToUser(t *testing.T) {
apiUser = toUser(t.Context(), user1, false, false)
assert.False(t, apiUser.IsAdmin)
assert.Equal(t, api.VisibleTypePublic.String(), apiUser.Visibility)
assert.Equal(t, api.UserVisibilityPublic, apiUser.Visibility)
user31 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31, IsAdmin: false, Visibility: api.VisibleTypePrivate})
apiUser = toUser(t.Context(), user31, true, true)
assert.False(t, apiUser.IsAdmin)
assert.Equal(t, api.VisibleTypePrivate.String(), apiUser.Visibility)
assert.Equal(t, api.UserVisibilityPrivate, apiUser.Visibility)
}
+50 -9
View File
@@ -22299,12 +22299,14 @@
"type": "object",
"properties": {
"permission": {
"description": "Permission level to grant the collaborator\nread RepoWritePermissionRead\nwrite RepoWritePermissionWrite\nadmin RepoWritePermissionAdmin",
"type": "string",
"enum": [
"read",
"write",
"admin"
],
"x-go-enum-desc": "read RepoWritePermissionRead\nwrite RepoWritePermissionWrite\nadmin RepoWritePermissionAdmin",
"x-go-name": "Permission"
}
},
@@ -24031,13 +24033,14 @@
"x-go-name": "UserName"
},
"visibility": {
"description": "possible values are `public` (default), `limited` or `private`",
"description": "possible values are `public` (default), `limited` or `private`\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
"type": "string",
"enum": [
"public",
"limited",
"private"
],
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
"x-go-name": "Visibility"
},
"website": {
@@ -24322,12 +24325,13 @@
"x-go-name": "Name"
},
"object_format_name": {
"description": "ObjectFormatName of the underlying git repository, empty string for default (sha1)",
"description": "ObjectFormatName of the underlying git repository, empty string for default (sha1)\nsha1 ObjectFormatSHA1\nsha256 ObjectFormatSHA256",
"type": "string",
"enum": [
"sha1",
"sha256"
],
"x-go-enum-desc": "sha1 ObjectFormatSHA1\nsha256 ObjectFormatSHA256",
"x-go-name": "ObjectFormatName"
},
"private": {
@@ -24480,6 +24484,7 @@
"write",
"admin"
],
"x-go-enum-desc": "read RepoWritePermissionRead\nwrite RepoWritePermissionWrite\nadmin RepoWritePermissionAdmin",
"x-go-name": "Permission"
},
"units": {
@@ -24574,8 +24579,14 @@
"x-go-name": "Username"
},
"visibility": {
"description": "User visibility level: public, limited, or private",
"description": "User visibility level: public, limited, or private\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
"type": "string",
"enum": [
"public",
"limited",
"private"
],
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
"x-go-name": "Visibility"
}
},
@@ -25200,13 +25211,14 @@
"x-go-name": "RepoAdminChangeTeamAccess"
},
"visibility": {
"description": "possible values are `public`, `limited` or `private`",
"description": "possible values are `public`, `limited` or `private`\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
"type": "string",
"enum": [
"public",
"limited",
"private"
],
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
"x-go-name": "Visibility"
},
"website": {
@@ -25570,6 +25582,7 @@
"write",
"admin"
],
"x-go-enum-desc": "read RepoWritePermissionRead\nwrite RepoWritePermissionWrite\nadmin RepoWritePermissionAdmin",
"x-go-name": "Permission"
},
"units": {
@@ -25700,8 +25713,14 @@
"x-go-name": "SourceID"
},
"visibility": {
"description": "User visibility level: public, limited, or private",
"description": "User visibility level: public, limited, or private\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
"type": "string",
"enum": [
"public",
"limited",
"private"
],
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
"x-go-name": "Visibility"
},
"website": {
@@ -27652,8 +27671,14 @@
"x-go-name": "UserName"
},
"visibility": {
"description": "The visibility level of the organization (public, limited, private)",
"description": "The visibility level of the organization (public, limited, private)\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
"type": "string",
"enum": [
"public",
"limited",
"private"
],
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
"x-go-name": "Visibility"
},
"website": {
@@ -28637,8 +28662,16 @@
"type": "object",
"properties": {
"permission": {
"description": "Permission level of the collaborator",
"description": "Permission level of the collaborator\nnone AccessLevelNameNone\nread AccessLevelNameRead\nwrite AccessLevelNameWrite\nadmin AccessLevelNameAdmin\nowner AccessLevelNameOwner",
"type": "string",
"enum": [
"none",
"read",
"write",
"admin",
"owner"
],
"x-go-enum-desc": "none AccessLevelNameNone\nread AccessLevelNameRead\nwrite AccessLevelNameWrite\nadmin AccessLevelNameAdmin\nowner AccessLevelNameOwner",
"x-go-name": "Permission"
},
"role_name": {
@@ -28915,12 +28948,13 @@
"x-go-name": "Name"
},
"object_format_name": {
"description": "ObjectFormatName of the underlying git repository",
"description": "ObjectFormatName of the underlying git repository\nsha1 ObjectFormatSHA1\nsha256 ObjectFormatSHA256",
"type": "string",
"enum": [
"sha1",
"sha256"
],
"x-go-enum-desc": "sha1 ObjectFormatSHA1\nsha256 ObjectFormatSHA256",
"x-go-name": "ObjectFormatName"
},
"open_issues_count": {
@@ -29294,6 +29328,7 @@
"admin",
"owner"
],
"x-go-enum-desc": "none AccessLevelNameNone\nread AccessLevelNameRead\nwrite AccessLevelNameWrite\nadmin AccessLevelNameAdmin\nowner AccessLevelNameOwner",
"x-go-name": "Permission"
},
"units": {
@@ -29840,8 +29875,14 @@
"x-go-name": "StarredRepos"
},
"visibility": {
"description": "User visibility level option: public, limited, private",
"description": "User visibility level option: public, limited, private\npublic UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
"type": "string",
"enum": [
"public",
"limited",
"private"
],
"x-go-enum-desc": "public UserVisibilityPublic\nlimited UserVisibilityLimited\nprivate UserVisibilityPrivate",
"x-go-name": "Visibility"
},
"website": {
File diff suppressed because it is too large Load Diff
@@ -96,12 +96,12 @@ func doAPIEditRepository(ctx APITestContext, editRepoOption *api.EditRepoOption,
func doAPIAddCollaborator(ctx APITestContext, username string, mode perm.AccessMode) func(*testing.T) {
return func(t *testing.T) {
permission := "read"
permission := api.RepoWritePermissionRead
if mode == perm.AccessModeAdmin {
permission = "admin"
permission = api.RepoWritePermissionAdmin
} else if mode > perm.AccessModeRead {
permission = "write"
permission = api.RepoWritePermissionWrite
}
addCollaboratorOption := &api.AddCollaboratorOption{
Permission: &permission,
+4 -4
View File
@@ -127,7 +127,7 @@ func testAPIOrgGeneral(t *testing.T) {
apiOrgList := DecodeJSON(t, resp, []*api.Organization{})
assert.Len(t, apiOrgList, 13)
assert.Equal(t, "Limited Org 36", apiOrgList[1].FullName)
assert.Equal(t, "limited", apiOrgList[1].Visibility)
assert.Equal(t, api.UserVisibilityLimited, apiOrgList[1].Visibility)
// accessing without a token will return only public orgs
req = NewRequest(t, "GET", "/api/v1/orgs")
@@ -136,7 +136,7 @@ func testAPIOrgGeneral(t *testing.T) {
apiOrgList = DecodeJSON(t, resp, []*api.Organization{})
assert.Len(t, apiOrgList, 9)
assert.Equal(t, "org 17", apiOrgList[0].FullName)
assert.Equal(t, "public", apiOrgList[0].Visibility)
assert.Equal(t, api.UserVisibilityPublic, apiOrgList[0].Visibility)
})
t.Run("OrgEdit", func(t *testing.T) {
@@ -148,7 +148,7 @@ func testAPIOrgGeneral(t *testing.T) {
Description: new("new description"),
Website: new("https://org3-new-website.example.com"),
Location: new("new location"),
Visibility: new("limited"),
Visibility: new(api.UserVisibilityLimited),
Email: new("org3-new-email@example.com"),
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org3Edit).AddTokenAuth(user1Token)
@@ -178,7 +178,7 @@ func testAPIOrgGeneral(t *testing.T) {
t.Run("OrgEditInvalidVisibility", func(t *testing.T) {
org := api.EditOrgOption{
Visibility: new("invalid-visibility"),
Visibility: new(api.UserVisibility("invalid-visibility")),
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
@@ -38,7 +38,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
assert.Equal(t, "owner", repoPermission.Permission)
assert.Equal(t, api.AccessLevelNameOwner, repoPermission.Permission)
})
t.Run("CollaboratorWithReadAccess", func(t *testing.T) {
@@ -50,7 +50,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
assert.Equal(t, "read", repoPermission.Permission)
assert.Equal(t, api.AccessLevelNameRead, repoPermission.Permission)
})
t.Run("CollaboratorWithWriteAccess", func(t *testing.T) {
@@ -62,7 +62,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
assert.Equal(t, "write", repoPermission.Permission)
assert.Equal(t, api.AccessLevelNameWrite, repoPermission.Permission)
})
t.Run("CollaboratorWithAdminAccess", func(t *testing.T) {
@@ -74,7 +74,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
assert.Equal(t, "admin", repoPermission.Permission)
assert.Equal(t, api.AccessLevelNameAdmin, repoPermission.Permission)
})
t.Run("CollaboratorNotFound", func(t *testing.T) {
@@ -101,7 +101,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
assert.Equal(t, "read", repoPermission.Permission)
assert.Equal(t, api.AccessLevelNameRead, repoPermission.Permission)
t.Run("CollaboratorCanReadOwnPermission", func(t *testing.T) {
session := loginUser(t, user5.Name)
@@ -112,7 +112,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
repoCollPerm := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
assert.Equal(t, "read", repoCollPerm.Permission)
assert.Equal(t, api.AccessLevelNameRead, repoCollPerm.Permission)
})
})
@@ -128,7 +128,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
assert.Equal(t, "read", repoPermission.Permission)
assert.Equal(t, api.AccessLevelNameRead, repoPermission.Permission)
})
t.Run("RepoAdminCanQueryACollaboratorsPermissions", func(t *testing.T) {
@@ -144,6 +144,6 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
repoPermission := DecodeJSON(t, resp, &api.RepoCollaboratorPermission{})
assert.Equal(t, "read", repoPermission.Permission)
assert.Equal(t, api.AccessLevelNameRead, repoPermission.Permission)
})
}
+2 -2
View File
@@ -39,12 +39,12 @@ func TestAPIRepoTeams(t *testing.T) {
assert.Equal(t, "Owners", teams[0].Name)
assert.True(t, teams[0].CanCreateOrgRepo)
assert.True(t, util.SliceSortedEqual(unit.AllUnitKeyNames(), teams[0].Units), "%v == %v", unit.AllUnitKeyNames(), teams[0].Units)
assert.Equal(t, "owner", teams[0].Permission)
assert.Equal(t, api.AccessLevelNameOwner, teams[0].Permission)
assert.Equal(t, "test_team", teams[1].Name)
assert.False(t, teams[1].CanCreateOrgRepo)
assert.Equal(t, []string{"repo.issues"}, teams[1].Units)
assert.Equal(t, "write", teams[1].Permission)
assert.Equal(t, api.AccessLevelNameWrite, teams[1].Permission)
}
// IsTeam
+16 -16
View File
@@ -75,9 +75,9 @@ func TestAPITeam(t *testing.T) {
resp = MakeRequest(t, req, http.StatusCreated)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "CreateTeam1", apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
"none", teamToCreate.Units, nil)
api.AccessLevelNameNone, teamToCreate.Units, nil)
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
"none", teamToCreate.Units, nil)
api.AccessLevelNameNone, teamToCreate.Units, nil)
teamID := apiTeam.ID
// Edit team.
@@ -96,9 +96,9 @@ func TestAPITeam(t *testing.T) {
resp = MakeRequest(t, req, http.StatusOK)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "EditTeam1", apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
api.AccessLevelName(teamToEdit.Permission), unit.AllUnitKeyNames(), nil)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
api.AccessLevelName(teamToEdit.Permission), unit.AllUnitKeyNames(), nil)
// Edit team Description only
editDescription = "first team"
@@ -108,9 +108,9 @@ func TestAPITeam(t *testing.T) {
resp = MakeRequest(t, req, http.StatusOK)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "EditTeam1_DescOnly", apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
api.AccessLevelName(teamToEdit.Permission), unit.AllUnitKeyNames(), nil)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
teamToEdit.Permission, unit.AllUnitKeyNames(), nil)
api.AccessLevelName(teamToEdit.Permission), unit.AllUnitKeyNames(), nil)
// Read team.
teamRead := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
@@ -120,7 +120,7 @@ func TestAPITeam(t *testing.T) {
resp = MakeRequest(t, req, http.StatusOK)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "ReadTeam1", apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
teamRead.AccessMode.ToString(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
api.AccessLevelName(teamRead.AccessMode.ToString()), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
// Delete team.
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID).
@@ -142,9 +142,9 @@ func TestAPITeam(t *testing.T) {
resp = MakeRequest(t, req, http.StatusCreated)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "CreateTeam2", apiTeam, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
"none", nil, teamToCreate.UnitsMap)
api.AccessLevelNameNone, nil, teamToCreate.UnitsMap)
checkTeamBean(t, apiTeam.ID, teamToCreate.Name, teamToCreate.Description, teamToCreate.IncludesAllRepositories,
"none", nil, teamToCreate.UnitsMap)
api.AccessLevelNameNone, nil, teamToCreate.UnitsMap)
teamID = apiTeam.ID
// Edit team.
@@ -163,9 +163,9 @@ func TestAPITeam(t *testing.T) {
resp = MakeRequest(t, req, http.StatusOK)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "EditTeam2", apiTeam, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
"none", nil, teamToEdit.UnitsMap)
api.AccessLevelNameNone, nil, teamToEdit.UnitsMap)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEdit.Description, *teamToEdit.IncludesAllRepositories,
"none", nil, teamToEdit.UnitsMap)
api.AccessLevelNameNone, nil, teamToEdit.UnitsMap)
// Edit team Description only
editDescription = "second team"
@@ -175,9 +175,9 @@ func TestAPITeam(t *testing.T) {
resp = MakeRequest(t, req, http.StatusOK)
apiTeam = DecodeJSON(t, resp, &api.Team{})
checkTeamResponse(t, "EditTeam2_DescOnly", apiTeam, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
"none", nil, teamToEdit.UnitsMap)
api.AccessLevelNameNone, nil, teamToEdit.UnitsMap)
checkTeamBean(t, apiTeam.ID, teamToEdit.Name, *teamToEditDesc.Description, *teamToEdit.IncludesAllRepositories,
"none", nil, teamToEdit.UnitsMap)
api.AccessLevelNameNone, nil, teamToEdit.UnitsMap)
// Read team.
teamRead = unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
@@ -187,7 +187,7 @@ func TestAPITeam(t *testing.T) {
apiTeam = DecodeJSON(t, resp, &api.Team{})
assert.NoError(t, teamRead.LoadUnits(t.Context()))
checkTeamResponse(t, "ReadTeam2", apiTeam, teamRead.Name, *teamToEditDesc.Description, teamRead.IncludesAllRepositories,
teamRead.AccessMode.ToString(), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
api.AccessLevelName(teamRead.AccessMode.ToString()), teamRead.GetUnitNames(), teamRead.GetUnitsMap())
// Delete team.
req = NewRequestf(t, "DELETE", "/api/v1/teams/%d", teamID).
@@ -227,7 +227,7 @@ func TestAPITeam(t *testing.T) {
unittest.AssertNotExistsBean(t, &organization.Team{ID: teamID})
}
func checkTeamResponse(t *testing.T, testName string, apiTeam *api.Team, name, description string, includesAllRepositories bool, permission string, units []string, unitsMap map[string]string) {
func checkTeamResponse(t *testing.T, testName string, apiTeam *api.Team, name, description string, includesAllRepositories bool, permission api.AccessLevelName, units []string, unitsMap map[string]string) {
t.Run(testName, func(t *testing.T) {
assert.Equal(t, name, apiTeam.Name, "name")
assert.Equal(t, description, apiTeam.Description, "description")
@@ -244,7 +244,7 @@ func checkTeamResponse(t *testing.T, testName string, apiTeam *api.Team, name, d
})
}
func checkTeamBean(t *testing.T, id int64, name, description string, includesAllRepositories bool, permission string, units []string, unitsMap map[string]string) {
func checkTeamBean(t *testing.T, id int64, name, description string, includesAllRepositories bool, permission api.AccessLevelName, units []string, unitsMap map[string]string) {
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: id})
assert.NoError(t, team.LoadUnits(t.Context()), "LoadUnits")
apiTeam, err := convert.ToTeam(t.Context(), team)
+2 -2
View File
@@ -47,7 +47,7 @@ func TestAPIUserSearchLoggedIn(t *testing.T) {
for _, user := range results.Data {
assert.Contains(t, user.UserName, query)
assert.NotEmpty(t, user.Email)
assert.Equal(t, "public", user.Visibility)
assert.Equal(t, api.UserVisibilityPublic, user.Visibility)
}
}
@@ -103,7 +103,7 @@ func TestAPIUserSearchAdminLoggedInUserHidden(t *testing.T) {
for _, user := range results.Data {
assert.Contains(t, user.UserName, query)
assert.NotEmpty(t, user.Email)
assert.Equal(t, "private", user.Visibility)
assert.Equal(t, api.UserVisibilityPrivate, user.Visibility)
}
}
+16 -10
View File
@@ -192,17 +192,21 @@ func testRenameInvalidUsername(t *testing.T) {
}
func testRenameReservedUsername(t *testing.T) {
reservedUsernames := []string{
// ".", "..", ".well-known", // The names are not only reserved but also invalid
// ".", "..", ".well-known" are also reserved but invalid as form input.
reservedNames := []string{
"api",
"openapi3.v1.json",
"swagger.v1.json",
}
patternNotAllowedNames := []string{
"name.keys",
}
session := loginUser(t, "user2")
locale := translation.NewLocale("en-US")
for _, reservedUsername := range reservedUsernames {
check := func(name, msgKey string) {
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
"name": reservedUsername,
"name": name,
"email": "user2@example.com",
"language": "en-US",
})
@@ -212,12 +216,14 @@ func testRenameReservedUsername(t *testing.T) {
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
actualMsg := strings.TrimSpace(htmlDoc.doc.Find(".ui.negative.message").Text())
expectedMsg := locale.TrString("user.form.name_reserved", reservedUsername)
if strings.Contains(reservedUsername, ".") {
expectedMsg = locale.TrString("user.form.name_pattern_not_allowed", reservedUsername)
}
assert.Equal(t, expectedMsg, actualMsg)
unittest.AssertNotExistsBean(t, &user_model.User{Name: reservedUsername})
assert.Equal(t, locale.TrString(msgKey, name), actualMsg)
unittest.AssertNotExistsBean(t, &user_model.User{Name: name})
}
for _, name := range reservedNames {
check(name, "user.form.name_reserved")
}
for _, name := range patternNotAllowedNames {
check(name, "user.form.name_pattern_not_allowed")
}
}