feat: validate destinations
This commit is contained in:
parent
cddcb0eb4d
commit
d3a6d6acdb
@ -117,22 +117,27 @@ func Run(ctx context.Context, params RunParams) error {
|
|||||||
logger.Debug("Command received", "cmd", cmd.Name())
|
logger.Debug("Command received", "cmd", cmd.Name())
|
||||||
switch c := cmd.(type) {
|
switch c := cmd.(type) {
|
||||||
case terminal.CommandAddDestination:
|
case terminal.CommandAddDestination:
|
||||||
cfg.Destinations = append(cfg.Destinations, config.Destination{
|
newCfg := cfg
|
||||||
|
newCfg.Destinations = append(newCfg.Destinations, config.Destination{
|
||||||
Name: c.DestinationName,
|
Name: c.DestinationName,
|
||||||
URL: c.URL,
|
URL: c.URL,
|
||||||
})
|
})
|
||||||
if err := params.ConfigService.SetConfig(cfg); err != nil {
|
if err := params.ConfigService.SetConfig(newCfg); err != nil {
|
||||||
// TODO: error handling
|
logger.Error("Config update failed", "err", err)
|
||||||
logger.Error("Failed to set config", "err", err)
|
ui.ConfigUpdateFailed(err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
ui.DestinationAdded()
|
||||||
case terminal.CommandRemoveDestination:
|
case terminal.CommandRemoveDestination:
|
||||||
mp.StopDestination(c.URL) // no-op if not live
|
mp.StopDestination(c.URL) // no-op if not live
|
||||||
cfg.Destinations = slices.DeleteFunc(cfg.Destinations, func(dest config.Destination) bool {
|
newCfg := cfg
|
||||||
|
newCfg.Destinations = slices.DeleteFunc(newCfg.Destinations, func(dest config.Destination) bool {
|
||||||
return dest.URL == c.URL
|
return dest.URL == c.URL
|
||||||
})
|
})
|
||||||
if err := params.ConfigService.SetConfig(cfg); err != nil {
|
if err := params.ConfigService.SetConfig(newCfg); err != nil {
|
||||||
// TODO: error handling
|
logger.Error("Config update failed", "err", err)
|
||||||
logger.Error("Failed to set config", "err", err)
|
ui.ConfigUpdateFailed(err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
case terminal.CommandStartDestination:
|
case terminal.CommandStartDestination:
|
||||||
mp.StartDestination(c.URL)
|
mp.StartDestination(c.URL)
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -43,7 +44,7 @@ func TestIntegration(t *testing.T) {
|
|||||||
destServerPort, err := destServer.MappedPort(ctx, "1936/tcp")
|
destServerPort, err := destServer.MappedPort(ctx, "1936/tcp")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
logger := testhelpers.NewTestLogger().With("component", "integration")
|
logger := testhelpers.NewTestLogger(t).With("component", "integration")
|
||||||
logger.Info("Initialised logger", "debug_level", logger.Enabled(ctx, slog.LevelDebug), "runner_debug", os.Getenv("RUNNER_DEBUG"))
|
logger.Info("Initialised logger", "debug_level", logger.Enabled(ctx, slog.LevelDebug), "runner_debug", os.Getenv("RUNNER_DEBUG"))
|
||||||
dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
|
dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -240,6 +241,144 @@ func TestIntegration(t *testing.T) {
|
|||||||
<-done
|
<-done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIntegrationDestinationValidations(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
logger := testhelpers.NewTestLogger(t).With("component", "integration")
|
||||||
|
dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
screen, screenCaptureC, getContents := setupSimulationScreen(t)
|
||||||
|
|
||||||
|
configService := setupConfigService(t, config.Config{
|
||||||
|
Sources: config.Sources{RTMP: config.RTMPSource{Enabled: true, StreamKey: "live"}},
|
||||||
|
})
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
err := app.Run(ctx, app.RunParams{
|
||||||
|
ConfigService: configService,
|
||||||
|
DockerClient: dockerClient,
|
||||||
|
Screen: &terminal.Screen{
|
||||||
|
Screen: screen,
|
||||||
|
Width: 160,
|
||||||
|
Height: 25,
|
||||||
|
CaptureC: screenCaptureC,
|
||||||
|
},
|
||||||
|
ClipboardAvailable: false,
|
||||||
|
BuildInfo: domain.BuildInfo{Version: "0.0.1", GoVersion: "go1.16.3"},
|
||||||
|
Logger: logger,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
done <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
require.EventuallyWithT(
|
||||||
|
t,
|
||||||
|
func(t *assert.CollectT) {
|
||||||
|
contents := getContents()
|
||||||
|
require.True(t, len(contents) > 2, "expected at least 3 lines of output")
|
||||||
|
|
||||||
|
assert.Contains(t, contents[2], "Status ready", "expected mediaserver status to be ready")
|
||||||
|
},
|
||||||
|
2*time.Minute,
|
||||||
|
time.Second,
|
||||||
|
"expected the mediaserver to start",
|
||||||
|
)
|
||||||
|
printScreen(getContents, "After starting the mediaserver")
|
||||||
|
|
||||||
|
sendKey(screen, tcell.KeyRune, 'a')
|
||||||
|
sendKey(screen, tcell.KeyTab, ' ')
|
||||||
|
sendBackspaces(screen, 10)
|
||||||
|
sendKey(screen, tcell.KeyTab, ' ')
|
||||||
|
sendKey(screen, tcell.KeyEnter, ' ')
|
||||||
|
|
||||||
|
require.EventuallyWithT(
|
||||||
|
t,
|
||||||
|
func(t *assert.CollectT) {
|
||||||
|
contents := getContents()
|
||||||
|
|
||||||
|
assert.True(t, contentsIncludes(contents, "Configuration update failed:"), "expected to see config update error")
|
||||||
|
assert.True(t, contentsIncludes(contents, "validate: destination URL must start with"), "expected to see config update error")
|
||||||
|
},
|
||||||
|
10*time.Second,
|
||||||
|
time.Second,
|
||||||
|
"expected a validation error for an empty URL",
|
||||||
|
)
|
||||||
|
printScreen(getContents, "After entering an empty destination URL")
|
||||||
|
|
||||||
|
sendKey(screen, tcell.KeyEnter, ' ')
|
||||||
|
sendKey(screen, tcell.KeyBacktab, ' ')
|
||||||
|
sendKeys(screen, "nope")
|
||||||
|
sendKey(screen, tcell.KeyTab, ' ')
|
||||||
|
sendKey(screen, tcell.KeyEnter, ' ')
|
||||||
|
|
||||||
|
require.EventuallyWithT(
|
||||||
|
t,
|
||||||
|
func(t *assert.CollectT) {
|
||||||
|
contents := getContents()
|
||||||
|
|
||||||
|
assert.True(t, contentsIncludes(contents, "Configuration update failed:"), "expected to see config update error")
|
||||||
|
assert.True(t, contentsIncludes(contents, "validate: destination URL must start with"), "expected to see config update error")
|
||||||
|
},
|
||||||
|
10*time.Second,
|
||||||
|
time.Second,
|
||||||
|
"expected a validation error for an invalid URL",
|
||||||
|
)
|
||||||
|
printScreen(getContents, "After entering an invalid destination URL")
|
||||||
|
|
||||||
|
sendKey(screen, tcell.KeyEnter, ' ')
|
||||||
|
sendKey(screen, tcell.KeyBacktab, ' ')
|
||||||
|
sendBackspaces(screen, len("nope"))
|
||||||
|
sendKeys(screen, "rtmp://rtmp.youtube.com:1935/live")
|
||||||
|
sendKey(screen, tcell.KeyTab, ' ')
|
||||||
|
sendKey(screen, tcell.KeyEnter, ' ')
|
||||||
|
|
||||||
|
require.EventuallyWithT(
|
||||||
|
t,
|
||||||
|
func(t *assert.CollectT) {
|
||||||
|
contents := getContents()
|
||||||
|
require.True(t, len(contents) > 2, "expected at least 3 lines of output")
|
||||||
|
|
||||||
|
require.Contains(t, contents[2], "My stream", "expected new destination to be present")
|
||||||
|
assert.Contains(t, contents[2], "off-air", "expected new destination to be off-air")
|
||||||
|
},
|
||||||
|
10*time.Second,
|
||||||
|
time.Second,
|
||||||
|
"expected to add the destination",
|
||||||
|
)
|
||||||
|
printScreen(getContents, "After adding the destination")
|
||||||
|
|
||||||
|
sendKey(screen, tcell.KeyRune, 'a')
|
||||||
|
sendKey(screen, tcell.KeyTab, ' ')
|
||||||
|
sendBackspaces(screen, 10)
|
||||||
|
sendKeys(screen, "rtmp://rtmp.youtube.com:1935/live")
|
||||||
|
sendKey(screen, tcell.KeyTab, ' ')
|
||||||
|
sendKey(screen, tcell.KeyEnter, ' ')
|
||||||
|
|
||||||
|
// Start streaming a test video to the app:
|
||||||
|
testhelpers.StreamFLV(t, "rtmp://localhost:1935/live")
|
||||||
|
|
||||||
|
require.EventuallyWithT(
|
||||||
|
t,
|
||||||
|
func(t *assert.CollectT) {
|
||||||
|
contents := getContents()
|
||||||
|
|
||||||
|
assert.True(t, contentsIncludes(contents, "Configuration update failed:"), "expected to see config update error")
|
||||||
|
assert.True(t, contentsIncludes(contents, "validate: duplicate destination URL: rtmp://"), "expected to see config update error")
|
||||||
|
},
|
||||||
|
10*time.Second,
|
||||||
|
time.Second,
|
||||||
|
"expected a validation error for a duplicate URL",
|
||||||
|
)
|
||||||
|
printScreen(getContents, "After entering a duplicate destination URL")
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
func setupSimulationScreen(t *testing.T) (tcell.SimulationScreen, chan<- terminal.ScreenCapture, func() []string) {
|
func setupSimulationScreen(t *testing.T) (tcell.SimulationScreen, chan<- terminal.ScreenCapture, func() []string) {
|
||||||
// Fetching the screen contents is tricky at this level of the test pyramid,
|
// Fetching the screen contents is tricky at this level of the test pyramid,
|
||||||
// because we need to:
|
// because we need to:
|
||||||
@ -299,6 +438,16 @@ func setupSimulationScreen(t *testing.T) (tcell.SimulationScreen, chan<- termina
|
|||||||
return screen, screenCaptureC, getContents
|
return screen, screenCaptureC, getContents
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func contentsIncludes(contents []string, search string) bool {
|
||||||
|
for _, line := range contents {
|
||||||
|
if strings.Contains(line, search) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func setupConfigService(t *testing.T, cfg config.Config) *config.Service {
|
func setupConfigService(t *testing.T, cfg config.Config) *config.Service {
|
||||||
tmpDir, err := os.MkdirTemp("", "octoplex_"+t.Name())
|
tmpDir, err := os.MkdirTemp("", "octoplex_"+t.Name())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -321,6 +470,12 @@ func sendKey(screen tcell.SimulationScreen, key tcell.Key, ch rune) {
|
|||||||
screen.InjectKey(key, ch, tcell.ModNone)
|
screen.InjectKey(key, ch, tcell.ModNone)
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendKeyShift(screen tcell.SimulationScreen, key tcell.Key, ch rune) {
|
||||||
|
screen.InjectKey(key, ch, tcell.ModShift)
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
func sendKeys(screen tcell.SimulationScreen, keys string) {
|
func sendKeys(screen tcell.SimulationScreen, keys string) {
|
||||||
screen.InjectKeyBytes([]byte(keys))
|
screen.InjectKeyBytes([]byte(keys))
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
@ -96,6 +96,10 @@ func (s *Service) ReadOrCreateConfig() (cfg Config, _ error) {
|
|||||||
// SetConfig sets the configuration to the given value and writes it to the
|
// SetConfig sets the configuration to the given value and writes it to the
|
||||||
// file.
|
// file.
|
||||||
func (s *Service) SetConfig(cfg Config) error {
|
func (s *Service) SetConfig(cfg Config) error {
|
||||||
|
if err := validate(cfg); err != nil {
|
||||||
|
return fmt.Errorf("validate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
cfgBytes, err := yaml.Marshal(cfg)
|
cfgBytes, err := yaml.Marshal(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshal: %w", err)
|
return fmt.Errorf("marshal: %w", err)
|
||||||
@ -176,13 +180,24 @@ func (s *Service) setDefaults(cfg *Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: validate URL format
|
||||||
func validate(cfg Config) error {
|
func validate(cfg Config) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
urlCounts := make(map[string]int)
|
||||||
|
|
||||||
for _, dest := range cfg.Destinations {
|
for _, dest := range cfg.Destinations {
|
||||||
if !strings.HasPrefix(dest.URL, "rtmp://") {
|
if !strings.HasPrefix(dest.URL, "rtmp://") {
|
||||||
err = errors.Join(err, fmt.Errorf("destination URL must start with rtmp://"))
|
err = errors.Join(err, fmt.Errorf("destination URL must start with rtmp://"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
urlCounts[dest.URL]++
|
||||||
|
}
|
||||||
|
|
||||||
|
for url, count := range urlCounts {
|
||||||
|
if count > 1 {
|
||||||
|
err = errors.Join(err, fmt.Errorf("duplicate destination URL: %s", url))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
@ -22,7 +22,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestClientRunContainer(t *testing.T) {
|
func TestClientRunContainer(t *testing.T) {
|
||||||
logger := testhelpers.NewTestLogger()
|
logger := testhelpers.NewTestLogger(t)
|
||||||
|
|
||||||
// channels returned by Docker's ContainerWait:
|
// channels returned by Docker's ContainerWait:
|
||||||
containerWaitC := make(chan dockercontainer.WaitResponse)
|
containerWaitC := make(chan dockercontainer.WaitResponse)
|
||||||
@ -128,7 +128,7 @@ func TestClientRunContainer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestClientRunContainerErrorStartingContainer(t *testing.T) {
|
func TestClientRunContainerErrorStartingContainer(t *testing.T) {
|
||||||
logger := testhelpers.NewTestLogger()
|
logger := testhelpers.NewTestLogger(t)
|
||||||
|
|
||||||
var dockerClient mocks.DockerClient
|
var dockerClient mocks.DockerClient
|
||||||
defer dockerClient.AssertExpectations(t)
|
defer dockerClient.AssertExpectations(t)
|
||||||
@ -174,7 +174,7 @@ func TestClientRunContainerErrorStartingContainer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestClientClose(t *testing.T) {
|
func TestClientClose(t *testing.T) {
|
||||||
logger := testhelpers.NewTestLogger()
|
logger := testhelpers.NewTestLogger(t)
|
||||||
|
|
||||||
var dockerClient mocks.DockerClient
|
var dockerClient mocks.DockerClient
|
||||||
defer dockerClient.AssertExpectations(t)
|
defer dockerClient.AssertExpectations(t)
|
||||||
@ -213,7 +213,7 @@ func TestClientClose(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRemoveUnusedNetworks(t *testing.T) {
|
func TestRemoveUnusedNetworks(t *testing.T) {
|
||||||
logger := testhelpers.NewTestLogger()
|
logger := testhelpers.NewTestLogger(t)
|
||||||
|
|
||||||
var dockerClient mocks.DockerClient
|
var dockerClient mocks.DockerClient
|
||||||
defer dockerClient.AssertExpectations(t)
|
defer dockerClient.AssertExpectations(t)
|
||||||
|
@ -22,7 +22,7 @@ func TestIntegrationClientStartStop(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
t.Cleanup(cancel)
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
logger := testhelpers.NewTestLogger()
|
logger := testhelpers.NewTestLogger(t)
|
||||||
apiClient, err := client.NewClientWithOpts(client.FromEnv)
|
apiClient, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
containerName := "octoplex-test-" + shortid.New().String()
|
containerName := "octoplex-test-" + shortid.New().String()
|
||||||
@ -72,7 +72,7 @@ func TestIntegrationClientRemoveContainers(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
t.Cleanup(cancel)
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
logger := testhelpers.NewTestLogger()
|
logger := testhelpers.NewTestLogger(t)
|
||||||
apiClient, err := client.NewClientWithOpts(client.FromEnv)
|
apiClient, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
component := "test-remove-containers"
|
component := "test-remove-containers"
|
||||||
@ -171,7 +171,7 @@ func TestContainerRestart(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
t.Cleanup(cancel)
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
logger := testhelpers.NewTestLogger()
|
logger := testhelpers.NewTestLogger(t)
|
||||||
apiClient, err := client.NewClientWithOpts(client.FromEnv)
|
apiClient, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
containerName := "octoplex-test-" + shortid.New().String()
|
containerName := "octoplex-test-" + shortid.New().String()
|
||||||
|
@ -33,7 +33,7 @@ func TestHandleStats(t *testing.T) {
|
|||||||
Return(dockercontainer.StatsResponseReader{Body: pr}, nil)
|
Return(dockercontainer.StatsResponseReader{Body: pr}, nil)
|
||||||
|
|
||||||
networkCountConfig := NetworkCountConfig{Rx: "eth0", Tx: "eth1"}
|
networkCountConfig := NetworkCountConfig{Rx: "eth0", Tx: "eth1"}
|
||||||
logger := testhelpers.NewTestLogger()
|
logger := testhelpers.NewTestLogger(t)
|
||||||
ch := make(chan stats)
|
ch := make(chan stats)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@ -79,7 +79,7 @@ func TestHandleStatsWithContainerRestart(t *testing.T) {
|
|||||||
Return(dockercontainer.StatsResponseReader{Body: pr}, nil)
|
Return(dockercontainer.StatsResponseReader{Body: pr}, nil)
|
||||||
|
|
||||||
networkCountConfig := NetworkCountConfig{Rx: "eth1", Tx: "eth0"}
|
networkCountConfig := NetworkCountConfig{Rx: "eth1", Tx: "eth0"}
|
||||||
logger := testhelpers.NewTestLogger()
|
logger := testhelpers.NewTestLogger(t)
|
||||||
ch := make(chan stats)
|
ch := make(chan stats)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -222,9 +222,11 @@ func StartUI(ctx context.Context, params StartParams) (*UI, error) {
|
|||||||
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
// Special case: allow all keys except Escape to be passed to the add
|
// Special case: allow all keys except Escape to be passed to the add
|
||||||
// destination modal.
|
// destination modal.
|
||||||
|
//
|
||||||
|
// TODO: catch Ctrl-c
|
||||||
if pageName, _ := pages.GetFrontPage(); pageName == pageNameAddDestination {
|
if pageName, _ := pages.GetFrontPage(); pageName == pageNameAddDestination {
|
||||||
if event.Key() == tcell.KeyEscape {
|
if event.Key() == tcell.KeyEscape {
|
||||||
ui.closeDestinationForm()
|
ui.closeAddDestinationForm()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -468,6 +470,7 @@ const (
|
|||||||
pageNameModalClipboard = "modal-clipboard"
|
pageNameModalClipboard = "modal-clipboard"
|
||||||
pageNameModalPullProgress = "modal-pull-progress"
|
pageNameModalPullProgress = "modal-pull-progress"
|
||||||
pageNameModalRemoveDestination = "modal-remove-destination"
|
pageNameModalRemoveDestination = "modal-remove-destination"
|
||||||
|
pageNameConfigUpdateFailed = "modal-config-update-failed"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ui *UI) showModal(pageName string, text string, buttons []string, doneFunc func(int, string)) {
|
func (ui *UI) showModal(pageName string, text string, buttons []string, doneFunc func(int, string)) {
|
||||||
@ -659,8 +662,24 @@ func (ui *UI) Close() {
|
|||||||
ui.app.Stop()
|
ui.app.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ui *UI) ConfigUpdateFailed(err error) {
|
||||||
|
ui.app.QueueUpdateDraw(func() {
|
||||||
|
ui.showModal(
|
||||||
|
pageNameConfigUpdateFailed,
|
||||||
|
"Configuration update failed:\n\n"+err.Error(),
|
||||||
|
[]string{"Ok"},
|
||||||
|
func(int, string) {
|
||||||
|
pageName, frontPage := ui.pages.GetFrontPage()
|
||||||
|
if pageName != pageNameAddDestination {
|
||||||
|
ui.logger.Warn("Unexpected page when configuration form closed", "page", pageName)
|
||||||
|
}
|
||||||
|
ui.app.SetFocus(frontPage)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (ui *UI) addDestination() {
|
func (ui *UI) addDestination() {
|
||||||
// TODO: check for existing
|
|
||||||
const (
|
const (
|
||||||
inputLen = 60
|
inputLen = 60
|
||||||
inputLabelName = "Name"
|
inputLabelName = "Name"
|
||||||
@ -687,11 +706,8 @@ func (ui *UI) addDestination() {
|
|||||||
DestinationName: form.GetFormItemByLabel(inputLabelName).(*tview.InputField).GetText(),
|
DestinationName: form.GetFormItemByLabel(inputLabelName).(*tview.InputField).GetText(),
|
||||||
URL: form.GetFormItemByLabel(inputLabelURL).(*tview.InputField).GetText(),
|
URL: form.GetFormItemByLabel(inputLabelURL).(*tview.InputField).GetText(),
|
||||||
}
|
}
|
||||||
ui.closeDestinationForm()
|
|
||||||
}).
|
|
||||||
AddButton("Cancel", func() {
|
|
||||||
ui.closeDestinationForm()
|
|
||||||
}).
|
}).
|
||||||
|
AddButton("Cancel", func() { ui.closeAddDestinationForm() }).
|
||||||
SetFieldBackgroundColor(tcell.ColorDarkSlateGrey).
|
SetFieldBackgroundColor(tcell.ColorDarkSlateGrey).
|
||||||
SetBorder(true).
|
SetBorder(true).
|
||||||
SetTitle("Add a new destination").
|
SetTitle("Add a new destination").
|
||||||
@ -731,7 +747,13 @@ func (ui *UI) removeDestination() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ui *UI) closeDestinationForm() {
|
func (ui *UI) DestinationAdded() {
|
||||||
|
ui.app.QueueUpdateDraw(func() {
|
||||||
|
ui.closeAddDestinationForm()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ui *UI) closeAddDestinationForm() {
|
||||||
ui.pages.RemovePage(pageNameAddDestination)
|
ui.pages.RemovePage(pageNameAddDestination)
|
||||||
ui.app.SetFocus(ui.destView)
|
ui.app.SetFocus(ui.destView)
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package testhelpers
|
|||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewNopLogger returns a logger that discards all log output.
|
// NewNopLogger returns a logger that discards all log output.
|
||||||
@ -11,11 +12,11 @@ func NewNopLogger() *slog.Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewTestLogger returns a logger that writes to stderr.
|
// NewTestLogger returns a logger that writes to stderr.
|
||||||
func NewTestLogger() *slog.Logger {
|
func NewTestLogger(t *testing.T) *slog.Logger {
|
||||||
var handlerOpts slog.HandlerOptions
|
var handlerOpts slog.HandlerOptions
|
||||||
// RUNNER_DEBUG is used in the GitHub actions runner to enable debug logging.
|
// RUNNER_DEBUG is used in the GitHub actions runner to enable debug logging.
|
||||||
if os.Getenv("DEBUG") != "" || os.Getenv("RUNNER_DEBUG") != "" {
|
if os.Getenv("DEBUG") != "" || os.Getenv("RUNNER_DEBUG") != "" {
|
||||||
handlerOpts.Level = slog.LevelDebug
|
handlerOpts.Level = slog.LevelDebug
|
||||||
}
|
}
|
||||||
return slog.New(slog.NewTextHandler(os.Stderr, &handlerOpts))
|
return slog.New(slog.NewTextHandler(os.Stderr, &handlerOpts)).With("test", t.Name())
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user