mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-15 20:25:18 +02:00
Closes #37670. Today, org members in Gitea only see teams they're a member of. In larger orgs that hurts onboarding and discoverability — there's no way to look up which team owns what without asking around. GitHub solves this with a per-team visibility setting; this PR brings the same model to Gitea. ## What changes - Every team gets a `visibility` setting: - `private` *(default)* — only team members and org owners can see the team. Same as today's behavior. - `limited` — listable by any member of the organization. Members and the repos the team has access to are visible too. Non-org-members still see nothing. - `public` — listable by any signed-in user. - The Owners team visibility is fixed and cannot be changed via settings. - Existing teams default to `private`, so this is a no-op for anyone who doesn't change anything. ## API - `Team`, `CreateTeamOption`, `EditTeamOption` all gain a `visibility` field (string enum: `private` | `limited` | `public`). - `GET /orgs/{org}/teams` and `/orgs/{org}/teams/search` now apply the same visibility rules as the web UI: - site admins and org owners still see every team - other org members see their own teams plus any `limited` or `public` team - `private` teams are no longer leaked through these endpoints - Swagger/OpenAPI specs regenerated. ## UI View from admin2 (not an owner): <img width="1669" height="726" src="https://github.com/user-attachments/assets/daf4bccb-644b-4426-b178-71963aeaf73b" /> View from admin (owner): <img width="2559" height="863" src="https://github.com/user-attachments/assets/4f22cebc-e9df-4fd2-8ed4-724d31fadb7a" /> --------- Signed-off-by: bircni <bircni@icloud.com> Co-authored-by: TheFox0x7 <thefox0x7@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
219 lines
6.7 KiB
Go
219 lines
6.7 KiB
Go
// 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 TestExtractEnumTypeName_TeamVisibility(t *testing.T) {
|
|
enum := []any{"public", "limited", "private"}
|
|
key := EnumKey(enum)
|
|
astMap := map[string][]string{key: {"UserVisibility", "TeamVisibility"}}
|
|
schema := &openapi3.Schema{
|
|
Type: &openapi3.Types{"string"},
|
|
Enum: enum,
|
|
Extensions: map[string]any{
|
|
"x-go-enum-desc": "public TeamVisibilityPublic\nlimited TeamVisibilityLimited\nprivate TeamVisibilityPrivate",
|
|
},
|
|
}
|
|
if got := extractEnumTypeName(schema, astMap); got != "TeamVisibility" {
|
|
t.Fatalf("got %q, want %q", got, "TeamVisibility")
|
|
}
|
|
}
|
|
|
|
func TestExtractEnumTypeName_ambiguousPrefixTie(t *testing.T) {
|
|
enum := []any{"one", "two"}
|
|
key := EnumKey(enum)
|
|
astMap := map[string][]string{key: {"AB", "AC"}}
|
|
schema := &openapi3.Schema{
|
|
Type: &openapi3.Types{"string"},
|
|
Enum: enum,
|
|
Extensions: map[string]any{
|
|
"x-go-enum-desc": "one ABOne\ntwo ACTwo",
|
|
},
|
|
}
|
|
if got := extractEnumTypeName(schema, astMap); got != "" {
|
|
t.Fatalf("got %q, want empty string for ambiguous tie", got)
|
|
}
|
|
}
|
|
|
|
func TestExtractEnumTypeName_rejectsIncidentalPrefix(t *testing.T) {
|
|
enum := []any{"a", "b"}
|
|
key := EnumKey(enum)
|
|
astMap := map[string][]string{key: {"Alpha", "Alphabet"}}
|
|
schema := &openapi3.Schema{
|
|
Type: &openapi3.Types{"string"},
|
|
Enum: enum,
|
|
Extensions: map[string]any{
|
|
"x-go-enum-desc": "a AlphabetA\nb AlphabetB",
|
|
},
|
|
}
|
|
if got := extractEnumTypeName(schema, astMap); got != "Alphabet" {
|
|
t.Fatalf("got %q, want %q", got, "Alphabet")
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|