// Copyright (c) 2020-2024 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 ( "reflect" "testing" "time" bolt "go.etcd.io/bbolt" ) func exampleTicket() Ticket { 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": "yes"}, TSpendPolicy: map[string]string{randString(64, hexCharset): "no"}, TreasuryPolicy: map[string]string{randString(66, hexCharset): "abstain"}, 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 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 !reflect.DeepEqual(retrieved, ticket) { 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 !reflect.DeepEqual(retrieved, ticket) { 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(_ *bolt.Bucket) 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 *bolt.Bucket) bool { return bytesToBool(t.Get(confirmedK)) }) 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 *bolt.Bucket) bool { return FeeStatus(t.Get(feeTxStatusK)) == 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, expectedExpired, expectedMissed int64) { voting, voted, expired, missed, 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 expired != expectedExpired { t.Fatalf("test %s: expected %d expired tickets, got %d", test, expectedExpired, expired) } if missed != expectedMissed { t.Fatalf("test %s: expected %d missed tickets, got %d", test, expectedMissed, missed) } } // Initial counts should all be zero. count("empty db", 0, 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, 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, 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, 0) // Insert an expired ticket into the database. // This should be counted. ticket4 := exampleTicket() ticket4.FeeTxStatus = FeeConfirmed ticket4.Outcome = Expired err = db.InsertNewTicket(ticket4) if err != nil { t.Fatalf("error storing ticket in database: %v", err) } count("expired", 1, 1, 1, 0) // Insert a missed ticket into the database. // This should be counted. ticket5 := exampleTicket() ticket5.FeeTxStatus = FeeConfirmed ticket5.Outcome = Missed err = db.InsertNewTicket(ticket5) if err != nil { t.Fatalf("error storing ticket in database: %v", err) } count("missed", 1, 1, 1, 1) // Insert a revoked ticket into the database. // This should be counted as expired. ticket6 := exampleTicket() ticket6.FeeTxStatus = FeeConfirmed ticket6.Outcome = Revoked err = db.InsertNewTicket(ticket6) if err != nil { t.Fatalf("error storing ticket in database: %v", err) } count("revoked", 1, 1, 2, 1) }