Replace GraphQL server with Connect RPC

This commit is contained in:
David Stotijn
2025-02-05 21:54:59 +01:00
parent 52c83a1989
commit 6889c9c183
53 changed files with 5875 additions and 11685 deletions

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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"),
},
},

View 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"))
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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)
}
}

View File

@ -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
}