github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/comments/cmds_test.go (about)

     1  // Copyright (c) 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 comments
     6  
     7  import (
     8  	"encoding/hex"
     9  	"encoding/json"
    10  	"strconv"
    11  	"testing"
    12  
    13  	"github.com/decred/politeia/politeiad/api/v1/identity"
    14  	backend "github.com/decred/politeia/politeiad/backendv2"
    15  	"github.com/decred/politeia/politeiad/plugins/comments"
    16  	"github.com/decred/politeia/politeiad/plugins/pi"
    17  	"github.com/pkg/errors"
    18  )
    19  
    20  func TestCollectVoteDigestsPage(t *testing.T) {
    21  	// Setup test data
    22  	userIDs := []string{"user1", "user2", "user3"}
    23  	commentIDs := []uint32{1, 2, 3}
    24  	token := "testtoken"
    25  	// Use page size 2 for testing
    26  	pageSize := uint32(2)
    27  
    28  	// Create a comment indexes map for testing with three comment IDs,
    29  	// which has one comment vote on the first comment from "user1",
    30  	// another two comment votes on the second second comment from
    31  	// "user1" and "user2", and lastly another three comment votes on
    32  	// the third comment from all three test users.
    33  	commentIdxes := make(map[uint32]commentIndex, len(commentIDs))
    34  	for _, commentID := range commentIDs {
    35  		// Prepare comment index Votes map
    36  		commentIdx := commentIndex{
    37  			Votes: make(map[string][]voteIndex, commentID),
    38  		}
    39  
    40  		users := userIDs[:commentID]
    41  		for _, userID := range users {
    42  			be, err := convertBlobEntryFromCommentVote(comments.CommentVote{
    43  				UserID:    userID,
    44  				State:     comments.RecordStateVetted,
    45  				Token:     token,
    46  				CommentID: commentID,
    47  				Vote:      comments.VoteUpvote,
    48  				PublicKey: "pubkey",
    49  				Signature: "signature",
    50  				Timestamp: 1,
    51  				Receipt:   "receipt",
    52  			})
    53  			if err != nil {
    54  				t.Error(err)
    55  			}
    56  			d, err := hex.DecodeString(be.Digest)
    57  			if err != nil {
    58  				t.Error(err)
    59  			}
    60  			commentIdx.Votes[userID] = []voteIndex{
    61  				{
    62  					Digest: d,
    63  					Vote:   comments.VoteUpvote,
    64  				},
    65  			}
    66  		}
    67  
    68  		commentIdxes[commentID] = commentIdx
    69  	}
    70  
    71  	// Setup tests
    72  	tests := []struct {
    73  		name                 string
    74  		page                 uint32
    75  		userID               string
    76  		resultExpectedLength int
    77  	}{
    78  		{
    79  			name:                 "first user's first page",
    80  			page:                 1,
    81  			userID:               userIDs[0],
    82  			resultExpectedLength: 2,
    83  		},
    84  		{
    85  			name:                 "first user's second page",
    86  			page:                 2,
    87  			userID:               userIDs[0],
    88  			resultExpectedLength: 1,
    89  		},
    90  		{
    91  			name:                 "first user's third page",
    92  			page:                 3,
    93  			userID:               userIDs[0],
    94  			resultExpectedLength: 0,
    95  		},
    96  		{
    97  			name:                 "second user's first page",
    98  			page:                 1,
    99  			userID:               userIDs[1],
   100  			resultExpectedLength: 2,
   101  		},
   102  		{
   103  			name:                 "second user's second page",
   104  			page:                 2,
   105  			userID:               userIDs[1],
   106  			resultExpectedLength: 0,
   107  		},
   108  		{
   109  			name:                 "third user's first page",
   110  			page:                 1,
   111  			userID:               userIDs[2],
   112  			resultExpectedLength: 1,
   113  		},
   114  		{
   115  			name:                 "third user's second page",
   116  			page:                 2,
   117  			userID:               userIDs[2],
   118  			resultExpectedLength: 0,
   119  		},
   120  		{
   121  			name:                 "all votes first page",
   122  			page:                 1,
   123  			userID:               "",
   124  			resultExpectedLength: 2,
   125  		},
   126  		{
   127  			name:                 "all votes second page",
   128  			page:                 2,
   129  			userID:               "",
   130  			resultExpectedLength: 2,
   131  		},
   132  		{
   133  			name:                 "all votes third page",
   134  			page:                 3,
   135  			userID:               "",
   136  			resultExpectedLength: 2,
   137  		},
   138  		{
   139  			name:                 "all votes forth page",
   140  			page:                 4,
   141  			userID:               "",
   142  			resultExpectedLength: 0,
   143  		},
   144  		{
   145  			name:                 "default to first page with filtering criteria",
   146  			page:                 0,
   147  			userID:               userIDs[2],
   148  			resultExpectedLength: 1,
   149  		},
   150  		{
   151  			name:                 "default to first page w/o filtering criteria",
   152  			page:                 0,
   153  			userID:               "",
   154  			resultExpectedLength: 2,
   155  		},
   156  	}
   157  
   158  	// Run tests
   159  	for _, tc := range tests {
   160  		t.Run(tc.name, func(t *testing.T) {
   161  			// Run test
   162  			digests := collectVoteDigestsPage(commentIdxes, tc.userID, tc.page,
   163  				pageSize)
   164  
   165  			// Verify length of returned page
   166  			if len(digests) != tc.resultExpectedLength {
   167  				t.Errorf("unexpected result length; want %v, got %v",
   168  					commentIdxes, digests)
   169  			}
   170  		})
   171  	}
   172  }
   173  
   174  func TestCmdEdit(t *testing.T) {
   175  	// Setup comments plugin
   176  	c, cleanup := newTestCommentsPlugin(t)
   177  	defer cleanup()
   178  
   179  	// Setup an identity that will be used to create the payload
   180  	// signatures.
   181  	fid, err := identity.New()
   182  	if err != nil {
   183  		t.Fatal(err)
   184  	}
   185  
   186  	// Setup test data
   187  	var (
   188  		// Valid input
   189  		token         = "45154fb45664714b"
   190  		userID        = "6dc1c8ca-abb5-4631-8ed4-f991b0169770"
   191  		state         = comments.RecordStateVetted
   192  		parentID      = uint32(0)
   193  		commentID     = uint32(1)
   194  		comment       = "comment"
   195  		extraData     = ""
   196  		extraDataHint = ""
   197  		publicKey     = fid.Public.String()
   198  
   199  		msg = strconv.FormatUint(uint64(state), 10) + token +
   200  			strconv.FormatUint(uint64(parentID), 10) +
   201  			strconv.FormatUint(uint64(commentID), 10) +
   202  			comment + extraData + extraDataHint
   203  		signatureb = fid.SignMessage([]byte(msg))
   204  		signature  = hex.EncodeToString(signatureb[:])
   205  
   206  		// signatureIsWrong is a valid hex encoded, ed25519 signature,
   207  		// but that does not correspond to the valid input parameters
   208  		// listed above.
   209  		signatureIsWrong = "b387f678e1236ca1784c4bc77912c754c6b122dd8b" +
   210  			"3e499617706dd0bd09167a113e59339d2ce4b3570af37a092ba88f39e7f" +
   211  			"c93a5ac7513e52dca3e5e13f705"
   212  	)
   213  	tokenb, err := hex.DecodeString(token)
   214  	if err != nil {
   215  		t.Fatal(err)
   216  	}
   217  
   218  	// Setup tests
   219  	var tests = []struct {
   220  		name       string // Test name
   221  		token      []byte
   222  		e          comments.Edit
   223  		allowEdits bool
   224  		err        error // Expected error output
   225  	}{
   226  		{
   227  			"comment edits not allowed",
   228  			tokenb,
   229  			edit(t, fid,
   230  				comments.Edit{
   231  					UserID:        userID,
   232  					State:         state,
   233  					Token:         token,
   234  					ParentID:      parentID,
   235  					CommentID:     commentID,
   236  					Comment:       comment,
   237  					ExtraData:     extraData,
   238  					ExtraDataHint: extraDataHint,
   239  				}),
   240  			false,
   241  			pluginError(comments.ErrorCodeEditNotAllowed),
   242  		},
   243  		{
   244  			"payload token invalid",
   245  			tokenb,
   246  			edit(t, fid,
   247  				comments.Edit{
   248  					UserID:        userID,
   249  					State:         state,
   250  					Token:         "invalid-token",
   251  					ParentID:      parentID,
   252  					CommentID:     commentID,
   253  					Comment:       comment,
   254  					ExtraData:     extraData,
   255  					ExtraDataHint: extraDataHint,
   256  				}),
   257  			true,
   258  			pluginError(comments.ErrorCodeTokenInvalid),
   259  		},
   260  		{
   261  			"payload token does not match cmd token",
   262  			tokenb,
   263  			edit(t, fid,
   264  				comments.Edit{
   265  					UserID:        userID,
   266  					State:         state,
   267  					Token:         "da70d0766348340c",
   268  					ParentID:      parentID,
   269  					CommentID:     commentID,
   270  					Comment:       comment,
   271  					ExtraData:     extraData,
   272  					ExtraDataHint: extraDataHint,
   273  				}),
   274  			true,
   275  			pluginError(comments.ErrorCodeTokenInvalid),
   276  		},
   277  		{
   278  			"signature is not hex",
   279  			tokenb,
   280  			comments.Edit{
   281  				UserID:        userID,
   282  				State:         state,
   283  				Token:         token,
   284  				ParentID:      parentID,
   285  				CommentID:     commentID,
   286  				Comment:       comment,
   287  				ExtraData:     extraData,
   288  				ExtraDataHint: extraDataHint,
   289  				PublicKey:     publicKey,
   290  				Signature:     "zzz",
   291  			},
   292  			true,
   293  			pluginError(comments.ErrorCodeSignatureInvalid),
   294  		},
   295  		{
   296  			"signature is the wrong size",
   297  			tokenb,
   298  			comments.Edit{
   299  				UserID:        userID,
   300  				State:         state,
   301  				Token:         token,
   302  				ParentID:      parentID,
   303  				CommentID:     commentID,
   304  				Comment:       comment,
   305  				ExtraData:     extraData,
   306  				ExtraDataHint: extraDataHint,
   307  				PublicKey:     publicKey,
   308  				Signature:     "123456",
   309  			},
   310  			true,
   311  			pluginError(comments.ErrorCodeSignatureInvalid),
   312  		},
   313  		{
   314  			"signature is wrong",
   315  			tokenb,
   316  			comments.Edit{
   317  				UserID:        userID,
   318  				State:         state,
   319  				Token:         token,
   320  				ParentID:      parentID,
   321  				CommentID:     commentID,
   322  				Comment:       comment,
   323  				ExtraData:     extraData,
   324  				ExtraDataHint: extraDataHint,
   325  				PublicKey:     publicKey,
   326  				Signature:     signatureIsWrong,
   327  			},
   328  			true,
   329  			pluginError(comments.ErrorCodeSignatureInvalid),
   330  		},
   331  		{
   332  			"public key is not a hex",
   333  			tokenb,
   334  			comments.Edit{
   335  				UserID:        userID,
   336  				State:         state,
   337  				Token:         token,
   338  				ParentID:      parentID,
   339  				CommentID:     commentID,
   340  				Comment:       comment,
   341  				ExtraData:     extraData,
   342  				ExtraDataHint: extraDataHint,
   343  				PublicKey:     "",
   344  				Signature:     signature,
   345  			},
   346  			true,
   347  			pluginError(comments.ErrorCodePublicKeyInvalid),
   348  		},
   349  		{
   350  			"public key is the wrong length",
   351  			tokenb,
   352  			comments.Edit{
   353  				UserID:        userID,
   354  				State:         state,
   355  				Token:         token,
   356  				ParentID:      parentID,
   357  				CommentID:     commentID,
   358  				Comment:       comment,
   359  				ExtraData:     extraData,
   360  				ExtraDataHint: extraDataHint,
   361  				PublicKey:     "123456",
   362  				Signature:     signature,
   363  			},
   364  			true,
   365  			pluginError(comments.ErrorCodePublicKeyInvalid),
   366  		},
   367  	}
   368  
   369  	// Run tests
   370  	for _, tc := range tests {
   371  		t.Run(tc.name, func(t *testing.T) {
   372  			// Setup command payload
   373  			b, err := json.Marshal(tc.e)
   374  			if err != nil {
   375  				t.Fatal(err)
   376  			}
   377  			payload := string(b)
   378  
   379  			// Decode the expected error into a PluginError. If
   380  			// an error is being returned it should always be a
   381  			// PluginError.
   382  			var wantErrorCode comments.ErrorCodeT
   383  			if tc.err != nil {
   384  				var pe backend.PluginError
   385  				if !errors.As(tc.err, &pe) {
   386  					t.Fatalf("error is not a plugin error '%v'", tc.err)
   387  				}
   388  				wantErrorCode = comments.ErrorCodeT(pe.ErrorCode)
   389  			}
   390  
   391  			// Run test
   392  			c.allowEdits = tc.allowEdits
   393  			_, err = c.cmdEdit(tc.token, payload)
   394  			switch {
   395  			case tc.err != nil && err == nil:
   396  				// Wanted an error but didn't get one
   397  				t.Errorf("want error '%v', got nil",
   398  					comments.ErrorCodes[wantErrorCode])
   399  				return
   400  
   401  			case tc.err == nil && err != nil:
   402  				// Wanted success but got an error
   403  				t.Errorf("want error nil, got '%v'", err)
   404  				return
   405  
   406  			case tc.err != nil && err != nil:
   407  				// Wanted an error and got an error. Verify that it's
   408  				// the correct error. All errors should be backend
   409  				// plugin errors.
   410  				var gotErr backend.PluginError
   411  				if !errors.As(err, &gotErr) {
   412  					t.Errorf("want plugin error, got '%v'", err)
   413  					return
   414  				}
   415  				if comments.PluginID != gotErr.PluginID {
   416  					t.Errorf("want plugin error with plugin ID '%v', got '%v'",
   417  						pi.PluginID, gotErr.PluginID)
   418  					return
   419  				}
   420  
   421  				gotErrorCode := comments.ErrorCodeT(gotErr.ErrorCode)
   422  				if wantErrorCode != gotErrorCode {
   423  					t.Errorf("want error '%v', got '%v'",
   424  						comments.ErrorCodes[wantErrorCode],
   425  						comments.ErrorCodes[gotErrorCode])
   426  				}
   427  
   428  				// Success; continue to next test
   429  				return
   430  
   431  			case tc.err == nil && err == nil:
   432  				// Success; continue to next test
   433  				return
   434  			}
   435  		})
   436  	}
   437  }
   438  
   439  // edit uses the provided arguments to return an Edit command
   440  // with a valid PublicKey and Signature.
   441  func edit(t *testing.T, fid *identity.FullIdentity, e comments.Edit) comments.Edit {
   442  	t.Helper()
   443  
   444  	msg := strconv.FormatUint(uint64(e.State), 10) + e.Token +
   445  		strconv.FormatUint(uint64(e.ParentID), 10) +
   446  		strconv.FormatUint(uint64(e.CommentID), 10) +
   447  		e.Comment + e.ExtraData + e.ExtraDataHint
   448  	sig := fid.SignMessage([]byte(msg))
   449  
   450  	return comments.Edit{
   451  		UserID:        e.UserID,
   452  		State:         e.State,
   453  		Token:         e.Token,
   454  		ParentID:      e.ParentID,
   455  		CommentID:     e.CommentID,
   456  		Comment:       e.Comment,
   457  		ExtraData:     e.ExtraData,
   458  		ExtraDataHint: e.ExtraDataHint,
   459  		PublicKey:     fid.Public.String(),
   460  		Signature:     hex.EncodeToString(sig[:]),
   461  	}
   462  }
   463  
   464  // pluginError returns a backend PluginError for the provided comments
   465  // ErrorCodeT.
   466  func pluginError(e comments.ErrorCodeT) error {
   467  	return backend.PluginError{
   468  		PluginID:  comments.PluginID,
   469  		ErrorCode: uint32(e),
   470  	}
   471  }
   472  
   473  func TestFinalCommentTimestamps(t *testing.T) {
   474  	token := "55154fb45664714a"
   475  
   476  	// Setup tests
   477  	tests := []struct {
   478  		name       string
   479  		commentIDs []uint32
   480  		token      string
   481  		resultIDs  []uint32
   482  	}{
   483  		{
   484  			name:       "map with one comment",
   485  			commentIDs: []uint32{1},
   486  			token:      token,
   487  			resultIDs:  []uint32{1},
   488  		},
   489  		{
   490  			name:       "map with two comments",
   491  			commentIDs: []uint32{1, 2},
   492  			token:      token,
   493  			resultIDs:  []uint32{1, 2},
   494  		},
   495  	}
   496  
   497  	// Run tests
   498  	for _, tc := range tests {
   499  		t.Run(tc.name, func(t *testing.T) {
   500  			// Create input map
   501  			m := make(map[uint32]comments.CommentTimestamp, len(tc.commentIDs))
   502  			for i := 1; i <= len(tc.commentIDs); i++ {
   503  				m[uint32(i)] = comments.CommentTimestamp{
   504  					Adds: []comments.Timestamp{{TxID: "notemty"}},
   505  				}
   506  			}
   507  
   508  			// Convert token to []byte
   509  			tokenb, err := hex.DecodeString(tc.token)
   510  			if err != nil {
   511  				t.Fatal(err)
   512  			}
   513  
   514  			// Call func
   515  			fts, err := finalCommentTimestamps(m, tokenb)
   516  			if err != nil {
   517  				t.Fatal(err)
   518  			}
   519  
   520  			// Verify result
   521  			if len(fts) != len(tc.resultIDs) {
   522  				t.Errorf("unexpected length of returned map; want: %v, got: %v",
   523  					len(tc.resultIDs), len(fts))
   524  			}
   525  			for _, cid := range tc.resultIDs {
   526  				if _, exists := fts[cid]; !exists {
   527  					t.Errorf("expected ID was not found: %v", cid)
   528  				}
   529  			}
   530  		})
   531  	}
   532  }