github.com/decred/politeia@v1.4.0/politeiawww/cmd/cmswww/testrun.go (about)

     1  // Copyright (c) 2020 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  import (
     8  	"encoding/base64"
     9  	"encoding/binary"
    10  	"encoding/hex"
    11  	"encoding/json"
    12  	"fmt"
    13  	"strconv"
    14  	"time"
    15  
    16  	"github.com/decred/dcrd/chaincfg/v3"
    17  	"github.com/decred/dcrd/hdkeychain/v3"
    18  	"github.com/decred/politeia/politeiad/api/v1/mime"
    19  	"github.com/decred/politeia/politeiad/backend/gitbe/cmsplugin"
    20  	cms "github.com/decred/politeia/politeiawww/api/cms/v1"
    21  	www "github.com/decred/politeia/politeiawww/api/www/v1"
    22  	"github.com/decred/politeia/politeiawww/cmd/shared"
    23  	"github.com/decred/politeia/util"
    24  )
    25  
    26  // TestRunCmd performs a test run of cmswww routes.
    27  type TestRunCmd struct {
    28  	Args struct {
    29  		AdminEmail    string `positional-arg-name:"adminemail"`
    30  		AdminPassword string `positional-arg-name:"adminpassword"`
    31  	} `positional-args:"true" required:"true"`
    32  }
    33  
    34  const (
    35  	// DCC support/oppose options. These are not the all contractor
    36  	// vote options. Those are defined in cmsplugin.
    37  	dccIssuanceSupport = "aye"
    38  	dccIssuanceOppose  = "nay"
    39  )
    40  
    41  // user represents a cms user that is used during the test run.
    42  type user struct {
    43  	ID           string
    44  	Email        string
    45  	Username     string
    46  	Password     string
    47  	AddressIndex uint32 // Used to generate unique payment addresses
    48  }
    49  
    50  func (u *user) paymentAddress() (string, error) {
    51  	xpub := "tpubVobLtToNtTq6TZNw4raWQok35PRPZou53vegZqNubtBTJMMFmuMpWybF" +
    52  		"CfweJ52N8uZJPZZdHE5SRnBBuuRPfC5jdNstfKjiAs8JtbYG9jx"
    53  
    54  	if u.AddressIndex == 0 {
    55  		// The address index is set to be the uint32 representation of
    56  		// the hex username, which was randomly generated, so that we
    57  		// don't have payment address collisions between the different
    58  		// test users. CMS doesn't allow the same payment address to
    59  		// be used more than a single time.
    60  		b, err := hex.DecodeString(u.Username)
    61  		if err != nil {
    62  			return "", err
    63  		}
    64  		u.AddressIndex = binary.LittleEndian.Uint32(b)
    65  	}
    66  
    67  	// Increment the address index for the user so that a unique
    68  	// payment address is generated each time.
    69  	u.AddressIndex += 1
    70  
    71  	// The index isn't allowed to be larger than the HardenKeyStart.
    72  	// See hdkeychain ExtendedKey.Child() for more info.
    73  	if u.AddressIndex >= hdkeychain.HardenedKeyStart {
    74  		u.AddressIndex = u.AddressIndex % hdkeychain.HardenedKeyStart
    75  	}
    76  
    77  	return util.DeriveChildAddress(chaincfg.TestNet3Params(),
    78  		xpub, u.AddressIndex)
    79  }
    80  
    81  func randomString() string {
    82  	b, err := util.Random(16)
    83  	if err != nil {
    84  		return "uh oh, randomString() failed"
    85  	}
    86  	return hex.EncodeToString(b)
    87  }
    88  
    89  // login logs the given user into politeiawww. The full LoginCmd must be used
    90  // instead of just calling client.Login() so that the persistent CLI data is
    91  // updated properly.
    92  func login(u user) error {
    93  	lc := shared.LoginCmd{}
    94  	lc.Args.Email = u.Email
    95  	lc.Args.Password = u.Password
    96  	return lc.Execute(nil)
    97  }
    98  
    99  // logout logs out whatever user is currently logged in with the CLI. The full
   100  // LogoutCmd must be used instead of just calling client.Logout() so that the
   101  // persistent CLI data is updated properly.
   102  func logout() error {
   103  	l := shared.LogoutCmd{}
   104  	return l.Execute(nil)
   105  }
   106  
   107  // userNew returns a new cms user that has been invited and registered. The
   108  // user credentials are randomly generated.
   109  //
   110  // This function returns with the admin logged out.
   111  func userNew(admin user) (*user, error) {
   112  	// Login the admin
   113  	err := login(admin)
   114  	if err != nil {
   115  		return nil, fmt.Errorf("login: %v", err)
   116  	}
   117  
   118  	// Generate random user credentials
   119  	b, err := util.Random(www.PolicyMinPasswordLength)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	u := user{
   124  		Email:    hex.EncodeToString(b) + "@example.com",
   125  		Username: hex.EncodeToString(b),
   126  		Password: hex.EncodeToString(b),
   127  	}
   128  
   129  	// Invite user
   130  	inu := cms.InviteNewUser{
   131  		Email: u.Email,
   132  	}
   133  	inur, err := client.InviteNewUser(&inu)
   134  	if err != nil {
   135  		return nil, fmt.Errorf("InviteNewUser: %v", err)
   136  	}
   137  	if inur.VerificationToken == "" {
   138  		return nil, fmt.Errorf("InviteNewUserReply: verification token not " +
   139  			"found; the politeiawww email server likely needs to be disabled")
   140  	}
   141  
   142  	// Register user. Use the full command so that the identity is
   143  	// saved to disk. This will also log the user in.
   144  	r := RegisterUserCmd{}
   145  	r.Args.Email = u.Email
   146  	r.Args.Username = u.Username
   147  	r.Args.Password = u.Password
   148  	r.Args.Token = inur.VerificationToken
   149  	err = r.Execute(nil)
   150  	if err != nil {
   151  		return nil, fmt.Errorf("RegisterUserCmd: %v", err)
   152  	}
   153  
   154  	// Get the user ID
   155  	lr, err := client.Me()
   156  	if err != nil {
   157  		return nil, fmt.Errorf("Me: %v", err)
   158  	}
   159  	u.ID = lr.UserID
   160  
   161  	// Log the user out
   162  	err = logout()
   163  	if err != nil {
   164  		return nil, fmt.Errorf("logout: %v", err)
   165  	}
   166  
   167  	return &u, nil
   168  }
   169  
   170  // contractorNew creates a new user then updates the user with the given
   171  // contractor details.
   172  //
   173  // This function returns with the admin logged out.
   174  func contractorNew(admin user, dt cms.DomainTypeT, ct cms.ContractorTypeT) (*user, error) {
   175  	// Invite and register a new user
   176  	u, err := userNew(admin)
   177  	if err != nil {
   178  		return nil, fmt.Errorf("userNew: %v", err)
   179  	}
   180  
   181  	err = login(admin)
   182  	if err != nil {
   183  		return nil, fmt.Errorf("login: %v", err)
   184  	}
   185  
   186  	// Update the user's contractor status
   187  	muc := CMSManageUserCmd{
   188  		Domain:         strconv.Itoa(int(dt)),
   189  		ContractorType: strconv.Itoa(int(ct)),
   190  	}
   191  	muc.Args.UserID = u.ID
   192  	err = muc.Execute(nil)
   193  	if err != nil {
   194  		return nil, fmt.Errorf("CMSManageUserCmd: %v", err)
   195  	}
   196  
   197  	err = logout()
   198  	if err != nil {
   199  		return nil, fmt.Errorf("logout: %v", err)
   200  	}
   201  
   202  	return u, err
   203  }
   204  
   205  // invoiceNew submits a new invoice for the provided user. The invoice is
   206  // submitted for the most recent month that the user has not submitted an
   207  // invoice for.
   208  //
   209  // contractorRate is in USD.
   210  // labor is in hours.
   211  //
   212  // This function returns with the user logged out.
   213  func invoiceNew(u user, contractorRate uint, labor float64) (*www.CensorshipRecord, error) {
   214  	err := login(u)
   215  	if err != nil {
   216  		return nil, fmt.Errorf("login: %v", err)
   217  	}
   218  
   219  	// Get user's previous invoices
   220  	uir, err := client.UserInvoices(&cms.UserInvoices{})
   221  	if err != nil {
   222  		return nil, fmt.Errorf("UserInvoices: %v", err)
   223  	}
   224  
   225  	// Find the first available date that we can submit an invoice
   226  	// for.
   227  	invoiceDates := make(map[uint]map[uint]struct{}) // [year][month]struct{}
   228  	for _, v := range uir.Invoices {
   229  		_, ok := invoiceDates[v.Input.Year]
   230  		if !ok {
   231  			invoiceDates[v.Input.Year] = make(map[uint]struct{}, 12)
   232  		}
   233  		invoiceDates[v.Input.Year][v.Input.Month] = struct{}{}
   234  	}
   235  
   236  	y, m, _ := time.Now().AddDate(0, -1, 0).Date()
   237  	var (
   238  		month = uint(m) // Last month
   239  		year  = uint(y) // Current year
   240  		found bool
   241  	)
   242  	for !found {
   243  		_, ok := invoiceDates[year]
   244  		if !ok {
   245  			found = true
   246  			continue
   247  		}
   248  		_, ok = invoiceDates[year][month]
   249  		if !ok {
   250  			found = true
   251  			continue
   252  		}
   253  
   254  		// Decrement month
   255  		switch {
   256  		case month > 1:
   257  			month -= 1
   258  		case month == 1:
   259  			month = 12
   260  			year -= 1
   261  		default:
   262  			return nil, fmt.Errorf("invalid date")
   263  		}
   264  	}
   265  
   266  	// Submit an invoice for the first available month
   267  	ier := cms.InvoiceExchangeRate{
   268  		Month: month,
   269  		Year:  year,
   270  	}
   271  	ierr, err := client.InvoiceExchangeRate(&ier)
   272  	if err != nil {
   273  		return nil, fmt.Errorf("InvoiceExchangeRate %v: %v",
   274  			ier, err)
   275  	}
   276  	address, err := u.paymentAddress()
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  	ii := cms.InvoiceInput{
   281  		Month:              month,
   282  		Year:               year,
   283  		ExchangeRate:       ierr.ExchangeRate,
   284  		ContractorName:     u.Username,
   285  		ContractorLocation: "Mars",
   286  		ContractorContact:  u.Email,
   287  		ContractorRate:     contractorRate * 100, // In cents
   288  		PaymentAddress:     address,
   289  		LineItems: []cms.LineItemsInput{
   290  			{
   291  				Type:          cms.LineItemTypeLabor,
   292  				Domain:        "Development",
   293  				Subdomain:     "politeia",
   294  				Description:   "PR 999: politeiawww: Add stuff.",
   295  				ProposalToken: "",
   296  				SubUserID:     "",
   297  				SubRate:       0,
   298  				Labor:         uint(labor * 60), // In minutes
   299  				Expenses:      0,
   300  			},
   301  		},
   302  	}
   303  	b, err := json.Marshal(ii)
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  	files := []www.File{
   308  		{
   309  			Name:    "invoice.json",
   310  			MIME:    mime.DetectMimeType(b),
   311  			Digest:  hex.EncodeToString(util.Digest(b)),
   312  			Payload: base64.StdEncoding.EncodeToString(b),
   313  		},
   314  	}
   315  	sig, err := signedMerkleRoot(files, nil, cfg.Identity)
   316  	if err != nil {
   317  		return nil, err
   318  	}
   319  	ni := &cms.NewInvoice{
   320  		Files:     files,
   321  		PublicKey: hex.EncodeToString(cfg.Identity.Public.Key[:]),
   322  		Signature: sig,
   323  		Month:     month,
   324  		Year:      year,
   325  	}
   326  	nir, err := client.NewInvoice(ni)
   327  	if err != nil {
   328  		return nil, fmt.Errorf("NewInvoice: %v", err)
   329  	}
   330  
   331  	// Verify the censorship record
   332  	vr, err := client.Version()
   333  	if err != nil {
   334  		return nil, fmt.Errorf("Version: %v", err)
   335  	}
   336  	ir := cms.InvoiceRecord{
   337  		Files:            ni.Files,
   338  		PublicKey:        ni.PublicKey,
   339  		Signature:        ni.Signature,
   340  		CensorshipRecord: nir.CensorshipRecord,
   341  	}
   342  	err = verifyInvoice(ir, vr.PubKey)
   343  	if err != nil {
   344  		return nil, fmt.Errorf("unable to verify invoice %v: %v",
   345  			ir.CensorshipRecord.Token, err)
   346  	}
   347  
   348  	// Log the user out
   349  	err = logout()
   350  	if err != nil {
   351  		return nil, fmt.Errorf("logout: %v", err)
   352  	}
   353  
   354  	return &nir.CensorshipRecord, nil
   355  }
   356  
   357  // invoiceSetStatus sets the status of the provided invoice.
   358  //
   359  // Changing an invoic's status to InvoiceStatusPaid is not a valid status
   360  // transition. The invoicesPay() function must be used instead if you want
   361  // to manually mark invoices as paid.
   362  //
   363  // This function returns with the admin logged out.
   364  func invoiceSetStatus(admin user, token string, s cms.InvoiceStatusT) error {
   365  	err := login(admin)
   366  	if err != nil {
   367  		return fmt.Errorf("login: %v", err)
   368  	}
   369  
   370  	// Get the most recent version of the DCC to pull the version
   371  	// from it.
   372  	idr, err := client.InvoiceDetails(token, nil)
   373  	if err != nil {
   374  		return fmt.Errorf("InvoiceDetails: %v", err)
   375  	}
   376  
   377  	// Set the invoice status
   378  	c := SetInvoiceStatusCmd{}
   379  	c.Args.Version = idr.Invoice.Version
   380  	c.Args.Token = token
   381  	c.Args.Status = strconv.Itoa(int(s))
   382  	c.Args.Reason = "some reason...."
   383  	err = c.Execute(nil)
   384  	if err != nil {
   385  		return fmt.Errorf("SetInvoiceStatusCmd: %v", err)
   386  	}
   387  
   388  	err = logout()
   389  	if err != nil {
   390  		return fmt.Errorf("logout: %v", err)
   391  	}
   392  
   393  	return nil
   394  }
   395  
   396  // invoicesPays marks all of the currently approved invoices as paid.
   397  //
   398  // This function returns with the admin logged out.
   399  func invoicesPay(admin user) error {
   400  	err := login(admin)
   401  	if err != nil {
   402  		return fmt.Errorf("login: %v", err)
   403  	}
   404  
   405  	_, err = client.PayInvoices(&cms.PayInvoices{})
   406  	if err != nil {
   407  		return fmt.Errorf("PayInvoices: %v", err)
   408  	}
   409  
   410  	err = logout()
   411  	if err != nil {
   412  		return fmt.Errorf("logout: %v", err)
   413  	}
   414  
   415  	return nil
   416  }
   417  
   418  // dccNew submits a new DCC to cmswww then returns the submitted DCC.
   419  //
   420  // This function returns with the user logged out.
   421  func dccNew(sponsor user, nomineeID string, dcct cms.DCCTypeT, dt cms.DomainTypeT, ct cms.ContractorTypeT) (*cms.DCCRecord, error) {
   422  	err := login(sponsor)
   423  	if err != nil {
   424  		return nil, fmt.Errorf("login: %v", err)
   425  	}
   426  
   427  	// We can't use the NewDCCCmd here because we need the
   428  	// censorship token that is returned in the reply. Run
   429  	// the command manually.
   430  	di := cms.DCCInput{
   431  		Type:             dcct,
   432  		NomineeUserID:    nomineeID,
   433  		Domain:           dt,
   434  		ContractorType:   ct,
   435  		SponsorStatement: "this person is good",
   436  	}
   437  	b, err := json.Marshal(di)
   438  	if err != nil {
   439  		return nil, err
   440  	}
   441  	f := www.File{
   442  		Name:    "dcc.json",
   443  		MIME:    mime.DetectMimeType(b),
   444  		Digest:  hex.EncodeToString(util.Digest(b)),
   445  		Payload: base64.StdEncoding.EncodeToString(b),
   446  	}
   447  	files := []www.File{f}
   448  	sig, err := signedMerkleRoot(files, nil, cfg.Identity)
   449  	if err != nil {
   450  		return nil, err
   451  	}
   452  	nd := cms.NewDCC{
   453  		File:      f,
   454  		PublicKey: hex.EncodeToString(cfg.Identity.Public.Key[:]),
   455  		Signature: sig,
   456  	}
   457  	ndr, err := client.NewDCC(nd)
   458  	if err != nil {
   459  		return nil, fmt.Errorf("NewDCC: %v", err)
   460  	}
   461  
   462  	// Get the full DCC record to return
   463  	dcc, err := dcc(ndr.CensorshipRecord.Token)
   464  	if err != nil {
   465  		return nil, fmt.Errorf("dcc: %v", err)
   466  	}
   467  
   468  	// Log the user out
   469  	err = logout()
   470  	if err != nil {
   471  		return nil, fmt.Errorf("logout: %v", err)
   472  	}
   473  
   474  	return dcc, nil
   475  }
   476  
   477  // dcc returns the DCCRecord for the given token.
   478  func dcc(token string) (*cms.DCCRecord, error) {
   479  	ddr, err := client.DCCDetails(token)
   480  	if err != nil {
   481  		return nil, err
   482  	}
   483  	return &ddr.DCC, nil
   484  }
   485  
   486  // dccSetStatus updates the status of the given DCC.
   487  //
   488  // This function returns with the admin logged out.
   489  func dccSetStatus(admin user, token string, st cms.DCCStatusT, reason string) error {
   490  	err := login(admin)
   491  	if err != nil {
   492  		return fmt.Errorf("login: %v", err)
   493  	}
   494  
   495  	sds := SetDCCStatusCmd{}
   496  	sds.Args.Token = token
   497  	sds.Args.Status = strconv.Itoa(int(st))
   498  	sds.Args.Reason = reason
   499  	err = sds.Execute(nil)
   500  	if err != nil {
   501  		return fmt.Errorf("SetDCCStatusCmd: %v", err)
   502  	}
   503  
   504  	err = logout()
   505  	if err != nil {
   506  		return fmt.Errorf("logout: %v", err)
   507  	}
   508  	return nil
   509  }
   510  
   511  // dccSupport casts a support or oppose vote with the provided user for the
   512  // provided dcc.
   513  //
   514  // This function returns with the user logged out.
   515  func dccSupport(u user, token, vote string) error {
   516  	err := login(u)
   517  	if err != nil {
   518  		return fmt.Errorf("login: %v", err)
   519  	}
   520  
   521  	c := SupportOpposeDCCCmd{}
   522  	c.Args.Token = token
   523  	c.Args.Vote = vote
   524  	err = c.Execute(nil)
   525  	if err != nil {
   526  		return fmt.Errorf("SupportOpposeDCCCmd: %v", err)
   527  	}
   528  
   529  	err = logout()
   530  	if err != nil {
   531  		return fmt.Errorf("logout: %v", err)
   532  	}
   533  
   534  	return nil
   535  }
   536  
   537  // dccStartVote starts an all contractor vote for the provided DCC.
   538  //
   539  // This function returns with the admin logged out.
   540  func dccStartVote(admin user, token string) error {
   541  	err := login(admin)
   542  	if err != nil {
   543  		return fmt.Errorf("login: %v", err)
   544  	}
   545  
   546  	svc := StartVoteCmd{}
   547  	svc.Args.Token = token
   548  	err = svc.Execute(nil)
   549  	if err != nil {
   550  		return fmt.Errorf("StartVoteCmd: %v", err)
   551  	}
   552  
   553  	err = logout()
   554  	if err != nil {
   555  		return fmt.Errorf("logout: %v", err)
   556  	}
   557  
   558  	return nil
   559  }
   560  
   561  // dccVote casts a support or oppose vote with the provided user for the
   562  // provided dcc all contractor vote.
   563  //
   564  // This function returns with the user logged out.
   565  func dccVote(u user, token, vote string) error {
   566  	err := login(u)
   567  	if err != nil {
   568  		return fmt.Errorf("login: %v", err)
   569  	}
   570  
   571  	c := VoteDCCCmd{}
   572  	c.Args.Token = token
   573  	c.Args.Vote = vote
   574  	err = c.Execute(nil)
   575  	if err != nil {
   576  		return fmt.Errorf("VoteDCCCmd: %v", err)
   577  	}
   578  
   579  	err = logout()
   580  	if err != nil {
   581  		return fmt.Errorf("logout: %v", err)
   582  	}
   583  
   584  	return nil
   585  }
   586  
   587  // dccVoteSummary returns the DCC vote summary for the provided DCC token.
   588  //
   589  // This function returns with the user logged out.
   590  func dccVoteSummary(u user, token string) (*cms.VoteSummary, error) {
   591  	err := login(u)
   592  	if err != nil {
   593  		return nil, fmt.Errorf("login: %v", err)
   594  	}
   595  
   596  	ddr, err := client.DCCDetails(token)
   597  	if err != nil {
   598  		return nil, fmt.Errorf("DCCDetails: %v", err)
   599  	}
   600  
   601  	err = logout()
   602  	if err != nil {
   603  		return nil, fmt.Errorf("logout: %v", err)
   604  	}
   605  
   606  	return &ddr.VoteSummary, nil
   607  }
   608  
   609  // dccCommentNew posts a new comment to the provided DCC.
   610  //
   611  // This function returns with the user logged out.
   612  func dccCommentNew(u user, token, parentID, comment string) error {
   613  	if parentID == "" {
   614  		parentID = "0"
   615  	}
   616  
   617  	err := login(u)
   618  	if err != nil {
   619  		return fmt.Errorf("login: %v", err)
   620  	}
   621  
   622  	c := NewDCCCommentCmd{}
   623  	c.Args.Token = token
   624  	c.Args.ParentID = parentID
   625  	c.Args.Comment = comment
   626  	err = c.Execute(nil)
   627  	if err != nil {
   628  		return fmt.Errorf("NewDCCCommentCmd: %v", err)
   629  	}
   630  
   631  	err = logout()
   632  	if err != nil {
   633  		return fmt.Errorf("logout: %v", err)
   634  	}
   635  
   636  	return nil
   637  }
   638  
   639  // testDCC tests the DCC (Decred Contractor Clearance) routes. See the proposal
   640  // below for more information about the DCC process.
   641  //
   642  // https://proposals.decred.org/proposals/fa38a35
   643  //
   644  // A new cms user must have their contractor type updated before they are able
   645  // to submit invoices. There are currently two ways for this to happen.
   646  //  1. An admin can update it manually using the CMSManageUser route.
   647  //  2. The contractor can undergo the DCC process. The DCC process is where
   648  //     the new contractor is nominated by an existing contractor then other
   649  //     existing contractors can support or oppose the DCC nomination. Admins
   650  //     currently have final say in the approval of a DCC. If a DCC is
   651  //     contentious, it can be put up for an all contractor vote where existing
   652  //     contractor votes are weighted by the amount of hours they've billed.
   653  //     Once a DCC has been approved, the user's ContractorType is automatically
   654  //     updated and they are able to submit invoices.
   655  //
   656  // testDCC runs through the full DCC process with the exception of the all
   657  // contractor vote for contentious DCCs. See testDCCVote() for details on the
   658  // all contractor vote.
   659  func testDCC(admin user) error {
   660  	fmt.Printf("Running testDCC\n")
   661  
   662  	// Create three users and make them existing contractors
   663  	fmt.Printf("  create existing contractors\n")
   664  
   665  	c1, err := contractorNew(admin, cms.DomainTypeDeveloper,
   666  		cms.ContractorTypeDirect)
   667  	if err != nil {
   668  		return err
   669  	}
   670  	c2, err := contractorNew(admin, cms.DomainTypeDeveloper,
   671  		cms.ContractorTypeDirect)
   672  	if err != nil {
   673  		return err
   674  	}
   675  	c3, err := contractorNew(admin, cms.DomainTypeDeveloper,
   676  		cms.ContractorTypeDirect)
   677  	if err != nil {
   678  		return err
   679  	}
   680  
   681  	// Create a nominee
   682  	fmt.Printf("  create a user to be the nominee\n")
   683  
   684  	n1, err := userNew(admin)
   685  	if err != nil {
   686  		return err
   687  	}
   688  
   689  	// Create a new DCC for the nominee
   690  	fmt.Printf("  create a DCC for the nominee: ")
   691  
   692  	dcc, err := dccNew(*c1, n1.ID, cms.DCCTypeIssuance,
   693  		cms.DomainTypeDeveloper, cms.ContractorTypeDirect)
   694  	if err != nil {
   695  		return err
   696  	}
   697  	dccToken := dcc.CensorshipRecord.Token
   698  
   699  	fmt.Printf("%v\n", dccToken)
   700  
   701  	// Comment on the DCC
   702  	fmt.Printf("  comment on the DCC\n")
   703  
   704  	err = dccCommentNew(*c1, dccToken, "", randomString())
   705  	if err != nil {
   706  		return err
   707  	}
   708  	err = dccCommentNew(*c2, dccToken, "", randomString())
   709  	if err != nil {
   710  		return err
   711  	}
   712  	err = dccCommentNew(*c3, dccToken, "", randomString())
   713  	if err != nil {
   714  		return err
   715  	}
   716  
   717  	// Support the DCC
   718  	fmt.Printf("  support the DCC\n")
   719  
   720  	err = dccSupport(*c2, dccToken, dccIssuanceSupport)
   721  	if err != nil {
   722  		return err
   723  	}
   724  	err = dccSupport(*c3, dccToken, dccIssuanceSupport)
   725  	if err != nil {
   726  		return err
   727  	}
   728  
   729  	// Have admin approve the DCC
   730  	fmt.Printf("  approve the DCC\n")
   731  
   732  	err = dccSetStatus(admin, dccToken, cms.DCCStatusApproved,
   733  		"there was non-contentious support")
   734  	if err != nil {
   735  		return err
   736  	}
   737  
   738  	// Create a nominee. This time we'll reject their DCC.
   739  	fmt.Printf("  create a user to be the nominee\n")
   740  
   741  	n2, err := userNew(admin)
   742  	if err != nil {
   743  		return err
   744  	}
   745  
   746  	// Create a new DCC for the nominee
   747  	fmt.Printf("  create a DCC for the nominee: ")
   748  
   749  	dcc, err = dccNew(*c1, n2.ID, cms.DCCTypeIssuance,
   750  		cms.DomainTypeDeveloper, cms.ContractorTypeDirect)
   751  	if err != nil {
   752  		return err
   753  	}
   754  	dccToken = dcc.CensorshipRecord.Token
   755  
   756  	fmt.Printf("%v\n", dccToken)
   757  
   758  	// Oppose the DCC
   759  	fmt.Printf("  oppose the DCC\n")
   760  
   761  	err = dccSupport(*c2, dccToken, dccIssuanceOppose)
   762  	if err != nil {
   763  		return err
   764  	}
   765  	err = dccSupport(*c3, dccToken, dccIssuanceOppose)
   766  	if err != nil {
   767  		return err
   768  	}
   769  
   770  	// Have admin reject the DCC
   771  	fmt.Printf("  reject the DCC\n")
   772  
   773  	err = dccSetStatus(admin, dccToken, cms.DCCStatusRejected,
   774  		"there was non-contentious opposition")
   775  	if err != nil {
   776  		return err
   777  	}
   778  
   779  	fmt.Printf("testDCC success!\n")
   780  
   781  	return nil
   782  }
   783  
   784  // testDCCVote tests the DCC all contractor vote routes.
   785  //
   786  // When a DCC proposal is deemed contentious by the admin, the admin can start
   787  // an all contractor vote for the DCC. Contractors are able to cast votes
   788  // proportional to the amount of time they've billed, and that has been paid,
   789  // over the previous 6 months. Admins still currently have final say over
   790  // whether to approve or reject the DCC.
   791  //
   792  // The admin starts an all contractor vote on a DCC by setting the DCC status
   793  // to DCCStatusAllVote.
   794  func testDCCVote(admin user) error {
   795  	fmt.Printf("Running testDCCVote\n")
   796  
   797  	// Create three users and make them existing contractors
   798  	fmt.Printf("  create existing contractors\n")
   799  
   800  	c1, err := contractorNew(admin, cms.DomainTypeDeveloper,
   801  		cms.ContractorTypeDirect)
   802  	if err != nil {
   803  		return err
   804  	}
   805  	c2, err := contractorNew(admin, cms.DomainTypeDeveloper,
   806  		cms.ContractorTypeDirect)
   807  	if err != nil {
   808  		return err
   809  	}
   810  	c3, err := contractorNew(admin, cms.DomainTypeDeveloper,
   811  		cms.ContractorTypeDirect)
   812  	if err != nil {
   813  		return err
   814  	}
   815  
   816  	// Submit and pay invoices for the users so that we can make sure
   817  	// the vote weights are being calculated correctly.
   818  	fmt.Printf("  submitting invoices for contractors\n")
   819  
   820  	cr1, err := invoiceNew(*c1, 40, 50)
   821  	if err != nil {
   822  		return err
   823  	}
   824  	cr2, err := invoiceNew(*c2, 40, 100)
   825  	if err != nil {
   826  		return err
   827  	}
   828  	cr3, err := invoiceNew(*c3, 40, 150)
   829  	if err != nil {
   830  		return err
   831  	}
   832  
   833  	fmt.Printf("  marking invoices as approved and paid\n")
   834  
   835  	err = invoiceSetStatus(admin, cr1.Token, cms.InvoiceStatusApproved)
   836  	if err != nil {
   837  		return err
   838  	}
   839  	err = invoiceSetStatus(admin, cr2.Token, cms.InvoiceStatusApproved)
   840  	if err != nil {
   841  		return err
   842  	}
   843  	err = invoiceSetStatus(admin, cr3.Token, cms.InvoiceStatusApproved)
   844  	if err != nil {
   845  		return err
   846  	}
   847  	err = invoicesPay(admin)
   848  	if err != nil {
   849  		return err
   850  	}
   851  
   852  	// Create a nominee
   853  	fmt.Printf("  create a user to be the nominee\n")
   854  
   855  	n1, err := userNew(admin)
   856  	if err != nil {
   857  		return err
   858  	}
   859  
   860  	// Create a new DCC for the nominee
   861  	fmt.Printf("  create a DCC for the nominee: ")
   862  
   863  	dcc, err := dccNew(*c1, n1.ID, cms.DCCTypeIssuance,
   864  		cms.DomainTypeDeveloper, cms.ContractorTypeDirect)
   865  	if err != nil {
   866  		return err
   867  	}
   868  	dccToken := dcc.CensorshipRecord.Token
   869  
   870  	fmt.Printf("%v\n", dccToken)
   871  
   872  	// Support/oppose the DCC
   873  	fmt.Printf("  support/oppose DCC\n")
   874  
   875  	err = dccSupport(*c2, dccToken, dccIssuanceSupport)
   876  	if err != nil {
   877  		return err
   878  	}
   879  	err = dccSupport(*c3, dccToken, dccIssuanceOppose)
   880  	if err != nil {
   881  		return err
   882  	}
   883  
   884  	// Start an all contractor vote for the DCC
   885  	fmt.Printf("  start DCC vote\n")
   886  	err = dccStartVote(admin, dccToken)
   887  	if err != nil {
   888  		return err
   889  	}
   890  
   891  	// Vote on the DCC
   892  	fmt.Printf("  cast DCC votes\n")
   893  	err = dccVote(*c2, dccToken, cmsplugin.DCCApprovalString)
   894  	if err != nil {
   895  		return err
   896  	}
   897  	expectedApprovalVotes := uint64(1)
   898  	err = dccVote(*c3, dccToken, cmsplugin.DCCDisapprovalString)
   899  	if err != nil {
   900  		return err
   901  	}
   902  	expectedDisapprovalVotes := uint64(1)
   903  	// Check to see that the votes are properly cast in gitbe
   904  	fmt.Printf("  check DCC votes")
   905  	vs, err := dccVoteSummary(*c2, dccToken)
   906  	if err != nil {
   907  		return err
   908  	}
   909  	expectedVoteOptionResults := 2
   910  	if len(vs.Results) != expectedVoteOptionResults {
   911  		return fmt.Errorf("unexpected number of vote option results: got %v,"+
   912  			" wanted %v", len(vs.Results), expectedVoteOptionResults)
   913  	}
   914  	for _, result := range vs.Results {
   915  		if result.Option.Id == cmsplugin.DCCApprovalString &&
   916  			result.VotesReceived != expectedApprovalVotes {
   917  			return fmt.Errorf("unexpected amount of %v votes, got %v wanted %v",
   918  				cmsplugin.DCCApprovalString, result.VotesReceived,
   919  				expectedApprovalVotes)
   920  		}
   921  		if result.Option.Id == cmsplugin.DCCDisapprovalString &&
   922  			result.VotesReceived != expectedDisapprovalVotes {
   923  			return fmt.Errorf("unexpected amount of %v votes, got %v wanted %v",
   924  				cmsplugin.DCCDisapprovalString, result.VotesReceived,
   925  				expectedDisapprovalVotes)
   926  		}
   927  	}
   928  	/*
   929  		// Have the admin approve the DCC
   930  		// This can't be done in the test run because the vote needs to
   931  		// have ended before the admin can change the status. This is how
   932  		// a DCC is ultimately decided as approved or rejected though.
   933  		err = dccSetStatus(admin, dccToken, cms.DCCStatusApproved,
   934  			"I decree it to be")
   935  		if err != nil {
   936  			return err
   937  		}
   938  	*/
   939  
   940  	fmt.Printf("testDCCVote success!\n")
   941  
   942  	return nil
   943  }
   944  
   945  // Execute executes the TestRun command.
   946  func (cmd *TestRunCmd) Execute(args []string) error {
   947  	const (
   948  		modeCMSWWW = "cmswww"
   949  	)
   950  
   951  	// Suppress output from cli commands
   952  	cfg.Silent = true
   953  
   954  	fmt.Printf("Running pre-testrun validation\n")
   955  
   956  	// Validate politeiawww setup
   957  	vr, err := client.Version()
   958  	switch {
   959  	case err != nil:
   960  		return fmt.Errorf("version: %v", err)
   961  	case vr.Mode != modeCMSWWW:
   962  		return fmt.Errorf("politeiawww is not in cmswww mode")
   963  	case !vr.TestNet:
   964  		return fmt.Errorf("politeiawww is not on testnet")
   965  	}
   966  
   967  	// Validate admin credentials
   968  	admin := user{
   969  		Email:    cmd.Args.AdminEmail,
   970  		Password: cmd.Args.AdminPassword,
   971  	}
   972  	err = login(admin)
   973  	if err != nil {
   974  		return err
   975  	}
   976  	lr, err := client.Me()
   977  	if err != nil {
   978  		return err
   979  	}
   980  	if !lr.IsAdmin {
   981  		return fmt.Errorf("%v is not an admin", admin.Email)
   982  	}
   983  	admin.Username = lr.Username
   984  
   985  	// Test runs
   986  	err = testDCC(admin)
   987  	if err != nil {
   988  		return fmt.Errorf("testDCC: %v", err)
   989  	}
   990  	err = testDCCVote(admin)
   991  	if err != nil {
   992  		return fmt.Errorf("testDCCVote: %v", err)
   993  	}
   994  
   995  	fmt.Printf("testrun complete!\n")
   996  
   997  	return nil
   998  }