diff options
author | Valery Piashchynski <[email protected]> | 2020-12-26 00:40:31 +0300 |
---|---|---|
committer | Valery Piashchynski <[email protected]> | 2020-12-26 00:40:31 +0300 |
commit | 7a0dee1a416705c621edbf50e1f43fb39845348f (patch) | |
tree | 0007a6b8c8ac9e7d31b8a5f3f7f27669c860d261 /tests | |
parent | 8526c03822e724bc2ebb64b6197085fea335b782 (diff) |
Huge tests refactoring. Reduce running time 2-3x times
Diffstat (limited to 'tests')
61 files changed, 4787 insertions, 98 deletions
diff --git a/tests/mocks/mock_log.go b/tests/mocks/mock_log.go index 8e3e2836..e9631805 100644 --- a/tests/mocks/mock_log.go +++ b/tests/mocks/mock_log.go @@ -4,7 +4,7 @@ import ( "reflect" "github.com/golang/mock/gomock" - "github.com/spiral/roadrunner-plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/logger" ) // MockLogger is a mock of Logger interface. diff --git a/tests/plugins/checker/configs/.rr-checker-init.yaml b/tests/plugins/checker/configs/.rr-checker-init.yaml new file mode 100755 index 00000000..1273529a --- /dev/null +++ b/tests/plugins/checker/configs/.rr-checker-init.yaml @@ -0,0 +1,31 @@ +rpc: + listen: tcp://127.0.0.1:6005 + disabled: false + +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +status: + address: "127.0.0.1:34333" +logs: + mode: development + level: debug +http: + debug: true + address: 127.0.0.1:11933 + maxRequestSize: 1024 + middleware: [ "" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s
\ No newline at end of file diff --git a/tests/plugins/checker/plugin_test.go b/tests/plugins/checker/plugin_test.go new file mode 100644 index 00000000..c346d91a --- /dev/null +++ b/tests/plugins/checker/plugin_test.go @@ -0,0 +1,190 @@ +package checker + +import ( + "io/ioutil" + "net" + "net/http" + "net/rpc" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + "github.com/spiral/roadrunner/v2/plugins/checker" + "github.com/spiral/roadrunner/v2/plugins/config" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestStatusHttp(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-checker-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &checker.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("CheckerGetStatus", checkHTTPStatus) + + stopCh <- struct{}{} + wg.Wait() +} + +const resp = `Service: http: Status: 200 +Service: rpc not found` + +func checkHTTPStatus(t *testing.T) { + req, err := http.NewRequest("GET", "http://127.0.0.1:34333/v1/health?plugin=http&plugin=rpc", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, resp, string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestStatusRPC(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-checker-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &checker.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("CheckerGetStatusRpc", checkRPCStatus) + stopCh <- struct{}{} + wg.Wait() +} + +func checkRPCStatus(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6005") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + + st := &checker.Status{} + + err = client.Call("status.Status", "http", &st) + assert.NoError(t, err) + assert.Equal(t, st.Code, 200) +} diff --git a/tests/plugins/config/.rr.yaml b/tests/plugins/config/.rr.yaml new file mode 100755 index 00000000..732a1366 --- /dev/null +++ b/tests/plugins/config/.rr.yaml @@ -0,0 +1,18 @@ +reload: + enabled: true + interval: 1s + patterns: [".php"] + services: + http: + recursive: true + ignore: ["vendor"] + patterns: [".php", ".go",".md",] + dirs: ["."] + jobs: + recursive: false + ignore: ["service/metrics"] + dirs: ["./jobs"] + rpc: + recursive: true + patterns: [".json"] + dirs: [""] diff --git a/tests/plugins/config/config_test.go b/tests/plugins/config/config_test.go new file mode 100755 index 00000000..858fcb80 --- /dev/null +++ b/tests/plugins/config/config_test.go @@ -0,0 +1,66 @@ +package config + +import ( + "os" + "os/signal" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/stretchr/testify/assert" +) + +func TestViperProvider_Init(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + vp := &config.Viper{} + vp.Path = ".rr.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Foo{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + errCh, err := container.Serve() + if err != nil { + t.Fatal(err) + } + + // stop by CTRL+C + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + tt := time.NewTicker(time.Second * 2) + + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + assert.NoError(t, container.Stop()) + return + case <-c: + er := container.Stop() + if er != nil { + panic(er) + } + return + case <-tt.C: + tt.Stop() + assert.NoError(t, container.Stop()) + return + } + } +} diff --git a/tests/plugins/config/plugin1.go b/tests/plugins/config/plugin1.go new file mode 100755 index 00000000..2afe79a4 --- /dev/null +++ b/tests/plugins/config/plugin1.go @@ -0,0 +1,53 @@ +package config + +import ( + "errors" + "time" + + "github.com/spiral/roadrunner/v2/plugins/config" +) + +// ReloadConfig is a Reload configuration point. +type ReloadConfig struct { + Interval time.Duration + Patterns []string + Services map[string]ServiceConfig +} + +type ServiceConfig struct { + Enabled bool + Recursive bool + Patterns []string + Dirs []string + Ignore []string +} + +type Foo struct { + configProvider config.Configurer +} + +// Depends on S2 and DB (S3 in the current case) +func (f *Foo) Init(p config.Configurer) error { + f.configProvider = p + return nil +} + +func (f *Foo) Serve() chan error { + errCh := make(chan error, 1) + + r := &ReloadConfig{} + err := f.configProvider.UnmarshalKey("reload", r) + if err != nil { + errCh <- err + } + + if len(r.Patterns) == 0 { + errCh <- errors.New("should be at least one pattern, but got 0") + } + + return errCh +} + +func (f *Foo) Stop() error { + return nil +} diff --git a/tests/plugins/gzip/configs/.rr-http-middlewareNotExist.yaml b/tests/plugins/gzip/configs/.rr-http-middlewareNotExist.yaml new file mode 100644 index 00000000..a2d12706 --- /dev/null +++ b/tests/plugins/gzip/configs/.rr-http-middlewareNotExist.yaml @@ -0,0 +1,25 @@ +server: + command: "php ../../psr-worker.php" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:18103 + maxRequestSize: 1024 + middleware: [ "gzip", "foo" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/gzip/configs/.rr-http-withGzip.yaml b/tests/plugins/gzip/configs/.rr-http-withGzip.yaml new file mode 100644 index 00000000..aff3efdb --- /dev/null +++ b/tests/plugins/gzip/configs/.rr-http-withGzip.yaml @@ -0,0 +1,25 @@ +server: + command: "php ../../psr-worker.php" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:18953 + maxRequestSize: 1024 + middleware: [ "gzip" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/gzip/plugin_test.go b/tests/plugins/gzip/plugin_test.go new file mode 100644 index 00000000..b09d430e --- /dev/null +++ b/tests/plugins/gzip/plugin_test.go @@ -0,0 +1,176 @@ +package gzip + +import ( + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/gzip" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/tests/mocks" + "github.com/stretchr/testify/assert" +) + +func TestGzipPlugin(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-withGzip.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("GzipCheckHeader", headerCheck) + + stopCh <- struct{}{} + wg.Wait() +} + +func headerCheck(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:18953", nil) + assert.NoError(t, err) + client := &http.Client{ + Transport: &http.Transport{ + DisableCompression: false, + }, + } + + r, err := client.Do(req) + assert.NoError(t, err) + assert.True(t, r.Uncompressed) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestMiddlewareNotExist(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-middlewareNotExist.yaml", + Prefix: "rr", + } + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Warn("requested middleware does not exist", "requested", "foo").AnyTimes() + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + stopCh <- struct{}{} + wg.Wait() +} diff --git a/tests/plugins/headers/configs/.rr-cors-headers.yaml b/tests/plugins/headers/configs/.rr-cors-headers.yaml new file mode 100644 index 00000000..9d4e8b36 --- /dev/null +++ b/tests/plugins/headers/configs/.rr-cors-headers.yaml @@ -0,0 +1,39 @@ +server: + command: "php ../../http/client.php headers pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:22855 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + cors: + allowedOrigin: "*" + allowedHeaders: "*" + allowedMethods: "GET,POST,PUT,DELETE" + allowCredentials: true + exposedHeaders: "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma" + maxAge: 600 + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/headers/configs/.rr-headers-init.yaml b/tests/plugins/headers/configs/.rr-headers-init.yaml new file mode 100644 index 00000000..8d63a187 --- /dev/null +++ b/tests/plugins/headers/configs/.rr-headers-init.yaml @@ -0,0 +1,39 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:33453 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + cors: + allowedOrigin: "*" + allowedHeaders: "*" + allowedMethods: "GET,POST,PUT,DELETE" + allowCredentials: true + exposedHeaders: "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma" + maxAge: 600 + request: + "Example-Request-Header": "Value" + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/headers/configs/.rr-req-headers.yaml b/tests/plugins/headers/configs/.rr-req-headers.yaml new file mode 100644 index 00000000..f8ab9bec --- /dev/null +++ b/tests/plugins/headers/configs/.rr-req-headers.yaml @@ -0,0 +1,32 @@ +server: + command: "php ../../http/client.php header pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:22655 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/headers/configs/.rr-res-headers.yaml b/tests/plugins/headers/configs/.rr-res-headers.yaml new file mode 100644 index 00000000..36ab4eb3 --- /dev/null +++ b/tests/plugins/headers/configs/.rr-res-headers.yaml @@ -0,0 +1,32 @@ +server: + command: "php ../../http/client.php header pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:22455 + maxRequestSize: 1024 + middleware: [ "headers" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + # Additional HTTP headers and CORS control. + headers: + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error + diff --git a/tests/plugins/headers/headers_plugin_test.go b/tests/plugins/headers/headers_plugin_test.go new file mode 100644 index 00000000..a2ad3357 --- /dev/null +++ b/tests/plugins/headers/headers_plugin_test.go @@ -0,0 +1,367 @@ +package headers + +import ( + "io/ioutil" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/headers" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestHeadersInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-headers-init.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + stopCh <- struct{}{} + wg.Wait() +} + +func TestRequestHeaders(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-req-headers.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("RequestHeaders", reqHeaders) + + stopCh <- struct{}{} + wg.Wait() +} + +func reqHeaders(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:22655?hello=value", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "CUSTOM-HEADER", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestResponseHeaders(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-res-headers.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("ResponseHeaders", resHeaders) + + stopCh <- struct{}{} + wg.Wait() +} + +func resHeaders(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:22455?hello=value", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + assert.Equal(t, "output-header", r.Header.Get("output")) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + assert.Equal(t, "CUSTOM-HEADER", string(b)) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func TestCORSHeaders(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-cors-headers.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &headers.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("CORSHeaders", corsHeaders) + t.Run("CORSHeadersPass", corsHeadersPass) + + stopCh <- struct{}{} + wg.Wait() +} + +func corsHeadersPass(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:22855", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Headers")) + assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Origin")) + assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) + + _, err = ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + err = r.Body.Close() + assert.NoError(t, err) +} + +func corsHeaders(t *testing.T) { + req, err := http.NewRequest("OPTIONS", "http://localhost:22855", nil) + assert.NoError(t, err) + + r, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + + assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Headers")) + assert.Equal(t, "GET,POST,PUT,DELETE", r.Header.Get("Access-Control-Allow-Methods")) + assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Origin")) + assert.Equal(t, "600", r.Header.Get("Access-Control-Max-Age")) + assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) + + _, err = ioutil.ReadAll(r.Body) + assert.NoError(t, err) + assert.Equal(t, 200, r.StatusCode) + + err = r.Body.Close() + assert.NoError(t, err) +} diff --git a/tests/plugins/http/attributes_test.go b/tests/plugins/http/attributes_test.go index 54998906..69200a30 100644 --- a/tests/plugins/http/attributes_test.go +++ b/tests/plugins/http/attributes_test.go @@ -4,7 +4,7 @@ import ( "net/http" "testing" - "github.com/spiral/roadrunner/v2/pkg/plugins/http/attributes" + "github.com/spiral/roadrunner/v2/plugins/http/attributes" "github.com/stretchr/testify/assert" ) diff --git a/tests/plugins/http/handler_test.go b/tests/plugins/http/handler_test.go index 193bf0cf..18558296 100644 --- a/tests/plugins/http/handler_test.go +++ b/tests/plugins/http/handler_test.go @@ -11,8 +11,8 @@ import ( "strings" "github.com/spiral/roadrunner/v2/pkg/pipe" - httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http" poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" "github.com/stretchr/testify/assert" "net/http" diff --git a/tests/plugins/http/http_test.go b/tests/plugins/http/http_plugin_test.go index f4a357f2..88857df5 100644 --- a/tests/plugins/http/http_test.go +++ b/tests/plugins/http/http_plugin_test.go @@ -19,18 +19,18 @@ import ( "github.com/golang/mock/gomock" "github.com/spiral/endure" goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" - "github.com/spiral/roadrunner-plugins/config" - "github.com/spiral/roadrunner-plugins/logger" - "github.com/spiral/roadrunner-plugins/resetter" "github.com/spiral/roadrunner/v2/interfaces/events" - "github.com/spiral/roadrunner/v2/pkg/plugins/informer" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/informer" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/resetter" + "github.com/spiral/roadrunner/v2/plugins/server" "github.com/spiral/roadrunner/v2/tests/mocks" "github.com/spiral/roadrunner/v2/tools" "github.com/yookoala/gofast" - rpcPlugin "github.com/spiral/roadrunner-plugins/rpc" - httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" "github.com/stretchr/testify/assert" ) @@ -74,7 +74,7 @@ func TestHTTPInit(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 5) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -92,7 +92,7 @@ func TestHTTPInit(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -103,6 +103,7 @@ func TestHTTPInit(t *testing.T) { } }() + stopCh <- struct{}{} wg.Wait() } @@ -140,8 +141,9 @@ func TestHTTPInformerReset(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) + stopCh := make(chan struct{}, 1) + go func() { - tt := time.NewTimer(time.Second * 10) defer wg.Done() for { select { @@ -157,7 +159,7 @@ func TestHTTPInformerReset(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -174,6 +176,8 @@ func TestHTTPInformerReset(t *testing.T) { t.Run("HTTPResetTest", resetTest) t.Run("HTTPEchoTestAfter", echoHTTP) + stopCh <- struct{}{} + wg.Wait() } @@ -258,7 +262,8 @@ func TestSSL(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -276,7 +281,7 @@ func TestSSL(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -291,6 +296,8 @@ func TestSSL(t *testing.T) { t.Run("SSLEcho", sslEcho) t.Run("SSLNoRedirect", sslNoRedirect) t.Run("fCGIecho", fcgiEcho) + + stopCh <- struct{}{} wg.Wait() } @@ -392,7 +399,8 @@ func TestSSLRedirect(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) + go func() { defer wg.Done() for { @@ -409,7 +417,7 @@ func TestSSLRedirect(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -422,6 +430,8 @@ func TestSSLRedirect(t *testing.T) { time.Sleep(time.Second * 1) t.Run("SSLRedirect", sslRedirect) + + stopCh <- struct{}{} wg.Wait() } @@ -477,7 +487,8 @@ func TestSSLPushPipes(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) + go func() { defer wg.Done() for { @@ -494,7 +505,7 @@ func TestSSLPushPipes(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -507,6 +518,8 @@ func TestSSLPushPipes(t *testing.T) { time.Sleep(time.Second * 1) t.Run("SSLPush", sslPush) + + stopCh <- struct{}{} wg.Wait() } @@ -565,7 +578,7 @@ func TestFastCGI_RequestUri(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -583,7 +596,7 @@ func TestFastCGI_RequestUri(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -596,6 +609,8 @@ func TestFastCGI_RequestUri(t *testing.T) { time.Sleep(time.Second * 1) t.Run("FastCGIServiceRequestUri", fcgiReqURI) + + stopCh <- struct{}{} wg.Wait() } @@ -650,7 +665,7 @@ func TestH2CUpgrade(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -668,7 +683,7 @@ func TestH2CUpgrade(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -681,6 +696,8 @@ func TestH2CUpgrade(t *testing.T) { time.Sleep(time.Second * 1) t.Run("H2cUpgrade", h2cUpgrade) + + stopCh <- struct{}{} wg.Wait() } @@ -739,7 +756,7 @@ func TestH2C(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -757,7 +774,7 @@ func TestH2C(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -770,6 +787,8 @@ func TestH2C(t *testing.T) { time.Sleep(time.Second * 1) t.Run("H2c", h2c) + + stopCh <- struct{}{} wg.Wait() } @@ -829,7 +848,7 @@ func TestHttpMiddleware(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 20) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -847,7 +866,7 @@ func TestHttpMiddleware(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -860,6 +879,8 @@ func TestHttpMiddleware(t *testing.T) { time.Sleep(time.Second * 1) t.Run("MiddlewareTest", middleware) + + stopCh <- struct{}{} wg.Wait() } @@ -895,7 +916,7 @@ func middleware(t *testing.T) { } func TestHttpEchoErr(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.DebugLevel)) + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) assert.NoError(t, err) rIn := ` @@ -971,7 +992,7 @@ logs: wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -989,7 +1010,7 @@ logs: assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -1002,6 +1023,8 @@ logs: time.Sleep(time.Second * 1) t.Run("HttpEchoError", echoError) + + stopCh <- struct{}{} wg.Wait() } @@ -1054,7 +1077,7 @@ func TestHttpEnvVariables(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - tt := time.NewTimer(time.Second * 10) + stopCh := make(chan struct{}, 1) go func() { defer wg.Done() @@ -1072,7 +1095,7 @@ func TestHttpEnvVariables(t *testing.T) { assert.FailNow(t, "error", err.Error()) } return - case <-tt.C: + case <-stopCh: // timeout err = cont.Stop() if err != nil { @@ -1085,6 +1108,8 @@ func TestHttpEnvVariables(t *testing.T) { time.Sleep(time.Second * 1) t.Run("EnvVariablesTest", envVarsTest) + + stopCh <- struct{}{} wg.Wait() } diff --git a/tests/plugins/http/parse_test.go b/tests/plugins/http/parse_test.go index df217202..5cc1ce32 100644 --- a/tests/plugins/http/parse_test.go +++ b/tests/plugins/http/parse_test.go @@ -3,7 +3,7 @@ package http import ( "testing" - "github.com/spiral/roadrunner/v2/pkg/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/http" ) var samples = []struct { diff --git a/tests/plugins/http/plugin1.go b/tests/plugins/http/plugin1.go index da887da8..0ec31211 100644 --- a/tests/plugins/http/plugin1.go +++ b/tests/plugins/http/plugin1.go @@ -1,7 +1,7 @@ package http import ( - "github.com/spiral/roadrunner-plugins/config" + "github.com/spiral/roadrunner/v2/plugins/config" ) type Plugin1 struct { diff --git a/tests/plugins/http/plugin_middleware.go b/tests/plugins/http/plugin_middleware.go index 6d67725d..8d02524d 100644 --- a/tests/plugins/http/plugin_middleware.go +++ b/tests/plugins/http/plugin_middleware.go @@ -3,7 +3,7 @@ package http import ( "net/http" - "github.com/spiral/roadrunner-plugins/config" + "github.com/spiral/roadrunner/v2/plugins/config" ) type PluginMiddleware struct { diff --git a/tests/plugins/http/response_test.go b/tests/plugins/http/response_test.go index 229163a2..a9cbf91a 100644 --- a/tests/plugins/http/response_test.go +++ b/tests/plugins/http/response_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/spiral/roadrunner/v2/pkg/payload" - httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" "github.com/stretchr/testify/assert" ) diff --git a/tests/plugins/http/uploads_config_test.go b/tests/plugins/http/uploads_config_test.go index 4a7927c5..e76078ee 100644 --- a/tests/plugins/http/uploads_config_test.go +++ b/tests/plugins/http/uploads_config_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" "github.com/stretchr/testify/assert" ) diff --git a/tests/plugins/http/uploads_test.go b/tests/plugins/http/uploads_test.go index e71ff8c1..7bb25cbf 100644 --- a/tests/plugins/http/uploads_test.go +++ b/tests/plugins/http/uploads_test.go @@ -17,8 +17,8 @@ import ( j "github.com/json-iterator/go" "github.com/spiral/roadrunner/v2/pkg/pipe" - httpPlugin "github.com/spiral/roadrunner/v2/pkg/plugins/http" poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" "github.com/stretchr/testify/assert" ) diff --git a/tests/plugins/informer/.rr-informer.yaml b/tests/plugins/informer/.rr-informer.yaml index d817684f..e50ca9c9 100644 --- a/tests/plugins/informer/.rr-informer.yaml +++ b/tests/plugins/informer/.rr-informer.yaml @@ -1,5 +1,5 @@ server: - command: "php ../../client.php echo pipes" + command: "php ../../http/client.php echo pipes" user: "" group: "" env: diff --git a/tests/plugins/informer/informer_test.go b/tests/plugins/informer/informer_test.go index 15063d7e..d9fc2143 100644 --- a/tests/plugins/informer/informer_test.go +++ b/tests/plugins/informer/informer_test.go @@ -5,17 +5,18 @@ import ( "net/rpc" "os" "os/signal" + "sync" "syscall" "testing" "time" "github.com/spiral/endure" goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" - "github.com/spiral/roadrunner-plugins/config" - "github.com/spiral/roadrunner-plugins/logger" - rpcPlugin "github.com/spiral/roadrunner-plugins/rpc" - "github.com/spiral/roadrunner/v2/pkg/plugins/informer" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/informer" + "github.com/spiral/roadrunner/v2/plugins/logger" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" "github.com/spiral/roadrunner/v2/tools" "github.com/stretchr/testify/assert" ) @@ -52,33 +53,43 @@ func TestInformerInit(t *testing.T) { sig := make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - tt := time.NewTimer(time.Second * 15) + stopCh := make(chan struct{}, 1) - t.Run("InformerRpcTest", informerRPCTest) + wg := &sync.WaitGroup{} + wg.Add(1) - for { - select { - case e := <-ch: - assert.Fail(t, "error", e.Error.Error()) - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) - } - case <-sig: - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return } - return - case <-tt.C: - // timeout - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) - } - return } - } + }() + + time.Sleep(time.Second) + t.Run("InformerRpcTest", informerRPCTest) + + stopCh <- struct{}{} + wg.Wait() } func informerRPCTest(t *testing.T) { diff --git a/tests/plugins/informer/test_plugin.go b/tests/plugins/informer/test_plugin.go index 95adfb07..ba281d02 100644 --- a/tests/plugins/informer/test_plugin.go +++ b/tests/plugins/informer/test_plugin.go @@ -4,10 +4,10 @@ import ( "context" "time" - "github.com/spiral/roadrunner-plugins/config" "github.com/spiral/roadrunner/v2/interfaces/worker" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" ) var testPoolConfig = poolImpl.Config{ diff --git a/tests/plugins/logger/.rr.yaml b/tests/plugins/logger/.rr.yaml new file mode 100644 index 00000000..cb555ec3 --- /dev/null +++ b/tests/plugins/logger/.rr.yaml @@ -0,0 +1,3 @@ +logs: + mode: development + level: debug
\ No newline at end of file diff --git a/tests/plugins/logger/logger_test.go b/tests/plugins/logger/logger_test.go new file mode 100644 index 00000000..cc788be3 --- /dev/null +++ b/tests/plugins/logger/logger_test.go @@ -0,0 +1,79 @@ +package logger + +import ( + "os" + "os/signal" + "sync" + "testing" + + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/stretchr/testify/assert" +) + +func TestLogger(t *testing.T) { + container, err := endure.NewContainer(nil, endure.RetryOnFail(true), endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + // config plugin + vp := &config.Viper{} + vp.Path = ".rr.yaml" + vp.Prefix = "rr" + err = container.Register(vp) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = container.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = container.Init() + if err != nil { + t.Fatal(err) + } + + errCh, err := container.Serve() + if err != nil { + t.Fatal(err) + } + + // stop by CTRL+C + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + + stopCh := make(chan struct{}, 1) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + assert.NoError(t, container.Stop()) + return + case <-c: + err = container.Stop() + assert.NoError(t, err) + return + case <-stopCh: + assert.NoError(t, container.Stop()) + return + } + } + }() + + stopCh <- struct{}{} + wg.Wait() +} diff --git a/tests/plugins/logger/plugin.go b/tests/plugins/logger/plugin.go new file mode 100644 index 00000000..9ddf9ec9 --- /dev/null +++ b/tests/plugins/logger/plugin.go @@ -0,0 +1,40 @@ +package logger + +import ( + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +type Plugin struct { + config config.Configurer + log logger.Logger +} + +func (p1 *Plugin) Init(cfg config.Configurer, log logger.Logger) error { + p1.config = cfg + p1.log = log + return nil +} + +func (p1 *Plugin) Serve() chan error { + errCh := make(chan error, 1) + p1.log.Error("error", "test", errors.E(errors.Str("test"))) + p1.log.Info("error", "test", errors.E(errors.Str("test"))) + p1.log.Debug("error", "test", errors.E(errors.Str("test"))) + p1.log.Warn("error", "test", errors.E(errors.Str("test"))) + + p1.log.Error("error", "test") + p1.log.Info("error", "test") + p1.log.Debug("error", "test") + p1.log.Warn("error", "test") + return errCh +} + +func (p1 *Plugin) Stop() error { + return nil +} + +func (p1 *Plugin) Name() string { + return "logger_plugin" +} diff --git a/tests/plugins/metrics/.rr-test.yaml b/tests/plugins/metrics/.rr-test.yaml new file mode 100644 index 00000000..37c50395 --- /dev/null +++ b/tests/plugins/metrics/.rr-test.yaml @@ -0,0 +1,16 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false + +metrics: + # prometheus client address (path /metrics added automatically) + address: localhost:2112 + collect: + app_metric: + type: histogram + help: "Custom application metric" + labels: [ "type" ] + buckets: [ 0.1, 0.2, 0.3, 1.0 ] +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/metrics/docker-compose.yml b/tests/plugins/metrics/docker-compose.yml new file mode 100644 index 00000000..610633b4 --- /dev/null +++ b/tests/plugins/metrics/docker-compose.yml @@ -0,0 +1,7 @@ +version: '3.7' + +services: + prometheus: + image: prom/prometheus + ports: + - 9090:9090 diff --git a/tests/plugins/metrics/metrics_test.go b/tests/plugins/metrics/metrics_test.go new file mode 100644 index 00000000..c94d51bc --- /dev/null +++ b/tests/plugins/metrics/metrics_test.go @@ -0,0 +1,739 @@ +package metrics + +import ( + "io/ioutil" + "net" + "net/http" + "net/rpc" + "os" + "os/signal" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/metrics" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/stretchr/testify/assert" +) + +const dialAddr = "127.0.0.1:6001" +const dialNetwork = "tcp" +const getAddr = "http://localhost:2112/metrics" + +// get request and return body +func get() (string, error) { + r, err := http.Get(getAddr) + if err != nil { + return "", err + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", err + } + + err = r.Body.Close() + if err != nil { + return "", err + } + // unsafe + return string(b), err +} + +func TestMetricsInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{} + cfg.Prefix = "rr" + cfg.Path = ".rr-test.yaml" + + err = cont.RegisterAll( + cfg, + &metrics.Plugin{}, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + tt := time.NewTimer(time.Second * 5) + + out, err := get() + assert.NoError(t, err) + + assert.Contains(t, out, "go_gc_duration_seconds") + + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } +} + +func TestMetricsGaugeCollector(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{} + cfg.Prefix = "rr" + cfg.Path = ".rr-test.yaml" + + err = cont.RegisterAll( + cfg, + &metrics.Plugin{}, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + time.Sleep(time.Second) + tt := time.NewTimer(time.Second * 5) + + out, err := get() + assert.NoError(t, err) + assert.Contains(t, out, "my_gauge 100") + assert.Contains(t, out, "my_gauge2 100") + + out, err = get() + assert.NoError(t, err) + assert.Contains(t, out, "go_gc_duration_seconds") + + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } +} + +func TestMetricsDifferentRPCCalls(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{} + cfg.Prefix = "rr" + cfg.Path = ".rr-test.yaml" + + err = cont.RegisterAll( + cfg, + &metrics.Plugin{}, + &rpcPlugin.Plugin{}, + &logger.ZapLogger{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + tt := time.NewTimer(time.Minute * 3) + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("DeclareMetric", declareMetricsTest) + genericOut, err := get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "test_metrics_named_collector") + + t.Run("AddMetric", addMetricsTest) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "test_metrics_named_collector 10000") + + t.Run("SetMetric", setMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "user_gauge_collector 100") + + t.Run("VectorMetric", vectorMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "gauge_2_collector{section=\"first\",type=\"core\"} 100") + + t.Run("MissingSection", missingSection) + t.Run("SetWithoutLabels", setWithoutLabels) + t.Run("SetOnHistogram", setOnHistogram) + t.Run("MetricSub", subMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "sub_gauge_subMetric 1") + + t.Run("SubVector", subVector) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "sub_gauge_subVector{section=\"first\",type=\"core\"} 1") + + t.Run("RegisterHistogram", registerHistogram) + + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, `TYPE histogram_registerHistogram`) + + // check buckets + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.1"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.2"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="0.5"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_bucket{le="+Inf"} 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_sum 0`) + assert.Contains(t, genericOut, `histogram_registerHistogram_count 0`) + + t.Run("CounterMetric", counterMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "HELP default_default_counter_CounterMetric test_counter") + assert.Contains(t, genericOut, `default_default_counter_CounterMetric{section="section2",type="type2"}`) + + t.Run("ObserveMetric", observeMetric) + genericOut, err = get() + assert.NoError(t, err) + assert.Contains(t, genericOut, "observe_observeMetric") + + t.Run("ObserveMetricNotEnoughLabels", observeMetricNotEnoughLabels) + + close(sig) +} + +func observeMetricNotEnoughLabels(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "observe_observeMetricNotEnoughLabels", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Help: "test_observe", + Type: metrics.Histogram, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + assert.Error(t, client.Call("metrics.Observe", metrics.Metric{ + Name: "observe_observeMetric", + Value: 100.0, + Labels: []string{"test"}, + }, &ret)) + assert.False(t, ret) +} + +func observeMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "observe_observeMetric", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Help: "test_observe", + Type: metrics.Histogram, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + assert.NoError(t, client.Call("metrics.Observe", metrics.Metric{ + Name: "observe_observeMetric", + Value: 100.0, + Labels: []string{"test", "test2"}, + }, &ret)) + assert.True(t, ret) +} + +func counterMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "counter_CounterMetric", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Help: "test_counter", + Type: metrics.Counter, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + assert.NoError(t, client.Call("metrics.Add", metrics.Metric{ + Name: "counter_CounterMetric", + Value: 100.0, + Labels: []string{"type2", "section2"}, + }, &ret)) + assert.True(t, ret) +} + +func registerHistogram(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "histogram_registerHistogram", + Collector: metrics.Collector{ + Help: "test_histogram", + Type: metrics.Histogram, + Buckets: []float64{0.1, 0.2, 0.5}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "histogram_registerHistogram", + Value: 10000, + Labels: nil, + } + + err = client.Call("metrics.Add", m, &ret) + assert.Error(t, err) + assert.False(t, ret) +} + +func subVector(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "sub_gauge_subVector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m := metrics.Metric{ + Name: "sub_gauge_subVector", + Value: 100000, + Labels: []string{"core", "first"}, + } + + err = client.Call("metrics.Add", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m = metrics.Metric{ + Name: "sub_gauge_subVector", + Value: 99999, + Labels: []string{"core", "first"}, + } + + err = client.Call("metrics.Sub", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func subMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "sub_gauge_subMetric", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m := metrics.Metric{ + Name: "sub_gauge_subMetric", + Value: 100000, + } + + err = client.Call("metrics.Add", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m = metrics.Metric{ + Name: "sub_gauge_subMetric", + Value: 99999, + } + + err = client.Call("metrics.Sub", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func setOnHistogram(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "histogram_setOnHistogram", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Histogram, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_setOnHistogram", + Value: 100.0, + } + + err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + assert.Error(t, err) + assert.False(t, ret) +} + +func setWithoutLabels(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "gauge_setWithoutLabels", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_setWithoutLabels", + Value: 100.0, + } + + err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + assert.Error(t, err) + assert.False(t, ret) +} + +func missingSection(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "gauge_missing_section_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_missing_section_collector", + Value: 100.0, + Labels: []string{"missing"}, + } + + err = client.Call("metrics.Set", m, &ret) // expected 2 label values but got 1 in []string{"missing"} + assert.Error(t, err) + assert.False(t, ret) +} + +func vectorMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "gauge_2_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + Labels: []string{"type", "section"}, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + + ret = false + + m := metrics.Metric{ + Name: "gauge_2_collector", + Value: 100.0, + Labels: []string{"core", "first"}, + } + + err = client.Call("metrics.Set", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func setMetric(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "user_gauge_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Gauge, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + m := metrics.Metric{ + Name: "user_gauge_collector", + Value: 100.0, + } + + err = client.Call("metrics.Set", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func addMetricsTest(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + m := metrics.Metric{ + Name: "test_metrics_named_collector", + Value: 10000, + Labels: nil, + } + + err = client.Call("metrics.Add", m, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} + +func declareMetricsTest(t *testing.T) { + conn, err := net.Dial(dialNetwork, dialAddr) + assert.NoError(t, err) + defer func() { + _ = conn.Close() + }() + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret bool + + nc := metrics.NamedCollector{ + Name: "test_metrics_named_collector", + Collector: metrics.Collector{ + Namespace: "default", + Subsystem: "default", + Type: metrics.Counter, + Help: "NO HELP!", + Labels: nil, + Buckets: nil, + }, + } + + err = client.Call("metrics.Declare", nc, &ret) + assert.NoError(t, err) + assert.True(t, ret) +} diff --git a/tests/plugins/metrics/plugin1.go b/tests/plugins/metrics/plugin1.go new file mode 100644 index 00000000..ae024a8a --- /dev/null +++ b/tests/plugins/metrics/plugin1.go @@ -0,0 +1,46 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spiral/roadrunner/v2/plugins/config" +) + +// Gauge ////////////// +type Plugin1 struct { + config config.Configurer +} + +func (p1 *Plugin1) Init(cfg config.Configurer) error { + p1.config = cfg + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "metrics_test.plugin1" +} + +func (p1 *Plugin1) MetricsCollector() []prometheus.Collector { + collector := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "my_gauge", + Help: "My gauge value", + }) + + collector.Set(100) + + collector2 := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "my_gauge2", + Help: "My gauge2 value", + }) + + collector2.Set(100) + return []prometheus.Collector{collector, collector2} +} diff --git a/tests/plugins/mocks/mock_log.go b/tests/plugins/mocks/mock_log.go new file mode 100644 index 00000000..e9631805 --- /dev/null +++ b/tests/plugins/mocks/mock_log.go @@ -0,0 +1,150 @@ +package mocks + +import ( + "reflect" + + "github.com/golang/mock/gomock" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +// MockLogger is a mock of Logger interface. +type MockLogger struct { + ctrl *gomock.Controller + recorder *MockLoggerMockRecorder +} + +// MockLoggerMockRecorder is the mock recorder for MockLogger. +type MockLoggerMockRecorder struct { + mock *MockLogger +} + +// NewMockLogger creates a new mock instance. +func NewMockLogger(ctrl *gomock.Controller) *MockLogger { + mock := &MockLogger{ctrl: ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLogger) EXPECT() *MockLoggerMockRecorder { + return m.recorder +} + +func (m *MockLogger) Init() error { + mock := &MockLogger{ctrl: m.ctrl} + mock.recorder = &MockLoggerMockRecorder{mock} + return nil +} + +// Debug mocks base method. +func (m *MockLogger) Debug(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Debug", varargs...) +} + +// Warn mocks base method. +func (m *MockLogger) Warn(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Warn", varargs...) +} + +// Info mocks base method. +func (m *MockLogger) Info(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Info", varargs...) +} + +// Error mocks base method. +func (m *MockLogger) Error(msg string, keyvals ...interface{}) { + m.ctrl.T.Helper() + varargs := []interface{}{msg} + for _, a := range keyvals { + varargs = append(varargs, a) + } + m.ctrl.Call(m, "Error", varargs...) +} + +// Warn indicates an expected call of Warn. +func (mr *MockLoggerMockRecorder) Warn(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), varargs...) +} + +// Debug indicates an expected call of Debug. +func (mr *MockLoggerMockRecorder) Debug(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), varargs...) +} + +// Error indicates an expected call of Error. +func (mr *MockLoggerMockRecorder) Error(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), varargs...) +} + +func (mr *MockLoggerMockRecorder) Init() error { + return nil +} + +// Info indicates an expected call of Info. +func (mr *MockLoggerMockRecorder) Info(msg interface{}, keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{msg}, keyvals...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), varargs...) +} + +// MockWithLogger is a mock of WithLogger interface. +type MockWithLogger struct { + ctrl *gomock.Controller + recorder *MockWithLoggerMockRecorder +} + +// MockWithLoggerMockRecorder is the mock recorder for MockWithLogger. +type MockWithLoggerMockRecorder struct { + mock *MockWithLogger +} + +// NewMockWithLogger creates a new mock instance. +func NewMockWithLogger(ctrl *gomock.Controller) *MockWithLogger { + mock := &MockWithLogger{ctrl: ctrl} + mock.recorder = &MockWithLoggerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockWithLogger) EXPECT() *MockWithLoggerMockRecorder { + return m.recorder +} + +// With mocks base method. +func (m *MockWithLogger) With(keyvals ...interface{}) logger.Logger { + m.ctrl.T.Helper() + varargs := []interface{}{} + for _, a := range keyvals { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "With", varargs...) + ret0, _ := ret[0].(logger.Logger) + return ret0 +} + +// With indicates an expected call of With. +func (mr *MockWithLoggerMockRecorder) With(keyvals ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "With", reflect.TypeOf((*MockWithLogger)(nil).With), keyvals...) +} diff --git a/tests/plugins/redis/plugin1.go b/tests/plugins/redis/plugin1.go new file mode 100644 index 00000000..e50213e5 --- /dev/null +++ b/tests/plugins/redis/plugin1.go @@ -0,0 +1,43 @@ +package redis + +import ( + "context" + "time" + + "github.com/go-redis/redis/v8" + "github.com/spiral/errors" + redisPlugin "github.com/spiral/roadrunner/v2/plugins/redis" +) + +type Plugin1 struct { + redisClient redis.UniversalClient +} + +func (p *Plugin1) Init(redis redisPlugin.Redis) error { + p.redisClient = redis.GetClient() + return nil +} + +func (p *Plugin1) Serve() chan error { + const op = errors.Op("plugin1 serve") + errCh := make(chan error, 1) + p.redisClient.Set(context.Background(), "foo", "bar", time.Minute) + + stringCmd := p.redisClient.Get(context.Background(), "foo") + data, err := stringCmd.Result() + if err != nil { + errCh <- errors.E(op, err) + return errCh + } + + if data != "bar" { + errCh <- errors.E(op, errors.Str("no such key")) + return errCh + } + + return errCh +} + +func (p *Plugin1) Stop() error { + return nil +} diff --git a/tests/plugins/redis/redis_plugin_test.go b/tests/plugins/redis/redis_plugin_test.go new file mode 100644 index 00000000..eba05752 --- /dev/null +++ b/tests/plugins/redis/redis_plugin_test.go @@ -0,0 +1,120 @@ +package redis + +import ( + "fmt" + "os" + "os/signal" + "sync" + "syscall" + "testing" + + "github.com/alicebob/miniredis/v2" + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/redis" + "github.com/spiral/roadrunner/v2/tests/mocks" + "github.com/stretchr/testify/assert" +) + +func redisConfig(port string) string { + cfg := ` +redis: + addrs: + - 'localhost:%s' + master_name: '' + username: '' + password: '' + db: 0 + sentinel_password: '' + route_by_latency: false + route_randomly: false + dial_timeout: 0 + max_retries: 1 + min_retry_backoff: 0 + max_retry_backoff: 0 + pool_size: 0 + min_idle_conns: 0 + max_conn_age: 0 + read_timeout: 0 + write_timeout: 0 + pool_timeout: 0 + idle_timeout: 0 + idle_check_freq: 0 + read_only: false +` + return fmt.Sprintf(cfg, port) +} + +func TestRedisInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + s, err := miniredis.Run() + assert.NoError(t, err) + + c := redisConfig(s.Port()) + + cfg := &config.Viper{} + cfg.Type = "yaml" + cfg.ReadInCfg = []byte(c) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + err = cont.RegisterAll( + cfg, + mockLogger, + &redis.Plugin{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + stopCh <- struct{}{} + wg.Wait() +} diff --git a/tests/plugins/reload/config_test.go b/tests/plugins/reload/config_test.go new file mode 100644 index 00000000..72c11070 --- /dev/null +++ b/tests/plugins/reload/config_test.go @@ -0,0 +1,63 @@ +package reload + +import ( + "testing" + "time" + + "github.com/spiral/roadrunner/v2/plugins/reload" + "github.com/stretchr/testify/assert" +) + +func Test_Config_Valid(t *testing.T) { + services := make(map[string]reload.ServiceConfig) + services["test"] = reload.ServiceConfig{ + Recursive: false, + Patterns: nil, + Dirs: nil, + Ignore: nil, + } + + cfg := &reload.Config{ + Interval: time.Second, + Patterns: nil, + Services: services, + } + assert.NoError(t, cfg.Valid()) +} + +func Test_Fake_ServiceConfig(t *testing.T) { + services := make(map[string]reload.ServiceConfig) + cfg := &reload.Config{ + Interval: time.Microsecond, + Patterns: nil, + Services: services, + } + assert.Error(t, cfg.Valid()) +} + +func Test_Interval(t *testing.T) { + services := make(map[string]reload.ServiceConfig) + services["test"] = reload.ServiceConfig{ + Enabled: false, + Recursive: false, + Patterns: nil, + Dirs: nil, + Ignore: nil, + } + + cfg := &reload.Config{ + Interval: time.Millisecond, // should crash here + Patterns: nil, + Services: services, + } + assert.Error(t, cfg.Valid()) +} + +func Test_NoServiceConfig(t *testing.T) { + cfg := &reload.Config{ + Interval: time.Second, + Patterns: nil, + Services: nil, + } + assert.Error(t, cfg.Valid()) +} diff --git a/tests/plugins/reload/configs/.rr-reload-2.yaml b/tests/plugins/reload/configs/.rr-reload-2.yaml new file mode 100644 index 00000000..ab32b2d1 --- /dev/null +++ b/tests/plugins/reload/configs/.rr-reload-2.yaml @@ -0,0 +1,44 @@ +server: + command: php ../../psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:27388' + maxRequestSize: 1024 + middleware: + - '' + uploads: + forbid: + - .php + - .exe + - .bat + trustedSubnets: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - '::1/128' + - 'fc00::/7' + - 'fe80::/10' + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error +reload: + interval: 2s + patterns: + - .txt + services: + http: + dirs: + - './unit_tests' + recursive: true diff --git a/tests/plugins/reload/configs/.rr-reload-3.yaml b/tests/plugins/reload/configs/.rr-reload-3.yaml new file mode 100644 index 00000000..881d9b88 --- /dev/null +++ b/tests/plugins/reload/configs/.rr-reload-3.yaml @@ -0,0 +1,46 @@ +server: + command: php ../../psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:37388' + maxRequestSize: 1024 + middleware: + - '' + uploads: + forbid: + - .php + - .exe + - .bat + trustedSubnets: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - '::1/128' + - 'fc00::/7' + - 'fe80::/10' + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error +reload: + interval: 2s + patterns: + - .txt + services: + http: + dirs: + - './unit_tests' + - './unit_tests_copied' + - './dir1' + recursive: true diff --git a/tests/plugins/reload/configs/.rr-reload-4.yaml b/tests/plugins/reload/configs/.rr-reload-4.yaml new file mode 100644 index 00000000..d47df558 --- /dev/null +++ b/tests/plugins/reload/configs/.rr-reload-4.yaml @@ -0,0 +1,46 @@ +server: + command: php ../../psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:22766' + maxRequestSize: 1024 + middleware: + - '' + uploads: + forbid: + - .php + - .exe + - .bat + trustedSubnets: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - '::1/128' + - 'fc00::/7' + - 'fe80::/10' + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error +reload: + interval: 2s + patterns: + - .aaa + services: + http: + dirs: + - './unit_tests' + - './unit_tests_copied' + - './dir1' + recursive: false diff --git a/tests/plugins/reload/configs/.rr-reload.yaml b/tests/plugins/reload/configs/.rr-reload.yaml new file mode 100644 index 00000000..794c41f2 --- /dev/null +++ b/tests/plugins/reload/configs/.rr-reload.yaml @@ -0,0 +1,44 @@ +server: + command: php ../../psr-worker-bench.php + user: '' + group: '' + env: + RR_HTTP: 'true' + relay: pipes + relayTimeout: 20s +http: + debug: true + address: '127.0.0.1:22388' + maxRequestSize: 1024 + middleware: + - '' + uploads: + forbid: + - .php + - .exe + - .bat + trustedSubnets: + - 10.0.0.0/8 + - 127.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/16 + - '::1/128' + - 'fc00::/7' + - 'fe80::/10' + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error +reload: + interval: 1s + patterns: + - .txt + services: + http: + dirs: + - './unit_tests' + recursive: true diff --git a/tests/plugins/reload/reload_plugin_test.go b/tests/plugins/reload/reload_plugin_test.go new file mode 100644 index 00000000..c83d4787 --- /dev/null +++ b/tests/plugins/reload/reload_plugin_test.go @@ -0,0 +1,827 @@ +package reload + +import ( + "io" + "io/ioutil" + "math/rand" + "os" + "os/signal" + "path/filepath" + "strconv" + "sync" + "syscall" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/reload" + "github.com/spiral/roadrunner/v2/plugins/resetter" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/tests/mocks" + "github.com/stretchr/testify/assert" +) + +const testDir string = "unit_tests" +const testCopyToDir string = "unit_tests_copied" +const dir1 string = "dir1" +const hugeNumberOfFiles uint = 500 + +func TestReloadInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + err = os.Mkdir(testDir, 0755) + assert.NoError(t, err) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", "file.txt", "size", gomock.Any()).Times(2) + mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", "file.txt", "size", gomock.Any()).Times(2) + mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").Times(1) + mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").Times(1) + mockLogger.EXPECT().Info("HTTP listeners successfully re-added").Times(1) + mockLogger.EXPECT().Info("HTTP plugin successfully restarted").Times(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("ReloadTestInit", reloadTestInit) + + stopCh <- struct{}{} + wg.Wait() + assert.NoError(t, freeResources(testDir)) +} + +func reloadTestInit(t *testing.T) { + err := ioutil.WriteFile(filepath.Join(testDir, "file.txt"), //nolint:gosec + []byte{}, 0755) + assert.NoError(t, err) +} + +func TestReloadHugeNumberOfFiles(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + + assert.NoError(t, os.Mkdir(testDir, 0755)) + assert.NoError(t, os.Mkdir(testCopyToDir, 0755)) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("file was updated", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").MinTimes(1) + mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").MinTimes(1) + mockLogger.EXPECT().Info("HTTP listeners successfully re-added").MinTimes(1) + mockLogger.EXPECT().Info("HTTP plugin successfully restarted").MinTimes(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("ReloadTestHugeNumberOfFiles", reloadHugeNumberOfFiles) + t.Run("ReloadRandomlyChangeFile", randomlyChangeFile) + + stopCh <- struct{}{} + wg.Wait() + + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) +} + +func randomlyChangeFile(t *testing.T) { + // we know, that directory contains 500 files (0-499) + // let's try to randomly change it + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(500) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec + err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".txt"), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } +} + +func reloadHugeNumberOfFiles(t *testing.T) { + for i := uint(0); i < hugeNumberOfFiles; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } +} + +// Should be events only about creating files with txt ext +func TestReloadFilterFileExt(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload-2.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, os.Mkdir(testDir, 0755)) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(100) + mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").Times(1) + mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").Times(1) + mockLogger.EXPECT().Info("HTTP listeners successfully re-added").Times(1) + mockLogger.EXPECT().Info("HTTP plugin successfully restarted").Times(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("ReloadMakeFiles", reloadMakeFiles) + t.Run("ReloadFilteredExt", reloadFilteredExt) + + stopCh <- struct{}{} + wg.Wait() + + assert.NoError(t, freeResources(testDir)) +} + +func reloadMakeFiles(t *testing.T) { + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def")) + } +} + +func reloadFilteredExt(t *testing.T) { + // change files with abc extension + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(1000) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec + err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".abc"), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } + + // change files with def extension + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(1000) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(hugeNumberOfFiles)) // nolint:gosec + err := ioutil.WriteFile(filepath.Join(testDir, "file_"+strconv.Itoa(int(rNum))+".def"), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } +} + +// Should be events only about creating files with txt ext +func TestReloadCopy500(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload-3.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources(dir1)) + + assert.NoError(t, os.Mkdir(testDir, 0755)) + assert.NoError(t, os.Mkdir(testCopyToDir, 0755)) + assert.NoError(t, os.Mkdir(dir1, 0755)) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + // + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file was created", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Debug("file was added to watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Debug("file was removed from watcher", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Debug("file was updated", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(50) + mockLogger.EXPECT().Info("HTTP plugin got restart request. Restarting...").MinTimes(1) + mockLogger.EXPECT().Info("HTTP workers Pool successfully restarted").MinTimes(1) + mockLogger.EXPECT().Info("HTTP listeners successfully re-added").MinTimes(1) + mockLogger.EXPECT().Info("HTTP plugin successfully restarted").MinTimes(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + // Scenario + // 1 + // Create 3k files with txt, abc, def extensions + // Copy files to the unit_tests_copy dir + // 2 + // Delete both dirs, recreate + // Create 3k files with txt, abc, def extensions + // Move files to the unit_tests_copy dir + // 3 + // Recursive + + t.Run("ReloadMake300Files", reloadMake300Files) + t.Run("ReloadCopyFiles", reloadCopyFiles) + t.Run("ReloadRecursiveDirsSupport", copyFilesRecursive) + t.Run("RandomChangesInRecursiveDirs", randomChangesInRecursiveDirs) + t.Run("RemoveFilesSupport", removeFilesSupport) + t.Run("ReloadMoveSupport", reloadMoveSupport) + + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources(dir1)) + + stopCh <- struct{}{} + wg.Wait() +} + +func reloadMoveSupport(t *testing.T) { + t.Run("MoveSupportCopy", copyFilesRecursive) + // move some files + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(500) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(100)) // nolint:gosec + rDir := rand.Int63n(9) // nolint:gosec + rExt := rand.Int63n(3) // nolint:gosec + + ext := []string{ + ".txt", + ".abc", + ".def", + } + + // change files with def extension + dirs := []string{ + "dir1", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/dir4", + "dir1/dir2/dir3/dir4/dir5", + "dir1/dir2/dir3/dir4/dir5/dir6", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10", + } + + // move file + err := os.Rename(filepath.Join(dirs[rDir], "file_"+strconv.Itoa(int(rNum))+ext[rExt]), filepath.Join(dirs[rDir+1], "file_"+strconv.Itoa(int(rNum))+ext[rExt])) + assert.NoError(t, err) + } +} + +func removeFilesSupport(t *testing.T) { + // remove some files + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(500) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(100)) // nolint:gosec + rDir := rand.Int63n(10) // nolint:gosec + rExt := rand.Int63n(3) // nolint:gosec + + ext := []string{ + ".txt", + ".abc", + ".def", + } + + // change files with def extension + dirs := []string{ + "dir1", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/dir4", + "dir1/dir2/dir3/dir4/dir5", + "dir1/dir2/dir3/dir4/dir5/dir6", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10", + } + // here can be a situation, when file already deleted + _ = os.Remove(filepath.Join(dirs[rDir], "file_"+strconv.Itoa(int(rNum))+ext[rExt])) + } +} + +func randomChangesInRecursiveDirs(t *testing.T) { + // change files with def extension + dirs := []string{ + "dir1", + "dir1/dir2", + "dir1/dir2/dir3", + "dir1/dir2/dir3/dir4", + "dir1/dir2/dir3/dir4/dir5", + "dir1/dir2/dir3/dir4/dir5/dir6", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9", + "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10", + } + + ext := []string{ + ".txt", + ".abc", + ".def", + } + + filenames := []string{ + "file_", // should be update + "foo_", // should be created + "bar_", // should be created + } + for i := 0; i < 10; i++ { + // rand sleep + rSleep := rand.Int63n(500) // nolint:gosec + time.Sleep(time.Millisecond * time.Duration(rSleep)) + rNum := rand.Int63n(int64(100)) // nolint:gosec + rDir := rand.Int63n(10) // nolint:gosec + rExt := rand.Int63n(3) // nolint:gosec + rName := rand.Int63n(3) // nolint:gosec + + err := ioutil.WriteFile(filepath.Join(dirs[rDir], filenames[rName]+strconv.Itoa(int(rNum))+ext[rExt]), []byte("Hello, Gophers!"), 0755) // nolint:gosec + assert.NoError(t, err) + } +} + +func copyFilesRecursive(t *testing.T) { + err := copyDir(testDir, "dir1") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9") + assert.NoError(t, err) + err = copyDir(testDir, "dir1/dir2/dir3/dir4/dir5/dir6/dir7/dir8/dir9/dir10") + assert.NoError(t, err) +} + +func reloadCopyFiles(t *testing.T) { + err := copyDir(testDir, testCopyToDir) + assert.NoError(t, err) + + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + + assert.NoError(t, os.Mkdir(testDir, 0755)) + assert.NoError(t, os.Mkdir(testCopyToDir, 0755)) + + // recreate files + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def")) + } + + err = copyDir(testDir, testCopyToDir) + assert.NoError(t, err) +} + +func reloadMake300Files(t *testing.T) { + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".txt")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".abc")) + } + for i := uint(0); i < 100; i++ { + assert.NoError(t, makeFile("file_"+strconv.Itoa(int(i))+".def")) + } +} + +func TestReloadNoRecursion(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-reload-4.yaml", + Prefix: "rr", + } + + // try to remove, skip error + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources(dir1)) + + assert.NoError(t, os.Mkdir(testDir, 0755)) + assert.NoError(t, os.Mkdir(dir1, 0755)) + assert.NoError(t, os.Mkdir(testCopyToDir, 0755)) + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + // http server should not be restarted. all event from wrong file extensions should be skipped + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").Times(1) + mockLogger.EXPECT().Debug("file added to the list of removed files", "path", gomock.Any(), "name", gomock.Any(), "size", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &reload.Plugin{}, + &resetter.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + assert.NoError(t, err) + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + t.Run("ReloadMakeFiles", reloadMakeFiles) // make files in the testDir + t.Run("ReloadCopyFilesRecursive", reloadCopyFiles) + + stopCh <- struct{}{} + wg.Wait() + + assert.NoError(t, freeResources(testDir)) + assert.NoError(t, freeResources(testCopyToDir)) + assert.NoError(t, freeResources(dir1)) +} + +// ======================================================================== + +func freeResources(path string) error { + return os.RemoveAll(path) +} + +func makeFile(filename string) error { + return ioutil.WriteFile(filepath.Join(testDir, filename), []byte{}, 0755) //nolint:gosec +} + +func copyDir(src string, dst string) error { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + si, err := os.Stat(src) + if err != nil { + return err + } + if !si.IsDir() { + return errors.E(errors.Str("source is not a directory")) + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + + err = os.MkdirAll(dst, si.Mode()) + if err != nil { + return err + } + + entries, err := ioutil.ReadDir(src) + if err != nil { + return err + } + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + err = copyDir(srcPath, dstPath) + if err != nil { + return err + } + } else { + // Skip symlinks. + if entry.Mode()&os.ModeSymlink != 0 { + continue + } + + err = copyFile(srcPath, dstPath) + if err != nil { + return err + } + } + } + return nil +} + +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return errors.E(err) + } + defer func() { + _ = in.Close() + }() + + out, err := os.Create(dst) + if err != nil { + return errors.E(err) + } + defer func() { + _ = out.Close() + }() + + _, err = io.Copy(out, in) + if err != nil { + return errors.E(err) + } + + err = out.Sync() + if err != nil { + return errors.E(err) + } + + si, err := os.Stat(src) + if err != nil { + return errors.E(err) + } + err = os.Chmod(dst, si.Mode()) + if err != nil { + return errors.E(err) + } + return nil +} diff --git a/tests/plugins/resetter/.rr-resetter.yaml b/tests/plugins/resetter/.rr-resetter.yaml new file mode 100644 index 00000000..e50ca9c9 --- /dev/null +++ b/tests/plugins/resetter/.rr-resetter.yaml @@ -0,0 +1,16 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_CONFIG": "/some/place/on/the/C134" + "RR_CONFIG2": "C138" + relay: "pipes" + relayTimeout: "20s" + +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/resetter/resetter_test.go b/tests/plugins/resetter/resetter_test.go new file mode 100644 index 00000000..89dd43c7 --- /dev/null +++ b/tests/plugins/resetter/resetter_test.go @@ -0,0 +1,113 @@ +package resetter + +import ( + "net" + "net/rpc" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/resetter" + rpcPlugin "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/stretchr/testify/assert" +) + +func TestResetterInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + cfg := &config.Viper{ + Path: ".rr-resetter.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &server.Plugin{}, + &logger.ZapLogger{}, + &resetter.Plugin{}, + &rpcPlugin.Plugin{}, + &Plugin1{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + stopCh := make(chan struct{}, 1) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + + t.Run("ResetterRpcTest", resetterRPCTest) + stopCh <- struct{}{} + wg.Wait() +} + +func resetterRPCTest(t *testing.T) { + conn, err := net.Dial("tcp", "127.0.0.1:6001") + assert.NoError(t, err) + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + // WorkerList contains list of workers. + + var ret bool + err = client.Call("resetter.Reset", "resetter.plugin1", &ret) + assert.NoError(t, err) + assert.True(t, ret) + ret = false + + var services []string + err = client.Call("resetter.List", nil, &services) + assert.NotNil(t, services) + assert.NoError(t, err) + if services[0] != "resetter.plugin1" { + t.Fatal("no enough services") + } +} diff --git a/tests/plugins/resetter/test_plugin.go b/tests/plugins/resetter/test_plugin.go new file mode 100644 index 00000000..7d53bca0 --- /dev/null +++ b/tests/plugins/resetter/test_plugin.go @@ -0,0 +1,66 @@ +package resetter + +import ( + "context" + "time" + + poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" +) + +var testPoolConfig = poolImpl.Config{ + NumWorkers: 10, + MaxJobs: 100, + AllocateTimeout: time.Second * 10, + DestroyTimeout: time.Second * 10, + Supervisor: &poolImpl.SupervisorConfig{ + WatchTick: 60, + TTL: 1000, + IdleTTL: 10, + ExecTTL: 10, + MaxWorkerMemory: 1000, + }, +} + +// Gauge ////////////// +type Plugin1 struct { + config config.Configurer + server server.Server +} + +func (p1 *Plugin1) Init(cfg config.Configurer, server server.Server) error { + p1.config = cfg + p1.server = server + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "resetter.plugin1" +} + +func (p1 *Plugin1) Reset() error { + pool, err := p1.server.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + panic(err) + } + pool.Destroy(context.Background()) + + pool, err = p1.server.NewWorkerPool(context.Background(), testPoolConfig, nil) + if err != nil { + panic(err) + } + + _ = pool + + return nil +} diff --git a/tests/plugins/rpc/config_test.go b/tests/plugins/rpc/config_test.go new file mode 100755 index 00000000..df5fa391 --- /dev/null +++ b/tests/plugins/rpc/config_test.go @@ -0,0 +1,135 @@ +package rpc + +import ( + "runtime" + "testing" + + "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/stretchr/testify/assert" +) + +func TestConfig_Listener(t *testing.T) { + cfg := &rpc.Config{Listen: "tcp://:18001"} + + ln, err := cfg.Listener() + assert.NoError(t, err) + assert.NotNil(t, ln) + defer func() { + err := ln.Close() + if err != nil { + t.Errorf("error closing the listener: error %v", err) + } + }() + + assert.Equal(t, "tcp", ln.Addr().Network()) + if runtime.GOOS == "windows" { + assert.Equal(t, "[::]:18001", ln.Addr().String()) + } else { + assert.Equal(t, "0.0.0.0:18001", ln.Addr().String()) + } +} + +func TestConfig_ListenerUnix(t *testing.T) { + cfg := &rpc.Config{Listen: "unix://file.sock"} + + ln, err := cfg.Listener() + assert.NoError(t, err) + assert.NotNil(t, ln) + defer func() { + err := ln.Close() + if err != nil { + t.Errorf("error closing the listener: error %v", err) + } + }() + + assert.Equal(t, "unix", ln.Addr().Network()) + assert.Equal(t, "file.sock", ln.Addr().String()) +} + +func Test_Config_Error(t *testing.T) { + cfg := &rpc.Config{Listen: "uni:unix.sock"} + ln, err := cfg.Listener() + assert.Nil(t, ln) + assert.Error(t, err) + assert.Equal(t, "invalid DSN (tcp://:6001, unix://file.sock)", err.Error()) +} + +func Test_Config_ErrorMethod(t *testing.T) { + cfg := &rpc.Config{Listen: "xinu://unix.sock"} + + ln, err := cfg.Listener() + assert.Nil(t, ln) + assert.Error(t, err) +} + +func TestConfig_Dialer(t *testing.T) { + cfg := &rpc.Config{Listen: "tcp://:18001"} + + ln, _ := cfg.Listener() + defer func() { + err := ln.Close() + if err != nil { + t.Errorf("error closing the listener: error %v", err) + } + }() + + conn, err := cfg.Dialer() + assert.NoError(t, err) + assert.NotNil(t, conn) + defer func() { + err := conn.Close() + if err != nil { + t.Errorf("error closing the connection: error %v", err) + } + }() + + assert.Equal(t, "tcp", conn.RemoteAddr().Network()) + assert.Equal(t, "127.0.0.1:18001", conn.RemoteAddr().String()) +} + +func TestConfig_DialerUnix(t *testing.T) { + cfg := &rpc.Config{Listen: "unix://file.sock"} + + ln, _ := cfg.Listener() + defer func() { + err := ln.Close() + if err != nil { + t.Errorf("error closing the listener: error %v", err) + } + }() + + conn, err := cfg.Dialer() + assert.NoError(t, err) + assert.NotNil(t, conn) + defer func() { + err := conn.Close() + if err != nil { + t.Errorf("error closing the connection: error %v", err) + } + }() + + assert.Equal(t, "unix", conn.RemoteAddr().Network()) + assert.Equal(t, "file.sock", conn.RemoteAddr().String()) +} + +func Test_Config_DialerError(t *testing.T) { + cfg := &rpc.Config{Listen: "uni:unix.sock"} + ln, err := cfg.Dialer() + assert.Nil(t, ln) + assert.Error(t, err) + assert.Equal(t, "invalid socket DSN (tcp://:6001, unix://file.sock)", err.Error()) +} + +func Test_Config_DialerErrorMethod(t *testing.T) { + cfg := &rpc.Config{Listen: "xinu://unix.sock"} + + ln, err := cfg.Dialer() + assert.Nil(t, ln) + assert.Error(t, err) +} + +func Test_Config_Defaults(t *testing.T) { + c := &rpc.Config{} + c.InitDefaults() + assert.Equal(t, "tcp://127.0.0.1:6001", c.Listen) +} diff --git a/tests/plugins/rpc/configs/.rr-rpc-disabled.yaml b/tests/plugins/rpc/configs/.rr-rpc-disabled.yaml new file mode 100644 index 00000000..d5c185e7 --- /dev/null +++ b/tests/plugins/rpc/configs/.rr-rpc-disabled.yaml @@ -0,0 +1,6 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: true +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/rpc/configs/.rr.yaml b/tests/plugins/rpc/configs/.rr.yaml new file mode 100644 index 00000000..d2cb6c70 --- /dev/null +++ b/tests/plugins/rpc/configs/.rr.yaml @@ -0,0 +1,6 @@ +rpc: + listen: tcp://127.0.0.1:6001 + disabled: false +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/rpc/plugin1.go b/tests/plugins/rpc/plugin1.go new file mode 100644 index 00000000..6843b396 --- /dev/null +++ b/tests/plugins/rpc/plugin1.go @@ -0,0 +1,42 @@ +package rpc + +import ( + "fmt" + + "github.com/spiral/roadrunner/v2/plugins/config" +) + +type Plugin1 struct { + config config.Configurer +} + +func (p1 *Plugin1) Init(cfg config.Configurer) error { + p1.config = cfg + return nil +} + +func (p1 *Plugin1) Serve() chan error { + errCh := make(chan error, 1) + return errCh +} + +func (p1 *Plugin1) Stop() error { + return nil +} + +func (p1 *Plugin1) Name() string { + return "rpc_test.plugin1" +} + +func (p1 *Plugin1) RPC() interface{} { + return &PluginRPC{srv: p1} +} + +type PluginRPC struct { + srv *Plugin1 +} + +func (r *PluginRPC) Hello(in string, out *string) error { + *out = fmt.Sprintf("Hello, username: %s", in) + return nil +} diff --git a/tests/plugins/rpc/plugin2.go b/tests/plugins/rpc/plugin2.go new file mode 100644 index 00000000..2c47158f --- /dev/null +++ b/tests/plugins/rpc/plugin2.go @@ -0,0 +1,53 @@ +package rpc + +import ( + "net" + "net/rpc" + "time" + + "github.com/spiral/errors" + goridgeRpc "github.com/spiral/goridge/v3/pkg/rpc" +) + +// plugin2 makes a call to the plugin1 via RPC +// this is just a simulation of external call FOR TEST +// you don't need to do such things :) +type Plugin2 struct { +} + +func (p2 *Plugin2) Init() error { + return nil +} + +func (p2 *Plugin2) Serve() chan error { + errCh := make(chan error, 1) + + go func() { + time.Sleep(time.Second * 3) + + conn, err := net.Dial("tcp", "127.0.0.1:6001") + if err != nil { + errCh <- errors.E(errors.Serve, err) + return + } + client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) + var ret string + err = client.Call("rpc_test.plugin1.Hello", "Valery", &ret) + if err != nil { + errCh <- err + return + } + if ret != "Hello, username: Valery" { + errCh <- errors.E("wrong response") + return + } + // to stop exec + errCh <- errors.E(errors.Disabled) + }() + + return errCh +} + +func (p2 *Plugin2) Stop() error { + return nil +} diff --git a/tests/plugins/rpc/rpc_test.go b/tests/plugins/rpc/rpc_test.go new file mode 100644 index 00000000..98959b28 --- /dev/null +++ b/tests/plugins/rpc/rpc_test.go @@ -0,0 +1,188 @@ +package rpc + +import ( + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/spiral/endure" + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/rpc" + "github.com/stretchr/testify/assert" +) + +// graph https://bit.ly/3ensdNb +func TestRpcInit(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin1{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin2{}) + if err != nil { + t.Fatal(err) + } + + v := &config.Viper{} + v.Path = "configs/.rr.yaml" + v.Prefix = "rr" + err = cont.Register(v) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&rpc.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + if err != nil { + t.Fatal(err) + } + + sig := make(chan os.Signal, 1) + + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + wg := &sync.WaitGroup{} + wg.Add(1) + + tt := time.NewTimer(time.Second * 10) + + go func() { + defer wg.Done() + defer tt.Stop() + for { + select { + case e := <-ch: + // just stop, this is ok + if errors.Is(errors.Disabled, e.Error) { + return + } + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + assert.Fail(t, "timeout") + } + } + }() + + wg.Wait() +} + +// graph https://bit.ly/3ensdNb +func TestRpcDisabled(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin1{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&Plugin2{}) + if err != nil { + t.Fatal(err) + } + + v := &config.Viper{} + v.Path = "configs/.rr-rpc-disabled.yaml" + v.Prefix = "rr" + err = cont.Register(v) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&rpc.Plugin{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Register(&logger.ZapLogger{}) + if err != nil { + t.Fatal(err) + } + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + if err != nil { + t.Fatal(err) + } + + sig := make(chan os.Signal, 1) + + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + tt := time.NewTimer(time.Second * 20) + + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + defer tt.Stop() + for { + select { + case e := <-ch: + // RPC is turned off, should be and dial error + if errors.Is(errors.Disabled, e.Error) { + assert.FailNow(t, "should not be disabled error") + } + assert.Error(t, e.Error) + err = cont.Stop() + assert.Error(t, err) + return + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-tt.C: + // timeout + return + } + } + }() + + wg.Wait() +} diff --git a/tests/plugins/server/plugin_pipes.go b/tests/plugins/server/plugin_pipes.go index 6f31395a..5eb2fed1 100644 --- a/tests/plugins/server/plugin_pipes.go +++ b/tests/plugins/server/plugin_pipes.go @@ -5,12 +5,12 @@ import ( "time" "github.com/spiral/errors" - "github.com/spiral/roadrunner-plugins/config" "github.com/spiral/roadrunner/v2/interfaces/pool" "github.com/spiral/roadrunner/v2/pkg/payload" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" "github.com/spiral/roadrunner/v2/pkg/worker" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" ) const ConfigSection = "server" diff --git a/tests/plugins/server/plugin_sockets.go b/tests/plugins/server/plugin_sockets.go index 1820b4c1..ede67ded 100644 --- a/tests/plugins/server/plugin_sockets.go +++ b/tests/plugins/server/plugin_sockets.go @@ -4,11 +4,11 @@ import ( "context" "github.com/spiral/errors" - "github.com/spiral/roadrunner-plugins/config" "github.com/spiral/roadrunner/v2/interfaces/pool" "github.com/spiral/roadrunner/v2/pkg/payload" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" "github.com/spiral/roadrunner/v2/pkg/worker" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" ) type Foo2 struct { diff --git a/tests/plugins/server/plugin_tcp.go b/tests/plugins/server/plugin_tcp.go index 01fa0cf5..98c13b2b 100644 --- a/tests/plugins/server/plugin_tcp.go +++ b/tests/plugins/server/plugin_tcp.go @@ -4,11 +4,11 @@ import ( "context" "github.com/spiral/errors" - "github.com/spiral/roadrunner-plugins/config" "github.com/spiral/roadrunner/v2/interfaces/pool" "github.com/spiral/roadrunner/v2/pkg/payload" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" "github.com/spiral/roadrunner/v2/pkg/worker" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/server" ) type Foo3 struct { diff --git a/tests/plugins/server/server_test.go b/tests/plugins/server/server_plugin_test.go index 75111ef7..d63b0ccd 100644 --- a/tests/plugins/server/server_test.go +++ b/tests/plugins/server/server_plugin_test.go @@ -3,13 +3,14 @@ package server import ( "os" "os/signal" + "sync" "testing" "time" "github.com/spiral/endure" - "github.com/spiral/roadrunner-plugins/config" - "github.com/spiral/roadrunner-plugins/logger" - "github.com/spiral/roadrunner/v2/pkg/plugins/server" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" "github.com/stretchr/testify/assert" ) @@ -56,27 +57,31 @@ func TestAppPipes(t *testing.T) { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) - // stop after 10 seconds - tt := time.NewTicker(time.Second * 10) - - for { - select { - case e := <-errCh: - assert.NoError(t, e.Error) - assert.NoError(t, container.Stop()) - return - case <-c: - er := container.Stop() - if er != nil { - panic(er) + tt := time.NewTimer(time.Second * 10) + wg := &sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + defer tt.Stop() + for { + select { + case e := <-errCh: + assert.NoError(t, e.Error) + assert.NoError(t, container.Stop()) + return + case <-c: + er := container.Stop() + assert.NoError(t, er) + return + case <-tt.C: + assert.NoError(t, container.Stop()) + return } - return - case <-tt.C: - tt.Stop() - assert.NoError(t, container.Stop()) - return } - } + }() + + wg.Wait() } func TestAppSockets(t *testing.T) { diff --git a/tests/plugins/static/config_test.go b/tests/plugins/static/config_test.go new file mode 100644 index 00000000..f458eed3 --- /dev/null +++ b/tests/plugins/static/config_test.go @@ -0,0 +1,49 @@ +package static + +import ( + "testing" + + "github.com/spiral/roadrunner/v2/plugins/static" + "github.com/stretchr/testify/assert" +) + +func TestConfig_Forbids(t *testing.T) { + cfg := static.Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "", Forbid: []string{".php"}, Always: nil, Request: nil, Response: nil}} + + assert.True(t, cfg.AlwaysForbid("index.php")) + assert.True(t, cfg.AlwaysForbid("index.PHP")) + assert.True(t, cfg.AlwaysForbid("phpadmin/index.bak.php")) + assert.False(t, cfg.AlwaysForbid("index.html")) +} + +func TestConfig_Valid(t *testing.T) { + assert.NoError(t, (&static.Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "./"}}).Valid()) + + assert.Error(t, (&static.Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "./config.go"}}).Valid()) + + assert.Error(t, (&static.Config{Static: struct { + Dir string + Forbid []string + Always []string + Request map[string]string + Response map[string]string + }{Dir: "./dir/"}}).Valid()) +} diff --git a/tests/plugins/static/configs/.rr-http-static-disabled.yaml b/tests/plugins/static/configs/.rr-http-static-disabled.yaml new file mode 100644 index 00000000..d8ee15e0 --- /dev/null +++ b/tests/plugins/static/configs/.rr-http-static-disabled.yaml @@ -0,0 +1,33 @@ +server: + command: "php ../../http/client.php pid pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:21234 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "abc" #not exists + forbid: [ ".php", ".htaccess" ] + request: + "Example-Request-Header": "Value" + # Automatically add headers to every response. + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/static/configs/.rr-http-static-files-disable.yaml b/tests/plugins/static/configs/.rr-http-static-files-disable.yaml new file mode 100644 index 00000000..563d95cf --- /dev/null +++ b/tests/plugins/static/configs/.rr-http-static-files-disable.yaml @@ -0,0 +1,33 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:45877 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../tests" + forbid: [ ".php" ] + request: + "Example-Request-Header": "Value" + # Automatically add headers to every response. + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/static/configs/.rr-http-static-files.yaml b/tests/plugins/static/configs/.rr-http-static-files.yaml new file mode 100644 index 00000000..8961c6f4 --- /dev/null +++ b/tests/plugins/static/configs/.rr-http-static-files.yaml @@ -0,0 +1,34 @@ +server: + command: "php ../../http/client.php echo pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + debug: true + address: 127.0.0.1:34653 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../tests" + forbid: [ ".php", ".htaccess" ] + always: [ ".ico" ] + request: + "Example-Request-Header": "Value" + # Automatically add headers to every response. + response: + "X-Powered-By": "RoadRunner" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/static/configs/.rr-http-static.yaml b/tests/plugins/static/configs/.rr-http-static.yaml new file mode 100644 index 00000000..0a1f5df4 --- /dev/null +++ b/tests/plugins/static/configs/.rr-http-static.yaml @@ -0,0 +1,31 @@ +server: + command: "php ../../http/client.php pid pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relayTimeout: "20s" + +http: + address: 127.0.0.1:21603 + maxRequestSize: 1024 + middleware: [ "gzip", "static" ] + trustedSubnets: [ "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "::1/128", "fc00::/7", "fe80::/10" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../tests" + forbid: [ "" ] + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + numWorkers: 2 + maxJobs: 0 + allocateTimeout: 60s + destroyTimeout: 60s +logs: + mode: development + level: error
\ No newline at end of file diff --git a/tests/plugins/static/static_plugin_test.go b/tests/plugins/static/static_plugin_test.go new file mode 100644 index 00000000..74daaa16 --- /dev/null +++ b/tests/plugins/static/static_plugin_test.go @@ -0,0 +1,437 @@ +package static + +import ( + "bytes" + "io" + "io/ioutil" + "net/http" + "os" + "os/signal" + "sync" + "syscall" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/spiral/endure" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/gzip" + httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/plugins/server" + "github.com/spiral/roadrunner/v2/plugins/static" + "github.com/spiral/roadrunner/v2/tests/mocks" + "github.com/stretchr/testify/assert" +) + +func TestStaticPlugin(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("ServeSample", serveStaticSample) + t.Run("StaticNotForbid", staticNotForbid) + t.Run("StaticHeaders", staticHeaders) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticHeaders(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:21603/client.php", nil) + if err != nil { + t.Fatal(err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + + if resp.Header.Get("Output") != "output-header" { + t.Fatal("can't find output header in response") + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + + defer func() { + _ = resp.Body.Close() + }() + + assert.Equal(t, all("../../../tests/client.php"), string(b)) + assert.Equal(t, all("../../../tests/client.php"), string(b)) +} + +func staticNotForbid(t *testing.T) { + b, r, err := get("http://localhost:21603/client.php") + assert.NoError(t, err) + assert.Equal(t, all("../../../tests/client.php"), b) + assert.Equal(t, all("../../../tests/client.php"), b) + _ = r.Body.Close() +} + +func serveStaticSample(t *testing.T) { + b, r, err := get("http://localhost:21603/sample.txt") + assert.NoError(t, err) + assert.Equal(t, "sample", b) + _ = r.Body.Close() +} + +func TestStaticDisabled(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-disabled.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticDisabled", staticDisabled) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticDisabled(t *testing.T) { + _, r, err := get("http://localhost:21234/sample.txt") //nolint:bodyclose + assert.Error(t, err) + assert.Nil(t, r) +} + +func TestStaticFilesDisabled(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-files-disable.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticFilesDisabled", staticFilesDisabled) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticFilesDisabled(t *testing.T) { + b, r, err := get("http://localhost:45877/client.php?hello=world") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "WORLD", b) + _ = r.Body.Close() +} + +func TestStaticFilesForbid(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-files.yaml", + Prefix: "rr", + } + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Info("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("http handler response received", "elapsed", gomock.Any(), "remote address", "127.0.0.1").AnyTimes() + mockLogger.EXPECT().Error("file open error", "error", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Info(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Gzip{}, + &static.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticTestFilesDir", staticTestFilesDir) + t.Run("StaticNotFound", staticNotFound) + t.Run("StaticFilesForbid", staticFilesForbid) + t.Run("StaticFilesAlways", staticFilesAlways) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticTestFilesDir(t *testing.T) { + b, r, err := get("http://localhost:34653/http?hello=world") + assert.NoError(t, err) + assert.Equal(t, "WORLD", b) + _ = r.Body.Close() +} + +func staticNotFound(t *testing.T) { + b, _, _ := get("http://localhost:34653/client.XXX?hello=world") //nolint:bodyclose + assert.Equal(t, "WORLD", b) +} + +func staticFilesAlways(t *testing.T) { + _, r, err := get("http://localhost:34653/favicon.ico") + assert.NoError(t, err) + assert.Equal(t, 404, r.StatusCode) + _ = r.Body.Close() +} + +func staticFilesForbid(t *testing.T) { + b, r, err := get("http://localhost:34653/client.php?hello=world") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "WORLD", b) + _ = r.Body.Close() +} + +// HELPERS +func get(url string) (string, *http.Response, error) { + r, err := http.Get(url) //nolint:gosec + if err != nil { + return "", nil, err + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", nil, err + } + + err = r.Body.Close() + if err != nil { + return "", nil, err + } + + return string(b), r, err +} + +func all(fn string) string { + f, _ := os.Open(fn) + + b := new(bytes.Buffer) + _, err := io.Copy(b, f) + if err != nil { + return "" + } + + err = f.Close() + if err != nil { + return "" + } + + return b.String() +} |