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 }