snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
604
server/public/plugin/context_rpc_test.go
Normal file
604
server/public/plugin/context_rpc_test.go
Normal file
@@ -0,0 +1,604 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/rpc"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
const rpcTestTimeout = 10 * time.Second
|
||||
|
||||
type blockingReadCloser struct {
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func (r *blockingReadCloser) Read(p []byte) (int, error) {
|
||||
<-r.closed
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
func (r *blockingReadCloser) Close() error {
|
||||
select {
|
||||
case <-r.closed:
|
||||
default:
|
||||
close(r.closed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRPCRequestContextDeadline(t *testing.T) {
|
||||
deadline := time.Now().Add(time.Minute).Round(0)
|
||||
ctx, cancel := context.WithDeadline(context.Background(), deadline)
|
||||
defer cancel()
|
||||
|
||||
remoteCtx, remoteCancel := newRPCRequestContext(ctx).Context()
|
||||
defer remoteCancel()
|
||||
remoteDeadline, ok := remoteCtx.Deadline()
|
||||
require.True(t, ok)
|
||||
require.Equal(t, deadline, remoteDeadline)
|
||||
}
|
||||
|
||||
func TestWatchRPCContextTransportErrorCancels(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go watchRPCContext(ctx, io.NopCloser(iotest.ErrReader(errors.New("transport error"))), cancel)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
require.ErrorIs(t, ctx.Err(), context.Canceled)
|
||||
case <-time.After(rpcTestTimeout):
|
||||
require.FailNow(t, "transport error did not cancel context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchRPCContextUnknownReasonCancels(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
go watchRPCContext(ctx, io.NopCloser(bytes.NewReader([]byte{255})), cancel)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
require.ErrorIs(t, ctx.Err(), context.Canceled)
|
||||
case <-time.After(rpcTestTimeout):
|
||||
require.FailNow(t, "unknown close reason did not cancel context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextReadCloserCancellation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
reader := &blockingReadCloser{closed: make(chan struct{})}
|
||||
body := newContextReadCloser(ctx, reader, nil)
|
||||
|
||||
readDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := body.Read(make([]byte, 1))
|
||||
readDone <- err
|
||||
}()
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case err := <-readDone:
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
case <-time.After(rpcTestTimeout):
|
||||
require.FailNow(t, "read did not stop after cancellation")
|
||||
}
|
||||
require.NoError(t, body.Close())
|
||||
require.NoError(t, body.Close())
|
||||
}
|
||||
|
||||
func TestContextReadCloserCancellationRunsCleanupWithoutReadOrClose(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
reader := &blockingReadCloser{closed: make(chan struct{})}
|
||||
cleanupDone := make(chan struct{})
|
||||
body := newContextReadCloser(ctx, reader, func() {
|
||||
close(cleanupDone)
|
||||
})
|
||||
|
||||
cancel()
|
||||
select {
|
||||
case <-cleanupDone:
|
||||
case <-time.After(rpcTestTimeout):
|
||||
require.FailNow(t, "cleanup did not run after cancellation")
|
||||
}
|
||||
select {
|
||||
case <-reader.closed:
|
||||
default:
|
||||
require.FailNow(t, "reader was not closed after cancellation")
|
||||
}
|
||||
require.NoError(t, body.Close())
|
||||
}
|
||||
|
||||
func TestRPCStreamCloserCloseBeforeSet(t *testing.T) {
|
||||
stream := &rpcStreamCloser{}
|
||||
require.NoError(t, stream.Close())
|
||||
|
||||
closer := &recordingWriteCloser{}
|
||||
require.False(t, stream.Set(closer))
|
||||
require.True(t, closer.closed)
|
||||
require.NoError(t, stream.Close())
|
||||
}
|
||||
|
||||
type recordingWriteCloser struct {
|
||||
bytes.Buffer
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (c *recordingWriteCloser) Close() error {
|
||||
c.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRPCStreamCloserCancelBeforeSet(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
err error
|
||||
reason closeReason
|
||||
}{
|
||||
{name: "canceled", err: context.Canceled, reason: rpcStreamCanceled},
|
||||
{name: "deadline exceeded", err: context.DeadlineExceeded, reason: rpcStreamDeadlineExceeded},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
stream := &rpcStreamCloser{}
|
||||
require.NoError(t, stream.Cancel(testCase.err))
|
||||
|
||||
closer := &recordingWriteCloser{}
|
||||
require.False(t, stream.Set(closer))
|
||||
require.Equal(t, []byte{byte(testCase.reason)}, closer.Bytes())
|
||||
require.True(t, closer.closed)
|
||||
require.NoError(t, stream.Close())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginHTTPAlreadyCanceledClosesRequestBody(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
body := &recordingWriteCloser{}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, "/plugin/path", body)
|
||||
require.NoError(t, err)
|
||||
|
||||
response := (&apiRPCClient{}).PluginHTTP(request)
|
||||
require.Nil(t, response)
|
||||
require.True(t, body.closed)
|
||||
}
|
||||
|
||||
func TestHooksRPCClientServeHTTPAlreadyCanceled(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
body := &recordingWriteCloser{}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, "/plugin/path", body)
|
||||
require.NoError(t, err)
|
||||
|
||||
muxBroker := newTestHooksRPCMuxBroker()
|
||||
hooks := &hooksRPCClient{contextHTTP: true, muxBroker: muxBroker}
|
||||
hooks.implemented[ServeHTTPID] = true
|
||||
hooks.ServeHTTP(&Context{}, httptest.NewRecorder(), request)
|
||||
|
||||
require.True(t, body.closed)
|
||||
require.Zero(t, muxBroker.nextID, "canceled request allocated a mux stream")
|
||||
}
|
||||
|
||||
type blockingResponseWriter struct {
|
||||
header http.Header
|
||||
writeStarted chan struct{}
|
||||
writeRelease chan struct{}
|
||||
}
|
||||
|
||||
func (w *blockingResponseWriter) Header() http.Header {
|
||||
return w.header
|
||||
}
|
||||
|
||||
func (w *blockingResponseWriter) Write(p []byte) (int, error) {
|
||||
close(w.writeStarted)
|
||||
<-w.writeRelease
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *blockingResponseWriter) WriteHeader(statusCode int) {
|
||||
}
|
||||
|
||||
type responseWritingRPCServer struct {
|
||||
muxBroker rpcMuxBroker
|
||||
}
|
||||
|
||||
func (s *responseWritingRPCServer) ServeHTTP(args *Z_ServeHTTPArgs, returns *struct{}) error {
|
||||
_, cleanupContext, err := openRPCRequestContext(s.muxBroker, args.RequestContextStream, args.RequestContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanupContext()
|
||||
|
||||
connection, err := s.muxBroker.Dial(args.ResponseWriterStream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
responseWriter := connectHTTPResponseWriter(connection)
|
||||
defer responseWriter.Close()
|
||||
|
||||
_, err = responseWriter.Write([]byte("response"))
|
||||
return err
|
||||
}
|
||||
|
||||
func TestHooksRPCClientServeHTTPCancellationWaitsForResponseWriter(t *testing.T) {
|
||||
muxBroker := newTestHooksRPCMuxBroker()
|
||||
serverConnection, clientConnection := net.Pipe()
|
||||
server := rpc.NewServer()
|
||||
require.NoError(t, server.RegisterName("Plugin", &responseWritingRPCServer{muxBroker: muxBroker}))
|
||||
go server.ServeConn(serverConnection)
|
||||
|
||||
client := rpc.NewClient(clientConnection)
|
||||
defer client.Close()
|
||||
hooks := &hooksRPCClient{client: client, log: mlog.CreateConsoleTestLogger(t), muxBroker: muxBroker, contextHTTP: true}
|
||||
hooks.implemented[ServeHTTPID] = true
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, "/plugin/path", nil)
|
||||
require.NoError(t, err)
|
||||
writeStarted := make(chan struct{})
|
||||
writeRelease := make(chan struct{})
|
||||
defer close(writeRelease)
|
||||
writer := &blockingResponseWriter{
|
||||
header: make(http.Header),
|
||||
writeStarted: writeStarted,
|
||||
writeRelease: writeRelease,
|
||||
}
|
||||
serveHTTPDone := make(chan struct{})
|
||||
go func() {
|
||||
hooks.ServeHTTP(&Context{}, writer, request)
|
||||
close(serveHTTPDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-writeStarted:
|
||||
case <-time.After(rpcTestTimeout):
|
||||
require.FailNow(t, "response write did not start")
|
||||
}
|
||||
cancel()
|
||||
require.Never(t, func() bool {
|
||||
select {
|
||||
case <-serveHTTPDone:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}, 100*time.Millisecond, 10*time.Millisecond, "ServeHTTP returned during an in-flight response write")
|
||||
|
||||
writeRelease <- struct{}{}
|
||||
select {
|
||||
case <-serveHTTPDone:
|
||||
case <-time.After(rpcTestTimeout):
|
||||
require.FailNow(t, "ServeHTTP did not return after the response write finished")
|
||||
}
|
||||
}
|
||||
|
||||
type stalledServeHTTPRPCServer struct {
|
||||
started chan struct{}
|
||||
release chan struct{}
|
||||
}
|
||||
|
||||
func (s *stalledServeHTTPRPCServer) ServeHTTP(args *Z_ServeHTTPArgs, returns *struct{}) error {
|
||||
close(s.started)
|
||||
<-s.release
|
||||
return nil
|
||||
}
|
||||
|
||||
type delayedAcceptMuxBroker struct {
|
||||
release chan struct{}
|
||||
nextID uint32
|
||||
}
|
||||
|
||||
func (b *delayedAcceptMuxBroker) NextId() uint32 {
|
||||
b.nextID++
|
||||
return b.nextID
|
||||
}
|
||||
|
||||
func (b *delayedAcceptMuxBroker) Accept(id uint32) (net.Conn, error) {
|
||||
<-b.release
|
||||
hostConnection, pluginConnection := net.Pipe()
|
||||
_ = pluginConnection.Close()
|
||||
return hostConnection, nil
|
||||
}
|
||||
|
||||
func (b *delayedAcceptMuxBroker) AcceptAndServe(id uint32, server any) {
|
||||
}
|
||||
|
||||
func (b *delayedAcceptMuxBroker) Dial(id uint32) (net.Conn, error) {
|
||||
return nil, errors.New("unexpected Dial")
|
||||
}
|
||||
|
||||
func TestHooksRPCClientServeHTTPCancellationBeforeResponseStreamAcceptance(t *testing.T) {
|
||||
muxBroker := &delayedAcceptMuxBroker{release: make(chan struct{})}
|
||||
serverConnection, clientConnection := net.Pipe()
|
||||
server := rpc.NewServer()
|
||||
serverStarted := make(chan struct{})
|
||||
serverRelease := make(chan struct{})
|
||||
defer close(serverRelease)
|
||||
defer close(muxBroker.release)
|
||||
require.NoError(t, server.RegisterName("Plugin", &stalledServeHTTPRPCServer{started: serverStarted, release: serverRelease}))
|
||||
go server.ServeConn(serverConnection)
|
||||
|
||||
client := rpc.NewClient(clientConnection)
|
||||
defer client.Close()
|
||||
hooks := &hooksRPCClient{client: client, log: mlog.CreateConsoleTestLogger(t), muxBroker: muxBroker, contextHTTP: true}
|
||||
hooks.implemented[ServeHTTPID] = true
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, "/plugin/path", nil)
|
||||
require.NoError(t, err)
|
||||
serveHTTPDone := make(chan struct{})
|
||||
go func() {
|
||||
hooks.ServeHTTP(&Context{}, httptest.NewRecorder(), request)
|
||||
close(serveHTTPDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-serverStarted:
|
||||
case <-time.After(rpcTestTimeout):
|
||||
require.FailNow(t, "ServeHTTP RPC did not start")
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case <-serveHTTPDone:
|
||||
case <-time.After(time.Second):
|
||||
require.FailNow(t, "ServeHTTP waited for a response stream that was not accepted")
|
||||
}
|
||||
}
|
||||
|
||||
type legacyRPCServer struct {
|
||||
muxBroker *testHooksRPCMuxBroker
|
||||
serveHTTPStarted chan struct{}
|
||||
serveHTTPRelease chan struct{}
|
||||
}
|
||||
|
||||
type testHooksRPCMuxBroker struct {
|
||||
nextID uint32
|
||||
mutex sync.Mutex
|
||||
streams map[uint32]chan net.Conn
|
||||
acceptErrors map[uint32]error
|
||||
}
|
||||
|
||||
func newTestHooksRPCMuxBroker() *testHooksRPCMuxBroker {
|
||||
return &testHooksRPCMuxBroker{streams: make(map[uint32]chan net.Conn)}
|
||||
}
|
||||
|
||||
func (b *testHooksRPCMuxBroker) NextId() uint32 {
|
||||
b.nextID++
|
||||
return b.nextID
|
||||
}
|
||||
|
||||
func (b *testHooksRPCMuxBroker) Accept(id uint32) (net.Conn, error) {
|
||||
if err := b.acceptErrors[id]; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hostConnection, pluginConnection := net.Pipe()
|
||||
b.stream(id) <- pluginConnection
|
||||
return hostConnection, nil
|
||||
}
|
||||
|
||||
func (b *testHooksRPCMuxBroker) AcceptAndServe(id uint32, server any) {
|
||||
}
|
||||
|
||||
func (b *testHooksRPCMuxBroker) Dial(id uint32) (net.Conn, error) {
|
||||
select {
|
||||
case connection := <-b.stream(id):
|
||||
return connection, nil
|
||||
case <-time.After(rpcTestTimeout):
|
||||
return nil, errors.New("timeout waiting for stream")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *testHooksRPCMuxBroker) stream(id uint32) chan net.Conn {
|
||||
b.mutex.Lock()
|
||||
defer b.mutex.Unlock()
|
||||
stream, ok := b.streams[id]
|
||||
if !ok {
|
||||
stream = make(chan net.Conn, 1)
|
||||
b.streams[id] = stream
|
||||
}
|
||||
return stream
|
||||
}
|
||||
|
||||
type LegacyOnActivateArgs struct {
|
||||
APIMuxId uint32
|
||||
DriverMuxId uint32
|
||||
}
|
||||
|
||||
type LegacyOnActivateReturns struct {
|
||||
A error
|
||||
}
|
||||
|
||||
type LegacyServeHTTPArgs struct {
|
||||
ResponseWriterStream uint32
|
||||
Request *HTTPRequestSubset
|
||||
Context *Context
|
||||
RequestBodyStream uint32
|
||||
}
|
||||
|
||||
func (s *legacyRPCServer) OnActivate(args *LegacyOnActivateArgs, returns *LegacyOnActivateReturns) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *legacyRPCServer) Ping(args struct{}, reply *bool) error {
|
||||
*reply = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *legacyRPCServer) PluginHTTP(args *Z_PluginHTTPArgs, returns *Z_PluginHTTPReturns) error {
|
||||
returns.Response = &http.Response{StatusCode: http.StatusOK}
|
||||
returns.ResponseBody = []byte("legacy")
|
||||
return nil
|
||||
}
|
||||
|
||||
type successfulPluginHTTPStreamRPCServer struct{}
|
||||
|
||||
func (s *successfulPluginHTTPStreamRPCServer) PluginHTTPStream(args *Z_PluginHTTPStreamArgs, returns *Z_PluginHTTPStreamReturns) error {
|
||||
returns.StatusCode = http.StatusOK
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPluginHTTPStreamResponseSetupFailureClosesRequestBody(t *testing.T) {
|
||||
muxBroker := newTestHooksRPCMuxBroker()
|
||||
muxBroker.acceptErrors = map[uint32]error{1: errors.New("response stream failed")}
|
||||
|
||||
serverConnection, clientConnection := net.Pipe()
|
||||
server := rpc.NewServer()
|
||||
require.NoError(t, server.RegisterName("Plugin", &successfulPluginHTTPStreamRPCServer{}))
|
||||
go server.ServeConn(serverConnection)
|
||||
|
||||
client := rpc.NewClient(clientConnection)
|
||||
defer client.Close()
|
||||
body := &blockingReadCloser{closed: make(chan struct{})}
|
||||
request, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "/plugin/path", body)
|
||||
require.NoError(t, err)
|
||||
|
||||
response, err := (&apiRPCClient{client: client, muxBroker: muxBroker}).pluginHTTPStream(request, false)
|
||||
if response != nil && response.Body != nil {
|
||||
require.NoError(t, response.Body.Close())
|
||||
}
|
||||
require.Nil(t, response)
|
||||
require.ErrorContains(t, err, "response stream failed")
|
||||
select {
|
||||
case <-body.closed:
|
||||
default:
|
||||
require.FailNow(t, "request body was not closed after response stream setup failed")
|
||||
}
|
||||
|
||||
requestConnection, err := muxBroker.Dial(2)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, requestConnection.Close())
|
||||
}
|
||||
|
||||
func (s *legacyRPCServer) ServeHTTP(args *LegacyServeHTTPArgs, returns *struct{}) error {
|
||||
requestConnection, err := s.muxBroker.Dial(args.RequestBodyStream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requestBody := connectIOReader(requestConnection)
|
||||
body := make([]byte, len("request"))
|
||||
_, err = io.ReadFull(requestBody, body)
|
||||
_ = requestBody.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
close(s.serveHTTPStarted)
|
||||
<-s.serveHTTPRelease
|
||||
|
||||
responseConnection, err := s.muxBroker.Dial(args.ResponseWriterStream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
responseWriter := connectHTTPResponseWriter(responseConnection)
|
||||
defer responseWriter.Close()
|
||||
responseWriter.Header().Set("X-Legacy-Plugin", "true")
|
||||
responseWriter.WriteHeader(http.StatusCreated)
|
||||
_, err = responseWriter.Write(append([]byte("legacy:"), body...))
|
||||
return err
|
||||
}
|
||||
|
||||
func TestLegacyPluginHTTPFallbackLeavesConnectionUsable(t *testing.T) {
|
||||
muxBroker := newTestHooksRPCMuxBroker()
|
||||
serverConnection, clientConnection := net.Pipe()
|
||||
server := rpc.NewServer()
|
||||
require.NoError(t, server.RegisterName("Plugin", &legacyRPCServer{}))
|
||||
go server.ServeConn(serverConnection)
|
||||
|
||||
client := rpc.NewClient(clientConnection)
|
||||
defer client.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, "/legacy", nil)
|
||||
require.NoError(t, err)
|
||||
response := (&apiRPCClient{client: client, muxBroker: muxBroker}).PluginHTTP(request)
|
||||
require.NotNil(t, response)
|
||||
body, err := io.ReadAll(response.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, response.Body.Close())
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
require.Equal(t, "legacy", string(body))
|
||||
|
||||
var pong bool
|
||||
require.NoError(t, client.Call("Plugin.Ping", struct{}{}, &pong))
|
||||
require.True(t, pong)
|
||||
}
|
||||
|
||||
func TestLegacyHooksRPCClientServeHTTP(t *testing.T) {
|
||||
muxBroker := newTestHooksRPCMuxBroker()
|
||||
serveHTTPStarted := make(chan struct{})
|
||||
serveHTTPRelease := make(chan struct{})
|
||||
serverConnection, clientConnection := net.Pipe()
|
||||
server := rpc.NewServer()
|
||||
require.NoError(t, server.RegisterName("Plugin", &legacyRPCServer{
|
||||
muxBroker: muxBroker,
|
||||
serveHTTPStarted: serveHTTPStarted,
|
||||
serveHTTPRelease: serveHTTPRelease,
|
||||
}))
|
||||
go server.ServeConn(serverConnection)
|
||||
|
||||
client := rpc.NewClient(clientConnection)
|
||||
defer client.Close()
|
||||
hooks := &hooksRPCClient{client: client, log: mlog.CreateConsoleTestLogger(t), muxBroker: muxBroker}
|
||||
hooks.implemented[ServeHTTPID] = true
|
||||
require.NoError(t, hooks.OnActivate())
|
||||
require.False(t, hooks.contextHTTP)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, "/legacy", strings.NewReader("request"))
|
||||
require.NoError(t, err)
|
||||
recorder := httptest.NewRecorder()
|
||||
serveHTTPDone := make(chan struct{})
|
||||
go func() {
|
||||
hooks.ServeHTTP(&Context{RequestId: "legacy"}, recorder, request)
|
||||
close(serveHTTPDone)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-serveHTTPStarted:
|
||||
case <-time.After(rpcTestTimeout):
|
||||
require.FailNow(t, "legacy ServeHTTP did not start")
|
||||
}
|
||||
cancel()
|
||||
require.Never(t, func() bool {
|
||||
select {
|
||||
case <-serveHTTPDone:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}, 100*time.Millisecond, 10*time.Millisecond, "legacy ServeHTTP unexpectedly stopped on context cancellation")
|
||||
|
||||
var pong bool
|
||||
require.NoError(t, client.Call("Plugin.Ping", struct{}{}, &pong))
|
||||
require.True(t, pong)
|
||||
close(serveHTTPRelease)
|
||||
select {
|
||||
case <-serveHTTPDone:
|
||||
case <-time.After(rpcTestTimeout):
|
||||
require.FailNow(t, "legacy ServeHTTP did not finish after release")
|
||||
}
|
||||
|
||||
require.Equal(t, http.StatusCreated, recorder.Code)
|
||||
require.Equal(t, "true", recorder.Header().Get("X-Legacy-Plugin"))
|
||||
require.Equal(t, "legacy:request", recorder.Body.String())
|
||||
}
|
||||
Reference in New Issue
Block a user