mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Replace GraphQL server with Connect RPC
This commit is contained in:
@ -2,15 +2,11 @@ package sender
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/oklog/ulid"
|
||||
|
||||
"github.com/dstotijn/hetty/pkg/scope"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
FindSenderRequestByID(ctx context.Context, projectID, id ulid.ULID) (Request, error)
|
||||
FindSenderRequests(ctx context.Context, filter FindRequestsFilter, scope *scope.Scope) ([]Request, error)
|
||||
StoreSenderRequest(ctx context.Context, req Request) error
|
||||
DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error
|
||||
FindSenderRequestByID(ctx context.Context, projectID, id string) (*Request, error)
|
||||
FindSenderRequests(ctx context.Context, projectID string, filterFn func(*Request) (bool, error)) ([]*Request, error)
|
||||
StoreSenderRequest(ctx context.Context, req *Request) error
|
||||
DeleteSenderRequests(ctx context.Context, projectID string) error
|
||||
}
|
||||
|
@ -5,31 +5,32 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/oklog/ulid"
|
||||
"github.com/oklog/ulid/v2"
|
||||
|
||||
"github.com/dstotijn/hetty/pkg/filter"
|
||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||
"github.com/dstotijn/hetty/pkg/http"
|
||||
"github.com/dstotijn/hetty/pkg/scope"
|
||||
)
|
||||
|
||||
var senderReqSearchKeyFns = map[string]func(req Request) string{
|
||||
"req.id": func(req Request) string { return req.ID.String() },
|
||||
"req.proto": func(req Request) string { return req.Proto },
|
||||
"req.url": func(req Request) string {
|
||||
if req.URL == nil {
|
||||
var senderReqSearchKeyFns = map[string]func(req *Request) string{
|
||||
"req.id": func(req *Request) string { return req.Id },
|
||||
"req.proto": func(req *Request) string { return req.GetHttpRequest().GetProtocol().String() },
|
||||
"req.url": func(req *Request) string { return req.GetHttpRequest().GetUrl() },
|
||||
"req.method": func(req *Request) string { return req.GetHttpRequest().GetMethod().String() },
|
||||
"req.body": func(req *Request) string { return string(req.GetHttpRequest().GetBody()) },
|
||||
"req.timestamp": func(req *Request) string {
|
||||
id, err := ulid.Parse(req.Id)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return req.URL.String()
|
||||
return ulid.Time(id.Time()).String()
|
||||
},
|
||||
"req.method": func(req Request) string { return req.Method },
|
||||
"req.body": func(req Request) string { return string(req.Body) },
|
||||
"req.timestamp": func(req Request) string { return ulid.Time(req.ID.Time()).String() },
|
||||
}
|
||||
|
||||
// TODO: Request and response headers search key functions.
|
||||
|
||||
// Matches returns true if the supplied search expression evaluates to true.
|
||||
func (req Request) Matches(expr filter.Expression) (bool, error) {
|
||||
func (req *Request) Matches(expr filter.Expression) (bool, error) {
|
||||
switch e := expr.(type) {
|
||||
case filter.PrefixExpression:
|
||||
return req.matchPrefixExpr(e)
|
||||
@ -42,7 +43,7 @@ func (req Request) Matches(expr filter.Expression) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (req Request) matchPrefixExpr(expr filter.PrefixExpression) (bool, error) {
|
||||
func (req *Request) matchPrefixExpr(expr filter.PrefixExpression) (bool, error) {
|
||||
switch expr.Operator {
|
||||
case filter.TokOpNot:
|
||||
match, err := req.Matches(expr.Right)
|
||||
@ -56,7 +57,7 @@ func (req Request) matchPrefixExpr(expr filter.PrefixExpression) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (req Request) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
|
||||
func (req *Request) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
|
||||
switch expr.Operator {
|
||||
case filter.TokOpAnd:
|
||||
left, err := req.Matches(expr.Left)
|
||||
@ -92,7 +93,7 @@ func (req Request) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
|
||||
leftVal := req.getMappedStringLiteral(left.Value)
|
||||
|
||||
if leftVal == "req.headers" {
|
||||
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.Header)
|
||||
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.GetHttpRequest().GetHeaders())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
|
||||
}
|
||||
@ -100,8 +101,8 @@ func (req Request) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
|
||||
return match, nil
|
||||
}
|
||||
|
||||
if leftVal == "res.headers" && req.Response != nil {
|
||||
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.Response.Header)
|
||||
if leftVal == "res.headers" && req.GetHttpResponse() != nil {
|
||||
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.GetHttpResponse().GetHeaders())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to match response HTTP headers: %w", err)
|
||||
}
|
||||
@ -152,7 +153,7 @@ func (req Request) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (req Request) getMappedStringLiteral(s string) string {
|
||||
func (req *Request) getMappedStringLiteral(s string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(s, "req."):
|
||||
fn, ok := senderReqSearchKeyFns[s]
|
||||
@ -160,28 +161,22 @@ func (req Request) getMappedStringLiteral(s string) string {
|
||||
return fn(req)
|
||||
}
|
||||
case strings.HasPrefix(s, "res."):
|
||||
if req.Response == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
fn, ok := reqlog.ResLogSearchKeyFns[s]
|
||||
fn, ok := http.ResponseSearchKeyFns[s]
|
||||
if ok {
|
||||
return fn(*req.Response)
|
||||
return fn(req.GetHttpResponse())
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (req Request) matchStringLiteral(strLiteral filter.StringLiteral) (bool, error) {
|
||||
for key, values := range req.Header {
|
||||
for _, value := range values {
|
||||
if strings.Contains(
|
||||
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
|
||||
strings.ToLower(strLiteral.Value),
|
||||
) {
|
||||
return true, nil
|
||||
}
|
||||
func (req *Request) matchStringLiteral(strLiteral filter.StringLiteral) (bool, error) {
|
||||
for _, header := range req.GetHttpRequest().GetHeaders() {
|
||||
if strings.Contains(
|
||||
strings.ToLower(fmt.Sprintf("%v: %v", header.Key, header.Value)),
|
||||
strings.ToLower(strLiteral.Value),
|
||||
) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,54 +189,47 @@ func (req Request) matchStringLiteral(strLiteral filter.StringLiteral) (bool, er
|
||||
}
|
||||
}
|
||||
|
||||
if req.Response != nil {
|
||||
for key, values := range req.Response.Header {
|
||||
for _, value := range values {
|
||||
if strings.Contains(
|
||||
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
|
||||
strings.ToLower(strLiteral.Value),
|
||||
) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
for _, header := range req.GetHttpResponse().GetHeaders() {
|
||||
if strings.Contains(
|
||||
strings.ToLower(fmt.Sprintf("%v: %v", header.Key, header.Value)),
|
||||
strings.ToLower(strLiteral.Value),
|
||||
) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, fn := range reqlog.ResLogSearchKeyFns {
|
||||
if strings.Contains(
|
||||
strings.ToLower(fn(*req.Response)),
|
||||
strings.ToLower(strLiteral.Value),
|
||||
) {
|
||||
return true, nil
|
||||
}
|
||||
for _, fn := range http.ResponseSearchKeyFns {
|
||||
if strings.Contains(
|
||||
strings.ToLower(fn(req.GetHttpResponse())),
|
||||
strings.ToLower(strLiteral.Value),
|
||||
) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (req Request) MatchScope(s *scope.Scope) bool {
|
||||
func (req *Request) MatchScope(s *scope.Scope) bool {
|
||||
for _, rule := range s.Rules() {
|
||||
if rule.URL != nil && req.URL != nil {
|
||||
if matches := rule.URL.MatchString(req.URL.String()); matches {
|
||||
if url := req.GetHttpRequest().GetUrl(); rule.URL != nil && url != "" {
|
||||
if matches := rule.URL.MatchString(url); matches {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for key, values := range req.Header {
|
||||
for _, headers := range req.GetHttpRequest().GetHeaders() {
|
||||
var keyMatches, valueMatches bool
|
||||
|
||||
if rule.Header.Key != nil {
|
||||
if matches := rule.Header.Key.MatchString(key); matches {
|
||||
if matches := rule.Header.Key.MatchString(headers.Key); matches {
|
||||
keyMatches = true
|
||||
}
|
||||
}
|
||||
|
||||
if rule.Header.Value != nil {
|
||||
for _, value := range values {
|
||||
if matches := rule.Header.Value.MatchString(value); matches {
|
||||
valueMatches = true
|
||||
break
|
||||
}
|
||||
if matches := rule.Header.Value.MatchString(headers.Value); matches {
|
||||
valueMatches = true
|
||||
}
|
||||
}
|
||||
// When only key or value is set, match on whatever is set.
|
||||
@ -257,7 +245,7 @@ func (req Request) MatchScope(s *scope.Scope) bool {
|
||||
}
|
||||
|
||||
if rule.Body != nil {
|
||||
if matches := rule.Body.Match(req.Body); matches {
|
||||
if matches := rule.Body.Match(req.GetHttpRequest().GetBody()); matches {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/dstotijn/hetty/pkg/filter"
|
||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||
"github.com/dstotijn/hetty/pkg/http"
|
||||
"github.com/dstotijn/hetty/pkg/sender"
|
||||
)
|
||||
|
||||
@ -14,15 +14,17 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
senderReq sender.Request
|
||||
senderReq *sender.Request
|
||||
expectedMatch bool
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "infix expression, equal operator, match",
|
||||
query: "req.body = foo",
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("foo"),
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("foo"),
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedError: nil,
|
||||
@ -30,8 +32,10 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "infix expression, not equal operator, match",
|
||||
query: "req.body != bar",
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("foo"),
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("foo"),
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedError: nil,
|
||||
@ -39,8 +43,10 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "infix expression, greater than operator, match",
|
||||
query: "req.body > a",
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("b"),
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("b"),
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedError: nil,
|
||||
@ -48,8 +54,10 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "infix expression, less than operator, match",
|
||||
query: "req.body < b",
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("a"),
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("a"),
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedError: nil,
|
||||
@ -57,8 +65,10 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "infix expression, greater than or equal operator, match greater than",
|
||||
query: "req.body >= a",
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("b"),
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("b"),
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedError: nil,
|
||||
@ -66,8 +76,10 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "infix expression, greater than or equal operator, match equal",
|
||||
query: "req.body >= a",
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("a"),
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("a"),
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedError: nil,
|
||||
@ -75,8 +87,10 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "infix expression, less than or equal operator, match less than",
|
||||
query: "req.body <= b",
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("a"),
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("a"),
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedError: nil,
|
||||
@ -84,8 +98,10 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "infix expression, less than or equal operator, match equal",
|
||||
query: "req.body <= b",
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("b"),
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("b"),
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedError: nil,
|
||||
@ -93,8 +109,10 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "infix expression, regular expression operator, match",
|
||||
query: `req.body =~ "^foo(.*)$"`,
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("foobar"),
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("foobar"),
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedError: nil,
|
||||
@ -102,8 +120,10 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "infix expression, negate regular expression operator, match",
|
||||
query: `req.body !~ "^foo(.*)$"`,
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("xoobar"),
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("xoobar"),
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedError: nil,
|
||||
@ -111,9 +131,11 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "infix expression, and operator, match",
|
||||
query: "req.body = bar AND res.body = yolo",
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("bar"),
|
||||
Response: &reqlog.ResponseLog{
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("bar"),
|
||||
},
|
||||
HttpResponse: &http.Response{
|
||||
Body: []byte("yolo"),
|
||||
},
|
||||
},
|
||||
@ -123,9 +145,11 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "infix expression, or operator, match",
|
||||
query: "req.body = bar OR res.body = yolo",
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("foo"),
|
||||
Response: &reqlog.ResponseLog{
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("foo"),
|
||||
},
|
||||
HttpResponse: &http.Response{
|
||||
Body: []byte("yolo"),
|
||||
},
|
||||
},
|
||||
@ -135,8 +159,10 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "prefix expression, not operator, match",
|
||||
query: "NOT (req.body = bar)",
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("foo"),
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("foo"),
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedError: nil,
|
||||
@ -144,8 +170,10 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "string literal expression, match in request log",
|
||||
query: "foo",
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("foo"),
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("foo"),
|
||||
},
|
||||
},
|
||||
expectedMatch: true,
|
||||
expectedError: nil,
|
||||
@ -153,8 +181,10 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "string literal expression, no match",
|
||||
query: "foo",
|
||||
senderReq: sender.Request{
|
||||
Body: []byte("bar"),
|
||||
senderReq: &sender.Request{
|
||||
HttpRequest: &http.Request{
|
||||
Body: []byte("bar"),
|
||||
},
|
||||
},
|
||||
expectedMatch: false,
|
||||
expectedError: nil,
|
||||
@ -162,8 +192,8 @@ func TestRequestLogMatch(t *testing.T) {
|
||||
{
|
||||
name: "string literal expression, match in response log",
|
||||
query: "foo",
|
||||
senderReq: sender.Request{
|
||||
Response: &reqlog.ResponseLog{
|
||||
senderReq: &sender.Request{
|
||||
HttpResponse: &http.Response{
|
||||
Body: []byte("foo"),
|
||||
},
|
||||
},
|
||||
|
311
pkg/sender/sender.connect.go
Normal file
311
pkg/sender/sender.connect.go
Normal file
@ -0,0 +1,311 @@
|
||||
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
|
||||
//
|
||||
// Source: sender/sender.proto
|
||||
|
||||
package sender
|
||||
|
||||
import (
|
||||
connect "connectrpc.com/connect"
|
||||
context "context"
|
||||
errors "errors"
|
||||
http "net/http"
|
||||
strings "strings"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file and the connect package are
|
||||
// compatible. If you get a compiler error that this constant is not defined, this code was
|
||||
// generated with a version of connect newer than the one compiled into your binary. You can fix the
|
||||
// problem by either regenerating this code with an older version of connect or updating the connect
|
||||
// version compiled into your binary.
|
||||
const _ = connect.IsAtLeastVersion1_13_0
|
||||
|
||||
const (
|
||||
// SenderServiceName is the fully-qualified name of the SenderService service.
|
||||
SenderServiceName = "sender.SenderService"
|
||||
)
|
||||
|
||||
// These constants are the fully-qualified names of the RPCs defined in this package. They're
|
||||
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
|
||||
//
|
||||
// Note that these are different from the fully-qualified method names used by
|
||||
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
|
||||
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
|
||||
// period.
|
||||
const (
|
||||
// SenderServiceGetRequestByIDProcedure is the fully-qualified name of the SenderService's
|
||||
// GetRequestByID RPC.
|
||||
SenderServiceGetRequestByIDProcedure = "/sender.SenderService/GetRequestByID"
|
||||
// SenderServiceListRequestsProcedure is the fully-qualified name of the SenderService's
|
||||
// ListRequests RPC.
|
||||
SenderServiceListRequestsProcedure = "/sender.SenderService/ListRequests"
|
||||
// SenderServiceSetRequestsFilterProcedure is the fully-qualified name of the SenderService's
|
||||
// SetRequestsFilter RPC.
|
||||
SenderServiceSetRequestsFilterProcedure = "/sender.SenderService/SetRequestsFilter"
|
||||
// SenderServiceGetRequestsFilterProcedure is the fully-qualified name of the SenderService's
|
||||
// GetRequestsFilter RPC.
|
||||
SenderServiceGetRequestsFilterProcedure = "/sender.SenderService/GetRequestsFilter"
|
||||
// SenderServiceCreateOrUpdateRequestProcedure is the fully-qualified name of the SenderService's
|
||||
// CreateOrUpdateRequest RPC.
|
||||
SenderServiceCreateOrUpdateRequestProcedure = "/sender.SenderService/CreateOrUpdateRequest"
|
||||
// SenderServiceCloneFromRequestLogProcedure is the fully-qualified name of the SenderService's
|
||||
// CloneFromRequestLog RPC.
|
||||
SenderServiceCloneFromRequestLogProcedure = "/sender.SenderService/CloneFromRequestLog"
|
||||
// SenderServiceSendRequestProcedure is the fully-qualified name of the SenderService's SendRequest
|
||||
// RPC.
|
||||
SenderServiceSendRequestProcedure = "/sender.SenderService/SendRequest"
|
||||
// SenderServiceDeleteRequestsProcedure is the fully-qualified name of the SenderService's
|
||||
// DeleteRequests RPC.
|
||||
SenderServiceDeleteRequestsProcedure = "/sender.SenderService/DeleteRequests"
|
||||
)
|
||||
|
||||
// SenderServiceClient is a client for the sender.SenderService service.
|
||||
type SenderServiceClient interface {
|
||||
GetRequestByID(context.Context, *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error)
|
||||
ListRequests(context.Context, *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error)
|
||||
SetRequestsFilter(context.Context, *connect.Request[SetRequestsFilterRequest]) (*connect.Response[SetRequestsFilterResponse], error)
|
||||
GetRequestsFilter(context.Context, *connect.Request[GetRequestsFilterRequest]) (*connect.Response[GetRequestsFilterResponse], error)
|
||||
CreateOrUpdateRequest(context.Context, *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error)
|
||||
CloneFromRequestLog(context.Context, *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error)
|
||||
SendRequest(context.Context, *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error)
|
||||
DeleteRequests(context.Context, *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error)
|
||||
}
|
||||
|
||||
// NewSenderServiceClient constructs a client for the sender.SenderService service. By default, it
|
||||
// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends
|
||||
// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or
|
||||
// connect.WithGRPCWeb() options.
|
||||
//
|
||||
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
|
||||
// http://api.acme.com or https://acme.com/grpc).
|
||||
func NewSenderServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) SenderServiceClient {
|
||||
baseURL = strings.TrimRight(baseURL, "/")
|
||||
senderServiceMethods := File_sender_sender_proto.Services().ByName("SenderService").Methods()
|
||||
return &senderServiceClient{
|
||||
getRequestByID: connect.NewClient[GetRequestByIDRequest, GetRequestByIDResponse](
|
||||
httpClient,
|
||||
baseURL+SenderServiceGetRequestByIDProcedure,
|
||||
connect.WithSchema(senderServiceMethods.ByName("GetRequestByID")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
listRequests: connect.NewClient[ListRequestsRequest, ListRequestsResponse](
|
||||
httpClient,
|
||||
baseURL+SenderServiceListRequestsProcedure,
|
||||
connect.WithSchema(senderServiceMethods.ByName("ListRequests")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
setRequestsFilter: connect.NewClient[SetRequestsFilterRequest, SetRequestsFilterResponse](
|
||||
httpClient,
|
||||
baseURL+SenderServiceSetRequestsFilterProcedure,
|
||||
connect.WithSchema(senderServiceMethods.ByName("SetRequestsFilter")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
getRequestsFilter: connect.NewClient[GetRequestsFilterRequest, GetRequestsFilterResponse](
|
||||
httpClient,
|
||||
baseURL+SenderServiceGetRequestsFilterProcedure,
|
||||
connect.WithSchema(senderServiceMethods.ByName("GetRequestsFilter")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
createOrUpdateRequest: connect.NewClient[CreateOrUpdateRequestRequest, CreateOrUpdateRequestResponse](
|
||||
httpClient,
|
||||
baseURL+SenderServiceCreateOrUpdateRequestProcedure,
|
||||
connect.WithSchema(senderServiceMethods.ByName("CreateOrUpdateRequest")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
cloneFromRequestLog: connect.NewClient[CloneFromRequestLogRequest, CloneFromRequestLogResponse](
|
||||
httpClient,
|
||||
baseURL+SenderServiceCloneFromRequestLogProcedure,
|
||||
connect.WithSchema(senderServiceMethods.ByName("CloneFromRequestLog")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
sendRequest: connect.NewClient[SendRequestRequest, SendRequestResponse](
|
||||
httpClient,
|
||||
baseURL+SenderServiceSendRequestProcedure,
|
||||
connect.WithSchema(senderServiceMethods.ByName("SendRequest")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
deleteRequests: connect.NewClient[DeleteRequestsRequest, DeleteRequestsResponse](
|
||||
httpClient,
|
||||
baseURL+SenderServiceDeleteRequestsProcedure,
|
||||
connect.WithSchema(senderServiceMethods.ByName("DeleteRequests")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// senderServiceClient implements SenderServiceClient.
|
||||
type senderServiceClient struct {
|
||||
getRequestByID *connect.Client[GetRequestByIDRequest, GetRequestByIDResponse]
|
||||
listRequests *connect.Client[ListRequestsRequest, ListRequestsResponse]
|
||||
setRequestsFilter *connect.Client[SetRequestsFilterRequest, SetRequestsFilterResponse]
|
||||
getRequestsFilter *connect.Client[GetRequestsFilterRequest, GetRequestsFilterResponse]
|
||||
createOrUpdateRequest *connect.Client[CreateOrUpdateRequestRequest, CreateOrUpdateRequestResponse]
|
||||
cloneFromRequestLog *connect.Client[CloneFromRequestLogRequest, CloneFromRequestLogResponse]
|
||||
sendRequest *connect.Client[SendRequestRequest, SendRequestResponse]
|
||||
deleteRequests *connect.Client[DeleteRequestsRequest, DeleteRequestsResponse]
|
||||
}
|
||||
|
||||
// GetRequestByID calls sender.SenderService.GetRequestByID.
|
||||
func (c *senderServiceClient) GetRequestByID(ctx context.Context, req *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error) {
|
||||
return c.getRequestByID.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// ListRequests calls sender.SenderService.ListRequests.
|
||||
func (c *senderServiceClient) ListRequests(ctx context.Context, req *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error) {
|
||||
return c.listRequests.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// SetRequestsFilter calls sender.SenderService.SetRequestsFilter.
|
||||
func (c *senderServiceClient) SetRequestsFilter(ctx context.Context, req *connect.Request[SetRequestsFilterRequest]) (*connect.Response[SetRequestsFilterResponse], error) {
|
||||
return c.setRequestsFilter.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// GetRequestsFilter calls sender.SenderService.GetRequestsFilter.
|
||||
func (c *senderServiceClient) GetRequestsFilter(ctx context.Context, req *connect.Request[GetRequestsFilterRequest]) (*connect.Response[GetRequestsFilterResponse], error) {
|
||||
return c.getRequestsFilter.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// CreateOrUpdateRequest calls sender.SenderService.CreateOrUpdateRequest.
|
||||
func (c *senderServiceClient) CreateOrUpdateRequest(ctx context.Context, req *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error) {
|
||||
return c.createOrUpdateRequest.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// CloneFromRequestLog calls sender.SenderService.CloneFromRequestLog.
|
||||
func (c *senderServiceClient) CloneFromRequestLog(ctx context.Context, req *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error) {
|
||||
return c.cloneFromRequestLog.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// SendRequest calls sender.SenderService.SendRequest.
|
||||
func (c *senderServiceClient) SendRequest(ctx context.Context, req *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error) {
|
||||
return c.sendRequest.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// DeleteRequests calls sender.SenderService.DeleteRequests.
|
||||
func (c *senderServiceClient) DeleteRequests(ctx context.Context, req *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error) {
|
||||
return c.deleteRequests.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// SenderServiceHandler is an implementation of the sender.SenderService service.
|
||||
type SenderServiceHandler interface {
|
||||
GetRequestByID(context.Context, *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error)
|
||||
ListRequests(context.Context, *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error)
|
||||
SetRequestsFilter(context.Context, *connect.Request[SetRequestsFilterRequest]) (*connect.Response[SetRequestsFilterResponse], error)
|
||||
GetRequestsFilter(context.Context, *connect.Request[GetRequestsFilterRequest]) (*connect.Response[GetRequestsFilterResponse], error)
|
||||
CreateOrUpdateRequest(context.Context, *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error)
|
||||
CloneFromRequestLog(context.Context, *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error)
|
||||
SendRequest(context.Context, *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error)
|
||||
DeleteRequests(context.Context, *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error)
|
||||
}
|
||||
|
||||
// NewSenderServiceHandler builds an HTTP handler from the service implementation. It returns the
|
||||
// path on which to mount the handler and the handler itself.
|
||||
//
|
||||
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
|
||||
// and JSON codecs. They also support gzip compression.
|
||||
func NewSenderServiceHandler(svc SenderServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
|
||||
senderServiceMethods := File_sender_sender_proto.Services().ByName("SenderService").Methods()
|
||||
senderServiceGetRequestByIDHandler := connect.NewUnaryHandler(
|
||||
SenderServiceGetRequestByIDProcedure,
|
||||
svc.GetRequestByID,
|
||||
connect.WithSchema(senderServiceMethods.ByName("GetRequestByID")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
senderServiceListRequestsHandler := connect.NewUnaryHandler(
|
||||
SenderServiceListRequestsProcedure,
|
||||
svc.ListRequests,
|
||||
connect.WithSchema(senderServiceMethods.ByName("ListRequests")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
senderServiceSetRequestsFilterHandler := connect.NewUnaryHandler(
|
||||
SenderServiceSetRequestsFilterProcedure,
|
||||
svc.SetRequestsFilter,
|
||||
connect.WithSchema(senderServiceMethods.ByName("SetRequestsFilter")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
senderServiceGetRequestsFilterHandler := connect.NewUnaryHandler(
|
||||
SenderServiceGetRequestsFilterProcedure,
|
||||
svc.GetRequestsFilter,
|
||||
connect.WithSchema(senderServiceMethods.ByName("GetRequestsFilter")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
senderServiceCreateOrUpdateRequestHandler := connect.NewUnaryHandler(
|
||||
SenderServiceCreateOrUpdateRequestProcedure,
|
||||
svc.CreateOrUpdateRequest,
|
||||
connect.WithSchema(senderServiceMethods.ByName("CreateOrUpdateRequest")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
senderServiceCloneFromRequestLogHandler := connect.NewUnaryHandler(
|
||||
SenderServiceCloneFromRequestLogProcedure,
|
||||
svc.CloneFromRequestLog,
|
||||
connect.WithSchema(senderServiceMethods.ByName("CloneFromRequestLog")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
senderServiceSendRequestHandler := connect.NewUnaryHandler(
|
||||
SenderServiceSendRequestProcedure,
|
||||
svc.SendRequest,
|
||||
connect.WithSchema(senderServiceMethods.ByName("SendRequest")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
senderServiceDeleteRequestsHandler := connect.NewUnaryHandler(
|
||||
SenderServiceDeleteRequestsProcedure,
|
||||
svc.DeleteRequests,
|
||||
connect.WithSchema(senderServiceMethods.ByName("DeleteRequests")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
return "/sender.SenderService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case SenderServiceGetRequestByIDProcedure:
|
||||
senderServiceGetRequestByIDHandler.ServeHTTP(w, r)
|
||||
case SenderServiceListRequestsProcedure:
|
||||
senderServiceListRequestsHandler.ServeHTTP(w, r)
|
||||
case SenderServiceSetRequestsFilterProcedure:
|
||||
senderServiceSetRequestsFilterHandler.ServeHTTP(w, r)
|
||||
case SenderServiceGetRequestsFilterProcedure:
|
||||
senderServiceGetRequestsFilterHandler.ServeHTTP(w, r)
|
||||
case SenderServiceCreateOrUpdateRequestProcedure:
|
||||
senderServiceCreateOrUpdateRequestHandler.ServeHTTP(w, r)
|
||||
case SenderServiceCloneFromRequestLogProcedure:
|
||||
senderServiceCloneFromRequestLogHandler.ServeHTTP(w, r)
|
||||
case SenderServiceSendRequestProcedure:
|
||||
senderServiceSendRequestHandler.ServeHTTP(w, r)
|
||||
case SenderServiceDeleteRequestsProcedure:
|
||||
senderServiceDeleteRequestsHandler.ServeHTTP(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// UnimplementedSenderServiceHandler returns CodeUnimplemented from all methods.
|
||||
type UnimplementedSenderServiceHandler struct{}
|
||||
|
||||
func (UnimplementedSenderServiceHandler) GetRequestByID(context.Context, *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.GetRequestByID is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedSenderServiceHandler) ListRequests(context.Context, *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.ListRequests is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedSenderServiceHandler) SetRequestsFilter(context.Context, *connect.Request[SetRequestsFilterRequest]) (*connect.Response[SetRequestsFilterResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.SetRequestsFilter is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedSenderServiceHandler) GetRequestsFilter(context.Context, *connect.Request[GetRequestsFilterRequest]) (*connect.Response[GetRequestsFilterResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.GetRequestsFilter is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedSenderServiceHandler) CreateOrUpdateRequest(context.Context, *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.CreateOrUpdateRequest is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedSenderServiceHandler) CloneFromRequestLog(context.Context, *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.CloneFromRequestLog is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedSenderServiceHandler) SendRequest(context.Context, *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.SendRequest is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedSenderServiceHandler) DeleteRequests(context.Context, *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.DeleteRequests is not implemented"))
|
||||
}
|
@ -5,21 +5,19 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/oklog/ulid"
|
||||
connect "connectrpc.com/connect"
|
||||
"github.com/oklog/ulid/v2"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/dstotijn/hetty/pkg/filter"
|
||||
httppb "github.com/dstotijn/hetty/pkg/http"
|
||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||
"github.com/dstotijn/hetty/pkg/scope"
|
||||
)
|
||||
|
||||
//nolint:gosec
|
||||
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
var defaultHTTPClient = &http.Client{
|
||||
Transport: &HTTPTransport{},
|
||||
Timeout: 30 * time.Second,
|
||||
@ -31,20 +29,14 @@ var (
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
activeProjectID ulid.ULID
|
||||
findReqsFilter FindRequestsFilter
|
||||
activeProjectID string
|
||||
reqsFilter *RequestsFilter
|
||||
scope *scope.Scope
|
||||
repo Repository
|
||||
reqLogSvc *reqlog.Service
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type FindRequestsFilter struct {
|
||||
ProjectID ulid.ULID
|
||||
OnlyInScope bool
|
||||
SearchExpr filter.Expression
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Scope *scope.Scope
|
||||
Repository Repository
|
||||
@ -71,165 +63,215 @@ func NewService(cfg Config) *Service {
|
||||
return svc
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
ID ulid.ULID
|
||||
ProjectID ulid.ULID
|
||||
SourceRequestLogID ulid.ULID
|
||||
func (svc *Service) GetRequestByID(ctx context.Context, req *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error) {
|
||||
if svc.activeProjectID == "" {
|
||||
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
|
||||
}
|
||||
|
||||
URL *url.URL
|
||||
Method string
|
||||
Proto string
|
||||
Header http.Header
|
||||
Body []byte
|
||||
|
||||
Response *reqlog.ResponseLog
|
||||
}
|
||||
|
||||
func (svc *Service) FindRequestByID(ctx context.Context, id ulid.ULID) (Request, error) {
|
||||
req, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, id)
|
||||
senderReq, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, req.Msg.RequestId)
|
||||
if err != nil {
|
||||
return Request{}, fmt.Errorf("sender: failed to find request: %w", err)
|
||||
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("sender: failed to find request: %w", err))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
return &connect.Response[GetRequestByIDResponse]{
|
||||
Msg: &GetRequestByIDResponse{Request: senderReq},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) FindRequests(ctx context.Context) ([]Request, error) {
|
||||
return svc.repo.FindSenderRequests(ctx, svc.findReqsFilter, svc.scope)
|
||||
}
|
||||
|
||||
func (svc *Service) CreateOrUpdateRequest(ctx context.Context, req Request) (Request, error) {
|
||||
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
|
||||
return Request{}, ErrProjectIDMustBeSet
|
||||
}
|
||||
|
||||
if req.ID.Compare(ulid.ULID{}) == 0 {
|
||||
req.ID = ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||
}
|
||||
|
||||
req.ProjectID = svc.activeProjectID
|
||||
|
||||
if req.Method == "" {
|
||||
req.Method = http.MethodGet
|
||||
}
|
||||
|
||||
if req.Proto == "" {
|
||||
req.Proto = HTTPProto20
|
||||
}
|
||||
|
||||
if !isValidProto(req.Proto) {
|
||||
return Request{}, fmt.Errorf("sender: unsupported HTTP protocol: %v", req.Proto)
|
||||
}
|
||||
|
||||
err := svc.repo.StoreSenderRequest(ctx, req)
|
||||
func (svc *Service) ListRequests(ctx context.Context, req *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error) {
|
||||
reqs, err := svc.repo.FindSenderRequests(ctx, svc.activeProjectID, svc.filterRequest)
|
||||
if err != nil {
|
||||
return Request{}, fmt.Errorf("sender: failed to store request: %w", err)
|
||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to find requests: %w", err))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
return &connect.Response[ListRequestsResponse]{
|
||||
Msg: &ListRequestsResponse{Requests: reqs},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) CloneFromRequestLog(ctx context.Context, reqLogID ulid.ULID) (Request, error) {
|
||||
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
|
||||
return Request{}, ErrProjectIDMustBeSet
|
||||
func (svc *Service) filterRequest(req *Request) (bool, error) {
|
||||
if svc.reqsFilter.OnlyInScope {
|
||||
if svc.scope != nil && !req.MatchScope(svc.scope) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
reqLog, err := svc.reqLogSvc.FindRequestLogByID(ctx, reqLogID)
|
||||
if svc.reqsFilter.SearchExpr == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
expr, err := filter.ParseQuery(svc.reqsFilter.SearchExpr)
|
||||
if err != nil {
|
||||
return Request{}, fmt.Errorf("sender: failed to find request log: %w", err)
|
||||
return false, fmt.Errorf("failed to parse search expression: %w", err)
|
||||
}
|
||||
|
||||
req := Request{
|
||||
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
||||
ProjectID: svc.activeProjectID,
|
||||
SourceRequestLogID: reqLogID,
|
||||
Method: reqLog.Method,
|
||||
URL: reqLog.URL,
|
||||
Proto: HTTPProto20, // Attempt HTTP/2.
|
||||
Header: reqLog.Header,
|
||||
Body: reqLog.Body,
|
||||
}
|
||||
|
||||
err = svc.repo.StoreSenderRequest(ctx, req)
|
||||
match, err := req.Matches(expr)
|
||||
if err != nil {
|
||||
return Request{}, fmt.Errorf("sender: failed to store request: %w", err)
|
||||
return false, fmt.Errorf("failed to match search expression for sender request (id: %v): %w",
|
||||
req.Id, err,
|
||||
)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
return match, nil
|
||||
}
|
||||
|
||||
func (svc *Service) SetFindReqsFilter(filter FindRequestsFilter) {
|
||||
svc.findReqsFilter = filter
|
||||
}
|
||||
func (svc *Service) CreateOrUpdateRequest(ctx context.Context, req *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error) {
|
||||
if svc.activeProjectID == "" {
|
||||
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
|
||||
}
|
||||
|
||||
func (svc *Service) FindReqsFilter() FindRequestsFilter {
|
||||
return svc.findReqsFilter
|
||||
}
|
||||
r := proto.Clone(req.Msg.Request).(*Request)
|
||||
|
||||
func (svc *Service) SendRequest(ctx context.Context, id ulid.ULID) (Request, error) {
|
||||
req, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, id)
|
||||
if r == nil {
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("sender: request is nil"))
|
||||
}
|
||||
|
||||
if r.HttpRequest == nil {
|
||||
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("sender: request.http_request is nil"))
|
||||
}
|
||||
|
||||
if r.Id == "" {
|
||||
r.Id = ulid.Make().String()
|
||||
}
|
||||
|
||||
r.ProjectId = svc.activeProjectID
|
||||
|
||||
if r.HttpRequest.Method == httppb.Method_METHOD_UNSPECIFIED {
|
||||
r.HttpRequest.Method = httppb.Method_METHOD_GET
|
||||
}
|
||||
|
||||
if r.HttpRequest.Protocol == httppb.Protocol_PROTOCOL_UNSPECIFIED {
|
||||
r.HttpRequest.Protocol = httppb.Protocol_PROTOCOL_HTTP20
|
||||
}
|
||||
|
||||
err := svc.repo.StoreSenderRequest(ctx, r)
|
||||
if err != nil {
|
||||
return Request{}, fmt.Errorf("sender: failed to find request: %w", err)
|
||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store request: %w", err))
|
||||
}
|
||||
|
||||
return &connect.Response[CreateOrUpdateRequestResponse]{
|
||||
Msg: &CreateOrUpdateRequestResponse{
|
||||
Request: r,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) CloneFromRequestLog(ctx context.Context, req *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error) {
|
||||
if svc.activeProjectID == "" {
|
||||
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
|
||||
}
|
||||
|
||||
reqLog, err := svc.reqLogSvc.FindRequestLogByID(ctx, req.Msg.RequestLogId)
|
||||
if err != nil {
|
||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to find request log: %w", err))
|
||||
}
|
||||
|
||||
clonedReqLog := proto.Clone(reqLog).(*reqlog.HttpRequestLog)
|
||||
|
||||
senderReq := &Request{
|
||||
Id: ulid.Make().String(),
|
||||
ProjectId: svc.activeProjectID,
|
||||
SourceRequestLogId: clonedReqLog.Id,
|
||||
HttpRequest: clonedReqLog.Request,
|
||||
}
|
||||
|
||||
err = svc.repo.StoreSenderRequest(ctx, senderReq)
|
||||
if err != nil {
|
||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store request: %w", err))
|
||||
}
|
||||
|
||||
return &connect.Response[CloneFromRequestLogResponse]{Msg: &CloneFromRequestLogResponse{
|
||||
Request: senderReq,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) SetRequestsFilter(filter *RequestsFilter) {
|
||||
svc.reqsFilter = filter
|
||||
}
|
||||
|
||||
func (svc *Service) RequestsFilter() *RequestsFilter {
|
||||
return svc.reqsFilter
|
||||
}
|
||||
|
||||
func (svc *Service) SendRequest(ctx context.Context, connReq *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error) {
|
||||
req, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, connReq.Msg.RequestId)
|
||||
if err != nil {
|
||||
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("sender: failed to find request: %w", err))
|
||||
}
|
||||
|
||||
httpReq, err := parseHTTPRequest(ctx, req)
|
||||
if err != nil {
|
||||
return Request{}, fmt.Errorf("sender: failed to parse HTTP request: %w", err)
|
||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to parse HTTP request: %w", err))
|
||||
}
|
||||
|
||||
resLog, err := svc.sendHTTPRequest(httpReq)
|
||||
httpRes, err := svc.sendHTTPRequest(httpReq)
|
||||
if err != nil {
|
||||
return Request{}, fmt.Errorf("sender: could not send HTTP request: %w", err)
|
||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: could not send HTTP request: %w", err))
|
||||
}
|
||||
|
||||
req.Response = &resLog
|
||||
req.HttpResponse = httpRes
|
||||
|
||||
err = svc.repo.StoreSenderRequest(ctx, req)
|
||||
if err != nil {
|
||||
return Request{}, fmt.Errorf("sender: failed to store sender response log: %w", err)
|
||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store sender response log: %w", err))
|
||||
}
|
||||
|
||||
req.Response = &resLog
|
||||
|
||||
return req, nil
|
||||
return &connect.Response[SendRequestResponse]{
|
||||
Msg: &SendRequestResponse{
|
||||
Request: req,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseHTTPRequest(ctx context.Context, req Request) (*http.Request, error) {
|
||||
ctx = context.WithValue(ctx, protoCtxKey{}, req.Proto)
|
||||
func parseHTTPRequest(ctx context.Context, req *Request) (*http.Request, error) {
|
||||
ctx = context.WithValue(ctx, protoCtxKey{}, req.GetHttpRequest().GetProtocol())
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, req.Method, req.URL.String(), bytes.NewReader(req.Body))
|
||||
httpReq, err := http.NewRequestWithContext(ctx,
|
||||
req.GetHttpRequest().GetMethod().String(),
|
||||
req.GetHttpRequest().GetUrl(),
|
||||
bytes.NewReader(req.GetHttpRequest().GetBody()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to construct HTTP request: %w", err)
|
||||
}
|
||||
|
||||
if req.Header != nil {
|
||||
httpReq.Header = req.Header
|
||||
for _, header := range req.GetHttpRequest().GetHeaders() {
|
||||
httpReq.Header.Add(header.Key, header.Value)
|
||||
}
|
||||
|
||||
return httpReq, nil
|
||||
}
|
||||
|
||||
func (svc *Service) sendHTTPRequest(httpReq *http.Request) (reqlog.ResponseLog, error) {
|
||||
func (svc *Service) sendHTTPRequest(httpReq *http.Request) (*httppb.Response, error) {
|
||||
res, err := svc.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return reqlog.ResponseLog{}, &SendError{err}
|
||||
return nil, &SendError{err}
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
resLog, err := reqlog.ParseHTTPResponse(res)
|
||||
resLog, err := httppb.ParseHTTPResponse(res)
|
||||
if err != nil {
|
||||
return reqlog.ResponseLog{}, fmt.Errorf("failed to parse http response: %w", err)
|
||||
return nil, fmt.Errorf("failed to parse http response: %w", err)
|
||||
}
|
||||
|
||||
return resLog, err
|
||||
}
|
||||
|
||||
func (svc *Service) SetActiveProjectID(id ulid.ULID) {
|
||||
func (svc *Service) SetActiveProjectID(id string) {
|
||||
svc.activeProjectID = id
|
||||
}
|
||||
|
||||
func (svc *Service) DeleteRequests(ctx context.Context, projectID ulid.ULID) error {
|
||||
return svc.repo.DeleteSenderRequests(ctx, projectID)
|
||||
func (svc *Service) DeleteRequests(ctx context.Context, req *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error) {
|
||||
if svc.activeProjectID == "" {
|
||||
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
|
||||
}
|
||||
|
||||
err := svc.repo.DeleteSenderRequests(ctx, svc.activeProjectID)
|
||||
if err != nil {
|
||||
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to delete requests: %w", err))
|
||||
}
|
||||
|
||||
return &connect.Response[DeleteRequestsResponse]{}, nil
|
||||
}
|
||||
|
||||
func (e SendError) Error() string {
|
||||
|
1040
pkg/sender/sender.pb.go
Normal file
1040
pkg/sender/sender.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,36 +4,24 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
http "net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/oklog/ulid"
|
||||
connect "connectrpc.com/connect"
|
||||
"go.etcd.io/bbolt"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
|
||||
"github.com/dstotijn/hetty/pkg/db/bolt"
|
||||
httppb "github.com/dstotijn/hetty/pkg/http"
|
||||
"github.com/dstotijn/hetty/pkg/proj"
|
||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||
"github.com/dstotijn/hetty/pkg/sender"
|
||||
"github.com/dstotijn/hetty/pkg/testutil"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
//nolint:gosec
|
||||
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
var exampleURL = func() *url.URL {
|
||||
u, err := url.Parse("https://example.com/foobar")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return u
|
||||
}()
|
||||
|
||||
func TestStoreRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@ -42,10 +30,16 @@ func TestStoreRequest(t *testing.T) {
|
||||
|
||||
svc := sender.NewService(sender.Config{})
|
||||
|
||||
_, err := svc.CreateOrUpdateRequest(context.Background(), sender.Request{
|
||||
URL: exampleURL,
|
||||
Method: http.MethodPost,
|
||||
Body: []byte("foobar"),
|
||||
_, err := svc.CreateOrUpdateRequest(context.Background(), &connect.Request[sender.CreateOrUpdateRequestRequest]{
|
||||
Msg: &sender.CreateOrUpdateRequestRequest{
|
||||
Request: &sender.Request{
|
||||
HttpRequest: &httppb.Request{
|
||||
Url: "https://example.com/foobar",
|
||||
Method: httppb.Method_METHOD_POST,
|
||||
Body: []byte("foobar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, sender.ErrProjectIDMustBeSet) {
|
||||
t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err)
|
||||
@ -72,75 +66,69 @@ func TestStoreRequest(t *testing.T) {
|
||||
Repository: db,
|
||||
})
|
||||
|
||||
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||
err = db.UpsertProject(context.Background(), proj.Project{
|
||||
ID: projectID,
|
||||
Name: "foobar",
|
||||
Settings: proj.Settings{},
|
||||
projectID := "foobar-project-id"
|
||||
err = db.UpsertProject(context.Background(), &proj.Project{
|
||||
Id: projectID,
|
||||
Name: "foobar",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error upserting project: %v", err)
|
||||
}
|
||||
|
||||
svc.SetActiveProjectID(projectID)
|
||||
svc.SetActiveProjectID(projectID)
|
||||
|
||||
exp := sender.Request{
|
||||
ProjectID: projectID,
|
||||
URL: exampleURL,
|
||||
Method: http.MethodPost,
|
||||
Proto: "HTTP/1.1",
|
||||
Header: http.Header{
|
||||
"X-Foo": []string{"bar"},
|
||||
exp := &sender.Request{
|
||||
ProjectId: projectID,
|
||||
HttpRequest: &httppb.Request{
|
||||
Method: httppb.Method_METHOD_POST,
|
||||
Protocol: httppb.Protocol_PROTOCOL_HTTP20,
|
||||
Url: "https://example.com/foobar",
|
||||
Headers: []*httppb.Header{
|
||||
{Key: "X-Foo", Value: "bar"},
|
||||
},
|
||||
Body: []byte("foobar"),
|
||||
},
|
||||
Body: []byte("foobar"),
|
||||
}
|
||||
|
||||
got, err := svc.CreateOrUpdateRequest(context.Background(), sender.Request{
|
||||
URL: exampleURL,
|
||||
Method: http.MethodPost,
|
||||
Proto: "HTTP/1.1",
|
||||
Header: http.Header{
|
||||
"X-Foo": []string{"bar"},
|
||||
createRes, err := svc.CreateOrUpdateRequest(context.Background(), &connect.Request[sender.CreateOrUpdateRequestRequest]{
|
||||
Msg: &sender.CreateOrUpdateRequestRequest{
|
||||
Request: exp,
|
||||
},
|
||||
Body: []byte("foobar"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error storing request: %v", err)
|
||||
}
|
||||
|
||||
if got.ID.Compare(ulid.ULID{}) == 0 {
|
||||
if createRes.Msg.Request.Id == "" {
|
||||
t.Fatal("expected request ID to be non-empty value")
|
||||
}
|
||||
|
||||
diff := cmp.Diff(exp, got, cmpopts.IgnoreFields(sender.Request{}, "ID"))
|
||||
if diff != "" {
|
||||
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
|
||||
}
|
||||
testutil.ProtoDiff(t, "request not equal", exp, createRes.Msg.Request, "id")
|
||||
|
||||
got, err = db.FindSenderRequestByID(context.Background(), projectID, got.ID)
|
||||
got, err := db.FindSenderRequestByID(context.Background(), projectID, createRes.Msg.Request.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to find request by ID: %v", err)
|
||||
}
|
||||
|
||||
diff = cmp.Diff(exp, got, cmpopts.IgnoreFields(sender.Request{}, "ID"))
|
||||
if diff != "" {
|
||||
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
|
||||
}
|
||||
testutil.ProtoDiff(t, "request not equal", exp, got, "id")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCloneFromRequestLog(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||
reqLogID := "foobar-req-log-id"
|
||||
|
||||
t.Run("without active project", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := sender.NewService(sender.Config{})
|
||||
|
||||
_, err := svc.CloneFromRequestLog(context.Background(), reqLogID)
|
||||
_, err := svc.CloneFromRequestLog(context.Background(), &connect.Request[sender.CloneFromRequestLogRequest]{
|
||||
Msg: &sender.CloneFromRequestLogRequest{
|
||||
RequestLogId: reqLogID,
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, sender.ErrProjectIDMustBeSet) {
|
||||
t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err)
|
||||
}
|
||||
@ -162,24 +150,32 @@ func TestCloneFromRequestLog(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||
err = db.UpsertProject(context.Background(), proj.Project{
|
||||
ID: projectID,
|
||||
projectID := "foobar-project-id"
|
||||
err = db.UpsertProject(context.Background(), &proj.Project{
|
||||
Id: projectID,
|
||||
Name: "foobar",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error upserting project: %v", err)
|
||||
}
|
||||
|
||||
reqLog := reqlog.RequestLog{
|
||||
ID: reqLogID,
|
||||
ProjectID: projectID,
|
||||
URL: exampleURL,
|
||||
Method: http.MethodPost,
|
||||
Proto: "HTTP/1.1",
|
||||
Header: http.Header{
|
||||
"X-Foo": []string{"bar"},
|
||||
reqLog := &reqlog.HttpRequestLog{
|
||||
Id: reqLogID,
|
||||
ProjectId: projectID,
|
||||
Request: &httppb.Request{
|
||||
Url: "https://example.com/foobar",
|
||||
Method: httppb.Method_METHOD_POST,
|
||||
Body: []byte("foobar"),
|
||||
Headers: []*httppb.Header{
|
||||
{Key: "X-Foo", Value: "bar"},
|
||||
},
|
||||
},
|
||||
Response: &httppb.Response{
|
||||
Protocol: httppb.Protocol_PROTOCOL_HTTP20,
|
||||
StatusCode: 200,
|
||||
Status: "200 OK",
|
||||
Body: []byte("foobar"),
|
||||
},
|
||||
Body: []byte("foobar"),
|
||||
}
|
||||
|
||||
if err := db.StoreRequestLog(context.Background(), reqLog); err != nil {
|
||||
@ -196,27 +192,29 @@ func TestCloneFromRequestLog(t *testing.T) {
|
||||
|
||||
svc.SetActiveProjectID(projectID)
|
||||
|
||||
exp := sender.Request{
|
||||
SourceRequestLogID: reqLogID,
|
||||
ProjectID: projectID,
|
||||
URL: exampleURL,
|
||||
Method: http.MethodPost,
|
||||
Proto: sender.HTTPProto20,
|
||||
Header: http.Header{
|
||||
"X-Foo": []string{"bar"},
|
||||
exp := &sender.Request{
|
||||
SourceRequestLogId: reqLogID,
|
||||
ProjectId: projectID,
|
||||
HttpRequest: &httppb.Request{
|
||||
Url: "https://example.com/foobar",
|
||||
Method: httppb.Method_METHOD_POST,
|
||||
Body: []byte("foobar"),
|
||||
Headers: []*httppb.Header{
|
||||
{Key: "X-Foo", Value: "bar"},
|
||||
},
|
||||
},
|
||||
Body: []byte("foobar"),
|
||||
}
|
||||
|
||||
got, err := svc.CloneFromRequestLog(context.Background(), reqLogID)
|
||||
got, err := svc.CloneFromRequestLog(context.Background(), &connect.Request[sender.CloneFromRequestLogRequest]{
|
||||
Msg: &sender.CloneFromRequestLogRequest{
|
||||
RequestLogId: reqLogID,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error cloning from request log: %v", err)
|
||||
}
|
||||
|
||||
diff := cmp.Diff(exp, got, cmpopts.IgnoreFields(sender.Request{}, "ID"))
|
||||
if diff != "" {
|
||||
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
|
||||
}
|
||||
testutil.ProtoDiff(t, "request not equal", exp, got.Msg.Request, "id")
|
||||
})
|
||||
}
|
||||
|
||||
@ -245,28 +243,27 @@ func TestSendRequest(t *testing.T) {
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
tsURL, _ := url.Parse(ts.URL)
|
||||
|
||||
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||
err = db.UpsertProject(context.Background(), proj.Project{
|
||||
ID: projectID,
|
||||
Settings: proj.Settings{},
|
||||
projectID := "foobar-project-id"
|
||||
err = db.UpsertProject(context.Background(), &proj.Project{
|
||||
Id: projectID,
|
||||
Name: "foobar",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error upserting project: %v", err)
|
||||
}
|
||||
|
||||
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||
req := sender.Request{
|
||||
ID: reqID,
|
||||
ProjectID: projectID,
|
||||
URL: tsURL,
|
||||
Method: http.MethodPost,
|
||||
Proto: "HTTP/1.1",
|
||||
Header: http.Header{
|
||||
"X-Foo": []string{"bar"},
|
||||
reqID := "foobar-req-id"
|
||||
req := &sender.Request{
|
||||
Id: reqID,
|
||||
ProjectId: projectID,
|
||||
HttpRequest: &httppb.Request{
|
||||
Url: ts.URL,
|
||||
Method: httppb.Method_METHOD_POST,
|
||||
Body: []byte("foobar"),
|
||||
Headers: []*httppb.Header{
|
||||
{Key: "X-Foo", Value: "bar"},
|
||||
},
|
||||
},
|
||||
Body: []byte("foobar"),
|
||||
}
|
||||
|
||||
if err := db.StoreSenderRequest(context.Background(), req); err != nil {
|
||||
@ -281,26 +278,38 @@ func TestSendRequest(t *testing.T) {
|
||||
})
|
||||
svc.SetActiveProjectID(projectID)
|
||||
|
||||
exp := &reqlog.ResponseLog{
|
||||
Proto: "HTTP/1.1",
|
||||
StatusCode: http.StatusOK,
|
||||
exp := &httppb.Response{
|
||||
Protocol: httppb.Protocol_PROTOCOL_HTTP11,
|
||||
StatusCode: 200,
|
||||
Status: "200 OK",
|
||||
Header: http.Header{
|
||||
"Content-Length": []string{"3"},
|
||||
"Content-Type": []string{"text/plain; charset=utf-8"},
|
||||
"Date": []string{date},
|
||||
"Foobar": []string{"baz"},
|
||||
Headers: []*httppb.Header{
|
||||
{Key: "Date", Value: date},
|
||||
{Key: "Foobar", Value: "baz"},
|
||||
{Key: "Content-Length", Value: "3"},
|
||||
{Key: "Content-Type", Value: "text/plain; charset=utf-8"},
|
||||
},
|
||||
Body: []byte("baz"),
|
||||
}
|
||||
|
||||
got, err := svc.SendRequest(context.Background(), reqID)
|
||||
got, err := svc.SendRequest(context.Background(), &connect.Request[sender.SendRequestRequest]{
|
||||
Msg: &sender.SendRequestRequest{
|
||||
RequestId: reqID,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error sending request: %v", err)
|
||||
}
|
||||
|
||||
diff := cmp.Diff(exp, got.Response, cmpopts.IgnoreFields(sender.Request{}, "ID"))
|
||||
if diff != "" {
|
||||
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
|
||||
opts := []cmp.Option{
|
||||
protocmp.Transform(),
|
||||
protocmp.SortRepeated(func(a, b *httppb.Header) bool {
|
||||
if a.Key != b.Key {
|
||||
return a.Key < b.Key
|
||||
}
|
||||
return a.Value < b.Value
|
||||
}),
|
||||
}
|
||||
if diff := cmp.Diff(exp, got.Msg.Request.HttpResponse, opts...); diff != "" {
|
||||
t.Fatalf("response not equal (-exp, +got):\n%v", diff)
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,3 @@ func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
|
||||
func isValidProto(proto string) bool {
|
||||
return proto == HTTPProto10 || proto == HTTPProto11 || proto == HTTPProto20
|
||||
}
|
||||
|
Reference in New Issue
Block a user