mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-26 17:08:25 +00:00 
			
		
		
		
	* feat: extend issue template for yaml * feat: support yaml template * feat: render form to markdown * feat: support yaml template for pr * chore: rename to Fields * feat: template unmarshal * feat: split template * feat: render to markdown * feat: use full name as template file name * chore: remove useless file * feat: use dropdown of fomantic ui * feat: update input style * docs: more comments * fix: render text without render * chore: fix lint error * fix: support use description as about in markdown * fix: add field class in form * chore: generate swagger * feat: validate template * feat: support is_nummber and regex * test: fix broken unit tests * fix: ignore empty body of md template * fix: make multiple easymde editors work in one page * feat: better UI * fix: js error in pr form * chore: generate swagger * feat: support regex validation * chore: generate swagger * fix: refresh each markdown editor * chore: give up required validation * fix: correct issue template candidates * fix: correct checkboxes style * chore: ignore .hugo_build.lock in docs * docs: separate out a new doc for merge templates * docs: introduce syntax of yaml template * feat: show a alert for invalid templates * test: add case for a valid template * fix: correct attributes of required checkbox * fix: add class not-under-easymde for dropzone * fix: use more back-quotes * chore: remove translation in zh-CN * fix EasyMDE statusbar margin * fix: remove repeated blocks * fix: reuse regex for quotes Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
		
			
				
	
	
		
			646 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			646 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2022 The Gitea Authors. All rights reserved.
 | |
| // Use of this source code is governed by a MIT-style
 | |
| // license that can be found in the LICENSE file.
 | |
| 
 | |
| package template
 | |
| 
 | |
| import (
 | |
| 	"net/url"
 | |
| 	"reflect"
 | |
| 	"testing"
 | |
| 
 | |
| 	"code.gitea.io/gitea/modules/json"
 | |
| 	api "code.gitea.io/gitea/modules/structs"
 | |
| )
 | |
| 
 | |
| func TestValidate(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name    string
 | |
| 		content string
 | |
| 		wantErr string
 | |
| 	}{
 | |
| 		{
 | |
| 			name:    "miss name",
 | |
| 			content: ``,
 | |
| 			wantErr: "'name' is required",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "miss about",
 | |
| 			content: `
 | |
| name: "test"
 | |
| `,
 | |
| 			wantErr: "'about' is required",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "miss body",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| `,
 | |
| 			wantErr: "'body' is required",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "markdown miss value",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "markdown"
 | |
| `,
 | |
| 			wantErr: "body[0](markdown): 'value' is required",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "markdown invalid value",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "markdown"
 | |
|     attributes:
 | |
|       value: true
 | |
| `,
 | |
| 			wantErr: "body[0](markdown): 'value' should be a string",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "markdown empty value",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "markdown"
 | |
|     attributes:
 | |
|       value: ""
 | |
| `,
 | |
| 			wantErr: "body[0](markdown): 'value' is required",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "textarea invalid id",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "textarea"
 | |
|     id: "?"
 | |
| `,
 | |
| 			wantErr: "body[0](textarea): 'id' should contain only alphanumeric, '-' and '_'",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "textarea miss label",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "textarea"
 | |
|     id: "1"
 | |
| `,
 | |
| 			wantErr: "body[0](textarea): 'label' is required",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "textarea conflict id",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "textarea"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
|   - type: "textarea"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "b"
 | |
| `,
 | |
| 			wantErr: "body[1](textarea): 'id' should be unique",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "textarea invalid description",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "textarea"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
|       description: true
 | |
| `,
 | |
| 			wantErr: "body[0](textarea): 'description' should be a string",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "textarea invalid required",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "textarea"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
|     validations:
 | |
|       required: "on"
 | |
| `,
 | |
| 			wantErr: "body[0](textarea): 'required' should be a bool",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "input invalid description",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "input"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
|       description: true
 | |
| `,
 | |
| 			wantErr: "body[0](input): 'description' should be a string",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "input invalid is_number",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "input"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
|     validations:
 | |
|       is_number: "yes"
 | |
| `,
 | |
| 			wantErr: "body[0](input): 'is_number' should be a bool",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "input invalid regex",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "input"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
|     validations:
 | |
|       regex: true
 | |
| `,
 | |
| 			wantErr: "body[0](input): 'regex' should be a string",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "dropdown invalid description",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "dropdown"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
|       description: true
 | |
| `,
 | |
| 			wantErr: "body[0](dropdown): 'description' should be a string",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "dropdown invalid multiple",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "dropdown"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
|       multiple: "on"
 | |
| `,
 | |
| 			wantErr: "body[0](dropdown): 'multiple' should be a bool",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "checkboxes invalid description",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "checkboxes"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
|       description: true
 | |
| `,
 | |
| 			wantErr: "body[0](checkboxes): 'description' should be a string",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "invalid type",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "video"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
| `,
 | |
| 			wantErr: "body[0](video): unknown type",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "dropdown miss options",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "dropdown"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
| `,
 | |
| 			wantErr: "body[0](dropdown): 'options' is required and should be a array",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "dropdown invalid options",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "dropdown"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
|       options:
 | |
|         - "a"
 | |
|         - true
 | |
| `,
 | |
| 			wantErr: "body[0](dropdown), option[1]: should be a string",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "checkboxes invalid options",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "checkboxes"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
|       options:
 | |
|         - "a"
 | |
|         - true
 | |
| `,
 | |
| 			wantErr: "body[0](checkboxes), option[0]: should be a dictionary",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "checkboxes option miss label",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "checkboxes"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
|       options:
 | |
|         - required: true
 | |
| `,
 | |
| 			wantErr: "body[0](checkboxes), option[0]: 'label' is required and should be a string",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "checkboxes option invalid required",
 | |
| 			content: `
 | |
| name: "test"
 | |
| about: "this is about"
 | |
| body:
 | |
|   - type: "checkboxes"
 | |
|     id: "1"
 | |
|     attributes:
 | |
|       label: "a"
 | |
|       options:
 | |
|         - label: "a"
 | |
|           required: "on"
 | |
| `,
 | |
| 			wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool",
 | |
| 		},
 | |
| 	}
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			tmpl, err := unmarshal("test.yaml", []byte(tt.content))
 | |
