refactor: remove duplicate write function in db #32
9 changed files with 318 additions and 164 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,3 +2,5 @@
|
||||||
/pelican
|
/pelican
|
||||||
/*.pelican
|
/*.pelican
|
||||||
/hack
|
/hack
|
||||||
|
/cover.out
|
||||||
|
/code-coverage.html
|
||||||
|
|
|
@ -41,7 +41,7 @@ func Open(path string) (Board, error) {
|
||||||
boltItems[i] = &newStatusList[i]
|
boltItems[i] = &newStatusList[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := db.WriteMany(database, db.StatusBucket, boltItems); err != nil {
|
if _, err := db.Write(database, db.WriteModeCreate, db.StatusBucket, boltItems); err != nil {
|
||||||
return Board{}, fmt.Errorf("unable to save the default status list to the db. %w", err)
|
return Board{}, fmt.Errorf("unable to save the default status list to the db. %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,7 +147,7 @@ func (b *Board) CreateStatus(args StatusArgs) error {
|
||||||
CardIds: nil,
|
CardIds: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := db.Write(b.db, db.StatusBucket, &status); err != nil {
|
if _, err := db.Write(b.db, db.WriteModeCreate, db.StatusBucket, []db.BoltItem{&status}); err != nil {
|
||||||
return fmt.Errorf("unable to write the status to the database; %w", err)
|
return fmt.Errorf("unable to write the status to the database; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ func (b *Board) UpdateStatus(args UpdateStatusArgs) error {
|
||||||
status.Position = args.Position
|
status.Position = args.Position
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := db.Write(b.db, db.StatusBucket, &status); err != nil {
|
if _, err := db.Write(b.db, db.WriteModeUpdate, db.StatusBucket, []db.BoltItem{&status}); err != nil {
|
||||||
return fmt.Errorf("unable to write the status to the db. %w", err)
|
return fmt.Errorf("unable to write the status to the db. %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +253,7 @@ func (b *Board) MoveToStatus(args MoveToStatusArgs) error {
|
||||||
|
|
||||||
boltItems := []db.BoltItem{¤tStatus, &nextStatus}
|
boltItems := []db.BoltItem{¤tStatus, &nextStatus}
|
||||||
|
|
||||||
if _, err := db.WriteMany(b.db, db.StatusBucket, boltItems); err != nil {
|
if _, err := db.Write(b.db, db.WriteModeUpdate, db.StatusBucket, boltItems); err != nil {
|
||||||
return fmt.Errorf("unable to update the statuses in the db. %w", err)
|
return fmt.Errorf("unable to update the statuses in the db. %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,32 +278,39 @@ func (b *Board) CreateCard(args CardArgs) (int, error) {
|
||||||
return 0, StatusListEmptyError{}
|
return 0, StatusListEmptyError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
card := Card{
|
boltItems := []db.BoltItem{
|
||||||
|
&Card{
|
||||||
ID: -1,
|
ID: -1,
|
||||||
Title: args.NewTitle,
|
Title: args.NewTitle,
|
||||||
Description: args.NewDescription,
|
Description: args.NewDescription,
|
||||||
Created: timestamp,
|
Created: timestamp,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cardID, err := db.Write(b.db, db.CardBucket, &card)
|
cardIDs, err := db.Write(b.db, db.WriteModeCreate, db.CardBucket, boltItems)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("unable to write card to the db. %w", err)
|
return 0, fmt.Errorf("unable to write card to the db. %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
initialStatus := statusList[0]
|
if len(cardIDs) != 1 {
|
||||||
|
return 0, fmt.Errorf("unexpected number of card IDs returned after writing the card to the database; want: 1, got %d", len(cardIDs))
|
||||||
initialStatus.AddCardID(cardID)
|
|
||||||
|
|
||||||
id, err := db.Write(b.db, db.StatusBucket, &initialStatus)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("unable to write the %s status to the db. %w", initialStatus.Name, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return id, nil
|
cardID := cardIDs[0]
|
||||||
|
|
||||||
|
statusInFirstPos := statusList[0]
|
||||||
|
|
||||||
|
statusInFirstPos.AddCardID(cardID)
|
||||||
|
|
||||||
|
_, err = db.Write(b.db, db.WriteModeUpdate, db.StatusBucket, []db.BoltItem{&statusInFirstPos})
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("unable to write the %s status to the db. %w", statusInFirstPos.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cardID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Card returns a Card value from the database.
|
// Card returns a Card value from the database.
|
||||||
// TODO: Handle edge case where the card does not exist in the db.
|
|
||||||
func (b *Board) Card(cardID int) (Card, error) {
|
func (b *Board) Card(cardID int) (Card, error) {
|
||||||
data, err := db.Read(b.db, db.CardBucket, cardID)
|
data, err := db.Read(b.db, db.CardBucket, cardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -328,7 +335,6 @@ func (b *Board) Card(cardID int) (Card, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CardList returns a list of Card values from the db.
|
// CardList returns a list of Card values from the db.
|
||||||
// TODO: function needs testing.
|
|
||||||
func (b *Board) CardList(ids []int) ([]Card, error) {
|
func (b *Board) CardList(ids []int) ([]Card, error) {
|
||||||
data, err := db.ReadMany(b.db, db.CardBucket, ids)
|
data, err := db.ReadMany(b.db, db.CardBucket, ids)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -374,8 +380,8 @@ func (b *Board) UpdateCard(args UpdateCardArgs) error {
|
||||||
card.Description = args.NewDescription
|
card.Description = args.NewDescription
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := db.Write(b.db, db.CardBucket, &card); err != nil {
|
if _, err := db.Write(b.db, db.WriteModeUpdate, db.CardBucket, []db.BoltItem{&card}); err != nil {
|
||||||
return fmt.Errorf("unable to write card to the db. %w", err)
|
return fmt.Errorf("unable to write the card to the database; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -399,7 +405,7 @@ func (b *Board) DeleteCard(args DeleteCardArgs) error {
|
||||||
|
|
||||||
status.RemoveCardID(args.CardID)
|
status.RemoveCardID(args.CardID)
|
||||||
|
|
||||||
if _, err := db.Write(b.db, db.StatusBucket, &status); err != nil {
|
if _, err := db.Write(b.db, db.WriteModeUpdate, db.StatusBucket, []db.BoltItem{&status}); err != nil {
|
||||||
return fmt.Errorf("unable to update the status in the database; %w", err)
|
return fmt.Errorf("unable to update the status in the database; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -12,7 +13,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCardLifecycle(t *testing.T) {
|
func TestCardLifecycle(t *testing.T) {
|
||||||
t.Log("Testing the lifecycle of a card.")
|
t.Log("Testing the lifecycle of a couple of cards...")
|
||||||
|
|
||||||
projectDir, err := projectRoot()
|
projectDir, err := projectRoot()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -31,116 +32,204 @@ func TestCardLifecycle(t *testing.T) {
|
||||||
_ = kanban.Close()
|
_ = kanban.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
initialCardTitle := "A test card."
|
t.Logf("We've opened a new board, let us create a couple of cards...")
|
||||||
initialCardContent := "Ensure that this card is safely stored in the database."
|
|
||||||
expectedCardID := 1
|
cardArgs := map[int]board.CardArgs{
|
||||||
expectedStatusID := 1
|
1: {
|
||||||
|
NewTitle: "Test Card 01",
|
||||||
|
NewDescription: "Ensure that the cards are safely stored in the database.",
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
NewTitle: "Test Card 02",
|
||||||
|
NewDescription: "Ensure that cards can be modified and that the modifications are saved in the database.",
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
NewTitle: "Test Card 03",
|
||||||
|
NewDescription: "Ensure that cards can be deleted from the database.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
timestamp := time.Now().Format(time.DateTime)
|
timestamp := time.Now().Format(time.DateTime)
|
||||||
|
|
||||||
t.Run("Test Create Card", testCreateCard(kanban, initialCardTitle, initialCardContent, expectedCardID, expectedStatusID))
|
t.Run("Test Create Cards", testCreateCards(kanban, cardArgs))
|
||||||
|
|
||||||
t.Run("Test Read Card", testReadCard(kanban, expectedCardID, initialCardTitle, initialCardContent, timestamp))
|
t.Logf("Now that we've created some cards, let's retrieve one from the database...")
|
||||||
|
|
||||||
modifiedCardTitle := "Test card updated."
|
t.Run("Test Read One Card", testReadOneCard(kanban, 1, cardArgs[1], timestamp))
|
||||||
modifiedCardContent1 := "Ensure that this card is safely updated in the database."
|
|
||||||
|
|
||||||
t.Run("Test Update Card", testUpdateCard(kanban, expectedCardID, modifiedCardTitle, modifiedCardContent1, timestamp))
|
t.Logf("Let us try retrieving multiple cards...")
|
||||||
|
|
||||||
modifiedCardContent2 := "Updated card content only."
|
subCardArgs := make(map[int]board.CardArgs)
|
||||||
|
subCardArgs[2] = cardArgs[2]
|
||||||
|
subCardArgs[3] = cardArgs[3]
|
||||||
|
|
||||||
t.Run("Test Update Card Content", testUpdateCardContent(kanban, expectedCardID, modifiedCardTitle, modifiedCardContent2, timestamp))
|
t.Run("Test Read Multiple Cards", testReadManyCards(kanban, subCardArgs))
|
||||||
|
|
||||||
t.Run("Test Card Delete", testDeleteCard(kanban, expectedCardID, expectedStatusID))
|
t.Logf("Let's see what happens when trying to read a card that does not exist in the database...")
|
||||||
|
|
||||||
|
t.Run("Test Read Non-Existent Card", testReadNonExistentCard(kanban, 1000))
|
||||||
|
|
||||||
|
t.Logf("Let us try modifying the title and description of one of the cards...")
|
||||||
|
|
||||||
|
modifiedCardArgs := board.UpdateCardArgs{
|
||||||
|
CardID: 1,
|
||||||
|
CardArgs: board.CardArgs{
|
||||||
|
NewTitle: cardArgs[1].NewTitle + " (edited).",
|
||||||
|
NewDescription: "This description has been edited.",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCreateCard(kanban board.Board, title, content string, expectedCardID, expectedStatusID int) func(t *testing.T) {
|
t.Run("Test Update Card", testUpdateCard(kanban, modifiedCardArgs, timestamp))
|
||||||
|
|
||||||
|
t.Logf("Let us also try only modifying the description of a card...")
|
||||||
|
|
||||||
|
expectedCardContent := "Only the description is updated in this card."
|
||||||
|
|
||||||
|
modifiedCardArgs = board.UpdateCardArgs{
|
||||||
|
CardID: 2,
|
||||||
|
CardArgs: board.CardArgs{
|
||||||
|
NewTitle: "",
|
||||||
|
NewDescription: expectedCardContent,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedCardTitle := "Test Card 02"
|
||||||
|
|
||||||
|
t.Run("Test Update Card Content Only", testUpdateCardContent(kanban, modifiedCardArgs, expectedCardTitle, expectedCardContent, timestamp))
|
||||||
|
|
||||||
|
t.Logf("Let's now delete a card from the database...")
|
||||||
|
|
||||||
|
t.Run("Test Card Delete", testDeleteCard(kanban, 1, 1, []int{2, 3}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreateCards(kanban board.Board, args map[int]board.CardArgs) func(t *testing.T) {
|
||||||
return func(t *testing.T) {
|
return func(t *testing.T) {
|
||||||
t.Log("When the card is created and saved to the database.")
|
t.Log("When creating and saving the cards to the database.")
|
||||||
|
|
||||||
args := board.CardArgs{
|
for i := 1; i <= len(args); i++ {
|
||||||
NewTitle: title,
|
if _, err := kanban.CreateCard(args[i]); err != nil {
|
||||||
NewDescription: content,
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := kanban.CreateCard(args); err != nil {
|
|
||||||
t.Fatalf("ERROR: Unable to create the test card, %s.", err)
|
t.Fatalf("ERROR: Unable to create the test card, %s.", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
t.Logf("\t\tVerifying that the card's ID is in the expected status...")
|
t.Logf("%s\tAll cards have been saved to the database.", success)
|
||||||
|
|
||||||
status, err := kanban.Status(expectedStatusID)
|
t.Logf("\tVerifying that the card IDs are present the status in first position...")
|
||||||
|
|
||||||
|
statusList, err := kanban.StatusList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ERROR: Unable to read status '%d', %v", expectedStatusID, err)
|
t.Fatalf("ERROR: Unable to retrieve the list of statuses; %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
numCardIDs := len(status.CardIds)
|
if len(statusList) == 0 {
|
||||||
|
t.Fatal("ERROR: No statuses were returned from the database.")
|
||||||
if numCardIDs != 1 {
|
|
||||||
t.Fatalf("ERROR: Unexpected number of cards in status '%d', want: %d, got %d.", expectedStatusID, 1, numCardIDs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if expectedCardID != status.CardIds[0] {
|
expectedStatus := statusList[0]
|
||||||
t.Errorf("%s\tUnexpected card ID found in the default status, want: %d, got %d.", failure, expectedCardID, status.CardIds[0])
|
|
||||||
|
wantCardIDs := []int{1, 2, 3}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(expectedStatus.CardIds, wantCardIDs) {
|
||||||
|
t.Errorf("%s\tUnexpected card IDs found in the expected status, want: %v, got %v.", failure, wantCardIDs, expectedStatus.CardIds)
|
||||||
} else {
|
} else {
|
||||||
t.Logf("%s\tExpected card ID found in the default status, got %d.", success, status.CardIds[0])
|
t.Logf("%s\tExpected card IDs found in the expected status, got %v.", success, expectedStatus.CardIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testReadCard(kanban board.Board, cardID int, wantTitle, wantDescription, wantTimestamp string) func(t *testing.T) {
|
func testReadOneCard(kanban board.Board, cardID int, cardArgs board.CardArgs, wantTimestamp string) func(t *testing.T) {
|
||||||
return func(t *testing.T) {
|
return func(t *testing.T) {
|
||||||
t.Log("When a card is read from the database.")
|
t.Log("When reading a card from the database.")
|
||||||
|
|
||||||
card, err := kanban.Card(cardID)
|
card, err := kanban.Card(cardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ERROR: Unable to read test card, %s.", err)
|
t.Fatalf("ERROR: Unable to read test card, %s.", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wantTitle := cardArgs.NewTitle
|
||||||
if card.Title != wantTitle {
|
if card.Title != wantTitle {
|
||||||
t.Errorf("%s\tUnexpected card title received, want: %s, got: %s.", failure, wantTitle, card.Title)
|
t.Errorf("%s\tUnexpected card title received, want: %q, got: %q.", failure, wantTitle, card.Title)
|
||||||
} else {
|
} else {
|
||||||
t.Logf("%s\tExpected card title received, got: %s.", success, card.Title)
|
t.Logf("%s\tExpected card title received, got: %q.", success, card.Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
if card.Description != wantDescription {
|
wantDescription := cardArgs.NewDescription
|
||||||
t.Errorf("%s\tUnexpected card content received, want: %s, got: %s.", failure, wantDescription, card.Description)
|
if card.Description != cardArgs.NewDescription {
|
||||||
|
t.Errorf("%s\tUnexpected card content received, want: %q, got: %q.", failure, wantDescription, card.Description)
|
||||||
} else {
|
} else {
|
||||||
t.Logf("%s\tExpected card content received, got: %s.", success, card.Description)
|
t.Logf("%s\tExpected card content received, got: %q.", success, card.Description)
|
||||||
}
|
}
|
||||||
|
|
||||||
if card.Created != wantTimestamp {
|
if card.Created != wantTimestamp {
|
||||||
t.Errorf("%s\tUnexpected timestamp received for the created card, want: %s, got %s.", failure, wantTimestamp, card.Created)
|
t.Errorf("%s\tUnexpected timestamp received for the created card, want: %q, got %q.", failure, wantTimestamp, card.Created)
|
||||||
} else {
|
} else {
|
||||||
t.Logf("%s\tExpected timestamp received for the created card, got: %s.", success, card.Created)
|
t.Logf("%s\tExpected timestamp received for the created card, got: %q.", success, card.Created)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUpdateCard(kanban board.Board, cardID int, newTitle, newContent, timestamp string) func(t *testing.T) {
|
func testReadManyCards(kanban board.Board, cardArgs map[int]board.CardArgs) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
t.Log("When reading multiple cards from the database.")
|
||||||
|
|
||||||
|
var cardIDList []int
|
||||||
|
for key := range cardArgs {
|
||||||
|
cardIDList = append(cardIDList, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
cards, err := kanban.CardList(cardIDList)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ERROR: Unable to retrieve the list of cards from the database; %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range cards {
|
||||||
|
cardID := cards[i].GetID()
|
||||||
|
wantTitle := cardArgs[cardID].NewTitle
|
||||||
|
gotTitle := cards[i].Title
|
||||||
|
|
||||||
|
if wantTitle != gotTitle {
|
||||||
|
t.Errorf("%s\tUnexpected card title received, want: %q, got: %q.", failure, wantTitle, gotTitle)
|
||||||
|
} else {
|
||||||
|
t.Logf("%s\tExpected card title received, got: %q.", success, gotTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testReadNonExistentCard(kanban board.Board, cardID int) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
t.Log("When attempting to retrieving a non-existent card from the database.")
|
||||||
|
|
||||||
|
_, err := kanban.Card(cardID)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
t.Errorf("%s\tWanted an error for retrieving a non-existent card but got 'nil' instead.", failure)
|
||||||
|
case errors.As(err, &board.CardNotExistError{}):
|
||||||
|
t.Logf("%s\tExpected error received after attempting to retrieve a non-existent card from the database; got '%v'", success, err)
|
||||||
|
default:
|
||||||
|
t.Errorf("%s\tUnexpected error received after attempting to retrieve a non-existent card from the database; got: '%v'", failure, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUpdateCard(kanban board.Board, modifiedCardArgs board.UpdateCardArgs, timestamp string) func(t *testing.T) {
|
||||||
return func(t *testing.T) {
|
return func(t *testing.T) {
|
||||||
t.Log("When a card is updated in the database.")
|
t.Log("When a card is updated in the database.")
|
||||||
|
|
||||||
args := board.UpdateCardArgs{
|
if err := kanban.UpdateCard(modifiedCardArgs); err != nil {
|
||||||
CardID: cardID,
|
|
||||||
CardArgs: board.CardArgs{
|
|
||||||
NewTitle: newTitle,
|
|
||||||
NewDescription: newContent,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := kanban.UpdateCard(args); err != nil {
|
|
||||||
t.Fatalf("ERROR: Unable to update the test card, %s", err)
|
t.Fatalf("ERROR: Unable to update the test card, %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
got, err := kanban.Card(cardID)
|
got, err := kanban.Card(modifiedCardArgs.CardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ERROR: Unable to read the modified test card, %s", err)
|
t.Fatalf("ERROR: Unable to read the modified test card, %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
want := board.Card{
|
want := board.Card{
|
||||||
ID: cardID,
|
ID: modifiedCardArgs.CardID,
|
||||||
Title: newTitle,
|
Title: modifiedCardArgs.NewTitle,
|
||||||
Description: newContent,
|
Description: modifiedCardArgs.NewDescription,
|
||||||
Created: timestamp,
|
Created: timestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,31 +241,23 @@ func testUpdateCard(kanban board.Board, cardID int, newTitle, newContent, timest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUpdateCardContent(kanban board.Board, cardID int, expectedTitle, newContent, timestamp string) func(t *testing.T) {
|
func testUpdateCardContent(kanban board.Board, modifiedCardArgs board.UpdateCardArgs, expectedTitle, expectedDescription, timestamp string) func(t *testing.T) {
|
||||||
return func(t *testing.T) {
|
return func(t *testing.T) {
|
||||||
t.Log("When (and only when) a card's content is updated in the database.")
|
t.Log("When (and only when) a card's content is updated in the database.")
|
||||||
|
|
||||||
args := board.UpdateCardArgs{
|
if err := kanban.UpdateCard(modifiedCardArgs); err != nil {
|
||||||
CardID: cardID,
|
|
||||||
CardArgs: board.CardArgs{
|
|
||||||
NewTitle: "",
|
|
||||||
NewDescription: newContent,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := kanban.UpdateCard(args); err != nil {
|
|
||||||
t.Fatalf("ERROR: Unable to update the test card, %s", err)
|
t.Fatalf("ERROR: Unable to update the test card, %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
got, err := kanban.Card(cardID)
|
got, err := kanban.Card(modifiedCardArgs.CardID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ERROR: Unable to read the modified test card, %s", err)
|
t.Fatalf("ERROR: Unable to read the modified test card, %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
want := board.Card{
|
want := board.Card{
|
||||||
ID: cardID,
|
ID: modifiedCardArgs.CardID,
|
||||||
Title: expectedTitle,
|
Title: expectedTitle,
|
||||||
Description: newContent,
|
Description: expectedDescription,
|
||||||
Created: timestamp,
|
Created: timestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +269,7 @@ func testUpdateCardContent(kanban board.Board, cardID int, expectedTitle, newCon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDeleteCard(kanban board.Board, cardID, statusID int) func(t *testing.T) {
|
func testDeleteCard(kanban board.Board, cardID, statusID int, expectedCardIDs []int) func(t *testing.T) {
|
||||||
return func(t *testing.T) {
|
return func(t *testing.T) {
|
||||||
t.Log("When deleting a card from the database.")
|
t.Log("When deleting a card from the database.")
|
||||||
|
|
||||||
|
@ -206,18 +287,18 @@ func testDeleteCard(kanban board.Board, cardID, statusID int) func(t *testing.T)
|
||||||
t.Logf("\tVerifying that the card is removed from the database...")
|
t.Logf("\tVerifying that the card is removed from the database...")
|
||||||
|
|
||||||
_, err := kanban.Card(cardID)
|
_, err := kanban.Card(cardID)
|
||||||
if err == nil {
|
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
t.Errorf("%s\tDid not receive the expected error when attempting to read the deleted card.", failure)
|
t.Errorf("%s\tDid not receive the expected error when attempting to read the deleted card.", failure)
|
||||||
} else {
|
case errors.As(err, &board.CardNotExistError{}):
|
||||||
if errors.Is(err, board.CardNotExistError{}) {
|
t.Logf("%s\tSuccessfully received board.CardNotExistError when attempting to retrieve the deleted card.", success)
|
||||||
|
default:
|
||||||
t.Errorf(
|
t.Errorf(
|
||||||
"%s\tDid not receive the expected board.CardNotExistError when attempting to retrieve the deleted card, instead got '%v'.",
|
"%s\tDid not receive the expected error when attempting to retrieve the deleted card; got '%v'.",
|
||||||
failure,
|
failure,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
t.Logf("%s\tSuccessfully received board.CardNotExistError when attempting to retrieve the deleted card.", success)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("\tVerifying that the card's ID is removed from the status list...")
|
t.Logf("\tVerifying that the card's ID is removed from the status list...")
|
||||||
|
@ -227,11 +308,13 @@ func testDeleteCard(kanban board.Board, cardID, statusID int) func(t *testing.T)
|
||||||
t.Fatalf("ERROR: Unable to read status '%d' from the database; %v", statusID, err)
|
t.Fatalf("ERROR: Unable to read status '%d' from the database; %v", statusID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
numCardIDs := len(status.CardIds)
|
sort.Ints(expectedCardIDs)
|
||||||
if numCardIDs != 0 {
|
sort.Ints(status.CardIds)
|
||||||
t.Errorf("%s\tUnexpected non-empty list of card IDs in status '%d', got '%+v' card IDs.", failure, statusID, status.CardIds)
|
|
||||||
|
if !reflect.DeepEqual(status.CardIds, expectedCardIDs) {
|
||||||
|
t.Errorf("%s\tUnexpected list of card IDs found in status '%d'; want %v, got %v", failure, statusID, expectedCardIDs, status.CardIds)
|
||||||
} else {
|
} else {
|
||||||
t.Logf("%s\tThe card ID was successfully removed from the list of card in status '%d'.", success, statusID)
|
t.Logf("%s\tExpected list of card IDs found in status '%d'; got %v", success, statusID, status.CardIds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
49
internal/board/card_test.go
Normal file
49
internal/board/card_test.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package board_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeflow.dananglin.me.uk/apollo/pelican/internal/board"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCardSetInvalidID(t *testing.T) {
|
||||||
|
card := board.Card{
|
||||||
|
ID: -1,
|
||||||
|
Title: "Title",
|
||||||
|
Description: "Description",
|
||||||
|
Created: time.Now().Format(time.DateTime),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := card.SetID(-1000)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
t.Errorf("%s\tWanted an error for setting an invalid card ID; got 'nil' instead.", failure)
|
||||||
|
case errors.As(err, &board.InvalidIDError{}):
|
||||||
|
t.Logf("%s\tGot expected error after attempting to set an invalid card ID; got '%v'", success, err)
|
||||||
|
default:
|
||||||
|
t.Errorf("%s\tGot unexpected error after attempting to set an invalid card ID; got '%v'", failure, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCardSetExistingID(t *testing.T) {
|
||||||
|
card := board.Card{
|
||||||
|
ID: 5,
|
||||||
|
Title: "Title",
|
||||||
|
Description: "Description",
|
||||||
|
Created: time.Now().Format(time.DateTime),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := card.SetID(10)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
t.Errorf("%s\tWanted an error for setting a card ID that's already been set; got 'nil' instead.", failure)
|
||||||
|
case errors.As(err, &board.IDAlreadySetError{}):
|
||||||
|
t.Logf("%s\tGot expected error after attempting to set a card ID that's already been set; got '%v'", success, err)
|
||||||
|
default:
|
||||||
|
t.Errorf("%s\tGot unexpected error after attempting to set a card ID that's already been set; got '%v'", failure, err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,6 +50,9 @@ func TestStatusLifecycle(t *testing.T) {
|
||||||
t.Run("Test Create Status (Next)", testCreateStatus(kanban, statusNextName, 0))
|
t.Run("Test Create Status (Next)", testCreateStatus(kanban, statusNextName, 0))
|
||||||
t.Run("Test Read Status (Next)", testReadStatus(kanban, statusNextExpectedID, statusNextName, statusNextExpectedBoardPosition))
|
t.Run("Test Read Status (Next)", testReadStatus(kanban, statusNextExpectedID, statusNextName, statusNextExpectedBoardPosition))
|
||||||
|
|
||||||
|
t.Logf("Let us see what happens when we try and receive a status that does not exist...")
|
||||||
|
t.Run("Test Read Non-Existent Status", testReadNonExistentStatus(kanban, 1000))
|
||||||
|
|
||||||
t.Logf("Let us now update the names of two of our statuses...")
|
t.Logf("Let us now update the names of two of our statuses...")
|
||||||
t.Run("Test Status Update (Doing to In Progress)", testUpdateStatus(kanban, 2, 2, "In Progress"))
|
t.Run("Test Status Update (Doing to In Progress)", testUpdateStatus(kanban, 2, 2, "In Progress"))
|
||||||
t.Run("Test Status Update (To Do to Backlog)", testUpdateStatus(kanban, 1, 1, "Backlog"))
|
t.Run("Test Status Update (To Do to Backlog)", testUpdateStatus(kanban, 1, 1, "Backlog"))
|
||||||
|
@ -262,3 +265,20 @@ func testDeleteNonEmptyStatus(kanban board.Board, statusID int) func(t *testing.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testReadNonExistentStatus(kanban board.Board, statusID int) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
t.Log("When attempting to retrieving a non-existent status from the database.")
|
||||||
|
|
||||||
|
_, err := kanban.Status(statusID)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
t.Errorf("%s\tWanted an error for trying to retrieve a non-existent status, but got 'nil' instead.", failure)
|
||||||
|
case errors.As(err, &board.StatusNotExistError{}):
|
||||||
|
t.Logf("%s\tExpected error received after attempting to retrieve a non-existent status from the database; got '%v'", success, err)
|
||||||
|
default:
|
||||||
|
t.Errorf("%s\tUnexpected error received after attempting to retrieve a non-existent status from the database; got '%v'", failure, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,13 @@ const (
|
||||||
CardBucket string = "card"
|
CardBucket string = "card"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type dbWriteMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
WriteModeCreate dbWriteMode = iota
|
||||||
|
WriteModeUpdate
|
||||||
|
)
|
||||||
|
|
||||||
type BoltItem interface {
|
type BoltItem interface {
|
||||||
SetID(int) error
|
SetID(int) error
|
||||||
GetID() int
|
GetID() int
|
||||||
|
@ -128,52 +135,10 @@ func ReadAll(db *bolt.DB, bucketName string) ([][]byte, error) {
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write creates or updates a Bolt item to a specified bucket.
|
// Write saves one or more Bolt items to the status bucket.
|
||||||
func Write(db *bolt.DB, bucketName string, item BoltItem) (int, error) {
|
func Write(database *bolt.DB, writeMode dbWriteMode, bucketName string, items []BoltItem) ([]int, error) {
|
||||||
bucketNameBytes := []byte(bucketName)
|
|
||||||
|
|
||||||
err := db.Update(func(tx *bolt.Tx) error {
|
|
||||||
var err error
|
|
||||||
bucket := tx.Bucket(bucketNameBytes)
|
|
||||||
|
|
||||||
if bucket == nil {
|
|
||||||
return bucketNotExistError{bucket: string(bucketNameBytes)}
|
|
||||||
}
|
|
||||||
|
|
||||||
if item.GetID() < 1 {
|
|
||||||
var id uint64
|
|
||||||
if id, err = bucket.NextSequence(); err != nil {
|
|
||||||
return fmt.Errorf("unable to generate an ID for the Bolt item; %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = item.SetID(int(id)); err != nil {
|
|
||||||
return fmt.Errorf("unable to set the generated ID to the Bolt item; %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
encoder := gob.NewEncoder(buf)
|
|
||||||
if err = encoder.Encode(item); err != nil {
|
|
||||||
return fmt.Errorf("unable to encode data, %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = bucket.Put([]byte(strconv.Itoa(item.GetID())), buf.Bytes()); err != nil {
|
|
||||||
return fmt.Errorf("unable to write the Bolt item to the bucket; %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("error while saving the Bolt item to the database; %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return item.GetID(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteMany saves one or more Bolt items to the status bucket.
|
|
||||||
func WriteMany(database *bolt.DB, bucketName string, items []BoltItem) ([]int, error) {
|
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return []int{}, nil
|
return nil, itemListEmptyError{}
|
||||||
}
|
}
|
||||||
|
|
||||||
ids := make([]int, len(items))
|
ids := make([]int, len(items))
|
||||||
|
@ -190,7 +155,7 @@ func WriteMany(database *bolt.DB, bucketName string, items []BoltItem) ([]int, e
|
||||||
for ind, item := range items {
|
for ind, item := range items {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if item.GetID() < 1 {
|
if writeMode == WriteModeCreate && item.GetID() < 1 {
|
||||||
var id uint64
|
var id uint64
|
||||||
if id, err = bucket.NextSequence(); err != nil {
|
if id, err = bucket.NextSequence(); err != nil {
|
||||||
return fmt.Errorf("unable to generate ID, %w", err)
|
return fmt.Errorf("unable to generate ID, %w", err)
|
||||||
|
|
|
@ -36,11 +36,11 @@ func TestWriteAndReadStatusList(t *testing.T) {
|
||||||
_ = database.Close()
|
_ = database.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
testWriteStatusList(t, database)
|
testCreateStatusList(t, database)
|
||||||
testReadStatusList(t, database)
|
testReadStatusList(t, database)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testWriteStatusList(t *testing.T, database *bolt.DB) {
|
func testCreateStatusList(t *testing.T, database *bolt.DB) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
newStatusList := []board.Status{
|
newStatusList := []board.Status{
|
||||||
|
@ -76,7 +76,7 @@ func testWriteStatusList(t *testing.T, database *bolt.DB) {
|
||||||
boltItems[i] = &newStatusList[i]
|
boltItems[i] = &newStatusList[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := db.WriteMany(database, db.StatusBucket, boltItems); err != nil {
|
if _, err := db.Write(database, db.WriteModeCreate, db.StatusBucket, boltItems); err != nil {
|
||||||
t.Fatalf("An error occurred whilst writing the initial status list to the database, %s", err)
|
t.Fatalf("An error occurred whilst writing the initial status list to the database, %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,7 +168,7 @@ func TestReadAndWriteCards(t *testing.T) {
|
||||||
Description: "This task should be completed.",
|
Description: "This task should be completed.",
|
||||||
}
|
}
|
||||||
|
|
||||||
singleCardID := testWriteOneCard(t, database, singleCard)
|
singleCardID := testCreateOneCard(t, database, singleCard)
|
||||||
testReadOneCard(t, database, singleCardID)
|
testReadOneCard(t, database, singleCardID)
|
||||||
|
|
||||||
manyCards := []board.Card{
|
manyCards := []board.Card{
|
||||||
|
@ -189,19 +189,23 @@ func TestReadAndWriteCards(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
manyCardIDs := testWriteManyCard(t, database, manyCards)
|
manyCardIDs := testCreateManyCard(t, database, manyCards)
|
||||||
testReadManyCards(t, database, manyCardIDs)
|
testReadManyCards(t, database, manyCardIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testWriteOneCard(t *testing.T, database *bolt.DB, card board.Card) int {
|
func testCreateOneCard(t *testing.T, database *bolt.DB, card board.Card) int {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
cardID, err := db.Write(database, db.CardBucket, &card)
|
cardIDs, err := db.Write(database, db.WriteModeCreate, db.CardBucket, []db.BoltItem{&card})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("An error occurred whilst writing the card to the database, %s", err)
|
t.Fatalf("An error occurred whilst writing the card to the database, %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cardID
|
if len(cardIDs) != 1 {
|
||||||
|
t.Fatalf("Unexpected number of card IDs returned from db.Write(); want: 1; got %d", len(cardIDs))
|
||||||
|
}
|
||||||
|
|
||||||
|
return cardIDs[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func testReadOneCard(t *testing.T, database *bolt.DB, cardID int) {
|
func testReadOneCard(t *testing.T, database *bolt.DB, cardID int) {
|
||||||
|
@ -235,7 +239,7 @@ func testReadOneCard(t *testing.T, database *bolt.DB, cardID int) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testWriteManyCard(t *testing.T, database *bolt.DB, cards []board.Card) []int {
|
func testCreateManyCard(t *testing.T, database *bolt.DB, cards []board.Card) []int {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
boltItems := make([]db.BoltItem, len(cards))
|
boltItems := make([]db.BoltItem, len(cards))
|
||||||
|
@ -244,7 +248,7 @@ func testWriteManyCard(t *testing.T, database *bolt.DB, cards []board.Card) []in
|
||||||
boltItems[i] = &cards[i]
|
boltItems[i] = &cards[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
ids, err := db.WriteMany(database, db.CardBucket, boltItems)
|
ids, err := db.Write(database, db.WriteModeCreate, db.CardBucket, boltItems)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("An error occurred whilst writing many cards to the database, %s", err)
|
t.Fatalf("An error occurred whilst writing many cards to the database, %s", err)
|
||||||
}
|
}
|
||||||
|
@ -331,11 +335,17 @@ func TestDeleteOneCard(t *testing.T) {
|
||||||
Description: "",
|
Description: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
cardID, err := db.Write(database, db.CardBucket, &card)
|
cardIDs, err := db.Write(database, db.WriteModeCreate, db.CardBucket, []db.BoltItem{&card})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ERROR: Unable to create the card in the database, %v", err)
|
t.Fatalf("ERROR: Unable to create the card in the database, %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(cardIDs) != 1 {
|
||||||
|
t.Fatalf("Unexpected number of card IDs returned from db.Write(); want: 1; got %d", len(cardIDs))
|
||||||
|
}
|
||||||
|
|
||||||
|
cardID := cardIDs[0]
|
||||||
|
|
||||||
cards, err := db.ReadAll(database, db.CardBucket)
|
cards, err := db.ReadAll(database, db.CardBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ERROR: Unable to read the cards from the database, %v", err)
|
t.Fatalf("ERROR: Unable to read the cards from the database, %v", err)
|
||||||
|
|
|
@ -9,3 +9,9 @@ type bucketNotExistError struct {
|
||||||
func (e bucketNotExistError) Error() string {
|
func (e bucketNotExistError) Error() string {
|
||||||
return fmt.Sprintf("bucket %s does not exist", e.bucket)
|
return fmt.Sprintf("bucket %s does not exist", e.bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type itemListEmptyError struct{}
|
||||||
|
|
||||||
|
func (e itemListEmptyError) Error() string {
|
||||||
|
return "the list of items is empty."
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ var Default = Build
|
||||||
// Test run the go tests
|
// Test run the go tests
|
||||||
// To enable verbose mode set PELICAN_TEST_VERBOSE=1.
|
// To enable verbose mode set PELICAN_TEST_VERBOSE=1.
|
||||||
// To enable coverage mode set PELICAN_TEST_COVER=1.
|
// To enable coverage mode set PELICAN_TEST_COVER=1.
|
||||||
|
// To produce a coverage report set PELICAN_TEST_COVER_REPORT=1.
|
||||||
func Test() error {
|
func Test() error {
|
||||||
test := sh.RunCmd("go", "test")
|
test := sh.RunCmd("go", "test")
|
||||||
|
|
||||||
|
@ -37,7 +38,19 @@ func Test() error {
|
||||||
args = append(args, "-cover")
|
args = append(args, "-cover")
|
||||||
}
|
}
|
||||||
|
|
||||||
return test(args...)
|
if os.Getenv("PELICAN_TEST_COVER_REPORT") == "1" {
|
||||||
|
args = append(args, "-coverprofile=cover.out")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := test(args...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("PELICAN_TEST_COVER_REPORT") == "1" {
|
||||||
|
return sh.Run("go", "tool", "cover", "-html=cover.out", "-o", "code-coverage.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lint runs golangci-lint against the code.
|
// Lint runs golangci-lint against the code.
|
||||||
|
|
Loading…
Reference in a new issue