package http_test import ( "bytes" "context" "fmt" "io" "io/ioutil" "mime/multipart" "net/http" "net/url" "strings" "sync" "testing" httpcli "go.unistack.org/micro-client-http/v3" jsoncodec "go.unistack.org/micro-codec-json/v3" jsonpbcodec "go.unistack.org/micro-codec-jsonpb/v3" urlencodecodec "go.unistack.org/micro-codec-urlencode/v3" xmlcodec "go.unistack.org/micro-codec-xml/v3" vmeter "go.unistack.org/micro-meter-victoriametrics/v3" httpsrv "go.unistack.org/micro-server-http/v3" pb "go.unistack.org/micro-tests/server/http/proto" "go.unistack.org/micro/v3/client" "go.unistack.org/micro/v3/logger" lwrapper "go.unistack.org/micro/v3/logger/wrapper" "go.unistack.org/micro/v3/metadata" handler "go.unistack.org/micro/v3/meter/handler" mwrapper "go.unistack.org/micro/v3/meter/wrapper" "go.unistack.org/micro/v3/register" "go.unistack.org/micro/v3/server" wrapperspb "google.golang.org/protobuf/types/known/wrapperspb" ) type Handler struct { t *testing.T } func multipartHandler(w http.ResponseWriter, r *http.Request) { // fmt.Printf("%#+v\n", r) } func upload(client *http.Client, url string, values map[string]io.Reader) error { var err error b := bytes.NewBuffer(nil) w := multipart.NewWriter(b) for key, r := range values { var fw io.Writer if x, ok := r.(io.Closer); ok { defer x.Close() } if fw, err = w.CreateFormFile(key, key); err != nil { return err } if _, err = io.Copy(fw, r); err != nil { return err } } // Don't forget to close the multipart writer. // If you don't close it, your request will be missing the terminating boundary. w.Close() // Now that you have a form, you can submit it to your handler. req, err := http.NewRequest("POST", url, b) if err != nil { return err } // Don't forget to set the content type, this will contain the boundary. req.Header.Set("Content-Type", w.FormDataContentType()) // Submit the request res, err := client.Do(req) if err != nil { return err } // Check the response if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusCreated { err = fmt.Errorf("bad status: %s", res.Status) } return err } func TestMultipart(t *testing.T) { reg := register.NewRegister() ctx := context.Background() // create server srv := httpsrv.NewServer( server.Address("127.0.0.1:0"), server.Name("helloworld"), server.Register(reg), server.Codec("application/json", jsoncodec.NewCodec()), server.Codec("application/x-www-form-urlencoded", urlencodecodec.NewCodec()), httpsrv.PathHandler(http.MethodPost, "/upload", multipartHandler), ) if err := srv.Init(); err != nil { t.Fatal(err) } h := &Handler{t: t} if err := pb.RegisterTestServer(srv, h); err != nil { t.Fatal(err) } // start server if err := srv.Start(); err != nil { t.Fatal(err) } // lookup server service, err := reg.LookupService(ctx, "helloworld") if err != nil { t.Fatal(err) } if len(service) != 1 { t.Fatalf("Expected 1 service got %d: %+v", len(service), service) } if len(service[0].Nodes) != 1 { t.Fatalf("Expected 1 node got %d: %+v", len(service[0].Nodes), service[0].Nodes) } // t.Logf("test multipart upload") values := make(map[string]io.Reader, 2) values["first.txt"] = bytes.NewReader([]byte("first content")) values["second.txt"] = bytes.NewReader([]byte("second content")) err = upload(http.DefaultClient, "http://"+service[0].Nodes[0].Address+"/upload", values) if err != nil { t.Fatal(err) } } func NewServerHandlerWrapper(t *testing.T) server.HandlerWrapper { return func(fn server.HandlerFunc) server.HandlerFunc { return func(ctx context.Context, req server.Request, rsp interface{}) error { md, ok := metadata.FromIncomingContext(ctx) if !ok { t.Fatal("metadata empty") } if v, ok := md.Get("Authorization"); ok && v == "test" { nmd := metadata.New(1) nmd.Set("my-key", "my-val") nmd.Set("Content-Type", "text/xml") metadata.SetOutgoingContext(ctx, nmd) httpsrv.SetRspCode(ctx, http.StatusUnauthorized) return httpsrv.SetError(&pb.CallRsp{Rsp: "name_my_name"}) } if v, ok := md.Get("Test-Content-Type"); ok && v != "" { nmd := metadata.New(1) nmd.Set("my-key", "my-val") nmd.Set("Content-Type", v) metadata.SetOutgoingContext(ctx, nmd) } return fn(ctx, req, rsp) } } } func (h *Handler) CallDouble(ctx context.Context, req *pb.CallReq, rsp *pb.CallRsp) error { rsp.Rsp = "name_double" httpsrv.SetRspCode(ctx, http.StatusCreated) return nil } func (h *Handler) CallRepeatedString(ctx context.Context, req *pb.CallReq, rsp *pb.CallRsp) error { if len(req.StringIds) != 2 || req.StringIds[0] != "123" { h.t.Fatalf("invalid reflect merging, strings_ids invalid: %v", req.StringIds) } rsp.Rsp = "name_my_name" httpsrv.SetRspCode(ctx, http.StatusCreated) return nil } func (h *Handler) CallRepeatedInt64(ctx context.Context, req *pb.CallReq, rsp *pb.CallRsp) error { if len(req.Int64Ids) != 2 || req.Int64Ids[0] != 123 { h.t.Fatalf("invalid reflect merging, int64_ids invalid: %v", req.Int64Ids) } rsp.Rsp = "name_my_name" httpsrv.SetRspCode(ctx, http.StatusCreated) return nil } func (h *Handler) Call(ctx context.Context, req *pb.CallReq, rsp *pb.CallRsp) error { if req.Nested == nil { h.t.Fatalf("invalid reflect merging") } if len(req.Nested.Uint64Args) != 3 || req.Nested.Uint64Args[2].Value != 3 { h.t.Fatalf("invalid reflect merging") } md, ok := metadata.FromIncomingContext(ctx) if !ok { h.t.Fatalf("context without metadata") } if _, ok := md.Get("User-Agent"); !ok { h.t.Fatalf("context metadata does not have User-Agent header") } if req.Name != "my_name" { h.t.Fatalf("invalid req received: %#+v", req) } if req.Clientid != "1234567890" { h.t.Fatalf("invalid req recevided %#+v", req) } rsp.Rsp = "name_my_name" httpsrv.SetRspCode(ctx, http.StatusCreated) md = metadata.New(1) md.Set("my-key", "my-val") metadata.SetOutgoingContext(ctx, md) return nil } func (h *Handler) CallError(ctx context.Context, req *pb.CallReq1, rsp *pb.CallRsp1) error { httpsrv.SetRspCode(ctx, http.StatusBadRequest) return httpsrv.SetError(&pb.Error{Msg: "my_error"}) } func TestNativeFormUrlencoded(t *testing.T) { reg := register.NewRegister() ctx := context.Background() // create server srv := httpsrv.NewServer( server.Address("127.0.0.1:0"), server.Name("helloworld"), server.Register(reg), server.Codec("application/json", jsoncodec.NewCodec()), server.Codec("application/x-www-form-urlencoded", urlencodecodec.NewCodec()), // server.WrapHandler(NewServerHandlerWrapper()), ) if err := srv.Init(); err != nil { t.Fatal(err) } h := &Handler{t: t} if err := pb.RegisterTestServer(srv, h); err != nil { t.Fatal(err) } // start server if err := srv.Start(); err != nil { t.Fatal(err) } // lookup server service, err := reg.LookupService(ctx, "helloworld") if err != nil { t.Fatal(err) } if len(service) != 1 { t.Fatalf("Expected 1 service got %d: %+v", len(service), service) } if len(service[0].Nodes) != 1 { t.Fatalf("Expected 1 node got %d: %+v", len(service[0].Nodes), service[0].Nodes) } data := url.Values{} data.Set("req", "fookey") data.Set("arg1", "arg1val") data.Add("nested.uint64_args", "1") data.Add("nested.uint64_args", "2") data.Add("nested.uint64_args", "3") // make request req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s/v1/test/call/my_name", service[0].Nodes[0].Address), strings.NewReader(data.Encode())) // URL-encoded payload req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Clientid", "1234567890") req.AddCookie(&http.Cookie{Name: "Csrftoken", Value: "csrftoken"}) // req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode()))) if err != nil { t.Fatalf("test net/http client with application/x-www-form-urlencoded err: %v", err) } rsp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("test net/http client with application/x-www-form-urlencoded err: %v", err) } b, err := ioutil.ReadAll(rsp.Body) rsp.Body.Close() if err != nil && err != io.EOF { t.Fatal(err) } if rsp.StatusCode != http.StatusCreated { t.Fatalf("invalid status received: %#+v\n%s\n", rsp, b) } if s := string(b); s != `{"rsp":"name_my_name"}` { t.Fatalf("Expected response %s, got %s", `{"rsp":"name_my_name"}`, s) } if v := rsp.Header.Get("My-Key"); v != "my-val" { t.Fatalf("empty response header: %#+v", rsp.Header) } cli := client.NewClientCallOptions( httpcli.NewClient( client.ContentType("application/x-www-form-urlencoded"), client.Codec("application/json", jsonpbcodec.NewCodec()), client.Codec("application/x-www-form-urlencoded", urlencodecodec.NewCodec()), ), client.WithAddress(fmt.Sprintf("http://%s", service[0].Nodes[0].Address))) svc1 := pb.NewTestClient("helloworld", cli) nrsp, err := svc1.Call(ctx, &pb.CallReq{ Name: "my_name", Arg1: "arg1val", Clientid: "1234567890", Csrftoken: "csrftoken", Nested: &pb.Nested{Uint64Args: []*wrapperspb.UInt64Value{ &wrapperspb.UInt64Value{Value: 1}, &wrapperspb.UInt64Value{Value: 2}, &wrapperspb.UInt64Value{Value: 3}, }}, }) if err != nil { t.Fatalf("test native client with application/x-www-form-urlencoded err: %v", err) } if nrsp.Rsp != "name_my_name" { t.Fatalf("invalid response: %#+v\n", nrsp) } // stop server if err := srv.Stop(); err != nil { t.Fatal(err) } } func TestNativeClientServer(t *testing.T) { reg := register.NewRegister() ctx := context.Background() var mwfOk bool mwf := func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mwfOk = true next.ServeHTTP(w, r) }) } m := vmeter.NewMeter() // create server srv := httpsrv.NewServer( server.Address("127.0.0.1:0"), server.Meter(m), server.Name("helloworld"), server.Register(reg), server.Codec("application/json", jsonpbcodec.NewCodec()), server.Codec("application/x-www-form-urlencoded", urlencodecodec.NewCodec()), server.WrapHandler(mwrapper.NewHandlerWrapper(mwrapper.Meter(m))), server.WrapHandler(lwrapper.NewServerHandlerWrapper(lwrapper.WithEnabled(false), lwrapper.WithLevel(logger.ErrorLevel))), httpsrv.Middleware(mwf), server.WrapHandler(NewServerHandlerWrapper(t)), ) h := &Handler{t: t} // init server if err := srv.Init(); err != nil { t.Fatal(err) } if err := pb.RegisterTestServer(srv, h); err != nil { t.Fatal(err) } if err := pb.RegisterTestDoubleServer(srv, h); err != nil { t.Fatal(err) } if err := handler.RegisterMeterServer(srv, handler.NewHandler(handler.Meter(srv.Options().Meter))); err != nil { t.Fatal(err) } // start server if err := srv.Start(); err != nil { t.Fatal(err) } // lookup server service, err := reg.LookupService(ctx, "helloworld") if err != nil { t.Fatal(err) } if len(service) != 1 { t.Fatalf("Expected 1 service got %d: %+v", len(service), service) } if len(service[0].Nodes) != 1 { t.Fatalf("Expected 1 node got %d: %+v", len(service[0].Nodes), service[0].Nodes) } cli := client.NewClientCallOptions(httpcli.NewClient(client.ContentType("application/json"), client.Codec("application/json", jsonpbcodec.NewCodec())), client.WithAddress(fmt.Sprintf("http://%s", service[0].Nodes[0].Address))) svc1 := pb.NewTestClient("helloworld", cli) rsp, err := svc1.Call(ctx, &pb.CallReq{ Name: "my_name", Clientid: "1234567890", Csrftoken: "csrftoken", Nested: &pb.Nested{Uint64Args: []*wrapperspb.UInt64Value{ &wrapperspb.UInt64Value{Value: 1}, &wrapperspb.UInt64Value{Value: 2}, &wrapperspb.UInt64Value{Value: 3}, }}, }) if err != nil { t.Fatal(err) } if rsp.Rsp != "name_my_name" { t.Fatalf("invalid response: %#+v\n", rsp) } if !mwfOk { t.Fatalf("http middleware not works") } hb, err := jsonpbcodec.NewCodec().Marshal(&pb.CallReq{ Clientid: "1234567890", Csrftoken: "csrftoken", Nested: &pb.Nested{Uint64Args: []*wrapperspb.UInt64Value{ &wrapperspb.UInt64Value{Value: 1}, &wrapperspb.UInt64Value{Value: 2}, &wrapperspb.UInt64Value{Value: 3}, }}, }) if err != nil { t.Fatal(err) } hr, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://%s/v1/test/call/my_name", service[0].Nodes[0].Address), bytes.NewReader(hb)) if err != nil { t.Fatalf("test rsp code from net/http client to native micro http server err: %v", err) } hr.Header.Set("Content-Type", "application/json") hrsp, err := http.DefaultClient.Do(hr) if err != nil { t.Fatalf("test rsp code from net/http client to native micro http server err: %v", err) } defer func() { _ = hrsp.Body.Close() }() _, err = io.ReadAll(hrsp.Body) if err != nil { t.Fatal(err) } if hrsp.StatusCode != 201 { t.Fatalf("invalid rsp code %#+v", hrsp) } svc2 := pb.NewTestDoubleClient("helloworld", cli) rsp, err = svc2.CallDouble(ctx, &pb.CallReq{ Name: "my_name", }) if err != nil { t.Fatalf("test second server err: %v", err) } if rsp.Rsp != "name_double" { t.Fatalf("test second server invalid response: %#+v\n", rsp) } hrsp, err = http.Get(fmt.Sprintf("http://%s/metrics", service[0].Nodes[0].Address)) if err != nil { t.Fatal(err) } buf, err := io.ReadAll(hrsp.Body) if err != nil { t.Fatal(err) } if !strings.Contains(string(buf), `micro_server_request_total`) { t.Fatalf("rsp not contains metrics: %s", buf) } // stop server if err := srv.Stop(); err != nil { t.Fatal(err) } } func TestNativeServer(t *testing.T) { reg := register.NewRegister() ctx := context.Background() // create server srv := httpsrv.NewServer( server.Address("127.0.0.1:0"), server.Name("helloworld"), server.Register(reg), server.Codec("text/xml", xmlcodec.NewCodec()), server.Codec("application/json", jsoncodec.NewCodec()), server.Codec("application/x-www-form-urlencoded", urlencodecodec.NewCodec()), server.WrapHandler(NewServerHandlerWrapper(t)), ) h := &Handler{t: t} if err := pb.RegisterTestServer(srv, h); err != nil { t.Fatal(err) } // start server if err := srv.Start(); err != nil { t.Fatal(err) } // lookup server service, err := reg.LookupService(ctx, "helloworld") if err != nil { t.Fatal(err) } if len(service) != 1 { t.Fatalf("Expected 1 service got %d: %+v", len(service), service) } if len(service[0].Nodes) != 1 { t.Fatalf("Expected 1 node got %d: %+v", len(service[0].Nodes), service[0].Nodes) } // make request req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/v1/test/call/my_name?req=key&arg1=arg1&arg2=12345&nested.string_args=str1&nested.string_args=str2&nested.uint64_args=1&nested.uint64_args=2&nested.uint64_args=3", service[0].Nodes[0].Address), nil) if err != nil { t.Fatal(err) } req.Header.Set("Authorization", "test") req.Header.Set("Content-Type", "application/json") rsp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } if rsp.StatusCode != http.StatusUnauthorized { t.Fatalf("invalid status received: %#+v\n", rsp) } b, err := ioutil.ReadAll(rsp.Body) rsp.Body.Close() if err != nil { t.Fatal(err) } if s := string(b); s != `name_my_name` { t.Fatalf("Expected response %s, got %s", `name_my_name`, s) } if v := rsp.Header.Get("My-Key"); v != "my-val" { t.Fatalf("empty response header: %#+v", rsp.Header) } // make request with error rsp, err = http.Post(fmt.Sprintf("http://%s/v1/test/callerror/my_name", service[0].Nodes[0].Address), "application/json", nil) if err != nil { t.Fatal(err) } if rsp.StatusCode != http.StatusBadRequest { t.Fatalf("invalid status received: %#+v\n", rsp) } b, err = ioutil.ReadAll(rsp.Body) rsp.Body.Close() if err != nil { t.Fatal(err) } if s := string(b); s != `{"msg":"my_error"}` { t.Fatalf("Expected response %s, got %s", `{"msg":"my_error"}`, s) } rsp, err = http.Post(fmt.Sprintf("http://%s/v1/test/call_repeated_string?string_ids=123&string_ids=321", service[0].Nodes[0].Address), "application/json", nil) if err != nil { t.Fatal(err) } if rsp.StatusCode != http.StatusCreated { buf, err := io.ReadAll(rsp.Body) if err != nil { t.Fatalf("invalid status received: %#+v err: %v\n", rsp, err) } t.Fatalf("invalid status received: %#+v buf: %s\n", rsp, buf) } b, err = ioutil.ReadAll(rsp.Body) rsp.Body.Close() if err != nil { t.Fatal(err) } if s := string(b); s != `{"rsp":"name_my_name"}` { t.Fatalf("Expected response %s, got %s", `{"rsp":"name_my_name"}`, s) } c := client.NewClientCallOptions(httpcli.NewClient(client.ContentType("application/json"), client.Codec("application/json", jsoncodec.NewCodec())), client.WithAddress("http://"+service[0].Nodes[0].Address)) pbc := pb.NewTestClient("test", c) prsp, err := pbc.CallRepeatedString(context.TODO(), &pb.CallReq{StringIds: []string{"123", "321"}}) if err != nil { t.Fatalf("test with string_ids err: %v", err) } if prsp.Rsp != "name_my_name" { t.Fatalf("invalid rsp received: %#+v\n", rsp) } prsp, err = pbc.CallRepeatedInt64(context.TODO(), &pb.CallReq{Int64Ids: []int64{123, 321}}) if err != nil { t.Fatalf("test with int64_ids err: %v", err) } if prsp.Rsp != "name_my_name" { t.Fatalf("invalid rsp received: %#+v\n", rsp) } // Test-Content-Type // stop server if err := srv.Stop(); err != nil { t.Fatal(err) } } func TestHTTPHandler(t *testing.T) { reg := register.NewRegister() ctx := context.Background() // create server srv := httpsrv.NewServer( server.Address("127.0.0.1:0"), server.Register(reg), server.Codec("application/json", jsoncodec.NewCodec()), server.Codec("application/x-www-form-urlencoded", urlencodecodec.NewCodec()), ) // create server mux mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`hello world`)) }) // create handler hd := srv.NewHandler(mux) // register handler if err := srv.Handle(hd); err != nil { t.Fatal(err) } // start server if err := srv.Start(); err != nil { t.Fatal(err) } // lookup server service, err := reg.LookupService(ctx, server.DefaultName) if err != nil { t.Fatal(err) } if len(service) != 1 { t.Fatalf("Expected 1 service got %d: %+v", len(service), service) } if len(service[0].Nodes) != 1 { t.Fatalf("Expected 1 node got %d: %+v", len(service[0].Nodes), service[0].Nodes) } // make request rsp, err := http.Get(fmt.Sprintf("http://%s", service[0].Nodes[0].Address)) if err != nil { t.Fatal(err) } defer rsp.Body.Close() b, err := ioutil.ReadAll(rsp.Body) if err != nil { t.Fatal(err) } if s := string(b); s != "hello world" { t.Fatalf("Expected response %s, got %s", "hello world", s) } // stop server if err := srv.Stop(); err != nil { t.Fatal(err) } } type handlerSwapper struct { mu sync.RWMutex handler http.Handler } func (h *handlerSwapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mu.RLock() handler := h.handler h.mu.RUnlock() handler.ServeHTTP(w, r) } func TestHTTPServer(t *testing.T) { reg := register.NewRegister() ctx := context.Background() // create server mux mux1 := http.NewServeMux() mux1.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`hello world`)) }) mux2 := http.NewServeMux() mux2.HandleFunc("/second", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`hello world`)) }) h := &handlerSwapper{handler: mux1} // create server srv := httpsrv.NewServer( server.Address("127.0.0.1:0"), server.Register(reg), httpsrv.Server(&http.Server{Handler: h}), server.Codec("application/json", jsoncodec.NewCodec()), ) if err := srv.Init(); err != nil { t.Fatal(err) } // start server if err := srv.Start(); err != nil { t.Fatal(err) } // lookup server service, err := reg.LookupService(ctx, server.DefaultName) if err != nil { t.Fatal(err) } if len(service) != 1 { t.Fatalf("Expected 1 service got %d: %+v", len(service), service) } if len(service[0].Nodes) != 1 { t.Fatalf("Expected 1 node got %d: %+v", len(service[0].Nodes), service[0].Nodes) } // make request rsp, err := http.Get(fmt.Sprintf("http://%s/first", service[0].Nodes[0].Address)) if err != nil { t.Fatal(err) } defer rsp.Body.Close() b, err := ioutil.ReadAll(rsp.Body) if err != nil { t.Fatal(err) } if s := string(b); s != "hello world" { t.Fatalf("Expected response %s, got %s", "hello world", s) } rsp, err = http.Get(fmt.Sprintf("http://%s/second", service[0].Nodes[0].Address)) if err != nil { t.Fatal(err) } if rsp.StatusCode != 404 { t.Fatal("second route must not exists") } h.mu.Lock() h.handler = mux2 h.mu.Unlock() rsp, err = http.Get(fmt.Sprintf("http://%s/first", service[0].Nodes[0].Address)) if err != nil { t.Fatal(err) } defer rsp.Body.Close() if rsp.StatusCode != 404 { t.Fatal("first route must not exists") } rsp, err = http.Get(fmt.Sprintf("http://%s/second", service[0].Nodes[0].Address)) if err != nil { t.Fatal(err) } defer rsp.Body.Close() b, err = ioutil.ReadAll(rsp.Body) if err != nil { t.Fatal(err) } if s := string(b); s != "hello world" { t.Fatalf("Expected response %s, got %s", "hello world", s) } // stop server if err := srv.Stop(); err != nil { t.Fatal(err) } }