forgejo/modules/issue/template/template_test.go
6543 ff8f7a7a0d
Extend issue template yaml engine (#29274)
Add new option:

`visible`: witch can hide a specific field of the form or the created
content afterwards

It is a string array witch can contain `form` and `content`. If only
`form` is present, it wont show up in the created issue afterwards and
the other way around. By default it sets both except for markdown

As they are optional and github don't have any similar thing, it is non
breaking and also do not conflict with it.

With this you can:
- define "post issue creation" elements like a TODO list to track an
issue state
- make sure to have a checkbox that reminds the user to check for a
thing but dont have it in the created issue afterwards
- define markdown for the created issue (was the downside of using yaml
instead of md in the past)
 - ...

## Demo

```yaml
name: New Contribution
description: External Contributor creating a pull

body:
- type: checkboxes
  id: extern-todo
  visible: [form]
  attributes:
    label: Contribution Guidelines
    options:
      - label: I checked there exist no similar feature to be extended
        required: true
      - label: I did read the CONTRIBUTION.MD
        required: true
- type: checkboxes
  id: intern-todo
  visible: [content]
  attributes:
    label: Maintainer Check-List
    options:
      - label: Does this pull follow the KISS principe
      - label: Checked if internal bord was notifyed
# ....
```
[Demo
Video](https://cloud.obermui.de/s/tm34fSAbJp9qw9z/download/vid-20240220-152751.mkv)

---
*Sponsored by Kithara Software GmbH*

---------

Co-authored-by: John Olheiser <john.olheiser@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
(cherry picked from commit 77e29e0c39392f142627303bd798fb55258072b2)
2024-03-06 12:10:47 +08:00

839 lines
17 KiB
Go

// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package template
import (
"net/url"
"testing"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidate(t *testing.T) {
tests := []struct {
name string
filename string
content string
want *api.IssueTemplate
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",
},
{
name: "field is required but hidden",
content: `
name: "test"
about: "this is about"
body:
- type: "input"
id: "1"
attributes:
label: "a"
validations:
required: true
visible: [content]
`,
wantErr: "body[0](input): can not require a hidden field",
},
{
name: "checkboxes is required but hidden",
content: `
name: "test"
about: "this is about"
body:
- type: checkboxes
id: "1"
attributes:
label: Label of checkboxes
description: Description of checkboxes
options:
- label: Option 1
required: false
- label: Required and hidden
required: true
visible: [content]
`,
wantErr: "body[0](checkboxes), option[1]: can not require a hidden checkbox",
},
{
name: "valid",
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: Hidden Option 3 of checkboxes
visible: [content]
- label: Required but not submitted
required: true
visible: [form]
`,
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]any{
"value": "Value of the markdown",
},
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
},
{
Type: "textarea",
ID: "id2",
Attributes: map[string]any{
"label": "Label of textarea",
"description": "Description of textarea",
"placeholder": "Placeholder of textarea",
"value": "Value of textarea",
"render": "bash",
},
Validations: map[string]any{
"required": true,
},
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
},
{
Type: "input",
ID: "id3",
Attributes: map[string]any{
"label": "Label of input",
"description": "Description of input",
"placeholder": "Placeholder of input",
"value": "Value of input",
},
Validations: map[string]any{
"required": true,
"is_number": true,
"regex": "[a-zA-Z0-9]+",
},
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
},
{
Type: "dropdown",
ID: "id4",
Attributes: map[string]any{
"label": "Label of dropdown",
"description": "Description of dropdown",
"multiple": true,
"options": []any{
"Option 1 of dropdown",
"Option 2 of dropdown",
"Option 3 of dropdown",
},
},
Validations: map[string]any{
"required": true,
},
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
},
{
Type: "checkboxes",
ID: "id5",
Attributes: map[string]any{
"label": "Label of checkboxes",
"description": "Description of checkboxes",
"options": []any{
map[string]any{"label": "Option 1 of checkboxes", "required": true},
map[string]any{"label": "Option 2 of checkboxes", "required": false},
map[string]any{"label": "Hidden Option 3 of checkboxes", "visible": []string{"content"}},
map[string]any{"label": "Required but not submitted", "required": true, "visible": []string{"form"}},
},
},
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent},
},
},
FileName: "test.yaml",
},
wantErr: "",
},
{
name: "single label",
content: `
name: Name
title: Title
about: About
labels: label1
ref: Ref
body:
- type: markdown
id: id1
attributes:
value: Value of the markdown shown in form
- type: markdown
id: id2
attributes:
value: Value of the markdown shown in created issue
visible: [content]
`,
want: &api.IssueTemplate{
Name: "Name",
Title: "Title",
About: "About",
Labels: []string{"label1"},
Ref: "Ref",
Fields: []*api.IssueFormField{
{
Type: "markdown",
ID: "id1",
Attributes: map[string]any{
"value": "Value of the markdown shown in form",
},
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
},
{
Type: "markdown",
ID: "id2",
Attributes: map[string]any{
"value": "Value of the markdown shown in created issue",
},
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleContent},
},
},
FileName: "test.yaml",
},
wantErr: "",
},
{
name: "comma-delimited labels",
content: `
name: Name
title: Title
about: About
labels: label1,label2,,label3 ,,
ref: Ref
body:
- type: markdown
id: id1
attributes:
value: Value of the markdown
`,
want: &api.IssueTemplate{
Name: "Name",
Title: "Title",
About: "About",
Labels: []string{"label1", "label2", "label3"},
Ref: "Ref",
Fields: []*api.IssueFormField{
{
Type: "markdown",
ID: "id1",
Attributes: map[string]any{
"value": "Value of the markdown",
},
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
},
},
FileName: "test.yaml",
},
wantErr: "",
},
{
name: "empty string as labels",
content: `
name: Name
title: Title
about: About
labels: ''
ref: Ref
body:
- type: markdown
id: id1
attributes:
value: Value of the markdown
`,
want: &api.IssueTemplate{
Name: "Name",
Title: "Title",
About: "About",
Labels: nil,
Ref: "Ref",
Fields: []*api.IssueFormField{
{
Type: "markdown",
ID: "id1",
Attributes: map[string]any{
"value": "Value of the markdown",
},
Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm},
},
},
FileName: "test.yaml",
},
wantErr: "",
},
{
name: "comma delimited labels in markdown",
filename: "test.md",
content: `---
name: Name
title: Title
about: About
labels: label1,label2,,label3 ,,
ref: Ref
---
Content
`,
want: &api.IssueTemplate{
Name: "Name",
Title: "Title",
About: "About",
Labels: []string{"label1", "label2", "label3"},
Ref: "Ref",
Fields: nil,
Content: "Content\n",
FileName: "test.md",
},
wantErr: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
filename := "test.yaml"
if tt.filename != "" {
filename = tt.filename
}
tmpl, err := unmarshal(filename, []byte(tt.content))
require.NoError(t, err)
if tt.wantErr != "" {
require.EqualError(t, Validate(tmpl), tt.wantErr)
} else {
require.NoError(t, Validate(tmpl))
want, _ := json.Marshal(tt.want)
got, _ := json.Marshal(tmpl)
require.JSONEq(t, string(want), string(got))
}
})
}
}
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 shown in form
- type: markdown
id: id2
attributes:
value: Value of the markdown shown in created issue
visible: [content]
- type: textarea
id: id3
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: id4
attributes:
label: Label of input
description: Description of input
placeholder: Placeholder of input
value: Value of input
hide_label: true
validations:
required: true
is_number: true
regex: "[a-zA-Z0-9]+"
- type: dropdown
id: id5
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: id6
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
visible: [form]
- label: Hidden Option of checkboxes
visible: [content]
`,
values: map[string][]string{
"form-field-id3": {"Value of id3"},
"form-field-id4": {"Value of id4"},
"form-field-id5": {"0,1"},
"form-field-id6-0": {"on"},
"form-field-id6-2": {"on"},
},
},
want: `Value of the markdown shown in created issue
### Label of textarea
` + "```bash\nValue of id3\n```" + `
Value of id4
### Label of dropdown
Option 1 of dropdown, Option 2 of dropdown
### Label of checkboxes
- [x] Option 1 of checkboxes
- [ ] Option 2 of checkboxes
- [ ] Hidden Option 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 {
assert.EqualValues(t, tt.want, got)
}
})
}
}
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)
}
})
}
}