fixup! wip: refactor: API

This commit is contained in:
Rob Watson 2025-05-11 06:12:44 +02:00
parent c706a41acd
commit b8a77d9c6c
3 changed files with 65 additions and 221 deletions

View File

@ -22,30 +22,10 @@ import (
"git.netflux.io/rob/octoplex/internal/domain" "git.netflux.io/rob/octoplex/internal/domain"
"git.netflux.io/rob/octoplex/internal/terminal" "git.netflux.io/rob/octoplex/internal/terminal"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go"
) )
func buildAppParams(
t *testing.T,
configService *config.Service,
dockerClient container.DockerClient,
screen tcell.SimulationScreen,
screenCaptureC chan<- terminal.ScreenCapture,
logger *slog.Logger,
) app.Params {
t.Helper()
return app.Params{
ConfigService: configService,
DockerClient: dockerClient,
ClipboardAvailable: false,
BuildInfo: domain.BuildInfo{Version: "0.0.1", GoVersion: "go1.16.3"},
Logger: logger,
}
}
func buildClientServer( func buildClientServer(
configService *config.Service, configService *config.Service,
dockerClient container.DockerClient, dockerClient container.DockerClient,
@ -54,7 +34,8 @@ func buildClientServer(
logger *slog.Logger, logger *slog.Logger,
) (*client.App, *app.App) { ) (*client.App, *app.App) {
buildInfo := domain.BuildInfo{Version: "0.0.1", GoVersion: "go1.16.3"} buildInfo := domain.BuildInfo{Version: "0.0.1", GoVersion: "go1.16.3"}
clientApp := client.New(client.NewParams{
client := client.New(client.NewParams{
BuildInfo: buildInfo, BuildInfo: buildInfo,
Screen: &terminal.Screen{ Screen: &terminal.Screen{
Screen: screen, Screen: screen,
@ -65,8 +46,7 @@ func buildClientServer(
Logger: logger, Logger: logger,
}) })
// TODO: use buildAppParams server := app.New(app.Params{
srvApp := app.New(app.Params{
ConfigService: configService, ConfigService: configService,
DockerClient: dockerClient, DockerClient: dockerClient,
ClipboardAvailable: false, ClipboardAvailable: false,
@ -74,29 +54,44 @@ func buildClientServer(
Logger: logger, Logger: logger,
}) })
return clientApp, srvApp return client, server
}
type clientServerResult struct {
errClient error
errServer error
} }
func runClientServer( func runClientServer(
ctx context.Context, ctx context.Context,
t *testing.T, _ *testing.T,
wg *sync.WaitGroup,
clientApp *client.App, clientApp *client.App,
serverApp *app.App, serverApp *app.App,
) { ) <-chan clientServerResult {
ch := make(chan clientServerResult, 1)
var wg sync.WaitGroup
var clientErr, srvErr error
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
assert.ErrorIs(t, serverApp.Run(ctx), context.Canceled) srvErr = serverApp.Run(ctx)
}() }()
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
// May be a gRPC error, not context.Canceled: clientErr = clientApp.Run(ctx)
assert.ErrorContains(t, clientApp.Run(ctx), "context canceled")
}() }()
go func() {
wg.Wait()
ch <- clientServerResult{errClient: clientErr, errServer: srvErr}
}()
return ch
} }
func setupSimulationScreen(t *testing.T) (tcell.SimulationScreen, chan<- terminal.ScreenCapture, func() []string) { func setupSimulationScreen(t *testing.T) (tcell.SimulationScreen, chan<- terminal.ScreenCapture, func() []string) {

View File

@ -8,26 +8,19 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors"
"fmt" "fmt"
"net"
"os" "os"
"sync"
"testing" "testing"
"time" "time"
"git.netflux.io/rob/octoplex/internal/app"
"git.netflux.io/rob/octoplex/internal/config" "git.netflux.io/rob/octoplex/internal/config"
"git.netflux.io/rob/octoplex/internal/container" "git.netflux.io/rob/octoplex/internal/container"
"git.netflux.io/rob/octoplex/internal/container/mocks"
"git.netflux.io/rob/octoplex/internal/domain" "git.netflux.io/rob/octoplex/internal/domain"
"git.netflux.io/rob/octoplex/internal/testhelpers" "git.netflux.io/rob/octoplex/internal/testhelpers"
"github.com/docker/docker/api/types/network"
dockerclient "github.com/docker/docker/client" dockerclient "github.com/docker/docker/client"
"github.com/docker/docker/errdefs" "github.com/docker/docker/errdefs"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait" "github.com/testcontainers/testcontainers-go/wait"
@ -127,8 +120,7 @@ func testIntegration(t *testing.T, mediaServerConfig config.MediaServerSource) {
}) })
client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger) client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
var wg sync.WaitGroup ch := runClientServer(ctx, t, client, server)
runClientServer(ctx, t, &wg, client, server)
require.EventuallyWithT( require.EventuallyWithT(
t, t,
@ -271,7 +263,10 @@ func testIntegration(t *testing.T, mediaServerConfig config.MediaServerSource) {
cancel() cancel()
wg.Wait() result := <-ch
// May be a gRPC error, not context.Canceled:
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
} }
func TestIntegrationCustomHost(t *testing.T) { func TestIntegrationCustomHost(t *testing.T) {
@ -293,8 +288,7 @@ func TestIntegrationCustomHost(t *testing.T) {
screen, screenCaptureC, getContents := setupSimulationScreen(t) screen, screenCaptureC, getContents := setupSimulationScreen(t)
client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger) client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
var wg sync.WaitGroup ch := runClientServer(ctx, t, client, server)
runClientServer(ctx, t, &wg, client, server)
time.Sleep(time.Second) time.Sleep(time.Second)
sendKey(t, screen, tcell.KeyF1, ' ') sendKey(t, screen, tcell.KeyF1, ' ')
@ -334,7 +328,10 @@ func TestIntegrationCustomHost(t *testing.T) {
cancel() cancel()
wg.Wait() result := <-ch
// May be a gRPC error, not context.Canceled:
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
} }
func TestIntegrationCustomTLSCerts(t *testing.T) { func TestIntegrationCustomTLSCerts(t *testing.T) {
@ -359,8 +356,7 @@ func TestIntegrationCustomTLSCerts(t *testing.T) {
screen, screenCaptureC, getContents := setupSimulationScreen(t) screen, screenCaptureC, getContents := setupSimulationScreen(t)
client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger) client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
var wg sync.WaitGroup ch := runClientServer(ctx, t, client, server)
runClientServer(ctx, t, &wg, client, server)
require.EventuallyWithT( require.EventuallyWithT(
t, t,
@ -395,7 +391,9 @@ func TestIntegrationCustomTLSCerts(t *testing.T) {
cancel() cancel()
wg.Wait() result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
} }
func TestIntegrationRestartDestination(t *testing.T) { func TestIntegrationRestartDestination(t *testing.T) {
@ -434,14 +432,8 @@ func TestIntegrationRestartDestination(t *testing.T) {
}}, }},
}) })
done := make(chan struct{}) client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
go func() { ch := runClientServer(ctx, t, client, server)
defer func() {
done <- struct{}{}
}()
require.Equal(t, context.Canceled, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}()
require.EventuallyWithT( require.EventuallyWithT(
t, t,
@ -553,7 +545,9 @@ func TestIntegrationRestartDestination(t *testing.T) {
cancel() cancel()
<-done result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
} }
func TestIntegrationStartDestinationFailed(t *testing.T) { func TestIntegrationStartDestinationFailed(t *testing.T) {
@ -571,14 +565,8 @@ func TestIntegrationStartDestinationFailed(t *testing.T) {
Destinations: []config.Destination{{Name: "Example server", URL: "rtmp://rtmp.example.com/live"}}, Destinations: []config.Destination{{Name: "Example server", URL: "rtmp://rtmp.example.com/live"}},
}) })
done := make(chan struct{}) client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
go func() { ch := runClientServer(ctx, t, client, server)
defer func() {
done <- struct{}{}
}()
require.Equal(t, context.Canceled, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}()
require.EventuallyWithT( require.EventuallyWithT(
t, t,
@ -627,7 +615,9 @@ func TestIntegrationStartDestinationFailed(t *testing.T) {
cancel() cancel()
<-done result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
} }
func TestIntegrationDestinationValidations(t *testing.T) { func TestIntegrationDestinationValidations(t *testing.T) {
@ -644,14 +634,8 @@ func TestIntegrationDestinationValidations(t *testing.T) {
Sources: config.Sources{MediaServer: config.MediaServerSource{StreamKey: "live", RTMP: config.RTMPSource{Enabled: true}}}, Sources: config.Sources{MediaServer: config.MediaServerSource{StreamKey: "live", RTMP: config.RTMPSource{Enabled: true}}},
}) })
done := make(chan struct{}) client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
go func() { ch := runClientServer(ctx, t, client, server)
defer func() {
done <- struct{}{}
}()
require.Equal(t, context.Canceled, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}()
require.EventuallyWithT( require.EventuallyWithT(
t, t,
@ -755,7 +739,9 @@ func TestIntegrationDestinationValidations(t *testing.T) {
printScreen(t, getContents, "After entering a duplicate destination URL") printScreen(t, getContents, "After entering a duplicate destination URL")
cancel() cancel()
<-done result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
} }
func TestIntegrationStartupCheck(t *testing.T) { func TestIntegrationStartupCheck(t *testing.T) {
@ -786,14 +772,8 @@ func TestIntegrationStartupCheck(t *testing.T) {
configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: config.RTMPSource{Enabled: true}}}}) configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: config.RTMPSource{Enabled: true}}}})
screen, screenCaptureC, getContents := setupSimulationScreen(t) screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{}) client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
go func() { ch := runClientServer(ctx, t, client, server)
defer func() {
done <- struct{}{}
}()
require.Equal(t, context.Canceled, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}()
require.EventuallyWithT( require.EventuallyWithT(
t, t,
@ -837,136 +817,9 @@ func TestIntegrationStartupCheck(t *testing.T) {
printScreen(t, getContents, "After starting the mediaserver") printScreen(t, getContents, "After starting the mediaserver")
cancel() cancel()
<-done result := <-ch
} assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
func TestIntegrationMediaServerError(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Minute)
defer cancel()
lis, err := net.Listen("tcp", ":1935")
require.NoError(t, err)
t.Cleanup(func() { lis.Close() })
logger := testhelpers.NewTestLogger(t).With("component", "integration")
dockerClient, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
require.NoError(t, err)
configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: config.RTMPSource{Enabled: true}}}})
screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
require.EqualError(
t,
app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx),
"media server exited",
)
}()
require.EventuallyWithT(
t,
func(c *assert.CollectT) {
assert.True(c, contentsIncludes(getContents(), "Server process exited unexpectedly."), "expected to see title")
assert.True(c, contentsIncludes(getContents(), "address already in use"), "expected to see message")
},
waitTime,
time.Second,
"expected to see media server error modal",
)
printScreen(t, getContents, "Ater displaying the media server error modal")
// Quit the app, this should cause the done channel to receive.
sendKey(t, screen, tcell.KeyEnter, ' ')
<-done
}
func TestIntegrationDockerClientError(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Minute)
defer cancel()
logger := testhelpers.NewTestLogger(t).With("component", "integration")
var dockerClient mocks.DockerClient
dockerClient.EXPECT().NetworkCreate(mock.Anything, mock.Anything, mock.Anything).Return(network.CreateResponse{}, errors.New("boom"))
configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: config.RTMPSource{Enabled: true}}}})
screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
require.EqualError(
t,
app.New(buildAppParams(t, configService, &dockerClient, screen, screenCaptureC, logger)).Run(ctx),
"create container client: network create: boom",
)
}()
require.EventuallyWithT(
t,
func(c *assert.CollectT) {
assert.True(c, contentsIncludes(getContents(), "An error occurred:"), "expected to see error message")
assert.True(c, contentsIncludes(getContents(), "create container client: network create: boom"), "expected to see message")
},
waitTime,
time.Second,
"expected to see fatal error modal",
)
printScreen(t, getContents, "Ater displaying the fatal error modal")
// Quit the app, this should cause the done channel to receive.
sendKey(t, screen, tcell.KeyEnter, ' ')
<-done
}
func TestIntegrationDockerConnectionError(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.WithHost("http://docker.example.com"))
require.NoError(t, err)
configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: config.MediaServerSource{RTMP: config.RTMPSource{Enabled: true}}}})
screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{})
go func() {
defer func() {
done <- struct{}{}
}()
err := app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx)
require.ErrorContains(t, err, "dial tcp: lookup docker.example.com")
require.ErrorContains(t, err, "no such host")
}()
require.EventuallyWithT(
t,
func(c *assert.CollectT) {
assert.True(c, contentsIncludes(getContents(), "An error occurred:"), "expected to see error message")
assert.True(c, contentsIncludes(getContents(), "Could not connect to Docker. Is Docker installed"), "expected to see message")
},
waitTime,
time.Second,
"expected to see fatal error modal",
)
printScreen(t, getContents, "Ater displaying the fatal error modal")
// Quit the app, this should cause the done channel to receive.
sendKey(t, screen, tcell.KeyEnter, ' ')
<-done
} }
func TestIntegrationCopyURLs(t *testing.T) { func TestIntegrationCopyURLs(t *testing.T) {
@ -1036,14 +889,8 @@ func TestIntegrationCopyURLs(t *testing.T) {
configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: tc.mediaServerConfig}}) configService := setupConfigService(t, config.Config{Sources: config.Sources{MediaServer: tc.mediaServerConfig}})
screen, screenCaptureC, getContents := setupSimulationScreen(t) screen, screenCaptureC, getContents := setupSimulationScreen(t)
done := make(chan struct{}) client, server := buildClientServer(configService, dockerClient, screen, screenCaptureC, logger)
go func() { ch := runClientServer(ctx, t, client, server)
defer func() {
done <- struct{}{}
}()
require.Equal(t, context.Canceled, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}()
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
printScreen(t, getContents, "Ater loading the app") printScreen(t, getContents, "Ater loading the app")
@ -1064,7 +911,9 @@ func TestIntegrationCopyURLs(t *testing.T) {
cancel() cancel()
<-done result := <-ch
assert.ErrorContains(t, result.errClient, "context canceled")
assert.ErrorIs(t, result.errServer, context.Canceled)
}) })
} }
} }

View File

@ -70,7 +70,7 @@ func (a *App) Run(ctx context.Context) error {
continue continue
} }
a.logger.Debug("Received event", "type", fmt.Sprintf("%T", evt)) a.logger.Debug("Received event")
a.bus.Send(protocol.EventFromProto(evt)) a.bus.Send(protocol.EventFromProto(evt))
} }
}) })