Skip to content

Commit 93bfee5

Browse files
committed
Adds initial framework for tests in commands.go
This adds initial tests and a pattern to use as a framework for adding unit tests for the rest of the `tea.Cmd` functions in `commands.go`, as well as explanation of the pattern for future contributors. The `tui.go` `Update()` command will receive `tea.Msg`s and return `tea.Cmd`s. All the `tea.Cmds` (except those in the actual Bubble Tea framework) will be collected in `commands.go`. Each command will have an outgoing `tea.Msg` that contains any relevant results. Commands may also have incoming `tea.Msg`s, but these are deprecated as the `Update()` function is converted to call commands directly rather than `func() tea.Msg {}` commands, which are an unnecessary intermediary. Signed-off-by: Chris Collins <collins.christopher@gmail.com>
1 parent 5657245 commit 93bfee5

File tree

6 files changed

+345
-73
lines changed

6 files changed

+345
-73
lines changed

pkg/pd/mock.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package pd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/PagerDuty/go-pagerduty"
8+
)
9+
10+
var ErrMockError = fmt.Errorf("pd.Mock(): mock error") // Used to mock errors in unit tests
11+
12+
type MockPagerDutyClient struct {
13+
PagerDutyClient
14+
}
15+
16+
func (m *MockPagerDutyClient) GetTeamWithContext(ctx context.Context, team string) (*pagerduty.Team, error) {
17+
return &pagerduty.Team{Name: team}, nil
18+
}
19+
20+
func (m *MockPagerDutyClient) GetIncidentWithContext(ctx context.Context, id string) (*pagerduty.Incident, error) {
21+
// Provided so we can mock error responses for unit tests
22+
if id == "err" {
23+
return &pagerduty.Incident{}, ErrMockError
24+
}
25+
return &pagerduty.Incident{
26+
APIObject: pagerduty.APIObject{
27+
ID: id, // Incidents will always come back with the same ID as the request
28+
},
29+
}, nil
30+
}
31+
32+
func (m *MockPagerDutyClient) ListIncidentAlertsWithContext(ctx context.Context, id string, opts pagerduty.ListIncidentAlertsOptions) (*pagerduty.ListAlertsResponse, error) {
33+
if id == "err" {
34+
return &pagerduty.ListAlertsResponse{}, ErrMockError
35+
}
36+
return &pagerduty.ListAlertsResponse{
37+
Alerts: []pagerduty.IncidentAlert{
38+
{
39+
APIObject: pagerduty.APIObject{
40+
ID: "QABCDEFG1234567",
41+
},
42+
},
43+
{
44+
APIObject: pagerduty.APIObject{
45+
ID: "QABCDEFG7654321",
46+
},
47+
},
48+
},
49+
}, nil
50+
}
51+
52+
func (m *MockPagerDutyClient) ListIncidentNotesWithContext(ctx context.Context, id string) ([]pagerduty.IncidentNote, error) {
53+
if id == "err" {
54+
return []pagerduty.IncidentNote{}, ErrMockError
55+
}
56+
return []pagerduty.IncidentNote{
57+
{
58+
ID: "QABCDEFG1234567",
59+
},
60+
{
61+
ID: "QABCDEFG7654321",
62+
},
63+
}, nil
64+
}

pkg/pd/pd_test.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package pd
22

