2023-05-06 12:49:40 +01:00
|
|
|
package board_test
|
|
|
|
|
|
|
|
import (
|
2024-01-18 22:36:33 +00:00
|
|
|
"errors"
|
2024-01-24 01:09:56 +00:00
|
|
|
"fmt"
|
2023-05-06 12:49:40 +01:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"reflect"
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"codeflow.dananglin.me.uk/apollo/pelican/internal/board"
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestStatusLifecycle(t *testing.T) {
|
2024-01-17 17:10:36 +00:00
|
|
|
t.Log("Testing the lifecycle of the statuses on the Kanban board...")
|
2023-05-06 12:49:40 +01:00
|
|
|
|
|
|
|
projectDir, err := projectRoot()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf(err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
testDBPath := filepath.Join(projectDir, "test", "databases", "Board_TestStatusLifecycle.db")
|
|
|
|
os.Remove(testDBPath)
|
|
|
|
|
|
|
|
kanban, err := board.Open(testDBPath)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Unable to open the test Kanban board, %s.", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
_ = kanban.Close()
|
|
|
|
}()
|
|
|
|
|
2024-01-17 17:10:36 +00:00
|
|
|
t.Logf("We've opened a new board with the default list of statuses: 'To Do', 'Doing' and 'Done'")
|
|
|
|
|
|
|
|
t.Logf("Let us now create a new status called 'On Hold' in the 4th position...")
|
|
|
|
|
2023-05-06 12:49:40 +01:00
|
|
|
statusOnHoldName := "On Hold"
|
|
|
|
statusOnHoldExpectedID := 4
|
2024-01-17 17:10:36 +00:00
|
|
|
statusOnHoldBoardPosition := 4
|
|
|
|
|
|
|
|
t.Run("Test Create Status (On Hold)", testCreateStatus(kanban, statusOnHoldName, statusOnHoldBoardPosition))
|
|
|
|
t.Run("Test Read Status (On Hold)", testReadStatus(kanban, statusOnHoldExpectedID, statusOnHoldName, statusOnHoldBoardPosition))
|
|
|
|
|
|
|
|
t.Logf("Additionally, let us create another status without specifying a position. It should take the last position on the board...")
|
|
|
|
|
|
|
|
statusNextName := "Next"
|
|
|
|
statusNextExpectedID := 5
|
|
|
|
statusNextExpectedBoardPosition := 5
|
2023-05-06 12:49:40 +01:00
|
|
|
|
2024-01-17 17:10:36 +00:00
|
|
|
t.Run("Test Create Status (Next)", testCreateStatus(kanban, statusNextName, 0))
|
|
|
|
t.Run("Test Read Status (Next)", testReadStatus(kanban, statusNextExpectedID, statusNextName, statusNextExpectedBoardPosition))
|
2023-05-06 12:49:40 +01:00
|
|
|
|
2024-01-23 18:31:01 +00:00
|
|
|
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))
|
|
|
|
|
2024-01-17 17:10:36 +00:00
|
|
|
t.Logf("Let us now update the names of two of our statuses...")
|
2024-01-18 22:36:33 +00:00
|
|
|
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"))
|
2023-05-06 12:49:40 +01:00
|
|
|
|
2024-01-24 01:09:56 +00:00
|
|
|
t.Logf("Our current column positioning is: Backlog, In Progress, Done, On Hold, Next.")
|
|
|
|
t.Logf("Let us rearrange the board so that the order is: Backlog, Next, In Progress, On Hold, Done...")
|
|
|
|
|
|
|
|
// NOTE: the statuses current index is the index in the slice that is received from the database.
|
|
|
|
// The wantPositions is used to evaluate each statuses Position value after all the reshuffling and renormalisation.
|
|
|
|
rearrangeCases := []struct {
|
|
|
|
statusName string
|
|
|
|
currentIndex int
|
|
|
|
targetIndex int
|
|
|
|
wantPositions map[int]string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
statusName: "Done",
|
|
|
|
currentIndex: 2,
|
|
|
|
targetIndex: 4,
|
|
|
|
wantPositions: map[int]string{
|
|
|
|
1: "Backlog",
|
|
|
|
2: "In Progress",
|
|
|
|
3: "On Hold",
|
|
|
|
4: "Next",
|
|
|
|
5: "Done",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
statusName: "Next",
|
|
|
|
currentIndex: 3,
|
|
|
|
targetIndex: 1,
|
|
|
|
wantPositions: map[int]string{
|
|
|
|
1: "Backlog",
|
|
|
|
2: "Next",
|
|
|
|
3: "In Progress",
|
|
|
|
4: "On Hold",
|
|
|
|
5: "Done",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range rearrangeCases {
|
|
|
|
t.Run(
|
|
|
|
fmt.Sprintf("Test Re-Arrange Status: %s", rearrangeCases[i].statusName),
|
|
|
|
testRepositionStatuses(kanban, rearrangeCases[i].currentIndex, rearrangeCases[i].targetIndex, rearrangeCases[i].wantPositions),
|
|
|
|
)
|
|
|
|
}
|
2024-01-17 17:10:36 +00:00
|
|
|
|
|
|
|
t.Logf("Let us now try moving a card from one status to another...")
|
2023-05-06 12:49:40 +01:00
|
|
|
t.Run("Test Move Card To Status", testMoveCardToStatus(kanban))
|
2024-01-18 22:36:33 +00:00
|
|
|
|
2024-01-24 01:09:56 +00:00
|
|
|
expectedPositionsAfterDelete := map[int]string{
|
2024-01-18 22:36:33 +00:00
|
|
|
1: "Backlog",
|
2024-01-24 01:09:56 +00:00
|
|
|
2: "Next",
|
|
|
|
3: "In Progress",
|
|
|
|
4: "Done",
|
2024-01-18 22:36:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
t.Logf("Let us now delete the 'On Hold' status from the database...")
|
2024-01-24 01:09:56 +00:00
|
|
|
t.Run("Test Delete Status (On Hold)", testDeleteEmptyStatus(kanban, statusOnHoldExpectedID, expectedPositionsAfterDelete))
|
2024-01-18 22:36:33 +00:00
|
|
|
|
|
|
|
t.Logf("Additionally, let us try to delete a status that contains a card...")
|
2024-01-24 01:09:56 +00:00
|
|
|
t.Run("Test Delete a non-empty status", testDeleteNonEmptyStatus(kanban, 2))
|
2023-05-06 12:49:40 +01:00
|
|
|
}
|
|
|
|
|
2024-01-17 17:10:36 +00:00
|
|
|
func testCreateStatus(kanban board.Board, name string, position int) func(t *testing.T) {
|
2023-05-06 12:49:40 +01:00
|
|
|
return func(t *testing.T) {
|
|
|
|
t.Log("When the status is created and saved to the database.")
|
|
|
|
|
|
|
|
args := board.StatusArgs{
|
2024-01-17 17:10:36 +00:00
|
|
|
Name: name,
|
|
|
|
Position: position,
|
2023-05-06 12:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := kanban.CreateStatus(args); err != nil {
|
|
|
|
t.Fatalf("ERROR: Unable to create the new status, %v.", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Logf("%s\tStatus '%s' was successfully saved to the database.", success, name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-17 17:10:36 +00:00
|
|
|
func testReadStatus(kanban board.Board, statusID int, wantName string, wantPosition int) func(t *testing.T) {
|
2023-05-06 12:49:40 +01:00
|
|
|
return func(t *testing.T) {
|
|
|
|
t.Log("When the status is read from the database.")
|
|
|
|
|
|
|
|
status, err := kanban.Status(statusID)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("ERROR: Unable to read the test status, %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if status.Name != wantName {
|
|
|
|
t.Errorf("%s\tUnexpected status received from the database, want: '%s', got: '%s'", failure, wantName, status.Name)
|
|
|
|
} else {
|
|
|
|
t.Logf("%s\tSuccessfully received the '%s' status from the database.", success, status.Name)
|
|
|
|
}
|
2024-01-17 17:10:36 +00:00
|
|
|
|
|
|
|
if status.Position != wantPosition {
|
|
|
|
t.Errorf("%s\tUnexpected position found for %s, want: '%d', got: '%d'", failure, status.Name, wantPosition, status.Position)
|
|
|
|
} else {
|
|
|
|
t.Logf("%s\tSuccessfully received the expected position number for the '%s' status, got : '%d'.", success, status.Name, status.Position)
|
|
|
|
}
|
2023-05-06 12:49:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-17 17:10:36 +00:00
|
|
|
func testUpdateStatus(kanban board.Board, statusID, expectedPosition int, newName string) func(t *testing.T) {
|
2023-05-06 12:49:40 +01:00
|
|
|
return func(t *testing.T) {
|
|
|
|
t.Log("When the status' name is updated in the database.")
|
|
|
|
|
|
|
|
args := board.UpdateStatusArgs{
|
|
|
|
StatusID: statusID,
|
|
|
|
StatusArgs: board.StatusArgs{
|
2024-01-17 17:10:36 +00:00
|
|
|
Name: newName,
|
|
|
|
Position: -1,
|
2023-05-06 12:49:40 +01:00
|
|
|
},
|
|
|
|
}
|
|
|
|
if err := kanban.UpdateStatus(args); err != nil {
|
|
|
|
t.Fatalf("ERROR: Unable to update the status, %v", err)
|
|
|
|
} else {
|
|
|
|
t.Logf("%s\tStatus successfully updated.", success)
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Log("\tVerifying the new status...")
|
|
|
|
|
|
|
|
status, err := kanban.Status(statusID)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("ERROR: Unable to retrieve the status from the database, %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
want := board.Status{
|
2024-01-23 19:42:35 +00:00
|
|
|
Identity: board.Identity{ID: statusID},
|
2023-05-06 12:49:40 +01:00
|
|
|
Name: newName,
|
|
|
|
CardIds: nil,
|
2024-01-17 17:10:36 +00:00
|
|
|
Position: expectedPosition,
|
2023-05-06 12:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if !reflect.DeepEqual(status, want) {
|
|
|
|
t.Errorf("%s\tUnexpected status received from the database, want: %+v, got: %+v", failure, want, status)
|
|
|
|
} else {
|
|
|
|
t.Logf("%s\tExpected status name received from the database, got: %+v", success, status)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-24 01:09:56 +00:00
|
|
|
func testRepositionStatuses(kanban board.Board, currentIndex, targetIndex int, wantPositions map[int]string) func(t *testing.T) {
|
|
|
|
return func(t *testing.T) {
|
|
|
|
t.Logf("When repositioning a status on the board.")
|
|
|
|
|
|
|
|
if err := kanban.RepositionStatus(currentIndex, targetIndex); err != nil {
|
|
|
|
t.Fatalf("ERROR: Unable to reposition the status; %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
statuses, err := kanban.StatusList()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("ERROR: an error was received when attempting to get the list of statuses from the database; %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
gotPositions := make(map[int]string)
|
|
|
|
for _, status := range statuses {
|
|
|
|
gotPositions[status.Position] = status.Name
|
|
|
|
}
|
|
|
|
|
|
|
|
if !reflect.DeepEqual(wantPositions, gotPositions) {
|
|
|
|
t.Errorf("%s\tUnexpected positions received from the database; want: %v, got %v", failure, wantPositions, gotPositions)
|
|
|
|
} else {
|
|
|
|
t.Logf("%s\tExpected positions received from the database; got %v", success, gotPositions)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-05-06 12:49:40 +01:00
|
|
|
|
|
|
|
func testMoveCardToStatus(kanban board.Board) func(t *testing.T) {
|
|
|
|
return func(t *testing.T) {
|
|
|
|
t.Log("When moving a card between statuses.")
|
|
|
|
|
|
|
|
title := "Test card."
|
|
|
|
|
2024-01-10 12:12:54 +00:00
|
|
|
cardArgs := board.CardArgs{NewTitle: title, NewDescription: ""}
|
2023-05-06 12:49:40 +01:00
|
|
|
|
|
|
|
cardID, err := kanban.CreateCard(cardArgs)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("ERROR: Unable to create the card in the database, %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
statusList, err := kanban.StatusList()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("ERROR: Unable to retrieve the list of statuses from the database, %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
status0, status2 := statusList[0], statusList[2]
|
|
|
|
|
|
|
|
moveArgs := board.MoveToStatusArgs{CardID: cardID, CurrentStatusID: status0.ID, NextStatusID: status2.ID}
|
|
|
|
|
|
|
|
if err := kanban.MoveToStatus(moveArgs); err != nil {
|
|
|
|
t.Fatalf("ERROR: Unable to move the Card ID from '%s' to '%s', %v", status0.Name, status2.Name, err)
|
2024-01-24 01:09:56 +00:00
|
|
|
} else {
|
|
|
|
t.Logf("%s\tThe MoveToStatus operation has completed without error.", success)
|
2023-05-06 12:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
t.Logf("\tVerifying that the card has moved to '%s'...", status2.Name)
|
|
|
|
|
|
|
|
statusList, err = kanban.StatusList()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("ERROR: Unable to retrieve the list of statuses from the database, %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
status0, status2 = statusList[0], statusList[2]
|
|
|
|
|
|
|
|
if len(status0.CardIds) != 0 {
|
|
|
|
t.Errorf("%s\tUnexpected number of card IDs found in '%s', want: 0, got: %d", failure, status0.Name, len(status0.CardIds))
|
|
|
|
} else {
|
|
|
|
t.Logf("%s\tThe number of card IDs in '%s' is now %d", success, status0.Name, len(status0.CardIds))
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(status2.CardIds) != 1 {
|
|
|
|
t.Errorf("%s\tUnexpected number of card IDs found in '%s', want: 1, got: %d", failure, status2.Name, len(status2.CardIds))
|
|
|
|
} else {
|
|
|
|
t.Logf("%s\tThe number of card IDs in '%s' is now %d", success, status2.Name, len(status2.CardIds))
|
|
|
|
}
|
|
|
|
|
|
|
|
card, err := kanban.Card(status2.CardIds[0])
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("ERROR: Unable to retrieve the card from the database, %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if card.Title != title {
|
|
|
|
t.Errorf("%s\tUnexpected card title found in '%s', want: '%s', got: '%s'", success, status2.Name, title, card.Title)
|
|
|
|
} else {
|
|
|
|
t.Logf("%s\tExpected card title found in '%s', got: '%s'", success, status2.Name, card.Title)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-01-18 22:36:33 +00:00
|
|
|
|
|
|
|
func testDeleteEmptyStatus(kanban board.Board, statusID int, wantPositions map[int]string) func(t *testing.T) {
|
|
|
|
return func(t *testing.T) {
|
|
|
|
t.Log("When deleting an empty status.")
|
|
|
|
|
|
|
|
err := kanban.DeleteStatus(statusID)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("ERROR: an error was received when attempting to delete the status from the database; %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
statuses, err := kanban.StatusList()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("ERROR: an error was received when attempting to get the list of statuses from the database; %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
gotPositions := make(map[int]string)
|
|
|
|
for _, status := range statuses {
|
|
|
|
gotPositions[status.Position] = status.Name
|
|
|
|
}
|
|
|
|
|
|
|
|
if !reflect.DeepEqual(wantPositions, gotPositions) {
|
|
|
|
t.Errorf("%s\tUnexpected positions received from the database; want: %v, got %v", failure, wantPositions, gotPositions)
|
|
|
|
} else {
|
|
|
|
t.Logf("%s\tExpected positions received from the database; got %v", success, gotPositions)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func testDeleteNonEmptyStatus(kanban board.Board, statusID int) func(t *testing.T) {
|
|
|
|
return func(t *testing.T) {
|
|
|
|
t.Log("When deleting a non-empty status.")
|
|
|
|
|
|
|
|
err := kanban.DeleteStatus(statusID)
|
|
|
|
|
|
|
|
switch {
|
|
|
|
case err == nil:
|
|
|
|
t.Errorf("%s\tExpected an error for deleting a non-empty status but received 'nil'", failure)
|
|
|
|
case errors.As(err, &board.StatusNotEmptyError{}):
|
|
|
|
t.Logf("%s\tExpected error received after attempting to delete a non-empty status; got: '%v'", success, err)
|
|
|
|
default:
|
|
|
|
t.Errorf("%s\tUnexpected error received after attempting to delete a non-empty status; got: '%v'", failure, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-01-23 18:31:01 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|