| 			if err != nil {
 | |
| 				t.Fatal(err)
 | |
| 			}
 | |
| 			if err := Validate(tmpl); (err == nil) != (tt.wantErr == "") || err != nil && err.Error() != tt.wantErr {
 | |
| 				t.Errorf("Validate() error = %v, wantErr %q", err, tt.wantErr)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	t.Run("valid", func(t *testing.T) {
 | |
| 		content := `
 | |
| name: Name
 | |
| title: Title
 | |
| about: About
 | |
| labels: ["label1", "label2"]
 | |
| ref: Ref
 | |
| body:
 | |
|   - type: markdown
 | |
|     id: id1
 | |
|     attributes:
 | |
|       value: Value of the markdown
 | |
|   - type: textarea
 | |
|     id: id2
 | |
|     attributes:
 | |
|       label: Label of textarea
 | |
|       description: Description of textarea
 | |
|       placeholder: Placeholder of textarea
 | |
|       value: Value of textarea
 | |
|       render: bash
 | |
|     validations:
 | |
|       required: true
 | |
|   - type: input
 | |
|     id: id3
 | |
|     attributes:
 | |
|       label: Label of input
 | |
|       description: Description of input
 | |
|       placeholder: Placeholder of input
 | |
|       value: Value of input
 | |
|     validations:
 | |
|       required: true
 | |
|       is_number: true
 | |
|       regex: "[a-zA-Z0-9]+"
 | |
|   - type: dropdown
 | |
|     id: id4
 | |
|     attributes:
 | |
|       label: Label of dropdown
 | |
|       description: Description of dropdown
 | |
|       multiple: true
 | |
|       options:
 | |
|         - Option 1 of dropdown
 | |
|         - Option 2 of dropdown
 | |
|         - Option 3 of dropdown
 | |
|     validations:
 | |
|       required: true
 | |
|   - type: checkboxes
 | |
|     id: id5
 | |
|     attributes:
 | |
|       label: Label of checkboxes
 | |
|       description: Description of checkboxes
 | |
|       options:
 | |
|         - label: Option 1 of checkboxes
 | |
|           required: true
 | |
|         - label: Option 2 of checkboxes
 | |
|           required: false
 | |
|         - label: Option 3 of checkboxes
 | |
|           required: true
 | |
| `
 | |
| 		want := &api.IssueTemplate{
 | |
| 			Name:   "Name",
 | |
| 			Title:  "Title",
 | |
| 			About:  "About",
 | |
| 			Labels: []string{"label1", "label2"},
 | |
| 			Ref:    "Ref",
 | |
| 			Fields: []*api.IssueFormField{
 | |
| 				{
 | |
| 					Type: "markdown",
 | |
| 					ID:   "id1",
 | |
| 					Attributes: map[string]interface{}{
 | |
| 						"value": "Value of the markdown",
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					Type: "textarea",
 | |
| 					ID:   "id2",
 | |
| 					Attributes: map[string]interface{}{
 | |
| 						"label":       "Label of textarea",
 | |
| 						"description": "Description of textarea",
 | |
| 						"placeholder": "Placeholder of textarea",
 | |
| 						"value":       "Value of textarea",
 | |
| 						"render":      "bash",
 | |
| 					},
 | |
| 					Validations: map[string]interface{}{
 | |
| 						"required": true,
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					Type: "input",
 | |
| 					ID:   "id3",
 | |
| 					Attributes: map[string]interface{}{
 | |
| 						"label":       "Label of input",
 | |
| 						"description": "Description of input",
 | |
| 						"placeholder": "Placeholder of input",
 | |
| 						"value":       "Value of input",
 | |
| 					},
 | |
| 					Validations: map[string]interface{}{
 | |
| 						"required":  true,
 | |
| 						"is_number": true,
 | |
| 						"regex":     "[a-zA-Z0-9]+",
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					Type: "dropdown",
 | |
| 					ID:   "id4",
 | |
| 					Attributes: map[string]interface{}{
 | |
| 						"label":       "Label of dropdown",
 | |
| 						"description": "Description of dropdown",
 | |
| 						"multiple":    true,
 | |
| 						"options": []interface{}{
 | |
| 							"Option 1 of dropdown",
 | |
| 							"Option 2 of dropdown",
 | |
| 							"Option 3 of dropdown",
 | |
| 						},
 | |
| 					},
 | |
| 					Validations: map[string]interface{}{
 | |
| 						"required": true,
 | |
| 					},
 | |
| 				},
 | |
| 				{
 | |
| 					Type: "checkboxes",
 | |
| 					ID:   "id5",
 | |
| 					Attributes: map[string]interface{}{
 | |
| 						"label":       "Label of checkboxes",
 | |
| 						"description": "Description of checkboxes",
 | |
| 						"options": []interface{}{
 | |
| 							map[interface{}]interface{}{"label": "Option 1 of checkboxes", "required": true},
 | |
| 							map[interface{}]interface{}{"label": "Option 2 of checkboxes", "required": false},
 | |
| 							map[interface{}]interface{}{"label": "Option 3 of checkboxes", "required": true},
 | |
| 						},
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			FileName: "test.yaml",
 | |
| 		}
 | |
| 		got, err := unmarshal("test.yaml", []byte(content))
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 		if err := Validate(got); err != nil {
 | |
| 			t.Errorf("Validate() error = %v", err)
 | |
| 		}
 | |
| 		if !reflect.DeepEqual(want, got) {
 | |
| 			jsonWant, _ := json.Marshal(want)
 | |
| 			jsonGot, _ := json.Marshal(got)
 | |
| 			t.Errorf("want:\n%s\ngot:\n%s", jsonWant, jsonGot)
 | |
| 		}
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func TestRenderToMarkdown(t *testing.T) {
 | |
| 	type args struct {
 | |
| 		template string
 | |
| 		values   url.Values
 | |
| 	}
 | |
| 	tests := []struct {
 | |
| 		name string
 | |
| 		args args
 | |
| 		want string
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "normal",
 | |
| 			args: args{
 | |
| 				template: `
 | |
| name: Name
 | |
| title: Title
 | |
| about: About
 | |
| labels: ["label1", "label2"]
 | |
| ref: Ref
 | |
| body:
 | |
|   - type: markdown
 | |
|     id: id1
 | |
|     attributes:
 | |
|       value: Value of the markdown
 | |
|   - type: textarea
 | |
|     id: id2
 | |
|     attributes:
 | |
|       label: Label of textarea
 | |
|       description: Description of textarea
 | |
|       placeholder: Placeholder of textarea
 | |
|       value: Value of textarea
 | |
|       render: bash
 | |
|     validations:
 | |
|       required: true
 | |
|   - type: input
 | |
|     id: id3
 | |
|     attributes:
 | |
|       label: Label of input
 | |
|       description: Description of input
 | |
|       placeholder: Placeholder of input
 | |
|       value: Value of input
 | |
|     validations:
 | |
|       required: true
 | |
|       is_number: true
 | |
|       regex: "[a-zA-Z0-9]+"
 | |
|   - type: dropdown
 | |
|     id: id4
 | |
|     attributes:
 | |
|       label: Label of dropdown
 | |
|       description: Description of dropdown
 | |
|       multiple: true
 | |
|       options:
 | |
|         - Option 1 of dropdown
 | |
|         - Option 2 of dropdown
 | |
|         - Option 3 of dropdown
 | |
|     validations:
 | |
|       required: true
 | |
|   - type: checkboxes
 | |
|     id: id5
 | |
|     attributes:
 | |
|       label: Label of checkboxes
 | |
|       description: Description of checkboxes
 | |
|       options:
 | |
|         - label: Option 1 of checkboxes
 | |
|           required: true
 | |
|         - label: Option 2 of checkboxes
 | |
|           required: false
 | |
|         - label: Option 3 of checkboxes
 | |
|           required: true
 | |
| `,
 | |
| 				values: map[string][]string{
 | |
| 					"form-field-id2":   {"Value of id2"},
 | |
| 					"form-field-id3":   {"Value of id3"},
 | |
| 					"form-field-id4":   {"0,1"},
 | |
| 					"form-field-id5-0": {"on"},
 | |
| 					"form-field-id5-2": {"on"},
 | |
| 				},
 | |
| 			},
 | |
| 			want: `### Label of textarea
 | |
| 
 | |
| ` + "```bash\nValue of id2\n```" + `
 | |
| 
 | |
| ### Label of input
 | |
| 
 | |
| Value of id3
 | |
| 
 | |
| ### Label of dropdown
 | |
| 
 | |
| Option 1 of dropdown, Option 2 of dropdown
 | |
| 
 | |
| ### Label of checkboxes
 | |
| 
 | |
| - [x] Option 1 of checkboxes
 | |
| - [ ] Option 2 of checkboxes
 | |
| - [x] Option 3 of checkboxes
 | |
| 
 | |
| `,
 | |
| 		},
 | |
| 	}
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			template, err := Unmarshal("test.yaml", []byte(tt.args.template))
 | |
| 			if err != nil {
 | |
| 				t.Fatal(err)
 | |
| 			}
 | |
| 			if got := RenderToMarkdown(template, tt.args.values); got != tt.want {
 | |
| 				t.Errorf("RenderToMarkdown() = %v, want %v", got, tt.want)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func Test_minQuotes(t *testing.T) {
 | |
| 	type args struct {
 | |
| 		value string
 | |
| 	}
 | |
| 	tests := []struct {
 | |
| 		name string
 | |
| 		args args
 | |
| 		want string
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "without quote",
 | |
| 			args: args{
 | |
| 				value: "Hello\nWorld",
 | |
| 			},
 | |
| 			want: "```",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "with 1 quote",
 | |
| 			args: args{
 | |
| 				value: "Hello\nWorld\n`text`\n",
 | |
| 			},
 | |
| 			want: "```",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "with 3 quotes",
 | |
| 			args: args{
 | |
| 				value: "Hello\nWorld\n`text`\n```go\ntext\n```\n",
 | |
| 			},
 | |
| 			want: "````",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "with more quotes",
 | |
| 			args: args{
 | |
| 				value: "Hello\nWorld\n`text`\n```go\ntext\n```\n``````````bash\ntext\n``````````\n",
 | |
| 			},
 | |
| 			want: "```````````",
 | |
| 		},
 | |
| 		{
 | |
| 			name: "not leading quotes",
 | |
| 			args: args{
 | |
| 				value: "Hello\nWorld`text````go\ntext`````````````bash\ntext``````````\n",
 | |
| 			},
 | |
| 			want: "```",
 | |
| 		},
 | |
| 	}
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			if got := minQuotes(tt.args.value); got != tt.want {
 | |
| 				t.Errorf("minQuotes() = %v, want %v", got, tt.want)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 |