33
import (
4-
"context"
54
"errors"
65
"reflect"
76
"testing"
@@ -10,14 +9,6 @@ import (
109
"github.com/stretchr/testify/assert"
1110
)
1211

13-
type MockPagerDutyClient struct {
14-
PagerDutyClient
15-
}
16-
17-
func (m *MockPagerDutyClient) GetTeamWithContext(ctx context.Context, team string) (*pagerduty.Team, error) {
18-
return &pagerduty.Team{Name: team}, nil
19-
}
20-
2112
func TestGetTeams(t *testing.T) {
2213
t.Run("GetTeams", func(t *testing.T) {
2314
mockClient := new(MockPagerDutyClient)

pkg/rand/rand.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package rand
2+
3+
// Credit to https://www.calhoun.io/creating-random-strings-in-go/
4+
5+
import (
6+
"math/rand"
7+
"time"
8+
)
9+
10+
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
11+
12+
var seededRand *rand.Rand = rand.New(
13+
rand.NewSource(time.Now().UnixNano()))
14+
15+
func StringWithCharset(length int, charset string) string {
16+
b := make([]byte, length)
17+
for i := range b {
18+
b[i] = charset[seededRand.Intn(len(charset))]
19+
}
20+
return string(b)
21+
}
22+
23+
func String(length int) string {
24+
return StringWithCharset(length, charset)
25+
}
26+
27+
func ID(prefix string) string {
28+
return prefix + String(13)
29+
}

pkg/tui/commands.go

Lines changed: 71 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -18,31 +18,89 @@ import (
1818
"github.com/clcollins/srepd/pkg/pd"
1919
)
2020

21+
// This file contains the commands that are used in the Bubble Tea update function.
22+
// These commands are functions that return a tea.Cmd, which performs I/O with
23+
// another system, such as the PagerDuty API, the filesystem, or the user's terminal,
24+
// and shouldn't perform any actual processing themselves.
25+
//
26+
// The returned tea.Cmd is executed by the Bubble Tea runtime, and the result of the
27+
// command is sent back to the update function as a tea.Msg.
28+
//
29+
// See getIncident() as a good example of this pattern.
30+
2131
const (
2232
gettingUserStatus = "getting user info..."
2333
loadingIncidentsStatus = "loading incidents..."
2434
)
2535

36+
// TODO: Deprecate incoming message
37+
// getIncidentMsg is a message that triggers the fetching of an incident
38+
type getIncidentMsg string
39+
40+
// gotIncidentMsg is a message that contains the fetched incident
41+
type gotIncidentMsg struct {
42+
incident *pagerduty.Incident
43+
err error
44+
}
45+
46+
// getIncident returns a command that fetches the incident with the given ID
47+
// or returns a setStatusMsg with nilIncidentMsg if the provided ID is empty
48+
func getIncident(p *pd.Config, id string) tea.Cmd {
49+
return func() tea.Msg {
50+
if id == "" {
51+
return setStatusMsg{nilIncidentMsg}
52+
}
53+
ctx := context.Background()
54+
i, err := p.Client.GetIncidentWithContext(ctx, id)
55+
return gotIncidentMsg{i, err}
56+
}
57+
}
58+
59+
// gotIncidentAlertsMsg is a message that contains the fetched incident alerts
60+
type gotIncidentAlertsMsg struct {
61+
alerts []pagerduty.IncidentAlert
62+
err error
63+
}
64+
65+
// getIncidentAlerts returns a command that fetches the alerts for the given incident
66+
func getIncidentAlerts(p *pd.Config, id string) tea.Cmd {
67+
return func() tea.Msg {
68+
if id == "" {
69+
return setStatusMsg{nilIncidentMsg}
70+
}
71+
a, err := pd.GetAlerts(p.Client, id, pagerduty.ListIncidentAlertsOptions{})
72+
return gotIncidentAlertsMsg{a, err}
73+
}
74+
}
75+
76+
// got IncidentNotesMsg is a message that contains the fetched incident notes
77+
type gotIncidentNotesMsg struct {
78+
notes []pagerduty.IncidentNote
79+
err error
80+
}
81+
82+
// getIncidentNotes returns a command that fetches the notes for the given incident
83+
func getIncidentNotes(p *pd.Config, id string) tea.Cmd {
84+
return func() tea.Msg {
85+
if id == "" {
86+
return setStatusMsg{nilIncidentMsg}
87+
}
88+
n, err := pd.GetNotes(p.Client, id)
89+
return gotIncidentNotesMsg{n, err}
90+
}
91+
}
92+
93+
// HOUSEKEEPING: The above are commands that have complete unit tests and incoming
94+
// and outgoing tea.Msg types, ordered alphabetically. Below are commands that need to
95+
// be refactored to have unit tests and incoming and outgoing tea.Msg types, ordered
96+
2697
type errMsg struct{ error }
2798
type setStatusMsg struct{ string }
2899
type waitForSelectedIncidentThenDoMsg struct {
29100
action tea.Cmd
30101
msg tea.Msg
31102
}
32103

33-
//lint:ignore U1000 - future proofing
34-
type waitForSelectedIncidentThenRenderMsg string
35-
36-
//lint:ignore U1000 - future proofing
37-
// func updateSelectedIncident(p *pd.Config, id string) tea.Msg {
38-
// This return won't work as is - these are not tea.Cmd functions
39-
// return tea.Sequence(
40-
// getIncident(p, id),
41-
// getIncidentAlerts(p, id),
42-
// getIncidentNotes(p, id),
43-
// )
44-
// }
45-
46104
type TickMsg struct {
47105
Tick int
48106
}
@@ -112,50 +170,6 @@ func renderIncident(m *model) tea.Cmd {
112170
}
113171
}
114172

115-
type getIncidentMsg string
116-
type gotIncidentMsg struct {
117-
incident *pagerduty.Incident
118-
err error
119-
}
120-
121-
func getIncident(p *pd.Config, id string) tea.Cmd {
122-
if id == "" {
123-
return func() tea.Msg {
124-
return setStatusMsg{"No incident selected"}
125-
}
126-
}
127-
return func() tea.Msg {
128-
ctx := context.Background()
129-
i, err := p.Client.GetIncidentWithContext(ctx, id)
130-
return gotIncidentMsg{i, err}
131-
}
132-
}
133-
134-
type gotIncidentAlertsMsg struct {
135-
alerts []pagerduty.IncidentAlert
136-
err error
137-
}
138-
139-
func getIncidentAlerts(p *pd.Config, id string) tea.Cmd {
140-
return func() tea.Msg {
141-
a, err := pd.GetAlerts(p.Client, id, pagerduty.ListIncidentAlertsOptions{})
142-
return gotIncidentAlertsMsg{a, err}
143-
}
144-
}
145-
146-
type gotIncidentNotesMsg struct {
147-
notes []pagerduty.IncidentNote
148-
err error
149-
}
150-
151-
// getIncidentNotes returns a command that fetches the notes for the given incident
152-
func getIncidentNotes(p *pd.Config, id string) tea.Cmd {
153-
return func() tea.Msg {
154-
n, err := pd.GetNotes(p.Client, id)
155-
return gotIncidentNotesMsg{n, err}
156-
}
157-
}
158-
159173
// AssignedToAnyUsers returns true if the incident is assigned to any of the given users
160174
func AssignedToAnyUsers(i pagerduty.Incident, ids []string) bool {
161175
for _, a := range i.Assignments {

0 commit comments

Comments
 (0)