**NOTE: This contains a backwards incompatible database migration, so if you plan to test it, please make a copy of your database first.** Moves tickets from a single database bucket containing JSON encoded strings, to a bucket for each ticket. This change is to preemptively deal with scaling issues seen with databases containing tens of thousands of tickets.
333 lines
8.9 KiB
Go
333 lines
8.9 KiB
Go
// Copyright (c) 2020-2021 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package database
|
|
|
|
import (
|
|
"math/rand"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
|
|
// randString randomly generates a string of the requested length, using only
|
|
// characters from the provided charset.
|
|
func randString(length int, charset string) string {
|
|
b := make([]byte, length)
|
|
for i := range b {
|
|
b[i] = charset[seededRand.Intn(len(charset))]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func exampleTicket() Ticket {
|
|
const hexCharset = "1234567890abcdef"
|
|
const addrCharset = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
|
|
return Ticket{
|
|
Hash: randString(64, hexCharset),
|
|
CommitmentAddress: randString(35, addrCharset),
|
|
FeeAddressIndex: 12345,
|
|
FeeAddress: randString(35, addrCharset),
|
|
FeeAmount: 10000000,
|
|
FeeExpiration: 4,
|
|
Confirmed: false,
|
|
VoteChoices: map[string]string{"AgendaID": "Choice"},
|
|
VotingWIF: randString(53, addrCharset),
|
|
FeeTxHex: randString(504, hexCharset),
|
|
FeeTxHash: randString(64, hexCharset),
|
|
FeeTxStatus: FeeBroadcast,
|
|
}
|
|
}
|
|
|
|
func testInsertNewTicket(t *testing.T) {
|
|
// Insert a ticket into the database.
|
|
ticket := exampleTicket()
|
|
err := db.InsertNewTicket(ticket)
|
|
if err != nil {
|
|
t.Fatalf("error storing ticket in database: %v", err)
|
|
}
|
|
|
|
// Insert another ticket.
|
|
err = db.InsertNewTicket(exampleTicket())
|
|
if err != nil {
|
|
t.Fatalf("error storing ticket in database: %v", err)
|
|
}
|
|
|
|
// Inserting a ticket with the same hash should fail.
|
|
ticket2 := exampleTicket()
|
|
ticket2.Hash = ticket.Hash
|
|
err = db.InsertNewTicket(ticket2)
|
|
if err == nil {
|
|
t.Fatal("expected an error inserting ticket with duplicate hash")
|
|
}
|
|
|
|
// Inserting a ticket with the same fee address should fail.
|
|
ticket3 := exampleTicket()
|
|
ticket3.FeeAddress = ticket.FeeAddress
|
|
err = db.InsertNewTicket(ticket3)
|
|
if err == nil {
|
|
t.Fatal("expected an error inserting ticket with duplicate fee addr")
|
|
}
|
|
|
|
// Inserting a ticket with empty hash should fail.
|
|
ticket.Hash = ""
|
|
err = db.InsertNewTicket(ticket)
|
|
if err == nil {
|
|
t.Fatal("expected an error inserting ticket with no hash")
|
|
}
|
|
}
|
|
|
|
func testDeleteTicket(t *testing.T) {
|
|
// Insert a ticket into the database.
|
|
ticket := exampleTicket()
|
|
err := db.InsertNewTicket(ticket)
|
|
if err != nil {
|
|
t.Fatalf("error storing ticket in database: %v", err)
|
|
}
|
|
|
|
// Delete ticket.
|
|
err = db.DeleteTicket(ticket)
|
|
if err != nil {
|
|
t.Fatalf("error deleting ticket: %v", err)
|
|
}
|
|
|
|
// Nothing should be in the db.
|
|
_, found, err := db.GetTicketByHash(ticket.Hash)
|
|
if err != nil {
|
|
t.Fatalf("error retrieving ticket by ticket hash: %v", err)
|
|
}
|
|
if found {
|
|
t.Fatal("expected found==false")
|
|
}
|
|
}
|
|
|
|
func testGetTicketByHash(t *testing.T) {
|
|
// Insert a ticket into the database.
|
|
ticket := exampleTicket()
|
|
err := db.InsertNewTicket(ticket)
|
|
if err != nil {
|
|
t.Fatalf("error storing ticket in database: %v", err)
|
|
}
|
|
|
|
// Retrieve ticket from database.
|
|
retrieved, found, err := db.GetTicketByHash(ticket.Hash)
|
|
if err != nil {
|
|
t.Fatalf("error retrieving ticket by ticket hash: %v", err)
|
|
}
|
|
if !found {
|
|
t.Fatal("expected found==true")
|
|
}
|
|
|
|
// Check ticket fields match expected.
|
|
if retrieved.Hash != ticket.Hash ||
|
|
retrieved.CommitmentAddress != ticket.CommitmentAddress ||
|
|
retrieved.FeeAddressIndex != ticket.FeeAddressIndex ||
|
|
retrieved.FeeAddress != ticket.FeeAddress ||
|
|
retrieved.FeeAmount != ticket.FeeAmount ||
|
|
retrieved.FeeExpiration != ticket.FeeExpiration ||
|
|
retrieved.Confirmed != ticket.Confirmed ||
|
|
!reflect.DeepEqual(retrieved.VoteChoices, ticket.VoteChoices) ||
|
|
retrieved.VotingWIF != ticket.VotingWIF ||
|
|
retrieved.FeeTxHex != ticket.FeeTxHex ||
|
|
retrieved.FeeTxHash != ticket.FeeTxHash ||
|
|
retrieved.FeeTxStatus != ticket.FeeTxStatus {
|
|
t.Fatal("retrieved ticket value didnt match expected")
|
|
}
|
|
|
|
// Check found==false when requesting a non-existent ticket.
|
|
_, found, err = db.GetTicketByHash("Not a real ticket hash")
|
|
if err != nil {
|
|
t.Fatalf("error retrieving ticket by ticket hash: %v", err)
|
|
}
|
|
if found {
|
|
t.Fatal("expected found==false")
|
|
}
|
|
}
|
|
|
|
func testUpdateTicket(t *testing.T) {
|
|
ticket := exampleTicket()
|
|
// Insert a ticket into the database.
|
|
err := db.InsertNewTicket(ticket)
|
|
if err != nil {
|
|
t.Fatalf("error storing ticket in database: %v", err)
|
|
}
|
|
|
|
// Update ticket with new values.
|
|
ticket.FeeAmount = ticket.FeeAmount + 1
|
|
ticket.FeeExpiration = ticket.FeeExpiration + 1
|
|
ticket.VoteChoices = map[string]string{"New agenda": "New value"}
|
|
|
|
err = db.UpdateTicket(ticket)
|
|
if err != nil {
|
|
t.Fatalf("error updating ticket: %v", err)
|
|
}
|
|
|
|
// Retrieve updated ticket from database.
|
|
retrieved, found, err := db.GetTicketByHash(ticket.Hash)
|
|
if err != nil {
|
|
t.Fatalf("error retrieving ticket by ticket hash: %v", err)
|
|
}
|
|
if !found {
|
|
t.Fatal("expected found==true")
|
|
}
|
|
|
|
if ticket.FeeAmount != retrieved.FeeAmount ||
|
|
ticket.FeeExpiration != retrieved.FeeExpiration ||
|
|
!reflect.DeepEqual(retrieved.VoteChoices, ticket.VoteChoices) {
|
|
t.Fatal("retrieved ticket value didnt match expected")
|
|
}
|
|
|
|
// Updating a non-existent ticket should fail.
|
|
ticket.Hash = "doesnt exist"
|
|
err = db.UpdateTicket(ticket)
|
|
if err == nil {
|
|
t.Fatal("expected an error updating a ticket with non-existent hash")
|
|
}
|
|
}
|
|
|
|
func testTicketFeeExpired(t *testing.T) {
|
|
ticket := exampleTicket()
|
|
|
|
now := time.Now()
|
|
hourBefore := now.Add(-time.Hour).Unix()
|
|
hourAfter := now.Add(time.Hour).Unix()
|
|
|
|
ticket.FeeExpiration = hourAfter
|
|
if ticket.FeeExpired() {
|
|
t.Fatal("expected ticket not to be expired")
|
|
}
|
|
|
|
ticket.FeeExpiration = hourBefore
|
|
if !ticket.FeeExpired() {
|
|
t.Fatal("expected ticket to be expired")
|
|
}
|
|
}
|
|
|
|
func testFilterTickets(t *testing.T) {
|
|
// Insert a ticket.
|
|
ticket := exampleTicket()
|
|
err := db.InsertNewTicket(ticket)
|
|
if err != nil {
|
|
t.Fatalf("error storing ticket in database: %v", err)
|
|
}
|
|
|
|
// Insert another ticket.
|
|
ticket2 := exampleTicket()
|
|
ticket2.Confirmed = !ticket.Confirmed
|
|
err = db.InsertNewTicket(ticket2)
|
|
if err != nil {
|
|
t.Fatalf("error storing ticket in database: %v", err)
|
|
}
|
|
|
|
// Expect all tickets returned.
|
|
retrieved, err := db.filterTickets(func(t Ticket) bool {
|
|
return true
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("error filtering tickets: %v", err)
|
|
}
|
|
if len(retrieved) != 2 {
|
|
t.Fatalf("expected to find 2 tickets, found %d", len(retrieved))
|
|
}
|
|
|
|
// Only one ticket should be confirmed.
|
|
retrieved, err = db.filterTickets(func(t Ticket) bool {
|
|
return t.Confirmed
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("error filtering tickets: %v", err)
|
|
}
|
|
if len(retrieved) != 1 {
|
|
t.Fatalf("expected to find 1 ticket, found %d", len(retrieved))
|
|
}
|
|
if retrieved[0].Confirmed != true {
|
|
t.Fatal("expected retrieved ticket to be confirmed")
|
|
}
|
|
|
|
// Expect no tickets with confirmed fee.
|
|
retrieved, err = db.filterTickets(func(t Ticket) bool {
|
|
return t.FeeTxStatus == FeeConfirmed
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("error filtering tickets: %v", err)
|
|
}
|
|
if len(retrieved) != 0 {
|
|
t.Fatalf("expected to find 0 tickets, found %d", len(retrieved))
|
|
}
|
|
}
|
|
|
|
func testCountTickets(t *testing.T) {
|
|
count := func(test string, expectedVoting, expectedVoted, expectedRevoked int64) {
|
|
voting, voted, revoked, err := db.CountTickets()
|
|
if err != nil {
|
|
t.Fatalf("error counting tickets: %v", err)
|
|
}
|
|
|
|
if voting != expectedVoting {
|
|
t.Fatalf("test %s: expected %d voting tickets, got %d",
|
|
test, expectedVoting, voting)
|
|
}
|
|
if voted != expectedVoted {
|
|
t.Fatalf("test %s: expected %d voted tickets, got %d",
|
|
test, expectedVoted, voted)
|
|
}
|
|
if revoked != expectedRevoked {
|
|
t.Fatalf("test %s: expected %d revoked tickets, got %d",
|
|
test, expectedRevoked, revoked)
|
|
}
|
|
}
|
|
|
|
// Initial counts should all be zero.
|
|
count("empty db", 0, 0, 0)
|
|
|
|
// Insert a ticket with non-confirmed fee into the database.
|
|
// This should not be counted.
|
|
ticket := exampleTicket()
|
|
ticket.FeeTxStatus = FeeReceieved
|
|
err := db.InsertNewTicket(ticket)
|
|
if err != nil {
|
|
t.Fatalf("error storing ticket in database: %v", err)
|
|
}
|
|
|
|
count("unconfirmed fee", 0, 0, 0)
|
|
|
|
// Insert a ticket with confirmed fee into the database.
|
|
// This should be counted.
|
|
ticket2 := exampleTicket()
|
|
ticket2.FeeTxStatus = FeeConfirmed
|
|
err = db.InsertNewTicket(ticket2)
|
|
if err != nil {
|
|
t.Fatalf("error storing ticket in database: %v", err)
|
|
}
|
|
|
|
count("confirmed fee", 1, 0, 0)
|
|
|
|
// Insert a voted ticket into the database.
|
|
// This should be counted.
|
|
ticket3 := exampleTicket()
|
|
ticket3.FeeTxStatus = FeeConfirmed
|
|
ticket3.Outcome = Voted
|
|
err = db.InsertNewTicket(ticket3)
|
|
if err != nil {
|
|
t.Fatalf("error storing ticket in database: %v", err)
|
|
}
|
|
|
|
count("voted", 1, 1, 0)
|
|
|
|
// Insert a revoked ticket into the database.
|
|
// This should be counted.
|
|
ticket4 := exampleTicket()
|
|
ticket4.FeeTxStatus = FeeConfirmed
|
|
ticket4.Outcome = Revoked
|
|
err = db.InsertNewTicket(ticket4)
|
|
if err != nil {
|
|
t.Fatalf("error storing ticket in database: %v", err)
|
|
}
|
|
|
|
count("revoked", 1, 1, 1)
|
|
}
|