code.gitea.io/gitea@v1.22.3/modules/issue/template/template_test.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package template 5 6 import ( 7 "net/url" 8 "testing" 9 10 "code.gitea.io/gitea/modules/json" 11 api "code.gitea.io/gitea/modules/structs" 12 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15 ) 16 17 func TestValidate(t *testing.T) { 18 tests := []struct { 19 name string 20 filename string 21 content string 22 want *api.IssueTemplate 23 wantErr string 24 }{ 25 { 26 name: "miss name", 27 content: ``, 28 wantErr: "'name' is required", 29 }, 30 { 31 name: "miss about", 32 content: ` 33 name: "test" 34 `, 35 wantErr: "'about' is required", 36 }, 37 { 38 name: "miss body", 39 content: ` 40 name: "test" 41 about: "this is about" 42 `, 43 wantErr: "'body' is required", 44 }, 45 { 46 name: "markdown miss value", 47 content: ` 48 name: "test" 49 about: "this is about" 50 body: 51 - type: "markdown" 52 `, 53 wantErr: "body[0](markdown): 'value' is required", 54 }, 55 { 56 name: "markdown invalid value", 57 content: ` 58 name: "test" 59 about: "this is about" 60 body: 61 - type: "markdown" 62 attributes: 63 value: true 64 `, 65 wantErr: "body[0](markdown): 'value' should be a string", 66 }, 67 { 68 name: "markdown empty value", 69 content: ` 70 name: "test" 71 about: "this is about" 72 body: 73 - type: "markdown" 74 attributes: 75 value: "" 76 `, 77 wantErr: "body[0](markdown): 'value' is required", 78 }, 79 { 80 name: "textarea invalid id", 81 content: ` 82 name: "test" 83 about: "this is about" 84 body: 85 - type: "textarea" 86 id: "?" 87 `, 88 wantErr: "body[0](textarea): 'id' should contain only alphanumeric, '-' and '_'", 89 }, 90 { 91 name: "textarea miss label", 92 content: ` 93 name: "test" 94 about: "this is about" 95 body: 96 - type: "textarea" 97 id: "1" 98 `, 99 wantErr: "body[0](textarea): 'label' is required", 100 }, 101 { 102 name: "textarea conflict id", 103 content: ` 104 name: "test" 105 about: "this is about" 106 body: 107 - type: "textarea" 108 id: "1" 109 attributes: 110 label: "a" 111 - type: "textarea" 112 id: "1" 113 attributes: 114 label: "b" 115 `, 116 wantErr: "body[1](textarea): 'id' should be unique", 117 }, 118 { 119 name: "textarea invalid description", 120 content: ` 121 name: "test" 122 about: "this is about" 123 body: 124 - type: "textarea" 125 id: "1" 126 attributes: 127 label: "a" 128 description: true 129 `, 130 wantErr: "body[0](textarea): 'description' should be a string", 131 }, 132 { 133 name: "textarea invalid required", 134 content: ` 135 name: "test" 136 about: "this is about" 137 body: 138 - type: "textarea" 139 id: "1" 140 attributes: 141 label: "a" 142 validations: 143 required: "on" 144 `, 145 wantErr: "body[0](textarea): 'required' should be a bool", 146 }, 147 { 148 name: "input invalid description", 149 content: ` 150 name: "test" 151 about: "this is about" 152 body: 153 - type: "input" 154 id: "1" 155 attributes: 156 label: "a" 157 description: true 158 `, 159 wantErr: "body[0](input): 'description' should be a string", 160 }, 161 { 162 name: "input invalid is_number", 163 content: ` 164 name: "test" 165 about: "this is about" 166 body: 167 - type: "input" 168 id: "1" 169 attributes: 170 label: "a" 171 validations: 172 is_number: "yes" 173 `, 174 wantErr: "body[0](input): 'is_number' should be a bool", 175 }, 176 { 177 name: "input invalid regex", 178 content: ` 179 name: "test" 180 about: "this is about" 181 body: 182 - type: "input" 183 id: "1" 184 attributes: 185 label: "a" 186 validations: 187 regex: true 188 `, 189 wantErr: "body[0](input): 'regex' should be a string", 190 }, 191 { 192 name: "dropdown invalid description", 193 content: ` 194 name: "test" 195 about: "this is about" 196 body: 197 - type: "dropdown" 198 id: "1" 199 attributes: 200 label: "a" 201 description: true 202 `, 203 wantErr: "body[0](dropdown): 'description' should be a string", 204 }, 205 { 206 name: "dropdown invalid multiple", 207 content: ` 208 name: "test" 209 about: "this is about" 210 body: 211 - type: "dropdown" 212 id: "1" 213 attributes: 214 label: "a" 215 multiple: "on" 216 `, 217 wantErr: "body[0](dropdown): 'multiple' should be a bool", 218 }, 219 { 220 name: "checkboxes invalid description", 221 content: ` 222 name: "test" 223 about: "this is about" 224 body: 225 - type: "checkboxes" 226 id: "1" 227 attributes: 228 label: "a" 229 description: true 230 `, 231 wantErr: "body[0](checkboxes): 'description' should be a string", 232 }, 233 { 234 name: "invalid type", 235 content: ` 236 name: "test" 237 about: "this is about" 238 body: 239 - type: "video" 240 id: "1" 241 attributes: 242 label: "a" 243 `, 244 wantErr: "body[0](video): unknown type", 245 }, 246 { 247 name: "dropdown miss options", 248 content: ` 249 name: "test" 250 about: "this is about" 251 body: 252 - type: "dropdown" 253 id: "1" 254 attributes: 255 label: "a" 256 `, 257 wantErr: "body[0](dropdown): 'options' is required and should be a array", 258 }, 259 { 260 name: "dropdown invalid options", 261 content: ` 262 name: "test" 263 about: "this is about" 264 body: 265 - type: "dropdown" 266 id: "1" 267 attributes: 268 label: "a" 269 options: 270 - "a" 271 - true 272 `, 273 wantErr: "body[0](dropdown), option[1]: should be a string", 274 }, 275 { 276 name: "checkboxes invalid options", 277 content: ` 278 name: "test" 279 about: "this is about" 280 body: 281 - type: "checkboxes" 282 id: "1" 283 attributes: 284 label: "a" 285 options: 286 - "a" 287 - true 288 `, 289 wantErr: "body[0](checkboxes), option[0]: should be a dictionary", 290 }, 291 { 292 name: "checkboxes option miss label", 293 content: ` 294 name: "test" 295 about: "this is about" 296 body: 297 - type: "checkboxes" 298 id: "1" 299 attributes: 300 label: "a" 301 options: 302 - required: true 303 `, 304 wantErr: "body[0](checkboxes), option[0]: 'label' is required and should be a string", 305 }, 306 { 307 name: "checkboxes option invalid required", 308 content: ` 309 name: "test" 310 about: "this is about" 311 body: 312 - type: "checkboxes" 313 id: "1" 314 attributes: 315 label: "a" 316 options: 317 - label: "a" 318 required: "on" 319 `, 320 wantErr: "body[0](checkboxes), option[0]: 'required' should be a bool", 321 }, 322 { 323 name: "field is required but hidden", 324 content: ` 325 name: "test" 326 about: "this is about" 327 body: 328 - type: "input" 329 id: "1" 330 attributes: 331 label: "a" 332 validations: 333 required: true 334 visible: [content] 335 `, 336 wantErr: "body[0](input): can not require a hidden field", 337 }, 338 { 339 name: "checkboxes is required but hidden", 340 content: ` 341 name: "test" 342 about: "this is about" 343 body: 344 - type: checkboxes 345 id: "1" 346 attributes: 347 label: Label of checkboxes 348 description: Description of checkboxes 349 options: 350 - label: Option 1 351 required: false 352 - label: Required and hidden 353 required: true 354 visible: [content] 355 `, 356 wantErr: "body[0](checkboxes), option[1]: can not require a hidden checkbox", 357 }, 358 { 359 name: "dropdown default is not an integer", 360 content: ` 361 name: "test" 362 about: "this is about" 363 body: 364 - type: dropdown 365 id: "1" 366 attributes: 367 label: Label of dropdown 368 description: Description of dropdown 369 multiple: true 370 options: 371 - Option 1 of dropdown 372 - Option 2 of dropdown 373 - Option 3 of dropdown 374 default: "def" 375 validations: 376 required: true 377 `, 378 wantErr: "body[0](dropdown): 'default' should be an int", 379 }, 380 { 381 name: "dropdown default is out of range", 382 content: ` 383 name: "test" 384 about: "this is about" 385 body: 386 - type: dropdown 387 id: "1" 388 attributes: 389 label: Label of dropdown 390 description: Description of dropdown 391 multiple: true 392 options: 393 - Option 1 of dropdown 394 - Option 2 of dropdown 395 - Option 3 of dropdown 396 default: 3 397 validations: 398 required: true 399 `, 400 wantErr: "body[0](dropdown): the value of 'default' is out of range", 401 }, 402 { 403 name: "dropdown without default is valid", 404 content: ` 405 name: "test" 406 about: "this is about" 407 body: 408 - type: dropdown 409 id: "1" 410 attributes: 411 label: Label of dropdown 412 description: Description of dropdown 413 multiple: true 414 options: 415 - Option 1 of dropdown 416 - Option 2 of dropdown 417 - Option 3 of dropdown 418 validations: 419 required: true 420 `, 421 want: &api.IssueTemplate{ 422 Name: "test", 423 About: "this is about", 424 Fields: []*api.IssueFormField{ 425 { 426 Type: "dropdown", 427 ID: "1", 428 Attributes: map[string]any{ 429 "label": "Label of dropdown", 430 "description": "Description of dropdown", 431 "multiple": true, 432 "options": []any{ 433 "Option 1 of dropdown", 434 "Option 2 of dropdown", 435 "Option 3 of dropdown", 436 }, 437 }, 438 Validations: map[string]any{ 439 "required": true, 440 }, 441 Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent}, 442 }, 443 }, 444 FileName: "test.yaml", 445 }, 446 wantErr: "", 447 }, 448 { 449 name: "valid", 450 content: ` 451 name: Name 452 title: Title 453 about: About 454 labels: ["label1", "label2"] 455 ref: Ref 456 body: 457 - type: markdown 458 id: id1 459 attributes: 460 value: Value of the markdown 461 - type: textarea 462 id: id2 463 attributes: 464 label: Label of textarea 465 description: Description of textarea 466 placeholder: Placeholder of textarea 467 value: Value of textarea 468 render: bash 469 validations: 470 required: true 471 - type: input 472 id: id3 473 attributes: 474 label: Label of input 475 description: Description of input 476 placeholder: Placeholder of input 477 value: Value of input 478 validations: 479 required: true 480 is_number: true 481 regex: "[a-zA-Z0-9]+" 482 - type: dropdown 483 id: id4 484 attributes: 485 label: Label of dropdown 486 description: Description of dropdown 487 multiple: true 488 options: 489 - Option 1 of dropdown 490 - Option 2 of dropdown 491 - Option 3 of dropdown 492 default: 1 493 validations: 494 required: true 495 - type: checkboxes 496 id: id5 497 attributes: 498 label: Label of checkboxes 499 description: Description of checkboxes 500 options: 501 - label: Option 1 of checkboxes 502 required: true 503 - label: Option 2 of checkboxes 504 required: false 505 - label: Hidden Option 3 of checkboxes 506 visible: [content] 507 - label: Required but not submitted 508 required: true 509 visible: [form] 510 `, 511 want: &api.IssueTemplate{ 512 Name: "Name", 513 Title: "Title", 514 About: "About", 515 Labels: []string{"label1", "label2"}, 516 Ref: "Ref", 517 Fields: []*api.IssueFormField{ 518 { 519 Type: "markdown", 520 ID: "id1", 521 Attributes: map[string]any{ 522 "value": "Value of the markdown", 523 }, 524 Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}, 525 }, 526 { 527 Type: "textarea", 528 ID: "id2", 529 Attributes: map[string]any{ 530 "label": "Label of textarea", 531 "description": "Description of textarea", 532 "placeholder": "Placeholder of textarea", 533 "value": "Value of textarea", 534 "render": "bash", 535 }, 536 Validations: map[string]any{ 537 "required": true, 538 }, 539 Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent}, 540 }, 541 { 542 Type: "input", 543 ID: "id3", 544 Attributes: map[string]any{ 545 "label": "Label of input", 546 "description": "Description of input", 547 "placeholder": "Placeholder of input", 548 "value": "Value of input", 549 }, 550 Validations: map[string]any{ 551 "required": true, 552 "is_number": true, 553 "regex": "[a-zA-Z0-9]+", 554 }, 555 Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent}, 556 }, 557 { 558 Type: "dropdown", 559 ID: "id4", 560 Attributes: map[string]any{ 561 "label": "Label of dropdown", 562 "description": "Description of dropdown", 563 "multiple": true, 564 "options": []any{ 565 "Option 1 of dropdown", 566 "Option 2 of dropdown", 567 "Option 3 of dropdown", 568 }, 569 "default": 1, 570 }, 571 Validations: map[string]any{ 572 "required": true, 573 }, 574 Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent}, 575 }, 576 { 577 Type: "checkboxes", 578 ID: "id5", 579 Attributes: map[string]any{ 580 "label": "Label of checkboxes", 581 "description": "Description of checkboxes", 582 "options": []any{ 583 map[string]any{"label": "Option 1 of checkboxes", "required": true}, 584 map[string]any{"label": "Option 2 of checkboxes", "required": false}, 585 map[string]any{"label": "Hidden Option 3 of checkboxes", "visible": []string{"content"}}, 586 map[string]any{"label": "Required but not submitted", "required": true, "visible": []string{"form"}}, 587 }, 588 }, 589 Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm, api.IssueFormFieldVisibleContent}, 590 }, 591 }, 592 FileName: "test.yaml", 593 }, 594 wantErr: "", 595 }, 596 { 597 name: "single label", 598 content: ` 599 name: Name 600 title: Title 601 about: About 602 labels: label1 603 ref: Ref 604 body: 605 - type: markdown 606 id: id1 607 attributes: 608 value: Value of the markdown shown in form 609 - type: markdown 610 id: id2 611 attributes: 612 value: Value of the markdown shown in created issue 613 visible: [content] 614 `, 615 want: &api.IssueTemplate{ 616 Name: "Name", 617 Title: "Title", 618 About: "About", 619 Labels: []string{"label1"}, 620 Ref: "Ref", 621 Fields: []*api.IssueFormField{ 622 { 623 Type: "markdown", 624 ID: "id1", 625 Attributes: map[string]any{ 626 "value": "Value of the markdown shown in form", 627 }, 628 Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}, 629 }, 630 { 631 Type: "markdown", 632 ID: "id2", 633 Attributes: map[string]any{ 634 "value": "Value of the markdown shown in created issue", 635 }, 636 Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleContent}, 637 }, 638 }, 639 FileName: "test.yaml", 640 }, 641 wantErr: "", 642 }, 643 { 644 name: "comma-delimited labels", 645 content: ` 646 name: Name 647 title: Title 648 about: About 649 labels: label1,label2,,label3 ,, 650 ref: Ref 651 body: 652 - type: markdown 653 id: id1 654 attributes: 655 value: Value of the markdown 656 `, 657 want: &api.IssueTemplate{ 658 Name: "Name", 659 Title: "Title", 660 About: "About", 661 Labels: []string{"label1", "label2", "label3"}, 662 Ref: "Ref", 663 Fields: []*api.IssueFormField{ 664 { 665 Type: "markdown", 666 ID: "id1", 667 Attributes: map[string]any{ 668 "value": "Value of the markdown", 669 }, 670 Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}, 671 }, 672 }, 673 FileName: "test.yaml", 674 }, 675 wantErr: "", 676 }, 677 { 678 name: "empty string as labels", 679 content: ` 680 name: Name 681 title: Title 682 about: About 683 labels: '' 684 ref: Ref 685 body: 686 - type: markdown 687 id: id1 688 attributes: 689 value: Value of the markdown 690 `, 691 want: &api.IssueTemplate{ 692 Name: "Name", 693 Title: "Title", 694 About: "About", 695 Labels: nil, 696 Ref: "Ref", 697 Fields: []*api.IssueFormField{ 698 { 699 Type: "markdown", 700 ID: "id1", 701 Attributes: map[string]any{ 702 "value": "Value of the markdown", 703 }, 704 Visible: []api.IssueFormFieldVisible{api.IssueFormFieldVisibleForm}, 705 }, 706 }, 707 FileName: "test.yaml", 708 }, 709 wantErr: "", 710 }, 711 { 712 name: "comma delimited labels in markdown", 713 filename: "test.md", 714 content: `--- 715 name: Name 716 title: Title 717 about: About 718 labels: label1,label2,,label3 ,, 719 ref: Ref 720 --- 721 Content 722 `, 723 want: &api.IssueTemplate{ 724 Name: "Name", 725 Title: "Title", 726 About: "About", 727 Labels: []string{"label1", "label2", "label3"}, 728 Ref: "Ref", 729 Fields: nil, 730 Content: "Content\n", 731 FileName: "test.md", 732 }, 733 wantErr: "", 734 }, 735 } 736 for _, tt := range tests { 737 t.Run(tt.name, func(t *testing.T) { 738 filename := "test.yaml" 739 if tt.filename != "" { 740 filename = tt.filename 741 } 742 tmpl, err := unmarshal(filename, []byte(tt.content)) 743 require.NoError(t, err) 744 if tt.wantErr != "" { 745 require.EqualError(t, Validate(tmpl), tt.wantErr) 746 } else { 747 require.NoError(t, Validate(tmpl)) 748 want, _ := json.Marshal(tt.want) 749 got, _ := json.Marshal(tmpl) 750 require.JSONEq(t, string(want), string(got)) 751 } 752 }) 753 } 754 } 755 756 func TestRenderToMarkdown(t *testing.T) { 757 type args struct { 758 template string 759 values url.Values 760 } 761 tests := []struct { 762 name string 763 args args 764 want string 765 }{ 766 { 767 name: "normal", 768 args: args{ 769 template: ` 770 name: Name 771 title: Title 772 about: About 773 labels: ["label1", "label2"] 774 ref: Ref 775 body: 776 - type: markdown 777 id: id1 778 attributes: 779 value: Value of the markdown shown in form 780 - type: markdown 781 id: id2 782 attributes: 783 value: Value of the markdown shown in created issue 784 visible: [content] 785 - type: textarea 786 id: id3 787 attributes: 788 label: Label of textarea 789 description: Description of textarea 790 placeholder: Placeholder of textarea 791 value: Value of textarea 792 render: bash 793 validations: 794 required: true 795 - type: input 796 id: id4 797 attributes: 798 label: Label of input 799 description: Description of input 800 placeholder: Placeholder of input 801 value: Value of input 802 hide_label: true 803 validations: 804 required: true 805 is_number: true 806 regex: "[a-zA-Z0-9]+" 807 - type: dropdown 808 id: id5 809 attributes: 810 label: Label of dropdown 811 description: Description of dropdown 812 multiple: true 813 options: 814 - Option 1 of dropdown 815 - Option 2 of dropdown 816 - Option 3 of dropdown 817 validations: 818 required: true 819 - type: checkboxes 820 id: id6 821 attributes: 822 label: Label of checkboxes 823 description: Description of checkboxes 824 options: 825 - label: Option 1 of checkboxes 826 required: true 827 - label: Option 2 of checkboxes 828 required: false 829 - label: Option 3 of checkboxes 830 required: true 831 visible: [form] 832 - label: Hidden Option of checkboxes 833 visible: [content] 834 `, 835 values: map[string][]string{ 836 "form-field-id3": {"Value of id3"}, 837 "form-field-id4": {"Value of id4"}, 838 "form-field-id5": {"0,1"}, 839 "form-field-id6-0": {"on"}, 840 "form-field-id6-2": {"on"}, 841 }, 842 }, 843 844 want: `Value of the markdown shown in created issue 845 846 ### Label of textarea 847 848 ` + "```bash\nValue of id3\n```" + ` 849 850 Value of id4 851 852 ### Label of dropdown 853 854 Option 1 of dropdown, Option 2 of dropdown 855 856 ### Label of checkboxes 857 858 - [x] Option 1 of checkboxes 859 - [ ] Option 2 of checkboxes 860 - [ ] Hidden Option of checkboxes 861 862 `, 863 }, 864 } 865 for _, tt := range tests { 866 t.Run(tt.name, func(t *testing.T) { 867 template, err := Unmarshal("test.yaml", []byte(tt.args.template)) 868 if err != nil { 869 t.Fatal(err) 870 } 871 if got := RenderToMarkdown(template, tt.args.values); got != tt.want { 872 assert.EqualValues(t, tt.want, got) 873 } 874 }) 875 } 876 } 877 878 func Test_minQuotes(t *testing.T) { 879 type args struct { 880 value string 881 } 882 tests := []struct { 883 name string 884 args args 885 want string 886 }{ 887 { 888 name: "without quote", 889 args: args{ 890 value: "Hello\nWorld", 891 }, 892 want: "```", 893 }, 894 { 895 name: "with 1 quote", 896 args: args{ 897 value: "Hello\nWorld\n`text`\n", 898 }, 899 want: "```", 900 }, 901 { 902 name: "with 3 quotes", 903 args: args{ 904 value: "Hello\nWorld\n`text`\n```go\ntext\n```\n", 905 }, 906 want: "````", 907 }, 908 { 909 name: "with more quotes", 910 args: args{ 911 value: "Hello\nWorld\n`text`\n```go\ntext\n```\n``````````bash\ntext\n``````````\n", 912 }, 913 want: "```````````", 914 }, 915 { 916 name: "not leading quotes", 917 args: args{ 918 value: "Hello\nWorld`text````go\ntext`````````````bash\ntext``````````\n", 919 }, 920 want: "```", 921 }, 922 } 923 for _, tt := range tests { 924 t.Run(tt.name, func(t *testing.T) { 925 if got := minQuotes(tt.args.value); got != tt.want { 926 t.Errorf("minQuotes() = %v, want %v", got, tt.want) 927 } 928 }) 929 } 930 }