github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/pi/hooks_test.go (about) 1 // Copyright (c) 2020-2021 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package pi 6 7 import ( 8 "bytes" 9 "encoding/base64" 10 "encoding/hex" 11 "encoding/json" 12 "errors" 13 "image" 14 "image/png" 15 "net/http" 16 "os" 17 "strings" 18 "testing" 19 "time" 20 21 backend "github.com/decred/politeia/politeiad/backendv2" 22 "github.com/decred/politeia/politeiad/backendv2/tstorebe/plugins" 23 "github.com/decred/politeia/politeiad/plugins/comments" 24 "github.com/decred/politeia/politeiad/plugins/pi" 25 "github.com/decred/politeia/util" 26 ) 27 28 func TestIsInCommentTree(t *testing.T) { 29 // Setup test data 30 oneNodeTree := []comments.Comment{ 31 { 32 CommentID: 1, 33 ParentID: 0, 34 }, 35 } 36 37 twoLeafsTree := []comments.Comment{ 38 { 39 CommentID: 1, 40 ParentID: 0, 41 }, 42 { 43 CommentID: 2, 44 ParentID: 0, 45 }, 46 } 47 48 threeLevelsTree := []comments.Comment{ 49 { 50 CommentID: 1, 51 ParentID: 0, 52 }, 53 { 54 CommentID: 2, 55 ParentID: 1, 56 }, 57 { 58 CommentID: 3, 59 ParentID: 2, 60 }, 61 { 62 CommentID: 4, 63 ParentID: 0, 64 }, 65 } 66 67 sixLevelsTree := []comments.Comment{ 68 { 69 CommentID: 1, 70 ParentID: 0, 71 }, 72 { 73 CommentID: 2, 74 ParentID: 1, 75 }, 76 { 77 CommentID: 3, 78 ParentID: 1, 79 }, 80 { 81 CommentID: 4, 82 ParentID: 2, 83 }, 84 { 85 CommentID: 5, 86 ParentID: 2, 87 }, 88 { 89 CommentID: 6, 90 ParentID: 3, 91 }, 92 { 93 CommentID: 7, 94 ParentID: 5, 95 }, 96 { 97 CommentID: 8, 98 ParentID: 5, 99 }, 100 { 101 CommentID: 9, 102 ParentID: 8, 103 }, 104 { 105 CommentID: 10, 106 ParentID: 9, 107 }, 108 } 109 110 // Setup tests 111 var tests = []struct { 112 name string // Test name 113 rootID, 114 childID uint32 115 comments []comments.Comment 116 res bool // Expected result 117 }{ 118 { 119 name: "one node tree true case", 120 rootID: 1, 121 childID: 1, 122 comments: oneNodeTree, 123 res: true, 124 }, 125 { 126 name: "one node tree false case", 127 rootID: 0, 128 childID: 1, 129 comments: oneNodeTree, 130 res: false, 131 }, 132 { 133 name: "two leafs tree false case", 134 rootID: 1, 135 childID: 2, 136 comments: twoLeafsTree, 137 res: false, 138 }, 139 { 140 name: "three levels tree true case", 141 rootID: 1, 142 childID: 3, 143 comments: threeLevelsTree, 144 res: true, 145 }, 146 { 147 name: "three levels tree false case", 148 rootID: 1, 149 childID: 4, 150 comments: threeLevelsTree, 151 res: false, 152 }, 153 { 154 name: "six levels tree true case", 155 rootID: 1, 156 childID: 10, 157 comments: sixLevelsTree, 158 res: true, 159 }, 160 { 161 name: "six levels tree false case", 162 rootID: 6, 163 childID: 10, 164 comments: sixLevelsTree, 165 res: false, 166 }, 167 } 168 169 // Run tests 170 for _, tc := range tests { 171 t.Run(tc.name, func(t *testing.T) { 172 res := isInCommentTree(tc.rootID, tc.childID, tc.comments) 173 if res != tc.res { 174 // Unexpected result 175 t.Errorf("unexpected result; wanted '%v', got '%v'", tc.res, res) 176 return 177 } 178 }) 179 } 180 } 181 182 func TestHookNewRecordPre(t *testing.T) { 183 // Setup pi plugin 184 p, cleanup := newTestPiPlugin(t) 185 defer cleanup() 186 187 // Run tests 188 runProposalFormatTests(t, p.hookNewRecordPre) 189 } 190 191 func TestHookEditRecordPre(t *testing.T) { 192 // Setup pi plugin 193 p, cleanup := newTestPiPlugin(t) 194 defer cleanup() 195 196 // Run tests 197 runProposalFormatTests(t, p.hookEditRecordPre) 198 } 199 200 // runProposalFormatTests runs the proposal format tests using the provided 201 // hook function as the test function. This allows us to run the same set of 202 // formatting tests of multiple hooks without needing to duplicate the setup 203 // and error handling code. 204 func runProposalFormatTests(t *testing.T, hookFn func(string) error) { 205 for _, v := range proposalFormatTests(t) { 206 t.Run(v.name, func(t *testing.T) { 207 // Decode the expected error into a PluginError. If 208 // an error is being returned it should always be a 209 // PluginError. 210 var wantErrorCode pi.ErrorCodeT 211 if v.err != nil { 212 var pe backend.PluginError 213 if !errors.As(v.err, &pe) { 214 t.Fatalf("error is not a plugin error '%v'", v.err) 215 } 216 wantErrorCode = pi.ErrorCodeT(pe.ErrorCode) 217 } 218 219 // Setup payload 220 hnrp := plugins.HookNewRecordPre{ 221 Files: v.files, 222 } 223 b, err := json.Marshal(hnrp) 224 if err != nil { 225 t.Fatal(err) 226 } 227 payload := string(b) 228 229 // Run test 230 err = hookFn(payload) 231 switch { 232 case v.err != nil && err == nil: 233 // Wanted an error but didn't get one 234 t.Errorf("want error '%v', got nil", 235 pi.ErrorCodes[wantErrorCode]) 236 return 237 238 case v.err == nil && err != nil: 239 // Wanted success but got an error 240 t.Errorf("want error nil, got '%v'", err) 241 return 242 243 case v.err != nil && err != nil: 244 // Wanted an error and got an error. Verify that it's 245 // the correct error. All errors should be backend 246 // plugin errors. 247 var gotErr backend.PluginError 248 if !errors.As(err, &gotErr) { 249 t.Errorf("want plugin error, got '%v'", err) 250 return 251 } 252 if pi.PluginID != gotErr.PluginID { 253 t.Errorf("want plugin error with plugin ID '%v', got '%v'", 254 pi.PluginID, gotErr.PluginID) 255 return 256 } 257 258 gotErrorCode := pi.ErrorCodeT(gotErr.ErrorCode) 259 if wantErrorCode != gotErrorCode { 260 t.Errorf("want error '%v', got '%v'", 261 pi.ErrorCodes[wantErrorCode], 262 pi.ErrorCodes[gotErrorCode]) 263 } 264 265 // Success; continue to next test 266 return 267 268 case v.err == nil && err == nil: 269 // Success; continue to next test 270 return 271 } 272 }) 273 } 274 } 275 276 // proposalFormatTest contains the input and output for a test that verifies 277 // the proposal format meets the pi plugin requirements. 278 type proposalFormatTest struct { 279 name string // Test name 280 files []backend.File // Input 281 err error // Expected output 282 } 283 284 // proposalFormatTests returns a list of tests that verify the files of a 285 // proposal meet all formatting criteria that the pi plugin requires. 286 func proposalFormatTests(t *testing.T) []proposalFormatTest { 287 t.Helper() 288 289 // Setup test files 290 var ( 291 index = fileProposalIndex() 292 293 indexTooLarge backend.File 294 png backend.File 295 pngTooLarge backend.File 296 ) 297 298 // Create a index file that is too large 299 var sb strings.Builder 300 for i := 0; i <= int(pi.SettingTextFileSizeMax); i++ { 301 sb.WriteString("a") 302 } 303 indexTooLarge = file(index.Name, []byte(sb.String())) 304 305 // Load test fixtures 306 b, err := os.ReadFile("testdata/valid.png") 307 if err != nil { 308 t.Fatal(err) 309 } 310 png = file("valid.png", b) 311 312 b, err = os.ReadFile("testdata/too-large.png") 313 if err != nil { 314 t.Fatal(err) 315 } 316 pngTooLarge = file("too-large.png", b) 317 318 // Setup tests 319 tests := []proposalFormatTest{ 320 { 321 "text file name invalid", 322 []backend.File{ 323 { 324 Name: "notallowed.txt", 325 MIME: index.MIME, 326 Digest: index.Digest, 327 Payload: index.Payload, 328 }, 329 fileProposalMetadata(t, nil), 330 }, 331 backend.PluginError{ 332 PluginID: pi.PluginID, 333 ErrorCode: uint32(pi.ErrorCodeTextFileNameInvalid), 334 }, 335 }, 336 { 337 "text file too large", 338 []backend.File{ 339 indexTooLarge, 340 fileProposalMetadata(t, nil), 341 }, 342 backend.PluginError{ 343 PluginID: pi.PluginID, 344 ErrorCode: uint32(pi.ErrorCodeTextFileSizeInvalid), 345 }, 346 }, 347 { 348 "image file too large", 349 []backend.File{ 350 fileProposalIndex(), 351 fileProposalMetadata(t, nil), 352 pngTooLarge, 353 }, 354 backend.PluginError{ 355 PluginID: pi.PluginID, 356 ErrorCode: uint32(pi.ErrorCodeImageFileSizeInvalid), 357 }, 358 }, 359 { 360 "index file missing", 361 []backend.File{ 362 fileProposalMetadata(t, nil), 363 }, 364 backend.PluginError{ 365 PluginID: pi.PluginID, 366 ErrorCode: uint32(pi.ErrorCodeTextFileMissing), 367 }, 368 }, 369 { 370 "too many images", 371 []backend.File{ 372 fileProposalIndex(), 373 fileProposalMetadata(t, nil), 374 fileEmptyPNG(t), fileEmptyPNG(t), fileEmptyPNG(t), 375 fileEmptyPNG(t), fileEmptyPNG(t), fileEmptyPNG(t), 376 }, 377 backend.PluginError{ 378 PluginID: pi.PluginID, 379 ErrorCode: uint32(pi.ErrorCodeImageFileCountInvalid), 380 }, 381 }, 382 { 383 "proposal metadata missing", 384 []backend.File{ 385 fileProposalIndex(), 386 }, 387 backend.PluginError{ 388 PluginID: pi.PluginID, 389 ErrorCode: uint32(pi.ErrorCodeTextFileMissing), 390 }, 391 }, 392 { 393 "success no attachments", 394 []backend.File{ 395 fileProposalIndex(), 396 fileProposalMetadata(t, nil), 397 }, 398 nil, 399 }, 400 { 401 "success with attachments", 402 []backend.File{ 403 fileProposalIndex(), 404 fileProposalMetadata(t, nil), 405 png, 406 }, 407 nil, 408 }, 409 } 410 411 tests = append(tests, proposalNameTests(t)...) 412 tests = append(tests, proposalAmountTests(t)...) 413 tests = append(tests, proposalStartDateTests(t)...) 414 tests = append(tests, proposalEndDateTests(t)...) 415 tests = append(tests, proposalDomainTests(t)...) 416 return tests 417 } 418 419 // proposalNameTests returns a list of tests that verify the proposal name 420 // requirements. 421 func proposalNameTests(t *testing.T) []proposalFormatTest { 422 t.Helper() 423 424 // Create names to test min and max lengths 425 var ( 426 nameTooShort string 427 nameTooLong string 428 nameMinLength string 429 nameMaxLength string 430 431 b strings.Builder 432 ) 433 for i := 0; i < int(pi.SettingTitleLengthMin)-1; i++ { 434 b.WriteString("a") 435 } 436 nameTooShort = b.String() 437 b.Reset() 438 439 for i := 0; i < int(pi.SettingTitleLengthMax)+1; i++ { 440 b.WriteString("a") 441 } 442 nameTooLong = b.String() 443 b.Reset() 444 445 for i := 0; i < int(pi.SettingTitleLengthMin); i++ { 446 b.WriteString("a") 447 } 448 nameMinLength = b.String() 449 b.Reset() 450 451 for i := 0; i < int(pi.SettingTitleLengthMax); i++ { 452 b.WriteString("a") 453 } 454 nameMaxLength = b.String() 455 456 // Setup files with an empty proposal name. This is done manually 457 // because the function that creates the proposal metadata uses 458 // a default value when the name is provided as an empty string. 459 filesEmptyName := filesForProposal(t, &pi.ProposalMetadata{ 460 Name: "", 461 }) 462 for k, v := range filesEmptyName { 463 if v.Name == pi.FileNameProposalMetadata { 464 b, err := base64.StdEncoding.DecodeString(v.Payload) 465 if err != nil { 466 t.Fatal(err) 467 } 468 var pm pi.ProposalMetadata 469 err = json.Unmarshal(b, &pm) 470 if err != nil { 471 t.Fatal(err) 472 } 473 pm.Name = "" 474 b, err = json.Marshal(pm) 475 if err != nil { 476 t.Fatal(err) 477 } 478 v.Payload = base64.StdEncoding.EncodeToString(b) 479 filesEmptyName[k] = v 480 } 481 } 482 483 // errNameInvalid is returned when proposal name validation 484 // fails. 485 errNameInvalid := backend.PluginError{ 486 PluginID: pi.PluginID, 487 ErrorCode: uint32(pi.ErrorCodeTitleInvalid), 488 } 489 490 return []proposalFormatTest{ 491 { 492 "name is empty", 493 filesEmptyName, 494 errNameInvalid, 495 }, 496 { 497 "name is too short", 498 filesForProposal(t, &pi.ProposalMetadata{ 499 Name: nameTooShort, 500 }), 501 errNameInvalid, 502 }, 503 { 504 "name is too long", 505 filesForProposal(t, &pi.ProposalMetadata{ 506 Name: nameTooLong, 507 }), 508 errNameInvalid, 509 }, 510 { 511 "name is the min length", 512 filesForProposal(t, &pi.ProposalMetadata{ 513 Name: nameMinLength, 514 }), 515 nil, 516 }, 517 { 518 "name is the max length", 519 filesForProposal(t, &pi.ProposalMetadata{ 520 Name: nameMaxLength, 521 }), 522 nil, 523 }, 524 { 525 "name contains A to Z", 526 filesForProposal(t, &pi.ProposalMetadata{ 527 Name: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 528 }), 529 nil, 530 }, 531 { 532 "name contains a to z", 533 filesForProposal(t, &pi.ProposalMetadata{ 534 Name: "abcdefghijklmnopqrstuvwxyz", 535 }), 536 nil, 537 }, 538 { 539 "name contains 0 to 9", 540 filesForProposal(t, &pi.ProposalMetadata{ 541 Name: "0123456789", 542 }), 543 nil, 544 }, 545 { 546 "name contains supported chars", 547 filesForProposal(t, &pi.ProposalMetadata{ 548 Name: "&.,:;- @+#/()!?\"'", 549 }), 550 nil, 551 }, 552 { 553 "name contains newline", 554 filesForProposal(t, &pi.ProposalMetadata{ 555 Name: "proposal name\n", 556 }), 557 errNameInvalid, 558 }, 559 { 560 "name contains tab", 561 filesForProposal(t, &pi.ProposalMetadata{ 562 Name: "proposal name\t", 563 }), 564 errNameInvalid, 565 }, 566 { 567 "name contains brackets", 568 filesForProposal(t, &pi.ProposalMetadata{ 569 Name: "{proposal name}", 570 }), 571 errNameInvalid, 572 }, 573 { 574 "name is valid lowercase", 575 filesForProposal(t, &pi.ProposalMetadata{ 576 Name: "proposal name", 577 }), 578 nil, 579 }, 580 { 581 "name is valid mixed case", 582 filesForProposal(t, &pi.ProposalMetadata{ 583 Name: "Proposal Name", 584 }), 585 nil, 586 }, 587 } 588 } 589 590 // proposalAmountTests returns a list of tests that verify the proposal 591 // amount requirements. 592 func proposalAmountTests(t *testing.T) []proposalFormatTest { 593 t.Helper() 594 595 // amount values to test min & max amount limits 596 var ( 597 amountMin = pi.SettingProposalAmountMin 598 amountMax = pi.SettingProposalAmountMax 599 amountTooSmall = amountMin - 1 600 amountTooBig = amountMax + 1 601 ) 602 603 // Setup files with a zero amount. This is done manually 604 // because the function that creates the proposal metadata uses 605 // a default value when the amount is provided as zero. 606 filesZeroAmount := filesForProposal(t, &pi.ProposalMetadata{ 607 Amount: 0, 608 }) 609 for k, v := range filesZeroAmount { 610 if v.Name == pi.FileNameProposalMetadata { 611 b, err := base64.StdEncoding.DecodeString(v.Payload) 612 if err != nil { 613 t.Fatal(err) 614 } 615 var pm pi.ProposalMetadata 616 err = json.Unmarshal(b, &pm) 617 if err != nil { 618 t.Fatal(err) 619 } 620 pm.Amount = 0 621 b, err = json.Marshal(pm) 622 if err != nil { 623 t.Fatal(err) 624 } 625 v.Payload = base64.StdEncoding.EncodeToString(b) 626 filesZeroAmount[k] = v 627 } 628 } 629 630 // errAmountInvalid is returned when proposal amount 631 // validation fails. 632 errAmountInvalid := backend.PluginError{ 633 PluginID: pi.PluginID, 634 ErrorCode: uint32(pi.ErrorCodeProposalAmountInvalid), 635 } 636 637 return []proposalFormatTest{ 638 { 639 "amount is zero", 640 filesZeroAmount, 641 errAmountInvalid, 642 }, 643 { 644 "amount too small", 645 filesForProposal(t, &pi.ProposalMetadata{ 646 Amount: amountTooSmall, 647 }), 648 errAmountInvalid, 649 }, 650 { 651 "amount too big", 652 filesForProposal(t, &pi.ProposalMetadata{ 653 Amount: amountTooBig, 654 }), 655 errAmountInvalid, 656 }, 657 { 658 "min amount", 659 filesForProposal(t, &pi.ProposalMetadata{ 660 Amount: amountMin, 661 }), 662 nil, 663 }, 664 { 665 "max amount", 666 filesForProposal(t, &pi.ProposalMetadata{ 667 Amount: amountMax, 668 }), 669 nil, 670 }, 671 } 672 } 673 674 // proposalStartDateTests returns a list of tests that verify the proposal 675 // start date requirements. 676 func proposalStartDateTests(t *testing.T) []proposalFormatTest { 677 t.Helper() 678 679 // Start date values to test min start date 680 var ( 681 sDateInPast = time.Now().Unix() - 172800 // two days ago 682 sDateInTwoMonths = time.Now().Unix() + 5256000 // in 2 months 683 ) 684 685 // Setup files with a zero start date. This is done manually 686 // because the function that creates the proposal metadata uses 687 // a default value when the start date is provided as zero. 688 filesZeroStartDate := filesForProposal(t, &pi.ProposalMetadata{ 689 StartDate: 0, 690 }) 691 for k, v := range filesZeroStartDate { 692 if v.Name == pi.FileNameProposalMetadata { 693 b, err := base64.StdEncoding.DecodeString(v.Payload) 694 if err != nil { 695 t.Fatal(err) 696 } 697 var pm pi.ProposalMetadata 698 err = json.Unmarshal(b, &pm) 699 if err != nil { 700 t.Fatal(err) 701 } 702 pm.StartDate = 0 703 b, err = json.Marshal(pm) 704 if err != nil { 705 t.Fatal(err) 706 } 707 v.Payload = base64.StdEncoding.EncodeToString(b) 708 filesZeroStartDate[k] = v 709 } 710 } 711 712 // errStartDateInvalid is returned when proposal start date 713 // validation fails. 714 errStartDateInvalid := backend.PluginError{ 715 PluginID: pi.PluginID, 716 ErrorCode: uint32(pi.ErrorCodeProposalStartDateInvalid), 717 } 718 719 return []proposalFormatTest{ 720 { 721 "start date in the past", 722 filesForProposal(t, &pi.ProposalMetadata{ 723 StartDate: sDateInPast, 724 }), 725 errStartDateInvalid, 726 }, 727 { 728 "start date is zero", 729 filesZeroStartDate, 730 errStartDateInvalid, 731 }, 732 { 733 "start date in two months", 734 filesForProposal(t, &pi.ProposalMetadata{ 735 StartDate: sDateInTwoMonths, 736 }), 737 nil, 738 }, 739 } 740 } 741 742 // proposalEndDateTests returns a list of tests that verify the proposal 743 // end date requirements. 744 func proposalEndDateTests(t *testing.T) []proposalFormatTest { 745 t.Helper() 746 747 // End date values to test end date validations. 748 var ( 749 now = time.Now().Unix() 750 eDateInPast = now - 172800 // two days ago 751 eDateBeforeStartDate = now + 172800 // in two days 752 eDateAfterMax = now + 753 pi.SettingProposalEndDateMax + 60 // 1 minute after max 754 eDateInEightMonths = now + 21040000 // in 8 months 755 ) 756 757 // Setup files with a zero end date. This is done manually 758 // because the function that creates the proposal metadata uses 759 // a default value when the end date is provided as zero. 760 filesZeroEndDate := filesForProposal(t, &pi.ProposalMetadata{ 761 EndDate: 0, 762 }) 763 for k, v := range filesZeroEndDate { 764 if v.Name == pi.FileNameProposalMetadata { 765 b, err := base64.StdEncoding.DecodeString(v.Payload) 766 if err != nil { 767 t.Fatal(err) 768 } 769 var pm pi.ProposalMetadata 770 err = json.Unmarshal(b, &pm) 771 if err != nil { 772 t.Fatal(err) 773 } 774 pm.EndDate = 0 775 b, err = json.Marshal(pm) 776 if err != nil { 777 t.Fatal(err) 778 } 779 v.Payload = base64.StdEncoding.EncodeToString(b) 780 filesZeroEndDate[k] = v 781 } 782 } 783 784 // errEndDateInvalid is returned when proposal end date 785 // validation fails. 786 errEndDateInvalid := backend.PluginError{ 787 PluginID: pi.PluginID, 788 ErrorCode: uint32(pi.ErrorCodeProposalEndDateInvalid), 789 } 790 791 return []proposalFormatTest{ 792 { 793 "end date in the past", 794 filesForProposal(t, &pi.ProposalMetadata{ 795 EndDate: eDateInPast, 796 }), 797 errEndDateInvalid, 798 }, 799 { 800 "start date is zero", 801 filesZeroEndDate, 802 errEndDateInvalid, 803 }, 804 { 805 "end date is before default start date", 806 filesForProposal(t, &pi.ProposalMetadata{ 807 EndDate: eDateBeforeStartDate, 808 }), 809 errEndDateInvalid, 810 }, 811 { 812 "end date is after max", 813 filesForProposal(t, &pi.ProposalMetadata{ 814 EndDate: eDateAfterMax, 815 }), 816 errEndDateInvalid, 817 }, 818 { 819 "end date is in 8 months", 820 filesForProposal(t, &pi.ProposalMetadata{ 821 EndDate: eDateInEightMonths, 822 }), 823 nil, 824 }, 825 } 826 } 827 828 // proposalDomainTests returns a list of tests that verify the proposal 829 // domain requirements. 830 func proposalDomainTests(t *testing.T) []proposalFormatTest { 831 t.Helper() 832 833 // Domain values to test domain validations. 834 var ( 835 validDomain = pi.SettingProposalDomains[0] 836 invalidDomain = "invalid-domain" 837 ) 838 839 // Setup files with an empty domain. This is done manually 840 // because the function that creates the proposal metadata uses 841 // a default value when the domain is provided as empty string. 842 filesEmptyDomain := filesForProposal(t, &pi.ProposalMetadata{ 843 Domain: "", 844 }) 845 for k, v := range filesEmptyDomain { 846 if v.Name == pi.FileNameProposalMetadata { 847 b, err := base64.StdEncoding.DecodeString(v.Payload) 848 if err != nil { 849 t.Fatal(err) 850 } 851 var pm pi.ProposalMetadata 852 err = json.Unmarshal(b, &pm) 853 if err != nil { 854 t.Fatal(err) 855 } 856 pm.Domain = "" 857 b, err = json.Marshal(pm) 858 if err != nil { 859 t.Fatal(err) 860 } 861 v.Payload = base64.StdEncoding.EncodeToString(b) 862 filesEmptyDomain[k] = v 863 } 864 } 865 866 // errDomainInvalid is returned when proposal domain 867 // validation fails. 868 errDomainInvalid := backend.PluginError{ 869 PluginID: pi.PluginID, 870 ErrorCode: uint32(pi.ErrorCodeProposalDomainInvalid), 871 } 872 873 return []proposalFormatTest{ 874 { 875 "invalid domain", 876 filesForProposal(t, &pi.ProposalMetadata{ 877 Domain: invalidDomain, 878 }), 879 errDomainInvalid, 880 }, 881 { 882 "empty domain", 883 filesEmptyDomain, 884 errDomainInvalid, 885 }, 886 { 887 "valid domain", 888 filesForProposal(t, &pi.ProposalMetadata{ 889 Domain: validDomain, 890 }), 891 nil, 892 }, 893 } 894 } 895 896 // file returns a backend file for the provided data. 897 func file(name string, payload []byte) backend.File { 898 return backend.File{ 899 Name: name, 900 MIME: http.DetectContentType(payload), 901 Digest: hex.EncodeToString(util.Digest(payload)), 902 Payload: base64.StdEncoding.EncodeToString(payload), 903 } 904 } 905 906 // fileProposalIndex returns a backend file that contains a proposal index 907 // file. 908 func fileProposalIndex() backend.File { 909 text := "Hello, world. This is my proposal. Pay me." 910 return file(pi.FileNameIndexFile, []byte(text)) 911 } 912 913 // fileProposalMetadata returns a backend file that contains a proposal 914 // metadata file. The proposal metadata can optionally be provided as an 915 // argument. Any required proposal metadata fields that are not provided by 916 // the caller will be filled in using valid defaults. 917 func fileProposalMetadata(t *testing.T, pm *pi.ProposalMetadata) backend.File { 918 t.Helper() 919 920 // Setup a default proposal metadata 921 pmd := &pi.ProposalMetadata{ 922 Name: "Test Proposal Name", 923 Amount: 2000000, // $20k in cents 924 StartDate: time.Now().Unix() + 2630000, // 1 month from now 925 EndDate: time.Now().Unix() + 10368000, // 4 months from now 926 Domain: "development", 927 } 928 929 // Sanity check. Verify that the default domain we used is 930 // one of the default domains defined by the pi plugin API. 931 var found bool 932 for _, v := range pi.SettingProposalDomains { 933 if v == pmd.Domain { 934 found = true 935 break 936 } 937 } 938 if !found { 939 t.Fatalf("%v is not a default domain", pmd.Domain) 940 } 941 942 // Overwrite the default values with the caller provided 943 // values if they exist. 944 if pm == nil { 945 pm = &pi.ProposalMetadata{} 946 } 947 if pm.Name != "" { 948 pmd.Name = pm.Name 949 } 950 if pm.Amount != 0 { 951 pmd.Amount = pm.Amount 952 } 953 if pm.StartDate != 0 { 954 pmd.StartDate = pm.StartDate 955 } 956 if pm.EndDate != 0 { 957 pmd.EndDate = pm.EndDate 958 } 959 if pm.Domain != "" { 960 pmd.Domain = pm.Domain 961 } 962 963 // Setup and return the backend file 964 b, err := json.Marshal(&pmd) 965 if err != nil { 966 t.Fatal(err) 967 } 968 969 return file(pi.FileNameProposalMetadata, b) 970 } 971 972 // fileEmptyPNG returns a backend File that contains an empty PNG image. The 973 // file name is randomly generated. 974 func fileEmptyPNG(t *testing.T) backend.File { 975 t.Helper() 976 977 var ( 978 b = new(bytes.Buffer) 979 img = image.NewRGBA(image.Rect(0, 0, 1000, 500)) 980 ) 981 err := png.Encode(b, img) 982 if err != nil { 983 t.Fatal(err) 984 } 985 r, err := util.Random(8) 986 if err != nil { 987 t.Fatal(err) 988 } 989 name := hex.EncodeToString(r) + ".png" 990 991 return file(name, b.Bytes()) 992 } 993 994 // filesForProposal returns the backend files for a valid proposal. The 995 // returned files only include the files required by the pi plugin API. No 996 // attachment files are included. The caller can pass in additional files that 997 // will be included in the returned list. 998 func filesForProposal(t *testing.T, pm *pi.ProposalMetadata, files ...backend.File) []backend.File { 999 t.Helper() 1000 1001 fs := []backend.File{ 1002 fileProposalIndex(), 1003 fileProposalMetadata(t, pm), 1004 } 1005 fs = append(fs, files...) 1006 1007 return fs 1008 }