Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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