Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44143a9734 |
+14
-245
@@ -126,7 +126,6 @@ const (
|
|||||||
privmsgCmd = "PRIVMSG"
|
privmsgCmd = "PRIVMSG"
|
||||||
joinCmd = "JOIN"
|
joinCmd = "JOIN"
|
||||||
apiMessages = "/api/v1/messages"
|
apiMessages = "/api/v1/messages"
|
||||||
apiLogout = "/api/v1/logout"
|
|
||||||
apiSession = "/api/v1/session"
|
apiSession = "/api/v1/session"
|
||||||
apiState = "/api/v1/state"
|
apiState = "/api/v1/state"
|
||||||
authCookieName = "neoirc_auth"
|
authCookieName = "neoirc_auth"
|
||||||
@@ -137,8 +136,6 @@ type testServer struct {
|
|||||||
httpServer *httptest.Server
|
httpServer *httptest.Server
|
||||||
t *testing.T
|
t *testing.T
|
||||||
fxApp *fxtest.App
|
fxApp *fxtest.App
|
||||||
broker *broker.Broker
|
|
||||||
database *db.Database
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestServer(
|
func newTestServer(
|
||||||
@@ -165,11 +162,7 @@ func newTestServerWith(
|
|||||||
)
|
)
|
||||||
copyTemplateDB(t, dbURL)
|
copyTemplateDB(t, dbURL)
|
||||||
|
|
||||||
var (
|
var srv *server.Server
|
||||||
srv *server.Server
|
|
||||||
brk *broker.Broker
|
|
||||||
database *db.Database
|
|
||||||
)
|
|
||||||
|
|
||||||
app := fxtest.New(t,
|
app := fxtest.New(t,
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
@@ -202,14 +195,13 @@ func newTestServerWith(
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
},
|
},
|
||||||
newTestDB,
|
newTestDB,
|
||||||
broker.New,
|
|
||||||
stats.New,
|
stats.New,
|
||||||
newTestHealthcheck,
|
newTestHealthcheck,
|
||||||
newTestMiddleware,
|
newTestMiddleware,
|
||||||
newTestHandlers,
|
newTestHandlers,
|
||||||
newTestServerFx,
|
newTestServerFx,
|
||||||
),
|
),
|
||||||
fx.Populate(&srv, &brk, &database),
|
fx.Populate(&srv),
|
||||||
)
|
)
|
||||||
|
|
||||||
app.RequireStart()
|
app.RequireStart()
|
||||||
@@ -225,8 +217,6 @@ func newTestServerWith(
|
|||||||
httpServer: httpSrv,
|
httpServer: httpSrv,
|
||||||
t: t,
|
t: t,
|
||||||
fxApp: app,
|
fxApp: app,
|
||||||
broker: brk,
|
|
||||||
database: database,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,8 +291,8 @@ func newTestHandlers(
|
|||||||
database *db.Database,
|
database *db.Database,
|
||||||
hcheck *healthcheck.Healthcheck,
|
hcheck *healthcheck.Healthcheck,
|
||||||
tracker *stats.Tracker,
|
tracker *stats.Tracker,
|
||||||
brk *broker.Broker,
|
|
||||||
) (*handlers.Handlers, error) {
|
) (*handlers.Handlers, error) {
|
||||||
|
brk := broker.New()
|
||||||
svc := service.New(service.Params{ //nolint:exhaustruct
|
svc := service.New(service.Params{ //nolint:exhaustruct
|
||||||
Logger: log,
|
Logger: log,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
@@ -335,7 +325,6 @@ func newTestServerFx(
|
|||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
mware *middleware.Middleware,
|
mware *middleware.Middleware,
|
||||||
hdlr *handlers.Handlers,
|
hdlr *handlers.Handlers,
|
||||||
shutdowner fx.Shutdowner,
|
|
||||||
) (*server.Server, error) {
|
) (*server.Server, error) {
|
||||||
srv, err := server.New(lifecycle, server.Params{ //nolint:exhaustruct
|
srv, err := server.New(lifecycle, server.Params{ //nolint:exhaustruct
|
||||||
Logger: log,
|
Logger: log,
|
||||||
@@ -343,7 +332,6 @@ func newTestServerFx(
|
|||||||
Config: cfg,
|
Config: cfg,
|
||||||
Middleware: mware,
|
Middleware: mware,
|
||||||
Handlers: hdlr,
|
Handlers: hdlr,
|
||||||
Shutdowner: shutdowner,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("test server: %w", err)
|
return nil, fmt.Errorf("test server: %w", err)
|
||||||
@@ -1414,7 +1402,7 @@ const quitRaceTrials = 5
|
|||||||
// is sent once.
|
// is sent once.
|
||||||
func TestConcurrentLogoutsSendOneQuit(t *testing.T) {
|
func TestConcurrentLogoutsSendOneQuit(t *testing.T) {
|
||||||
checkQuitRace(t, func(tserver *testServer, tokens []string) {
|
checkQuitRace(t, func(tserver *testServer, tokens []string) {
|
||||||
tserver.postConcurrently(apiLogout, nil, tokens...)
|
tserver.postConcurrently("/api/v1/logout", nil, tokens...)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1529,7 +1517,15 @@ func TestLogoutOfOneClientKeepsSession(t *testing.T) {
|
|||||||
t.Fatalf("other client's state: got %d, want 200", status)
|
t.Fatalf("other client's state: got %d, want 200", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
names := channelNames(state)
|
channels, _ := state["channels"].([]any)
|
||||||
|
names := make([]string, 0, len(channels))
|
||||||
|
|
||||||
|
for _, entry := range channels {
|
||||||
|
info, _ := entry.(map[string]any)
|
||||||
|
name, _ := info["name"].(string)
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
|
||||||
if !slices.Equal(names, []string{channel}) {
|
if !slices.Equal(names, []string{channel}) {
|
||||||
t.Errorf("session's channels: got %v, want [%s]", names, channel)
|
t.Errorf("session's channels: got %v, want [%s]", names, channel)
|
||||||
}
|
}
|
||||||
@@ -1551,201 +1547,6 @@ func TestLogoutOfOneClientKeepsSession(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestFailedLogoutChangesNothing checks that a logout whose
|
|
||||||
// session cannot be deleted answers 500 and changes
|
|
||||||
// nothing: the client's token still works, the session is
|
|
||||||
// still on its channel and its channel peer has no QUIT.
|
|
||||||
func TestFailedLogoutChangesNothing(t *testing.T) {
|
|
||||||
const (
|
|
||||||
nick = "stuckleaver"
|
|
||||||
channel = "#stuck"
|
|
||||||
)
|
|
||||||
|
|
||||||
tserver := newTestServer(t)
|
|
||||||
token := tserver.createSession(nick)
|
|
||||||
observerToken := tserver.createSession("stuckobserver")
|
|
||||||
|
|
||||||
for _, joiner := range []string{token, observerToken} {
|
|
||||||
tserver.sendCommand(joiner, map[string]any{
|
|
||||||
commandKey: joinCmd, toKey: channel,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_, lastID := tserver.pollMessages(observerToken, 0)
|
|
||||||
|
|
||||||
_, err := tserver.database.GetDB().ExecContext(t.Context(),
|
|
||||||
`CREATE TRIGGER keep_sessions
|
|
||||||
BEFORE DELETE ON sessions
|
|
||||||
BEGIN
|
|
||||||
SELECT RAISE(ABORT, 'sessions are kept');
|
|
||||||
END`)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := doRequestAuth(
|
|
||||||
t, http.MethodPost, tserver.url(apiLogout), token, nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("logout: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusInternalServerError {
|
|
||||||
t.Errorf("logout: got %d, want 500", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
status, state := tserver.getState(token)
|
|
||||||
if status != http.StatusOK {
|
|
||||||
t.Fatalf("state after the failed logout: got %d, want 200", status)
|
|
||||||
}
|
|
||||||
|
|
||||||
names := channelNames(state)
|
|
||||||
if !slices.Equal(names, []string{channel}) {
|
|
||||||
t.Errorf("session's channels: got %v, want [%s]", names, channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
msgs, _ := tserver.pollMessages(observerToken, lastID)
|
|
||||||
|
|
||||||
quits := countQuits(msgs, nick)
|
|
||||||
if quits != 0 {
|
|
||||||
t.Errorf("%d QUITs, want 0: %v", quits, msgs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// quitWakePollTimeout is the timeout, in seconds, of the
|
|
||||||
// long poll in checkQuitWakesPoll.
|
|
||||||
quitWakePollTimeout = 10
|
|
||||||
|
|
||||||
// quitWakeDeadline is how long checkQuitWakesPoll waits
|
|
||||||
// for that long poll to start waiting, and then to
|
|
||||||
// return once the session has left.
|
|
||||||
quitWakeDeadline = 5 * time.Second
|
|
||||||
|
|
||||||
// pollWaitStep is how often checkQuitWakesPoll checks
|
|
||||||
// whether the long poll is waiting yet.
|
|
||||||
pollWaitStep = 10 * time.Millisecond
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestQuitWakesPeerLongPoll checks that a session's QUIT
|
|
||||||
// wakes the long poll of a user who shares a channel with
|
|
||||||
// it.
|
|
||||||
func TestQuitWakesPeerLongPoll(t *testing.T) {
|
|
||||||
const reason = "gone fishing"
|
|
||||||
|
|
||||||
checkQuitWakesPoll(t, reason, func(tserver *testServer, token string) {
|
|
||||||
tserver.sendCommand(token, map[string]any{
|
|
||||||
commandKey: irc.CmdQuit, bodyKey: []string{reason},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLastLogoutWakesPeerLongPoll checks that the logout of
|
|
||||||
// a session's last client wakes the long poll of a user who
|
|
||||||
// shares a channel with it.
|
|
||||||
func TestLastLogoutWakesPeerLongPoll(t *testing.T) {
|
|
||||||
checkQuitWakesPoll(t, "Connection closed", func(tserver *testServer, token string) {
|
|
||||||
tserver.logout(token)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkQuitWakesPoll puts a session and a peer on a
|
|
||||||
// channel, and calls leave with the session's token while
|
|
||||||
// the peer long-polls with a timeout of quitWakePollTimeout
|
|
||||||
// seconds. It checks that the poll returns within
|
|
||||||
// quitWakeDeadline with the session's QUIT, whose body is
|
|
||||||
// reason.
|
|
||||||
func checkQuitWakesPoll(
|
|
||||||
t *testing.T,
|
|
||||||
reason string,
|
|
||||||
leave func(tserver *testServer, token string),
|
|
||||||
) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
const (
|
|
||||||
nick = "wakequitter"
|
|
||||||
peerNick = "wakepeer"
|
|
||||||
)
|
|
||||||
|
|
||||||
tserver := newTestServer(t)
|
|
||||||
token := tserver.createSession(nick)
|
|
||||||
peerToken := tserver.createSession(peerNick)
|
|
||||||
|
|
||||||
for _, joiner := range []string{token, peerToken} {
|
|
||||||
tserver.sendCommand(joiner, map[string]any{
|
|
||||||
commandKey: joinCmd, toKey: "#wake",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_, lastID := tserver.pollMessages(peerToken, 0)
|
|
||||||
|
|
||||||
peerID, err := tserver.database.GetSessionByNick(t.Context(), peerNick)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("get session of %s: %v", peerNick, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
msgs []map[string]any
|
|
||||||
pollErr error
|
|
||||||
)
|
|
||||||
|
|
||||||
polled := make(chan struct{})
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer close(polled)
|
|
||||||
|
|
||||||
msgs, pollErr = tserver.longPoll(peerToken, lastID, quitWakePollTimeout)
|
|
||||||
}()
|
|
||||||
|
|
||||||
deadline := time.Now().Add(quitWakeDeadline)
|
|
||||||
|
|
||||||
for tserver.broker.ListenerCount(peerID) == 0 {
|
|
||||||
if time.Now().After(deadline) {
|
|
||||||
t.Fatal("the long poll did not start waiting")
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(pollWaitStep)
|
|
||||||
}
|
|
||||||
|
|
||||||
leave(tserver, token)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-polled:
|
|
||||||
case <-time.After(quitWakeDeadline):
|
|
||||||
t.Fatalf("the long poll did not return within %v", quitWakeDeadline)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pollErr != nil {
|
|
||||||
t.Fatalf("long poll: %v", pollErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(msgs) != 1 || countQuits(msgs, nick) != 1 {
|
|
||||||
t.Fatalf("long poll: got %v, want one QUIT from %s", msgs, nick)
|
|
||||||
}
|
|
||||||
|
|
||||||
lines, _ := msgs[0][bodyKey].([]any)
|
|
||||||
if len(lines) != 1 || lines[0] != reason {
|
|
||||||
t.Errorf("QUIT body: got %v, want [%s]", lines, reason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// channelNames returns the names of the channels in state,
|
|
||||||
// a response to GET /api/v1/state.
|
|
||||||
func channelNames(state map[string]any) []string {
|
|
||||||
channels, _ := state["channels"].([]any)
|
|
||||||
names := make([]string, 0, len(channels))
|
|
||||||
|
|
||||||
for _, entry := range channels {
|
|
||||||
info, _ := entry.(map[string]any)
|
|
||||||
name, _ := info["name"].(string)
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
|
|
||||||
// countQuits returns how many of msgs are a QUIT from nick.
|
// countQuits returns how many of msgs are a QUIT from nick.
|
||||||
func countQuits(msgs []map[string]any, nick string) int {
|
func countQuits(msgs []map[string]any, nick string) int {
|
||||||
quits := 0
|
quits := 0
|
||||||
@@ -1766,7 +1567,7 @@ func (tserver *testServer) logout(token string) {
|
|||||||
|
|
||||||
resp, err := doRequestAuth(
|
resp, err := doRequestAuth(
|
||||||
tserver.t, http.MethodPost,
|
tserver.t, http.MethodPost,
|
||||||
tserver.url(apiLogout), token, nil,
|
tserver.url("/api/v1/logout"), token, nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tserver.t.Fatalf("logout: %v", err)
|
tserver.t.Fatalf("logout: %v", err)
|
||||||
@@ -1779,38 +1580,6 @@ func (tserver *testServer) logout(token string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// longPoll returns the messages after afterID that the
|
|
||||||
// client token has been sent, waiting up to timeout seconds
|
|
||||||
// for one if there are none yet. It returns errors instead
|
|
||||||
// of failing the test, so a goroutine can call it.
|
|
||||||
func (tserver *testServer) longPoll(
|
|
||||||
token string, afterID int64, timeout int,
|
|
||||||
) ([]map[string]any, error) {
|
|
||||||
resp, err := doRequestAuth(
|
|
||||||
tserver.t, http.MethodGet,
|
|
||||||
tserver.url(fmt.Sprintf(
|
|
||||||
"%s?timeout=%d&after=%d", apiMessages, timeout, afterID,
|
|
||||||
)),
|
|
||||||
token, nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
Messages []map[string]any `json:"messages"`
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("decode poll: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Messages, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// login logs in to the session nick with testPassword and
|
// login logs in to the session nick with testPassword and
|
||||||
// returns the new client's token.
|
// returns the new client's token.
|
||||||
func (tserver *testServer) login(nick string) string {
|
func (tserver *testServer) login(nick string) string {
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import "github.com/go-chi/chi/v5"
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewTestSPAServer creates a Server whose router serves only
|
// NewTestSPAServer creates a Server whose router serves only
|
||||||
// the embedded web client.
|
// the embedded web client.
|
||||||
@@ -14,9 +10,3 @@ func NewTestSPAServer() *Server {
|
|||||||
|
|
||||||
return srv
|
return srv
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeUntilShutdown runs serveUntilShutdown, which serves the
|
|
||||||
// HTTP server on listener, and returns when it does.
|
|
||||||
func (srv *Server) ServeUntilShutdown(listener net.Listener) {
|
|
||||||
srv.serveUntilShutdown(listener)
|
|
||||||
}
|
|
||||||
|
|||||||
+14
-36
@@ -6,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -40,7 +39,6 @@ type Params struct {
|
|||||||
Config *config.Config
|
Config *config.Config
|
||||||
Middleware *middleware.Middleware
|
Middleware *middleware.Middleware
|
||||||
Handlers *handlers.Handlers
|
Handlers *handlers.Handlers
|
||||||
Shutdowner fx.Shutdowner
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server is the main HTTP server.
|
// Server is the main HTTP server.
|
||||||
@@ -68,7 +66,7 @@ func New(
|
|||||||
}
|
}
|
||||||
|
|
||||||
lifecycle.Append(fx.Hook{
|
lifecycle.Append(fx.Hook{
|
||||||
OnStart: func(ctx context.Context) error {
|
OnStart: func(_ context.Context) error {
|
||||||
srv.startupTime = time.Now()
|
srv.startupTime = time.Now()
|
||||||
|
|
||||||
// Build the router before OnStart returns, so srv can
|
// Build the router before OnStart returns, so srv can
|
||||||
@@ -80,7 +78,9 @@ func New(
|
|||||||
// A closed stdout or stderr must not end the process.
|
// A closed stdout or stderr must not end the process.
|
||||||
signal.Ignore(syscall.SIGPIPE)
|
signal.Ignore(syscall.SIGPIPE)
|
||||||
|
|
||||||
return srv.startHTTP(ctx)
|
srv.startHTTP()
|
||||||
|
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
OnStop: func(ctx context.Context) error {
|
OnStop: func(ctx context.Context) error {
|
||||||
srv.cleanShutdown(ctx)
|
srv.cleanShutdown(ctx)
|
||||||
@@ -162,23 +162,15 @@ func (srv *Server) configure() {
|
|||||||
// Server configuration placeholder.
|
// Server configuration placeholder.
|
||||||
}
|
}
|
||||||
|
|
||||||
// startHTTP binds the HTTP listen address, so that a port
|
// startHTTP creates the HTTP server and serves it from a new
|
||||||
// that is taken fails startup, then serves the HTTP server
|
// goroutine. srv.httpServer is set before that goroutine
|
||||||
// from a new goroutine. srv.httpServer is set before that
|
// starts and never changes, so it and cleanShutdown read it
|
||||||
// goroutine starts and never changes, so it and cleanShutdown
|
// without a lock.
|
||||||
// read it without a lock.
|
func (srv *Server) startHTTP() {
|
||||||
func (srv *Server) startHTTP(ctx context.Context) error {
|
|
||||||
listenAddr := fmt.Sprintf(
|
listenAddr := fmt.Sprintf(
|
||||||
":%d", srv.params.Config.Port,
|
":%d", srv.params.Config.Port,
|
||||||
)
|
)
|
||||||
|
|
||||||
var listenConfig net.ListenConfig
|
|
||||||
|
|
||||||
listener, err := listenConfig.Listen(ctx, "tcp", listenAddr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("http listen: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv.httpServer = &http.Server{ //nolint:exhaustruct // optional fields
|
srv.httpServer = &http.Server{ //nolint:exhaustruct // optional fields
|
||||||
Addr: listenAddr,
|
Addr: listenAddr,
|
||||||
ReadTimeout: httpReadTimeout,
|
ReadTimeout: httpReadTimeout,
|
||||||
@@ -187,30 +179,16 @@ func (srv *Server) startHTTP(ctx context.Context) error {
|
|||||||
Handler: srv,
|
Handler: srv,
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go srv.serveUntilShutdown()
|
||||||
srv.serveUntilShutdown(listener)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveUntilShutdown serves the HTTP server on listener until
|
func (srv *Server) serveUntilShutdown() {
|
||||||
// cleanShutdown stops it. If serving fails before then, it
|
|
||||||
// stops the app with exit code 1.
|
|
||||||
func (srv *Server) serveUntilShutdown(listener net.Listener) {
|
|
||||||
srv.log.Info(
|
srv.log.Info(
|
||||||
"http begin listen", "listenaddr", srv.httpServer.Addr,
|
"http begin listen", "listenaddr", srv.httpServer.Addr,
|
||||||
)
|
)
|
||||||
|
|
||||||
err := srv.httpServer.Serve(listener)
|
err := srv.httpServer.ListenAndServe()
|
||||||
if errors.Is(err, http.ErrServerClosed) {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
return
|
srv.log.Error("listen error", "error", err)
|
||||||
}
|
|
||||||
|
|
||||||
srv.log.Error("listen error", "error", err)
|
|
||||||
|
|
||||||
err = srv.params.Shutdowner.Shutdown(fx.ExitCode(1))
|
|
||||||
if err != nil {
|
|
||||||
srv.log.Error("shutdown failed", "error", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-127
@@ -1,7 +1,3 @@
|
|||||||
// Tests use a global viper instance for configuration,
|
|
||||||
// making parallel execution unsafe.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest
|
|
||||||
package server_test
|
package server_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -10,7 +6,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -45,6 +40,8 @@ const (
|
|||||||
// for a long poll the HTTP server is serving to finish, and
|
// for a long poll the HTTP server is serving to finish, and
|
||||||
// that the poll gets its response.
|
// that the poll gets its response.
|
||||||
func TestStopWaitsForRequestInProgress(t *testing.T) {
|
func TestStopWaitsForRequestInProgress(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
port := freePort(t)
|
port := freePort(t)
|
||||||
addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
|
addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
|
||||||
|
|
||||||
@@ -106,114 +103,12 @@ func TestStopWaitsForRequestInProgress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestStartFailsWhenHTTPPortIsTaken checks that starting the
|
// startApp starts the server with the dependencies
|
||||||
// app, which runs the start hook server.New registers,
|
// cmd/neoircd gives it, on a fresh in-memory database, with
|
||||||
// returns an error naming the HTTP listen address when
|
// the HTTP server listening on port.
|
||||||
// something else is already listening on its port.
|
|
||||||
func TestStartFailsWhenHTTPPortIsTaken(t *testing.T) {
|
|
||||||
var listenConfig net.ListenConfig
|
|
||||||
|
|
||||||
taken, err := listenConfig.Listen(t.Context(), "tcp", ":0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("listen: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Cleanup(func() { _ = taken.Close() })
|
|
||||||
|
|
||||||
tcpAddr, ok := taken.Addr().(*net.TCPAddr)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("listener address %v is not TCP", taken.Addr())
|
|
||||||
}
|
|
||||||
|
|
||||||
app, _, _ := newApp(t, tcpAddr.Port)
|
|
||||||
|
|
||||||
err = app.Start(t.Context())
|
|
||||||
if err == nil {
|
|
||||||
app.RequireStop()
|
|
||||||
t.Fatal("the app started while its HTTP port was taken")
|
|
||||||
}
|
|
||||||
|
|
||||||
listenAddr := fmt.Sprintf(":%d", tcpAddr.Port)
|
|
||||||
if !strings.Contains(err.Error(), listenAddr) {
|
|
||||||
t.Errorf("start error %q does not name %s", err, listenAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestServeFailureStopsApp checks that when serving HTTP fails
|
|
||||||
// after the start hook has bound the listen address, the
|
|
||||||
// server stops the app with exit code 1.
|
|
||||||
func TestServeFailureStopsApp(t *testing.T) {
|
|
||||||
var srv *server.Server
|
|
||||||
|
|
||||||
app, _, _ := startApp(t, freePort(t), fx.Populate(&srv))
|
|
||||||
|
|
||||||
shutdowns := app.Wait()
|
|
||||||
|
|
||||||
srv.ServeUntilShutdown(closedListener(t))
|
|
||||||
|
|
||||||
select {
|
|
||||||
case shutdown := <-shutdowns:
|
|
||||||
if shutdown.ExitCode != 1 {
|
|
||||||
t.Errorf("exit code %d, want 1", shutdown.ExitCode)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Error("the server did not stop the app")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestServeAfterStopDoesNotStopApp checks that serving HTTP
|
|
||||||
// after the app has stopped, which ends at once because the
|
|
||||||
// stop hook has shut the HTTP server down, does not stop the
|
|
||||||
// app. Serving ends the same way on every normal stop.
|
|
||||||
func TestServeAfterStopDoesNotStopApp(t *testing.T) {
|
|
||||||
var srv *server.Server
|
|
||||||
|
|
||||||
app, _, _ := startApp(t, freePort(t), fx.Populate(&srv))
|
|
||||||
|
|
||||||
shutdowns := app.Wait()
|
|
||||||
|
|
||||||
app.RequireStop()
|
|
||||||
|
|
||||||
srv.ServeUntilShutdown(closedListener(t))
|
|
||||||
|
|
||||||
select {
|
|
||||||
case shutdown := <-shutdowns:
|
|
||||||
t.Errorf(
|
|
||||||
"the server stopped the app with exit code %d",
|
|
||||||
shutdown.ExitCode,
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// startApp starts the app newApp builds, with the HTTP server
|
|
||||||
// listening on port.
|
|
||||||
func startApp(
|
func startApp(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
port int,
|
port int,
|
||||||
options ...fx.Option,
|
|
||||||
) (*fxtest.App, *broker.Broker, *db.Database) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
app, brk, database := newApp(t, port, options...)
|
|
||||||
|
|
||||||
app.RequireStart()
|
|
||||||
|
|
||||||
// Stops the app if the test ends before stopping it.
|
|
||||||
t.Cleanup(app.RequireStop)
|
|
||||||
|
|
||||||
return app, brk, database
|
|
||||||
}
|
|
||||||
|
|
||||||
// newApp builds, without starting it, an app that runs the
|
|
||||||
// server with the dependencies cmd/neoircd gives it, on a
|
|
||||||
// fresh in-memory database, with the HTTP port set to port.
|
|
||||||
// options are added to the app, for a test that needs more
|
|
||||||
// from it than the broker and the database.
|
|
||||||
func newApp(
|
|
||||||
t *testing.T,
|
|
||||||
port int,
|
|
||||||
options ...fx.Option,
|
|
||||||
) (*fxtest.App, *broker.Broker, *db.Database) {
|
) (*fxtest.App, *broker.Broker, *db.Database) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -251,9 +146,13 @@ func newApp(
|
|||||||
}),
|
}),
|
||||||
fx.Invoke(func(*server.Server) {}),
|
fx.Invoke(func(*server.Server) {}),
|
||||||
fx.Populate(&brk, &database),
|
fx.Populate(&brk, &database),
|
||||||
fx.Options(options...),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.RequireStart()
|
||||||
|
|
||||||
|
// Stops the app if the test ends before stopping it.
|
||||||
|
t.Cleanup(app.RequireStop)
|
||||||
|
|
||||||
return app, brk, database
|
return app, brk, database
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,21 +161,6 @@ func newApp(
|
|||||||
func freePort(t *testing.T) int {
|
func freePort(t *testing.T) int {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
listener := closedListener(t)
|
|
||||||
|
|
||||||
tcpAddr, ok := listener.Addr().(*net.TCPAddr)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("listener address %v is not TCP", listener.Addr())
|
|
||||||
}
|
|
||||||
|
|
||||||
return tcpAddr.Port
|
|
||||||
}
|
|
||||||
|
|
||||||
// closedListener returns a TCP listener that has already been
|
|
||||||
// closed, so that accepting a connection on it fails.
|
|
||||||
func closedListener(t *testing.T) net.Listener {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var listenConfig net.ListenConfig
|
var listenConfig net.ListenConfig
|
||||||
|
|
||||||
listener, err := listenConfig.Listen(
|
listener, err := listenConfig.Listen(
|
||||||
@@ -286,12 +170,17 @@ func closedListener(t *testing.T) net.Listener {
|
|||||||
t.Fatalf("listen: %v", err)
|
t.Fatalf("listen: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tcpAddr, ok := listener.Addr().(*net.TCPAddr)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("listener address %v is not TCP", listener.Addr())
|
||||||
|
}
|
||||||
|
|
||||||
err = listener.Close()
|
err = listener.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("close listener: %v", err)
|
t.Fatalf("close listener: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return listener
|
return tcpAddr.Port
|
||||||
}
|
}
|
||||||
|
|
||||||
// canConnect reports whether addr accepts a TCP connection.
|
// canConnect reports whether addr accepts a TCP connection.
|
||||||
|
|||||||
@@ -829,10 +829,9 @@ func (s *Service) ChangeNick(
|
|||||||
// BroadcastQuit parts the session from all its channels,
|
// BroadcastQuit parts the session from all its channels,
|
||||||
// deletes it, and queues a QUIT to the users who shared a
|
// deletes it, and queues a QUIT to the users who shared a
|
||||||
// channel with it, in one transaction, so that the QUIT is
|
// channel with it, in one transaction, so that the QUIT is
|
||||||
// queued before anyone else can take the nick, and then
|
// queued before anyone else can take the nick. Of several
|
||||||
// wakes those users' long polls. Of several calls for one
|
// calls for one session at once, only the one that deletes
|
||||||
// session at once, only the one that deletes it sends the
|
// it sends the QUIT.
|
||||||
// QUIT.
|
|
||||||
func (s *Service) BroadcastQuit(
|
func (s *Service) BroadcastQuit(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sessionID int64,
|
sessionID int64,
|
||||||
@@ -854,8 +853,6 @@ func (s *Service) BroadcastQuit(
|
|||||||
|
|
||||||
// Logout deletes a client of the session and, if that was
|
// Logout deletes a client of the session and, if that was
|
||||||
// the session's last client, does what BroadcastQuit does.
|
// the session's last client, does what BroadcastQuit does.
|
||||||
// If that fails, nothing changes and Logout returns the
|
|
||||||
// error.
|
|
||||||
func (s *Service) Logout(
|
func (s *Service) Logout(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sessionID, clientID int64,
|
sessionID, clientID int64,
|
||||||
|
|||||||
Reference in New Issue
Block a user