diff --git a/acn.go b/acn.go index 26d522b..bc598f6 100644 --- a/acn.go +++ b/acn.go @@ -35,17 +35,6 @@ func isConnected(m model) (bool, tea.Msg) { } func turnAcnOn(m *model) tea.Msg { - if ok, msg := isConnected(*m); !ok { - switch msg.(type) { - case acnConnectingMsg: - return msg - case acnAlreadyOnlineMsg: - return msg - } - } - - m.acnState = connecting - mrand.New(mrand.NewSource(int64(time.Now().Nanosecond()))) port := mrand.Intn(1000) + 9600 controlPort := port + 1 @@ -113,22 +102,10 @@ func turnAcnOn(m *model) tea.Msg { } func turnAcnOff(m *model, quitProgram bool) tea.Msg { - switch m.acnState { - case offline: - if !quitProgram { - return acnOfflineMsg{} - } - case connecting: - return acnConnectingMsg{} - } - - if m.app != nil { + if m.acnState != offline { + logToFile(*m, "Shutting down application and ACN now") m.app.Shutdown() - } - - if m.acn != nil { m.acn.Close() } - return acnOffMsg{quitProgram: quitProgram} } diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..5c48011 --- /dev/null +++ b/logger.go @@ -0,0 +1,10 @@ +package main + +import "log" + +// LogToFile logs a message to a log file. +func logToFile(m model, msg string) { + if m.debug { + log.Printf("debug: %s", msg) + } +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..547dae5 --- /dev/null +++ b/main_test.go @@ -0,0 +1,320 @@ +package main + +import ( + "bytes" + "os" + "os/user" + "testing" + "time" + + openPrivacyLog "git.openprivacy.ca/openprivacy/log" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/x/exp/teatest" +) + +const ( + cairdeTestLogFile = "cairde-test.log" + cwtchTestLogFile = "cwtch-test.log" +) + +var currentUser *user.User +var cairdeLogHandler *os.File +var cwtchLogHandler *openPrivacyLog.Logger + +func TestMain(m *testing.M) { + defer setupAndTeardown()() + m.Run() +} + +func setupAndTeardown() func() { + _, err := tea.LogToFile(cairdeTestLogFile, "debug") + if err != nil { + panic(err) + } + + cairdeLogHandler, err = os.OpenFile(cairdeTestLogFile, os.O_RDONLY, 0644) + if err != nil { + panic(err) + } + + cwtchLogHandler, err = openPrivacyLog.NewFile(openPrivacyLog.LevelDebug, cwtchTestLogFile) + if err == nil { + openPrivacyLog.SetStd(cwtchLogHandler) + } + + currentUser, err = user.Current() + if err != nil { + panic(err) + } + + return func() { + if err := cairdeLogHandler.Close(); err != nil { + panic(err) + } + + if err := os.Remove(cairdeTestLogFile); err != nil { + panic(err) + } + + if err := os.Remove(cwtchTestLogFile); err != nil { + panic(err) + } + } +} + +func TestOnOff(t *testing.T) { + m := newModel(currentUser.Username, currentUser.HomeDir, true) + testModel := teatest.NewTestModel(t, m) + + testModel.Send(tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune("ctrl+c"), + }) + + testModel.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) +} + +func TestAcnOnOff(t *testing.T) { + m := newModel(currentUser.Username, currentUser.HomeDir, true) + testModel := teatest.NewTestModel(t, m) + + testModel.Type("/acn on") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("ACN is up and running, all engines go")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*10), + ) + + testModel.Type("/acn off") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("ACN successfully turned off")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + testModel.Send(tea.KeyMsg{ + Type: tea.KeyRunes, + Runes: []rune("ctrl+c"), + }) + + testModel.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) +} + +func TestAcnOnThenQuit(t *testing.T) { + m := newModel(currentUser.Username, currentUser.HomeDir, true) + testModel := teatest.NewTestModel(t, m) + + testModel.Type("/acn on") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("ACN is up and running, all engines go")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*10), + ) + + testModel.Type("/quit") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("Shutting down application and ACN now")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + testModel.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) +} + +func TestAcnOnThenOnAsksToHold(t *testing.T) { + m := newModel(currentUser.Username, currentUser.HomeDir, true) + testModel := teatest.NewTestModel(t, m) + + testModel.Type("/acn on") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("Initialising Tor ACN")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*10), + ) + + testModel.Type("/acn on") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("ACN still initialising, please hold")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("ACN is up and running, all engines go")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*10), + ) + + testModel.Type("/quit") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("Shutting down application and ACN now")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + testModel.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) +} + +func TestOnThenOffAsksToWait(t *testing.T) { + m := newModel(currentUser.Username, currentUser.HomeDir, true) + testModel := teatest.NewTestModel(t, m) + + testModel.Type("/acn on") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("Initialising Tor ACN...")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*10), + ) + + testModel.Type("/acn on") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("ACN still initialising, please hold")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("ACN is up and running, all engines go")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*10), + ) + + testModel.Type("/quit") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("Shutting down application and ACN now")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + testModel.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) +} + +func TestAcnOnThenQuitAsksToWait(t *testing.T) { + m := newModel(currentUser.Username, currentUser.HomeDir, true) + testModel := teatest.NewTestModel(t, m) + + testModel.Type("/acn on") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("Initialising Tor ACN...")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*10), + ) + + testModel.Type("/quit") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("ACN still initialising, please hold")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("ACN is up and running, all engines go")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*10), + ) + + testModel.Type("/quit") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("Shutting down application and ACN now")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*3), + ) + + testModel.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) +} + +func TestAcnOffIsOff(t *testing.T) { + m := newModel(currentUser.Username, currentUser.HomeDir, true) + testModel := teatest.NewTestModel(t, m) + + testModel.Type("/acn off") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + teatest.WaitFor( + t, cairdeLogHandler, + func(bts []byte) bool { + return bytes.Contains(bts, []byte("ACN is currently offline")) + }, + teatest.WithCheckInterval(time.Millisecond*100), + teatest.WithDuration(time.Second*10), + ) + + testModel.Type("/quit") + testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) + + testModel.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) +} diff --git a/model.go b/model.go index 608a3e3..9afd823 100644 --- a/model.go +++ b/model.go @@ -133,6 +133,7 @@ func (m model) sendStatusCmd(lines ...string) tea.Cmd { // we want to render messages to the status buffer but still have work to do. func (m model) sendStatus(lines ...string) { for _, line := range lines { + logToFile(m, line) m.statusBuffer <- line } } @@ -263,6 +264,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, m.sendStatusCmd(msg.output...)) case turnAcnOffMsg: + switch m.acnState { + case offline: + if !msg.quitProgram { + cmds = append(cmds, func() tea.Msg { return acnOfflineMsg{} }) + return m, tea.Batch(cmds...) + } + case connecting: + cmds = append(cmds, func() tea.Msg { return acnConnectingMsg{} }) + return m, tea.Batch(cmds...) + } + m.input.Reset() cmds = append(cmds, func() tea.Msg { return turnAcnOff(&m, msg.quitProgram) @@ -276,9 +288,19 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.acnState = offline case turnAcnOnMsg: - // TODO: stop if we're already connecting! - // state management is getting a little tricky :/ + switch m.acnState { + case connecting: + cmds = append(cmds, func() tea.Msg { return acnConnectingMsg{} }) + return m, tea.Batch(cmds...) + case connected: + cmds = append(cmds, func() tea.Msg { return acnAlreadyOnlineMsg{} }) + return m, tea.Batch(cmds...) + } + + m.acnState = connecting + m.input.Reset() + cmds = append(cmds, func() tea.Msg { return turnAcnOn(&m) }) @@ -390,6 +412,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case sendStatusMsg: cmds = append(cmds, func() tea.Msg { for _, line := range msg.lines { + logToFile(m, line) m.statusBuffer <- line } return nil diff --git a/model_test.go b/model_test.go deleted file mode 100644 index 7e8fe53..0000000 --- a/model_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "os/user" - "testing" - "time" - - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/x/exp/teatest" -) - -func TestCairdeOnOffWorks(t *testing.T) { - user, err := user.Current() - if err != nil { - t.Fatal(err) - } - - m := newModel(user.Username, user.HomeDir, false) - tm := teatest.NewTestModel(t, m) - - tm.Send(tea.KeyMsg{ - Type: tea.KeyRunes, - Runes: []rune("ctrl+c"), - }) - - tm.WaitFinished(t, teatest.WithFinalTimeout(time.Second)) -}