github.com/decred/politeia@v1.4.0/politeiawww/cmd/pictl/cmdtestrun.go (about)

     1  // Copyright (c) 2017-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 main
     6  
     7  // cmdTestRun performs a test run of all the politeiawww routes.
     8  type cmdTestRun struct {
     9  	Args struct {
    10  		AdminEmail    string `positional-arg-name:"adminemail"`
    11  		AdminPassword string `positional-arg-name:"adminpassword"`
    12  	} `positional-args:"true" required:"true"`
    13  }
    14  
    15  /*
    16  var (
    17  	minPasswordLength int
    18  	publicKey         string
    19  )
    20  
    21  // testUser stores user details that are used throughout the test run.
    22  type testUser struct {
    23  	ID             string // UUID
    24  	Email          string // Email
    25  	Username       string // Username
    26  	Password       string // Password (not hashed)
    27  	PublicKey      string // Public key of active identity
    28  	PaywallAddress string // Paywall address
    29  	PaywallAmount  uint64 // Paywall amount
    30  }
    31  
    32  // login logs in the specified user.
    33  func login(u testUser) error {
    34  	lc := shared.LoginCmd{}
    35  	lc.Args.Email = u.Email
    36  	lc.Args.Password = u.Password
    37  	return lc.Execute(nil)
    38  }
    39  
    40  // logout logs out current logged in user.
    41  func logout() error {
    42  	// Logout admin
    43  	lc := shared.LogoutCmd{}
    44  	err := lc.Execute(nil)
    45  	if err != nil {
    46  		return err
    47  	}
    48  	return nil
    49  }
    50  
    51  // userRegistrationPayment ensures current logged in user has paid registration
    52  // fee
    53  func userRegistrationPayment() (www.UserRegistrationPaymentReply, error) {
    54  	urvr, err := client.UserRegistrationPayment()
    55  	if err != nil {
    56  		return www.UserRegistrationPaymentReply{}, err
    57  	}
    58  	return *urvr, nil
    59  }
    60  
    61  // randomString generates a random string
    62  func randomString(length int) (string, error) {
    63  	b, err := util.Random(length)
    64  	if err != nil {
    65  		return "", err
    66  	}
    67  	return hex.EncodeToString(b), nil
    68  }
    69  
    70  // userNew creates a new user and returnes user's public key.
    71  func userNew(email, password, username string) (*identity.FullIdentity, string, error) {
    72  	fmt.Printf("  Creating user: %v\n", email)
    73  
    74  	// Create user identity and save it to disk
    75  	id, err := shared.NewIdentity()
    76  	if err != nil {
    77  		return nil, "", err
    78  	}
    79  
    80  	// Setup new user request
    81  	nu := &www.NewUser{
    82  		Email:     email,
    83  		Username:  username,
    84  		Password:  shared.DigestSHA3(password),
    85  		PublicKey: hex.EncodeToString(id.Public.Key[:]),
    86  	}
    87  	nur, err := client.NewUser(nu)
    88  	if err != nil {
    89  		return nil, "", err
    90  	}
    91  
    92  	return id, nur.VerificationToken, nil
    93  }
    94  
    95  // userManage sends a usermanage command
    96  func userManage(userID, action, reason string) error {
    97  	muc := shared.UserManageCmd{}
    98  	muc.Args.UserID = userID
    99  	muc.Args.Action = action
   100  	muc.Args.Reason = reason
   101  	err := muc.Execute(nil)
   102  	if err != nil {
   103  		return err
   104  	}
   105  	return nil
   106  }
   107  
   108  // userEmailVerify verifies user's email
   109  func userEmailVerify(vt, email string, id *identity.FullIdentity) error {
   110  	fmt.Printf("  Verify user's email\n")
   111  	sig := id.SignMessage([]byte(vt))
   112  	_, err := client.VerifyNewUser(
   113  		&www.VerifyNewUser{
   114  			Email:             email,
   115  			VerificationToken: vt,
   116  			Signature:         hex.EncodeToString(sig[:]),
   117  		})
   118  	if err != nil {
   119  		return err
   120  	}
   121  	return nil
   122  }
   123  
   124  // userCreate creates new user & returns the created testUser
   125  func userCreate() (*testUser, *identity.FullIdentity, string, error) {
   126  	// Create user and verify email
   127  	randomStr, err := randomString(minPasswordLength)
   128  	if err != nil {
   129  		return nil, nil, "", err
   130  	}
   131  	email := randomStr + "@example.com"
   132  	username := randomStr
   133  	password := randomStr
   134  	id, vt, err := userNew(email, password, username)
   135  	if err != nil {
   136  		return nil, nil, "", err
   137  	}
   138  
   139  	return &testUser{
   140  		Email:    email,
   141  		Username: username,
   142  		Password: password,
   143  	}, id, vt, nil
   144  }
   145  
   146  // userDetals accepts a pointer to a testUser calls client's login command
   147  // and stores additional information on given testUser struct
   148  func userDetails(u *testUser) error {
   149  	// Login and store user details
   150  	fmt.Printf("  Login user\n")
   151  	lr, err := client.Login(&www.Login{
   152  		Email:    u.Email,
   153  		Password: shared.DigestSHA3(u.Password),
   154  	})
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	u.PublicKey = lr.PublicKey
   160  	u.PaywallAddress = lr.PaywallAddress
   161  	u.ID = lr.UserID
   162  	u.PaywallAmount = lr.PaywallAmount
   163  
   164  	return nil
   165  }
   166  
   167  // testUser tests pictl user specific routes.
   168  func testUserRoutes(admin testUser) error {
   169  	// sleepInterval is the time to wait in between requests
   170  	// when polling politeiawww for paywall tx confirmations.
   171  	const sleepInterval = 15 * time.Second
   172  
   173  	var (
   174  		// paywallEnabled represents whether the politeiawww paywall
   175  		// has been enabled.  A disabled paywall will have a paywall
   176  		// address of "" and a paywall amount of 0.
   177  		paywallEnabled bool
   178  
   179  		// numCredits is the number of proposal credits that will be
   180  		// purchased using the testnet faucet.
   181  		numCredits = 1
   182  
   183  		// Test user
   184  		user *testUser
   185  	)
   186  	// Run user routes.
   187  	fmt.Printf("Running user routes\n")
   188  
   189  	// Create new user
   190  	user, id, _, err := userCreate()
   191  	if err != nil {
   192  		return err
   193  	}
   194  
   195  	// Resed email verification
   196  	fmt.Printf("  Resend email Verification\n")
   197  	rvr, err := client.ResendVerification(www.ResendVerification{
   198  		PublicKey: hex.EncodeToString(id.Public.Key[:]),
   199  		Email:     user.Email,
   200  	})
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	// Verify email
   206  	err = userEmailVerify(rvr.VerificationToken, user.Email, id)
   207  	if err != nil {
   208  		return err
   209  	}
   210  
   211  	// Populate user's details
   212  	err = userDetails(user)
   213  	if err != nil {
   214  		return err
   215  	}
   216  
   217  	// Logout user
   218  	fmt.Printf("  Logout user\n")
   219  	err = logout()
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	// Update user key
   225  	err = userKeyUpdate(*user)
   226  	if err != nil {
   227  		return err
   228  	}
   229  
   230  	// Log back in
   231  	err = login(*user)
   232  	if err != nil {
   233  		return err
   234  	}
   235  
   236  	// Me
   237  	fmt.Printf("  Me\n")
   238  	_, err = client.Me()
   239  	if err != nil {
   240  		return err
   241  	}
   242  
   243  	// Edit user
   244  	fmt.Printf("  Edit user\n")
   245  	var n uint64 = 1 << 0
   246  	_, err = client.EditUser(
   247  		&www.EditUser{
   248  			EmailNotifications: &n,
   249  		})
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	// Change username
   255  	fmt.Printf("  Change username\n")
   256  	randomStr, err := randomString(minPasswordLength)
   257  	if err != nil {
   258  		return err
   259  	}
   260  	cuc := shared.UserUsernameChangeCmd{}
   261  	cuc.Args.Password = user.Password
   262  	cuc.Args.NewUsername = randomStr
   263  	err = cuc.Execute(nil)
   264  	if err != nil {
   265  		return err
   266  	}
   267  	user.Username = cuc.Args.NewUsername
   268  
   269  	// Change password
   270  	fmt.Printf("  Change password\n")
   271  	cpc := shared.UserPasswordChangeCmd{}
   272  	cpc.Args.Password = user.Password
   273  	cpc.Args.NewPassword = randomStr
   274  	err = cpc.Execute(nil)
   275  	if err != nil {
   276  		return err
   277  	}
   278  	user.Password = cpc.Args.NewPassword
   279  
   280  	// Reset user password
   281  	fmt.Printf("  Reset user password\n")
   282  	// Generate new random password
   283  	randomStr, err = randomString(minPasswordLength)
   284  	if err != nil {
   285  		return err
   286  	}
   287  	uprc := shared.UserPasswordResetCmd{}
   288  	uprc.Args.Email = user.Email
   289  	uprc.Args.Username = user.Username
   290  	uprc.Args.NewPassword = randomStr
   291  	err = uprc.Execute(nil)
   292  	if err != nil {
   293  		return err
   294  	}
   295  	user.Password = randomStr
   296  
   297  	// Login with new password
   298  	err = login(*user)
   299  	if err != nil {
   300  		return err
   301  	}
   302  
   303  	// Check if paywall is enabled.  Paywall address and paywall
   304  	// amount will be zero values if paywall has been disabled.
   305  	if user.PaywallAddress != "" && user.PaywallAmount != 0 {
   306  		paywallEnabled = true
   307  	} else {
   308  		fmt.Printf("WARNING: politeiawww paywall is disabled\n")
   309  	}
   310  
   311  	// Pay user registration fee
   312  	if paywallEnabled {
   313  		// Pay user registration fee
   314  		fmt.Printf("  Paying user registration fee\n")
   315  		txID, err := util.PayWithTestnetFaucet(context.Background(),
   316  			cfg.FaucetHost, user.PaywallAddress, user.PaywallAmount, "")
   317  		if err != nil {
   318  			return err
   319  		}
   320  
   321  		dcr := float64(user.PaywallAmount) / 1e8
   322  		fmt.Printf("  Paid %v DCR to %v with txID %v\n",
   323  			dcr, user.PaywallAddress, txID)
   324  	}
   325  
   326  	// Wait for user registration payment confirmations
   327  	// If the paywall has been disable this will be marked
   328  	// as true. If the paywall has been enabled this will
   329  	// be true once the payment tx has the required number
   330  	// of confirmations.
   331  	upvr, err := userRegistrationPayment()
   332  	if err != nil {
   333  		return err
   334  	}
   335  	for !upvr.HasPaid {
   336  		upvr, err = userRegistrationPayment()
   337  		if err != nil {
   338  			return err
   339  		}
   340  
   341  		fmt.Printf("  Verify user payment: waiting for tx confirmations...\n")
   342  		time.Sleep(sleepInterval)
   343  	}
   344  
   345  	// Purchase proposal credits
   346  	fmt.Printf("  User proposal paywall\n")
   347  	ppdr, err := client.UserProposalPaywall()
   348  	if err != nil {
   349  		return err
   350  	}
   351  
   352  	if paywallEnabled {
   353  		// Purchase proposal credits
   354  		fmt.Printf("  Purchasing %v proposal credits\n", numCredits)
   355  
   356  		atoms := ppdr.CreditPrice * uint64(numCredits)
   357  		txID, err := util.PayWithTestnetFaucet(context.Background(),
   358  			cfg.FaucetHost, ppdr.PaywallAddress, atoms, "")
   359  		if err != nil {
   360  			return err
   361  		}
   362  
   363  		fmt.Printf("  Paid %v DCR to %v with txID %v\n",
   364  			float64(atoms)/1e8, user.PaywallAddress, txID)
   365  	}
   366  
   367  	// Keep track of when the pending proposal credit payment
   368  	// receives the required number of confirmations.
   369  	for {
   370  		pppr, err := client.UserProposalPaywallTx()
   371  		if err != nil {
   372  			return err
   373  		}
   374  
   375  		// TxID will be blank if the paywall has been disabled
   376  		// or if the payment is no longer pending.
   377  		if pppr.TxID == "" {
   378  			// Verify that the correct number of proposal credits
   379  			// have been added to the user's account.
   380  			upcr, err := client.UserProposalCredits()
   381  			if err != nil {
   382  				return err
   383  			}
   384  
   385  			if !paywallEnabled || len(upcr.UnspentCredits) == numCredits {
   386  				break
   387  			}
   388  		}
   389  
   390  		fmt.Printf("  Proposal paywall payment: waiting for tx confirmations...\n")
   391  		time.Sleep(sleepInterval)
   392  	}
   393  
   394  	// Fetch user by usernam
   395  	fmt.Printf("  Fetch user by username\n")
   396  	usersr, err := client.Users(&www.Users{
   397  		Username: user.Username,
   398  	})
   399  	if err != nil {
   400  		return err
   401  	}
   402  	if usersr.TotalMatches != 1 {
   403  		return fmt.Errorf("Wrong matching users: want %v, got %v", 1,
   404  			usersr.TotalMatches)
   405  	}
   406  
   407  	// Fetch user by public key
   408  	fmt.Printf("  Fetch user by public key\n")
   409  	usersr, err = client.Users(&www.Users{
   410  		PublicKey: user.PublicKey,
   411  	})
   412  	if err != nil {
   413  		return err
   414  	}
   415  	if usersr.TotalMatches != 1 {
   416  		return fmt.Errorf("Wrong matching users: want %v, got %v", 1,
   417  			usersr.TotalMatches)
   418  	}
   419  
   420  	// User details
   421  	fmt.Printf("  User details\n")
   422  	udc := userDetailsCmd{}
   423  	udc.Args.UserID = user.ID
   424  	err = udc.Execute(nil)
   425  	if err != nil {
   426  		return err
   427  	}
   428  
   429  	// Login admin
   430  	fmt.Printf("  Login as admin\n")
   431  	err = login(admin)
   432  	if err != nil {
   433  		return err
   434  	}
   435  
   436  	// Rescan user credits
   437  	fmt.Printf("  Rescan user credits\n")
   438  	upayrc := userPaymentsRescanCmd{}
   439  	upayrc.Args.UserID = user.ID
   440  	err = upayrc.Execute(nil)
   441  	if err != nil {
   442  		return err
   443  	}
   444  
   445  	// Deactivate user
   446  	fmt.Printf("  Deactivate user\n")
   447  	const userDeactivateAction = "deactivate"
   448  	err = userManage(user.ID, userDeactivateAction, "testing")
   449  	if err != nil {
   450  		return err
   451  	}
   452  
   453  	// Reactivate user
   454  	fmt.Printf("  Reactivate user\n")
   455  	const userReactivateAction = "reactivate"
   456  	err = userManage(user.ID, userReactivateAction, "testing")
   457  	if err != nil {
   458  		return err
   459  	}
   460  
   461  	// Fetch user by email
   462  	fmt.Printf("  Fetch user by email\n")
   463  	usersr, err = client.Users(&www.Users{
   464  		Email: user.Email,
   465  	})
   466  	if err != nil {
   467  		return err
   468  	}
   469  	if usersr.TotalMatches != 1 {
   470  		return fmt.Errorf("Wrong matching users: want %v, got %v", 1,
   471  			usersr.TotalMatches)
   472  	}
   473  
   474  	return nil
   475  }
   476  
   477  // proposalNewNormal is a wrapper func which creates a proposal by calling
   478  // proposalNew
   479  func proposalNewNormal() (*pi.ProposalNew, error) {
   480  	return proposalNew(false, "")
   481  }
   482  
   483  // proposalNew returns a NewProposal object contains randonly generated
   484  // markdown text and a signature from the logged in user. If given `rfp` bool
   485  // is true it creates an RFP. If given `linkto` it creates a RFP submission.
   486  func proposalNew(rfp bool, linkto string) (*pi.ProposalNew, error) {
   487  	md, err := createMDFile()
   488  	if err != nil {
   489  		return nil, fmt.Errorf("create MD file: %v", err)
   490  	}
   491  	files := []pi.File{*md}
   492  
   493  	pm := www.ProposalMetadata{
   494  		Name: "Some proposal name",
   495  	}
   496  	if rfp {
   497  		pm.LinkBy = time.Now().Add(time.Hour * 24 * 30).Unix()
   498  	}
   499  	if linkto != "" {
   500  		pm.LinkTo = linkto
   501  	}
   502  	pmb, err := json.Marshal(pm)
   503  	if err != nil {
   504  		return nil, err
   505  	}
   506  	metadata := []pi.Metadata{
   507  		{
   508  			Digest:  hex.EncodeToString(util.Digest(pmb)),
   509  			Hint:    pi.HintProposalMetadata,
   510  			Payload: base64.StdEncoding.EncodeToString(pmb),
   511  		},
   512  	}
   513  
   514  	sig, err := signedMerkleRoot(files, metadata, cfg.Identity)
   515  	if err != nil {
   516  		return nil, fmt.Errorf("sign merkle root: %v", err)
   517  	}
   518  
   519  	return &pi.ProposalNew{
   520  		Files:     files,
   521  		Metadata:  metadata,
   522  		PublicKey: hex.EncodeToString(cfg.Identity.Public.Key[:]),
   523  		Signature: sig,
   524  	}, nil
   525  }
   526  
   527  // submitNewPropsal submits new proposal and verifies it
   528  //
   529  // This function returns with the user logged out
   530  func submitNewProposal(user testUser) (string, error) {
   531  	// Login user
   532  	err := login(user)
   533  	if err != nil {
   534  		return "", err
   535  	}
   536  
   537  	fmt.Printf("  New proposal\n")
   538  	pn, err := proposalNewNormal()
   539  	if err != nil {
   540  		return "", err
   541  	}
   542  	pnr, err := client.ProposalNew(*pn)
   543  	if err != nil {
   544  		return "", err
   545  	}
   546  
   547  	// Verify proposal censorship record
   548  	pr := &pi.ProposalRecord{
   549  		Files:            pn.Files,
   550  		Metadata:         pn.Metadata,
   551  		PublicKey:        pn.PublicKey,
   552  		Signature:        pn.Signature,
   553  		CensorshipRecord: pnr.Proposal.CensorshipRecord,
   554  	}
   555  	err = verifyProposal(*pr, publicKey)
   556  	if err != nil {
   557  		return "", fmt.Errorf("verify proposal failed: %v", err)
   558  	}
   559  
   560  	token := pr.CensorshipRecord.Token
   561  	fmt.Printf("  Proposal submitted: %v\n", token)
   562  
   563  	// Logout
   564  	err = logout()
   565  	if err != nil {
   566  		return "", err
   567  	}
   568  
   569  	return token, nil
   570  }
   571  
   572  // proposalSetStatus calls proposal set status command
   573  //
   574  // This function returns with the user logged out
   575  func proposalSetStatus(user testUser, state pi.PropStateT, token, reason string, status pi.PropStatusT) error {
   576  	// Login user
   577  	err := login(user)
   578  	if err != nil {
   579  		return err
   580  	}
   581  
   582  	pssc := proposalSetStatusCmd{
   583  		Unvetted: state == pi.PropStateUnvetted,
   584  	}
   585  	pssc.Args.Token = token
   586  	pssc.Args.Status = strconv.Itoa(int(status))
   587  	pssc.Args.Reason = reason
   588  	err = pssc.Execute(nil)
   589  	if err != nil {
   590  		return err
   591  	}
   592  
   593  	return logout()
   594  }
   595  
   596  // proposalCensor censors given proposal
   597  //
   598  // This function returns with the user logged out
   599  func proposalCensor(user testUser, state pi.PropStateT, token, reason string) error {
   600  	err := proposalSetStatus(user, state, token, reason, pi.PropStatusCensored)
   601  	if err != nil {
   602  		return err
   603  	}
   604  	return nil
   605  }
   606  
   607  // proposalPublic makes given proposal public
   608  //
   609  // This function returns with the user logged out
   610  func proposalPublic(user testUser, token string) error {
   611  	err := proposalSetStatus(user, pi.PropStateUnvetted, token, "", pi.PropStatusPublic)
   612  	if err != nil {
   613  		return err
   614  	}
   615  	return nil
   616  }
   617  
   618  // proposalAbandon abandons given proposal
   619  //
   620  // This function returns with the user logged out
   621  func proposalAbandon(user testUser, token, reason string) error {
   622  	err := proposalSetStatus(user, pi.PropStateVetted, token, reason,
   623  		pi.PropStatusAbandoned)
   624  	if err != nil {
   625  		return err
   626  	}
   627  	return nil
   628  }
   629  
   630  // proposalEdit edits given proposal
   631  //
   632  // This function returns with the user logged out
   633  func proposalEdit(user testUser, state pi.PropStateT, token string) error {
   634  	// Login user
   635  	err := login(user)
   636  	if err != nil {
   637  		return err
   638  	}
   639  
   640  	epc := proposalEditCmd{
   641  		Random:   true,
   642  		Unvetted: state == pi.PropStateUnvetted,
   643  	}
   644  	epc.Args.Token = token
   645  	err = epc.Execute(nil)
   646  	if err != nil {
   647  		return err
   648  	}
   649  
   650  	// Logout
   651  	return logout()
   652  }
   653  
   654  // proposals fetchs requested proposals and verifies returned map length
   655  //
   656  // This function returns with the user logged out
   657  func proposals(user testUser, ps pi.Proposals) (map[string]pi.ProposalRecord, error) {
   658  	// Login user
   659  	err := login(user)
   660  	if err != nil {
   661  		return nil, err
   662  	}
   663  	psr, err := client.Proposals(ps)
   664  	if err != nil {
   665  		return nil, err
   666  	}
   667  
   668  	if len(psr.Proposals) != len(ps.Requests) {
   669  		return nil, fmt.Errorf("Received wrong number of proposals: want %v,"+
   670  			" got %v", len(ps.Requests), len(psr.Proposals))
   671  	}
   672  
   673  	// Logout
   674  	err = logout()
   675  	if err != nil {
   676  		return nil, err
   677  	}
   678  
   679  	return psr.Proposals, nil
   680  }
   681  
   682  // userKeyUpdate updates user's key
   683  //
   684  // This function returns with the user logged out
   685  func userKeyUpdate(user testUser) error {
   686  	// Login user
   687  	err := login(user)
   688  	if err != nil {
   689  		return err
   690  	}
   691  
   692  	fmt.Printf("  Update user key\n")
   693  	ukuc := shared.UserKeyUpdateCmd{}
   694  	err = ukuc.Execute(nil)
   695  	if err != nil {
   696  		return err
   697  	}
   698  
   699  	return logout()
   700  }
   701  
   702  // testProposalRoutes tests the propsal routes
   703  func testProposalRoutes(admin testUser) error {
   704  	// Run proposal routes.
   705  	fmt.Printf("Running proposal routes\n")
   706  
   707  	// Create test user
   708  	fmt.Printf("  Creating test user\n")
   709  	user, id, vt, err := userCreate()
   710  	if err != nil {
   711  		return err
   712  	}
   713  
   714  	// Verify email
   715  	err = userEmailVerify(vt, user.Email, id)
   716  	if err != nil {
   717  		return err
   718  	}
   719  
   720  	// Update user key
   721  	err = userKeyUpdate(*user)
   722  	if err != nil {
   723  		return err
   724  	}
   725  
   726  	// Submit new proposal
   727  	censoredToken1, err := submitNewProposal(*user)
   728  	if err != nil {
   729  		return err
   730  	}
   731  
   732  	// Edit unvetted proposal
   733  	fmt.Printf("  Edit unvetted proposal\n")
   734  	err = proposalEdit(*user, pi.PropStateUnvetted, censoredToken1)
   735  	if err != nil {
   736  		return err
   737  	}
   738  
   739  	// Censor unvetted proposal
   740  	fmt.Printf("  Censor unvetted proposal\n")
   741  	const reason = "because!"
   742  	err = proposalCensor(admin, pi.PropStateUnvetted, censoredToken1, reason)
   743  	if err != nil {
   744  		return err
   745  	}
   746  
   747  	// Submit new proposal
   748  	censoredToken2, err := submitNewProposal(*user)
   749  	if err != nil {
   750  		return err
   751  	}
   752  
   753  	// Make the proposal public
   754  	fmt.Printf("  Set proposal status: public\n")
   755  	err = proposalPublic(admin, censoredToken2)
   756  	if err != nil {
   757  		return err
   758  	}
   759  
   760  	// Edit vetted proposal
   761  	fmt.Printf("  Edit vetted proposal\n")
   762  	err = proposalEdit(*user, pi.PropStateVetted, censoredToken2)
   763  	if err != nil {
   764  		return err
   765  	}
   766  
   767  	// Censor public proposal
   768  	fmt.Printf("  Censor public proposal\n")
   769  	err = proposalCensor(admin, pi.PropStateVetted, censoredToken2, reason)
   770  	if err != nil {
   771  		return err
   772  	}
   773  
   774  	// Submit new proposal
   775  	abandonedToken, err := submitNewProposal(*user)
   776  	if err != nil {
   777  		return err
   778  	}
   779  
   780  	// Make the proposal public
   781  	fmt.Printf("  Set proposal status: public\n")
   782  	err = proposalPublic(admin, abandonedToken)
   783  	if err != nil {
   784  		return err
   785  	}
   786  
   787  	// Abandon public proposal
   788  	fmt.Printf("  Abandon proposal\n")
   789  	err = proposalAbandon(admin, abandonedToken, reason)
   790  	if err != nil {
   791  		return err
   792  	}
   793  
   794  	// Submit new proposal and leave it unvetted
   795  	unvettedToken, err := submitNewProposal(*user)
   796  	if err != nil {
   797  		return err
   798  	}
   799  
   800  	// Submit new proposal and make it public
   801  	publicToken, err := submitNewProposal(*user)
   802  	if err != nil {
   803  		return err
   804  	}
   805  
   806  	// Make the proposal public
   807  	fmt.Printf("  Set proposal status: public\n")
   808  	err = proposalPublic(admin, publicToken)
   809  	if err != nil {
   810  		return err
   811  	}
   812  
   813  	// Login admin
   814  	err = login(admin)
   815  	if err != nil {
   816  		return err
   817  	}
   818  
   819  	// Proposal inventory
   820  	var publicExists, censoredExists, abandonedExists, unvettedExists bool
   821  	fmt.Printf("  Proposal inventory\n")
   822  	pir, err := client.ProposalInventory(pi.ProposalInventory{})
   823  	if err != nil {
   824  		return err
   825  	}
   826  	// Vetted proposals map
   827  	vettedProps := pir.Vetted
   828  
   829  	// Ensure public proposal token received
   830  	publicProps, ok := vettedProps[pi.PropStatuses[pi.PropStatusPublic]]
   831  	if !ok {
   832  		return fmt.Errorf("No public proposals returned")
   833  	}
   834  	for _, t := range publicProps {
   835  		if t == publicToken {
   836  			publicExists = true
   837  		}
   838  	}
   839  	if !publicExists {
   840  		return fmt.Errorf("Proposal inventory missing public proposal: %v",
   841  			publicToken)
   842  	}
   843  
   844  	// Ensure vetted censored proposal token received
   845  	vettedCensored, ok := vettedProps[pi.PropStatuses[pi.PropStatusCensored]]
   846  	if !ok {
   847  		return fmt.Errorf("No vetted censrored proposals returned")
   848  	}
   849  	for _, t := range vettedCensored {
   850  		if t == censoredToken2 {
   851  			censoredExists = true
   852  		}
   853  	}
   854  	if !censoredExists {
   855  		return fmt.Errorf("Proposal inventory missing vetted censored proposal"+
   856  			": %v",
   857  			censoredToken1)
   858  	}
   859  
   860  	// Ensure abandoned proposal token received
   861  	abandonedProps, ok := vettedProps[pi.PropStatuses[pi.PropStatusAbandoned]]
   862  	if !ok {
   863  		return fmt.Errorf("No abandoned proposals returned")
   864  	}
   865  	for _, t := range abandonedProps {
   866  		if t == abandonedToken {
   867  			abandonedExists = true
   868  		}
   869  	}
   870  	if !abandonedExists {
   871  		return fmt.Errorf("Proposal inventory missing abandoned proposal: %v",
   872  			abandonedToken)
   873  	}
   874  
   875  	// Unvetted propsoals
   876  	unvettedProps := pir.Unvetted
   877  
   878  	// Ensure unvetted proposal token received
   879  	unreviewedProps, ok := unvettedProps[pi.PropStatuses[pi.PropStatusUnreviewed]]
   880  	if !ok {
   881  		return fmt.Errorf("No unreviewed proposals returned")
   882  	}
   883  	for _, t := range unreviewedProps {
   884  		if t == unvettedToken {
   885  			unvettedExists = true
   886  		}
   887  	}
   888  	if !unvettedExists {
   889  		return fmt.Errorf("Proposal inventory missing unvetted proposal: %v",
   890  			unvettedToken)
   891  	}
   892  
   893  	// Ensure unvetted censored proposal token received
   894  	unvettedCensored, ok := unvettedProps["censored"]
   895  	if !ok {
   896  		return fmt.Errorf("No unvetted censrored proposals returned")
   897  	}
   898  	for _, t := range unvettedCensored {
   899  		if t == censoredToken1 {
   900  			censoredExists = true
   901  		}
   902  	}
   903  	if !censoredExists {
   904  		return fmt.Errorf("Proposal inventory missing unvetted censored proposal"+
   905  			": %v",
   906  			censoredToken1)
   907  	}
   908  
   909  	// Get vetted proposals
   910  	fmt.Printf("  Fetch vetted proposals\n")
   911  	props, err := proposals(*user, pi.Proposals{
   912  		State: pi.PropStateVetted,
   913  		Requests: []pi.ProposalRequest{
   914  			{
   915  				Token: publicToken,
   916  			},
   917  			{
   918  				Token: abandonedToken,
   919  			},
   920  		},
   921  	})
   922  	if err != nil {
   923  		return err
   924  	}
   925  	_, publicExists = props[publicToken]
   926  	_, abandonedExists = props[abandonedToken]
   927  	if !publicExists || !abandonedExists {
   928  		return fmt.Errorf("Proposal batch missing requested vetted proposals")
   929  	}
   930  
   931  	// Get vetted proposals with short tokens
   932  	fmt.Printf("  Fetch vetted proposals with short tokens\n")
   933  	shortPublicToken := publicToken[0:7]
   934  	shortAbandonedToken := abandonedToken[0:7]
   935  	props, err = proposals(*user, pi.Proposals{
   936  		State: pi.PropStateVetted,
   937  		Requests: []pi.ProposalRequest{
   938  			{
   939  				Token: shortPublicToken,
   940  			},
   941  			{
   942  				Token: shortAbandonedToken,
   943  			},
   944  		},
   945  	})
   946  	if err != nil {
   947  		return err
   948  	}
   949  	_, publicExists = props[publicToken]
   950  	_, abandonedExists = props[abandonedToken]
   951  	if !publicExists || !abandonedExists {
   952  		return fmt.Errorf("Proposal batch missing requested vetted proposals")
   953  	}
   954  
   955  	// Get unvetted proposal
   956  	fmt.Printf("  Fetch unvetted proposal\n")
   957  	props, err = proposals(*user, pi.Proposals{
   958  		State: pi.PropStateUnvetted,
   959  		Requests: []pi.ProposalRequest{
   960  			{
   961  				Token: unvettedToken,
   962  			},
   963  		},
   964  	})
   965  	if err != nil {
   966  		return err
   967  	}
   968  	_, unvettedExists = props[unvettedToken]
   969  	if !unvettedExists {
   970  		return fmt.Errorf("Proposal batch missing requested unvetted proposals")
   971  	}
   972  
   973  	// Get unvetted proposal with short token
   974  	fmt.Printf("  Fetch unvetted proposal with short token\n")
   975  	shortUnvettedToken := unvettedToken[0:7]
   976  	props, err = proposals(*user, pi.Proposals{
   977  		State: pi.PropStateUnvetted,
   978  		Requests: []pi.ProposalRequest{
   979  			{
   980  				Token: shortUnvettedToken,
   981  			},
   982  		},
   983  	})
   984  	if err != nil {
   985  		return err
   986  	}
   987  	_, unvettedExists = props[unvettedToken]
   988  	if !unvettedExists {
   989  		return fmt.Errorf("Proposal batch missing requested unvetted proposals")
   990  	}
   991  
   992  	return nil
   993  }
   994  
   995  // commentNew submits a new comment
   996  //
   997  // This function returns with the user logged out
   998  func commentNew(user testUser, state pi.PropStateT, token, comment, parentID string) error {
   999  	// Login user
  1000  	err := login(user)
  1001  	if err != nil {
  1002  		return err
  1003  	}
  1004  
  1005  	ncc := commentNewCmd{
  1006  		Unvetted: state == pi.PropStateUnvetted,
  1007  	}
  1008  	ncc.Args.Token = token
  1009  	ncc.Args.Comment = comment
  1010  	ncc.Args.ParentID = parentID
  1011  	err = ncc.Execute(nil)
  1012  	if err != nil {
  1013  		return err
  1014  	}
  1015  
  1016  	return logout()
  1017  }
  1018  
  1019  // verifyCommentSctore accepts array of comments, a commentID and the expected
  1020  // up & down votes and ensures given comment has expected score
  1021  func verifyCommentScore(comments []pi.Comment, commentID uint32, upvotes, downvotes uint64) error {
  1022  	var commentExists bool
  1023  	for _, v := range comments {
  1024  		if v.CommentID == commentID {
  1025  			commentExists = true
  1026  			switch {
  1027  			case v.Upvotes != upvotes:
  1028  				return fmt.Errorf("comment result up votes got %v, want %v",
  1029  					v.Upvotes, upvotes)
  1030  			case v.Downvotes != downvotes:
  1031  				return fmt.Errorf("comment result down votes got %v, want %v",
  1032  					v.Downvotes, downvotes)
  1033  			}
  1034  		}
  1035  	}
  1036  	if !commentExists {
  1037  		return fmt.Errorf("comment not found: %v", commentID)
  1038  	}
  1039  
  1040  	return nil
  1041  }
  1042  
  1043  // verifyCommentVotes accepts comment votes array of all user's comment votes
  1044  // on a proposals and a comment id, it verifies the number of the total votes,
  1045  // the number of upvotes and the number of downvotes on given comment
  1046  func verifyCommentVotes(votes []pi.CommentVoteDetails, commentID uint32, totalVotes, upvotes, downvotes int) error {
  1047  	var (
  1048  		uvotes        int
  1049  		dvotes        int
  1050  		total         int
  1051  		commentExists bool
  1052  	)
  1053  	for _, v := range votes {
  1054  		if v.CommentID == commentID {
  1055  			commentExists = true
  1056  			switch v.Vote {
  1057  			case pi.CommentVoteDownvote:
  1058  				dvotes++
  1059  			case pi.CommentVoteUpvote:
  1060  				uvotes++
  1061  			}
  1062  			total++
  1063  		}
  1064  	}
  1065  	if !commentExists {
  1066  		return fmt.Errorf("comment not found: %v", commentID)
  1067  	}
  1068  	if total != totalVotes {
  1069  		return fmt.Errorf("wrong num of comment votes got %v, want %v",
  1070  			total, totalVotes)
  1071  	}
  1072  	if uvotes != upvotes {
  1073  		return fmt.Errorf("wrong num of upvotes: got %v, want %v",
  1074  			uvotes, upvotes)
  1075  	}
  1076  	if dvotes != downvotes {
  1077  		return fmt.Errorf("wrong num of downvotes: got %v, want %v",
  1078  			dvotes, downvotes)
  1079  	}
  1080  
  1081  	return nil
  1082  }
  1083  
  1084  // testCommentRoutes tests the comment routes
  1085  func testCommentRoutes(admin testUser) error {
  1086  	// Run commment routes.
  1087  	fmt.Printf("Running comment routes\n")
  1088  
  1089  	// Create test user
  1090  	fmt.Printf("  Creating test user\n")
  1091  	user, id, vt, err := userCreate()
  1092  	if err != nil {
  1093  		return err
  1094  	}
  1095  
  1096  	// Verify email
  1097  	err = userEmailVerify(vt, user.Email, id)
  1098  	if err != nil {
  1099  		return err
  1100  	}
  1101  
  1102  	// Update user key
  1103  	err = userKeyUpdate(*user)
  1104  	if err != nil {
  1105  		return err
  1106  	}
  1107  
  1108  	// Populate user's info
  1109  	err = userDetails(user)
  1110  	if err != nil {
  1111  		return err
  1112  	}
  1113  
  1114  	// Submit new proposal
  1115  	token, err := submitNewProposal(*user)
  1116  	if err != nil {
  1117  		return err
  1118  	}
  1119  
  1120  	// Make proposal public
  1121  	fmt.Printf("  Set proposal status: public\n")
  1122  	err = proposalPublic(admin, token)
  1123  	if err != nil {
  1124  		return err
  1125  	}
  1126  
  1127  	// Abandon proposal
  1128  	reason := "because!"
  1129  	fmt.Printf("  Abandon proposal\n")
  1130  	err = proposalAbandon(admin, token, reason)
  1131  	if err != nil {
  1132  		return err
  1133  	}
  1134  
  1135  	// Comment on abandoned proposal
  1136  	fmt.Printf("  Ensure commenting on abandoned proposal isn't allowed\n")
  1137  	comment := "this is a comment"
  1138  	err = commentNew(*user, pi.PropStateVetted, token, comment, "0")
  1139  	if err == nil {
  1140  		return fmt.Errorf("Commented on an abandoned proposal: %v", token)
  1141  	}
  1142  
  1143  	// Submit new proposal
  1144  	token, err = submitNewProposal(*user)
  1145  	if err != nil {
  1146  		return err
  1147  	}
  1148  
  1149  	// Censor proposal
  1150  	fmt.Printf("  Censor proposal\n")
  1151  	err = proposalCensor(admin, pi.PropStateUnvetted, token, reason)
  1152  	if err != nil {
  1153  		return err
  1154  	}
  1155  
  1156  	// Comment on abandoned proposal
  1157  	fmt.Printf("  Ensure commenting on censored proposal isn't allowed\n")
  1158  	err = commentNew(*user, pi.PropStateVetted, token, comment, "0")
  1159  	if err == nil {
  1160  		return fmt.Errorf("Commented on a censored proposal: %v", token)
  1161  	}
  1162  
  1163  	// Submit new proposal
  1164  	token, err = submitNewProposal(*user)
  1165  	if err != nil {
  1166  		return err
  1167  	}
  1168  
  1169  	// Author comment on unvetted proposal
  1170  	fmt.Printf("  Author comment on an unvetted proposal\n")
  1171  	err = commentNew(*user, pi.PropStateUnvetted, token, comment, "0")
  1172  	if err != nil {
  1173  		return err
  1174  	}
  1175  
  1176  	// Admin comment on an unvetted proposal
  1177  	fmt.Print("  Admin comment on an unvetted proposal\n")
  1178  	err = commentNew(admin, pi.PropStateUnvetted, token, comment, "0")
  1179  	if err != nil {
  1180  		return err
  1181  	}
  1182  
  1183  	// Make proposal a public
  1184  	fmt.Printf("  Set proposal status: public\n")
  1185  	err = proposalPublic(admin, token)
  1186  	if err != nil {
  1187  		return err
  1188  	}
  1189  
  1190  	// Author comment on a public proposal
  1191  	fmt.Printf("  Author comment on a public proposal\n")
  1192  	err = commentNew(*user, pi.PropStateVetted, token, comment, "0")
  1193  	if err != nil {
  1194  		return err
  1195  	}
  1196  
  1197  	// Create another user to comment
  1198  	fmt.Printf("  Creating another test user\n")
  1199  	thirdU, id, vt, err := userCreate()
  1200  	if err != nil {
  1201  		return err
  1202  	}
  1203  
  1204  	// Verify email
  1205  	err = userEmailVerify(vt, thirdU.Email, id)
  1206  	if err != nil {
  1207  		return err
  1208  	}
  1209  
  1210  	// Update user key
  1211  	err = userKeyUpdate(*thirdU)
  1212  	if err != nil {
  1213  		return err
  1214  	}
  1215  
  1216  	// Another user comment on a public proposal
  1217  	fmt.Print("  Another user comment on a public proposal\n")
  1218  	err = commentNew(*user, pi.PropStateVetted, token, comment, "0")
  1219  	if err != nil {
  1220  		return err
  1221  	}
  1222  
  1223  	// Comment on a public proposal - reply
  1224  	fmt.Printf("  Comment on a public proposal: reply\n")
  1225  	reply := "this is a comment reply"
  1226  	err = commentNew(*user, pi.PropStateVetted, token, reply, "1")
  1227  	if err != nil {
  1228  		return err
  1229  	}
  1230  
  1231  	// Validate comments
  1232  	fmt.Printf("  Proposal details\n")
  1233  	propReq := pi.ProposalRequest{
  1234  		Token: token,
  1235  	}
  1236  	pdr, err := client.Proposals(pi.Proposals{
  1237  		State:        pi.PropStateVetted,
  1238  		Requests:     []pi.ProposalRequest{propReq},
  1239  		IncludeFiles: false,
  1240  	})
  1241  	if err != nil {
  1242  		return err
  1243  	}
  1244  	prop := pdr.Proposals[token]
  1245  	if prop.Comments != 3 {
  1246  		return fmt.Errorf("proposal num comments got %v, want 3",
  1247  			prop.Comments)
  1248  	}
  1249  
  1250  	fmt.Printf("  Proposal comments\n")
  1251  	gcr, err := client.Comments(pi.Comments{
  1252  		Token: token,
  1253  		State: pi.PropStateVetted,
  1254  	})
  1255  	if err != nil {
  1256  		return fmt.Errorf("Comments: %v", err)
  1257  	}
  1258  
  1259  	if len(gcr.Comments) != 3 {
  1260  		return fmt.Errorf("num comments got %v, want 3",
  1261  			len(gcr.Comments))
  1262  	}
  1263  
  1264  	for _, c := range gcr.Comments {
  1265  		// We check the userID because userIDs are not part of
  1266  		// the politeiad comment record. UserIDs are stored in
  1267  		// in politeiawww and are added to the comments at the
  1268  		// time of the request. This introduces the potential
  1269  		// for errors.
  1270  		if c.UserID != user.ID && c.CommentID == 1 {
  1271  			return fmt.Errorf("comment %v has wrong userID got %v, want %v",
  1272  				c.CommentID, c.UserID, user.ID)
  1273  		}
  1274  	}
  1275  
  1276  	// Login with admin to be able to vote on user's comments
  1277  	fmt.Printf("  Login admin\n")
  1278  	err = login(admin)
  1279  	if err != nil {
  1280  		return err
  1281  	}
  1282  
  1283  	// Comment vote sequence
  1284  	var (
  1285  		// Comment actions
  1286  		commentActionUpvote   = strconv.Itoa(int(pi.CommentVoteUpvote))
  1287  		commentActionDownvote = strconv.Itoa(int(pi.CommentVoteDownvote))
  1288  	)
  1289  	cvc := commentVoteCmd{}
  1290  	cvc.Args.Token = token
  1291  	cvc.Args.CommentID = "1"
  1292  	cvc.Args.Vote = commentActionUpvote
  1293  
  1294  	fmt.Printf("  Comment vote: upvote\n")
  1295  	err = cvc.Execute(nil)
  1296  	if err != nil {
  1297  		return err
  1298  	}
  1299  
  1300  	// XXX workaround to prevent comment votes from having the same timestamp
  1301  	time.Sleep(time.Second)
  1302  
  1303  	fmt.Printf("  Comment vote: upvote\n")
  1304  	err = cvc.Execute(nil)
  1305  	if err != nil {
  1306  		return err
  1307  	}
  1308  
  1309  	// XXX workaround to prevent comment votes from having the same timestamp
  1310  	time.Sleep(time.Second)
  1311  
  1312  	fmt.Printf("  Comment vote: upvote\n")
  1313  	err = cvc.Execute(nil)
  1314  	if err != nil {
  1315  		return err
  1316  	}
  1317  
  1318  	// XXX workaround to prevent comment votes from having the same timestamp
  1319  	time.Sleep(time.Second)
  1320  
  1321  	fmt.Printf("  Comment vote: downvote\n")
  1322  	cvc.Args.Vote = commentActionDownvote
  1323  	err = cvc.Execute(nil)
  1324  	if err != nil {
  1325  		return err
  1326  	}
  1327  
  1328  	// Validate comment votes
  1329  	fmt.Printf("  Fetch proposal's comments & verify first comment score\n")
  1330  	gcr, err = client.Comments(pi.Comments{
  1331  		Token: token,
  1332  		State: pi.PropStateVetted,
  1333  	})
  1334  	if err != nil {
  1335  		return err
  1336  	}
  1337  
  1338  	// Verify first comment score
  1339  	err = verifyCommentScore(gcr.Comments, 1, 0, 1)
  1340  	if err != nil {
  1341  		return err
  1342  	}
  1343  
  1344  	// Validate comment votes using short token
  1345  	fmt.Printf("  Fetch proposal's comments using short token & verify first " +
  1346  		"comment score\n")
  1347  	gcr, err = client.Comments(pi.Comments{
  1348  		Token: token[0:7],
  1349  		State: pi.PropStateVetted,
  1350  	})
  1351  	if err != nil {
  1352  		return err
  1353  	}
  1354  
  1355  	// Verify first comment score
  1356  	err = verifyCommentScore(gcr.Comments, 1, 0, 1)
  1357  	if err != nil {
  1358  		return err
  1359  	}
  1360  
  1361  	fmt.Printf("  Verify admin's comment votes\n")
  1362  	cvr, err := client.CommentVotes(pi.CommentVotes{
  1363  		State:  pi.PropStateVetted,
  1364  		Token:  token,
  1365  		UserID: admin.ID,
  1366  	})
  1367  	if err != nil {
  1368  		return err
  1369  	}
  1370  
  1371  	err = verifyCommentVotes(cvr.Votes, 1, 4, 3, 1)
  1372  	if err != nil {
  1373  		return err
  1374  	}
  1375  
  1376  	fmt.Printf("  Verify admin's comment votes using short token\n")
  1377  	cvr, err = client.CommentVotes(pi.CommentVotes{
  1378  		State:  pi.PropStateVetted,
  1379  		Token:  token[0:7],
  1380  		UserID: admin.ID,
  1381  	})
  1382  	if err != nil {
  1383  		return err
  1384  	}
  1385  
  1386  	err = verifyCommentVotes(cvr.Votes, 1, 4, 3, 1)
  1387  	if err != nil {
  1388  		return err
  1389  	}
  1390  
  1391  	return nil
  1392  }
  1393  
  1394  // Execute executes the cmdTestRun command.
  1395  //
  1396  // This function satisfies the go-flags Commander interface.
  1397  func (cmd *cmdTestRun) Execute(args []string) error {
  1398  	// Suppress output from cli commands
  1399  	cfg.Silent = true
  1400  
  1401  	fmt.Printf("Running pre-testrun validation\n")
  1402  
  1403  	// Policy
  1404  	fmt.Printf("  Policy\n")
  1405  	policy, err := client.Policy()
  1406  	if err != nil {
  1407  		return err
  1408  	}
  1409  	minPasswordLength = int(policy.MinPasswordLength)
  1410  
  1411  	// Version (CSRF tokens)
  1412  	fmt.Printf("  Version\n")
  1413  	version, err := client.Version()
  1414  	if err != nil {
  1415  		return err
  1416  	}
  1417  	publicKey = version.PubKey
  1418  
  1419  	// Verify politeiawww settings
  1420  	switch {
  1421  	case !version.TestNet:
  1422  		return fmt.Errorf("this command must be run on testnet")
  1423  	case policy.MinVoteDuration > 3:
  1424  		// Min vote duration must be <=3 since this command waits for
  1425  		// proposal votes to finish as part of the test run.
  1426  		return fmt.Errorf("politeiawww min vote duration is currently %v. "+
  1427  			"This command requires a min vote duration of <=3 blocks. Use "+
  1428  			"the politeiawww --votedurationmin flag to update this setting.",
  1429  			policy.MinVoteDuration)
  1430  	}
  1431  
  1432  	// Ensure admin credentials are valid
  1433  	admin := testUser{
  1434  		Email:    cmd.Args.AdminEmail,
  1435  		Password: cmd.Args.AdminPassword,
  1436  	}
  1437  	err = login(admin)
  1438  	if err != nil {
  1439  		return err
  1440  	}
  1441  
  1442  	// Populate admin's info
  1443  	err = userDetails(&admin)
  1444  	if err != nil {
  1445  		return err
  1446  	}
  1447  
  1448  	// Ensure admin paid registration free
  1449  	urpr, err := userRegistrationPayment()
  1450  	if err != nil {
  1451  		return err
  1452  	}
  1453  	if !urpr.HasPaid {
  1454  		return fmt.Errorf("admin has not paid registration fee")
  1455  	}
  1456  
  1457  	// Logout admin
  1458  	err = logout()
  1459  	if err != nil {
  1460  		return err
  1461  	}
  1462  
  1463  	// Test user routes
  1464  	err = testUserRoutes(admin)
  1465  	if err != nil {
  1466  		return err
  1467  	}
  1468  
  1469  	// Test proposal routes
  1470  	err = testProposalRoutes(admin)
  1471  	if err != nil {
  1472  		return err
  1473  	}
  1474  
  1475  	// Test comment routes
  1476  	err = testCommentRoutes(admin)
  1477  	if err != nil {
  1478  		return err
  1479  	}
  1480  
  1481  	fmt.Printf("Test run successful!\n")
  1482  
  1483  	return nil
  1484  }
  1485  */
  1486  
  1487  // testRunHelpMsg is the printed to stdout by the help command.
  1488  const testRunHelpMsg = `testrun "adminusername" "adminpassword"
  1489  
  1490  Run a series of tests on the politeiawww routes.  This command can only be run
  1491  on testnet.
  1492  
  1493  Paywall: 
  1494  If the politeiawww paywall is enabled the test run will use the Decred tesnet
  1495  faucet to pay the user registration fee and to purchase proposal credits.  If
  1496  the politeiawww paywall has been disabled a warning will be logged and the
  1497  payments will be skipped.
  1498  
  1499  Voting:
  1500  The test run will attempt to vote on a proposal.  If a dcrwallet instance is
  1501  not being run locally or if the wallet does not contain any eligible tickets
  1502  a warning will be logged and voting will be skipped.
  1503  
  1504  Arguments:
  1505  1. adminusername   (string, required)   Admin username
  1506  2. adminpassword   (string, required)   Admin password
  1507  `