Repository: googleforgames/open-match
Pull request #1162 (2020-03-19 08:43:04)

Add a built in created time field for Tickets and the ability to filter Tickets by created time.
Here are the highlights of this change:

- Adds a create_time field to Ticket proto and populates this field during Ticket creation
- Adds created_before, created_after to the Pool and filters Tickets based on create time if this field is specified. 

One implementation nuance was that the current filter package directly operates on Pools. In this case (and may be similar in future), we need to convert the proto to time field - which doing for each Ticket is a lot of unnecessary repetitive work. Further, this conversion can fail - at which point, failing it at a Ticket level does not make sense (that validation / failure should be outside of the filter package). 

Hence, this change abstracts the Pool from the filter package and instead introduces a Criteria structure that contains validated filtering criteria. Any validations on Pool specification can happen outside (in query service) before passing the Criteria to filter package. 

One nuance is the test cases. The way the test is currently structured, the 'testcases' package is used by E2E as well as filter_tests which is great - however, with this change, the filter tests need Criteria. Hence, the subtle change is that the testcases package still uses Pool but filter_test converts that to criteria as it intends to test only the criteria whereas the e2e will continue to use more E2E Pool specification.


sawagh: @googlebot rescan

Patch:
diff --git a/api/backend.swagger.json b/api/backend.swagger.json
index d7c9e5a1..2fda2cc8 100644
--- a/api/backend.swagger.json
+++ b/api/backend.swagger.json
@@ -299,7 +299,7 @@
           "items": {
             "$ref": "#/definitions/openmatchDoubleRangeFilter"
           },
-          "description": "Set of Filters indicating the filtering criteria. Selected players must\nmatch every Filter."
+          "description": "Set of Filters indicating the filtering criteria. Selected tickets must\nmatch every Filter."
         },
         "string_equals_filters": {
           "type": "array",
@@ -312,8 +312,19 @@
           "items": {
             "$ref": "#/definitions/openmatchTagPresentFilter"
           }
+        },
+        "created_before": {
+          "type": "string",
+          "format": "date-time",
+          "description": "If specified, only Tickets created before the specified time are selected."
+        },
+        "created_after": {
+          "type": "string",
+          "format": "date-time",
+          "description": "If specified, only Tickets created after the specified time are selected."
         }
-      }
+      },
+      "description": "Pool specfies a set of criteria that are used to select a subset of Tickets\nthat meet all the criteria."
     },
     "openmatchReleaseTicketsRequest": {
       "type": "object",
@@ -401,6 +412,11 @@
             "$ref": "#/definitions/protobufAny"
           },
           "description": "Customized information not inspected by Open Match, to be used by the match\nmaking function, evaluator, and components making calls to Open Match.\nOptional, depending on the requirements of the connected systems."
+        },
+        "create_time": {
+          "type": "string",
+          "format": "date-time",
+          "description": "Create time represents the time at which this Ticket was created. It is\npopulated by Open Match at the time of Ticket creation."
         }
       },
       "description": "A Ticket is a basic matchmaking entity in Open Match. A Ticket represents either an\nindividual 'Player' or a 'Group' of players. Open Match will not interpret\nwhat the Ticket represents but just treat it as a matchmaking unit with a set\nof SearchFields. Open Match stores the Ticket in state storage and enables an\nAssignment to be associated with this Ticket."
diff --git a/api/evaluator.swagger.json b/api/evaluator.swagger.json
index 21156463..9f328807 100644
--- a/api/evaluator.swagger.json
+++ b/api/evaluator.swagger.json
@@ -177,6 +177,11 @@
             "$ref": "#/definitions/protobufAny"
           },
           "description": "Customized information not inspected by Open Match, to be used by the match\nmaking function, evaluator, and components making calls to Open Match.\nOptional, depending on the requirements of the connected systems."
+        },
+        "create_time": {
+          "type": "string",
+          "format": "date-time",
+          "description": "Create time represents the time at which this Ticket was created. It is\npopulated by Open Match at the time of Ticket creation."
         }
       },
       "description": "A Ticket is a basic matchmaking entity in Open Match. A Ticket represents either an\nindividual 'Player' or a 'Group' of players. Open Match will not interpret\nwhat the Ticket represents but just treat it as a matchmaking unit with a set\nof SearchFields. Open Match stores the Ticket in state storage and enables an\nAssignment to be associated with this Ticket."
diff --git a/api/frontend.swagger.json b/api/frontend.swagger.json
index fea091b5..eb01a8e3 100644
--- a/api/frontend.swagger.json
+++ b/api/frontend.swagger.json
@@ -253,6 +253,11 @@
             "$ref": "#/definitions/protobufAny"
           },
           "description": "Customized information not inspected by Open Match, to be used by the match\nmaking function, evaluator, and components making calls to Open Match.\nOptional, depending on the requirements of the connected systems."
+        },
+        "create_time": {
+          "type": "string",
+          "format": "date-time",
+          "description": "Create time represents the time at which this Ticket was created. It is\npopulated by Open Match at the time of Ticket creation."
         }
       },
       "description": "A Ticket is a basic matchmaking entity in Open Match. A Ticket represents either an\nindividual 'Player' or a 'Group' of players. Open Match will not interpret\nwhat the Ticket represents but just treat it as a matchmaking unit with a set\nof SearchFields. Open Match stores the Ticket in state storage and enables an\nAssignment to be associated with this Ticket."
diff --git a/api/matchfunction.swagger.json b/api/matchfunction.swagger.json
index 6291e1d5..268d1f47 100644
--- a/api/matchfunction.swagger.json
+++ b/api/matchfunction.swagger.json
@@ -165,7 +165,7 @@
           "items": {
             "$ref": "#/definitions/openmatchDoubleRangeFilter"
           },
-          "description": "Set of Filters indicating the filtering criteria. Selected players must\nmatch every Filter."
+          "description": "Set of Filters indicating the filtering criteria. Selected tickets must\nmatch every Filter."
         },
         "string_equals_filters": {
           "type": "array",
@@ -178,8 +178,19 @@
           "items": {
             "$ref": "#/definitions/openmatchTagPresentFilter"
           }
+        },
+        "created_before": {
+          "type": "string",
+          "format": "date-time",
+          "description": "If specified, only Tickets created before the specified time are selected."
+        },
+        "created_after": {
+          "type": "string",
+          "format": "date-time",
+          "description": "If specified, only Tickets created after the specified time are selected."
         }
-      }
+      },
+      "description": "Pool specfies a set of criteria that are used to select a subset of Tickets\nthat meet all the criteria."
     },
     "openmatchRunRequest": {
       "type": "object",
@@ -270,6 +281,11 @@
             "$ref": "#/definitions/protobufAny"
           },
           "description": "Customized information not inspected by Open Match, to be used by the match\nmaking function, evaluator, and components making calls to Open Match.\nOptional, depending on the requirements of the connected systems."
+        },
+        "create_time": {
+          "type": "string",
+          "format": "date-time",
+          "description": "Create time represents the time at which this Ticket was created. It is\npopulated by Open Match at the time of Ticket creation."
         }
       },
       "description": "A Ticket is a basic matchmaking entity in Open Match. A Ticket represents either an\nindividual 'Player' or a 'Group' of players. Open Match will not interpret\nwhat the Ticket represents but just treat it as a matchmaking unit with a set\nof SearchFields. Open Match stores the Ticket in state storage and enables an\nAssignment to be associated with this Ticket."
diff --git a/api/messages.proto b/api/messages.proto
index f50b3611..f17b1f82 100644
--- a/api/messages.proto
+++ b/api/messages.proto
@@ -19,6 +19,7 @@ option csharp_namespace = "OpenMatch";
 
 import "google/rpc/status.proto";
 import "google/protobuf/any.proto";
+import "google/protobuf/timestamp.proto";
 
 // A Ticket is a basic matchmaking entity in Open Match. A Ticket represents either an
 // individual 'Player' or a 'Group' of players. Open Match will not interpret
@@ -42,6 +43,10 @@ message Ticket {
   // Optional, depending on the requirements of the connected systems.
   map<string, google.protobuf.Any> extensions = 5;
 
+  // Create time represents the time at which this Ticket was created. It is
+  // populated by Open Match at the time of Ticket creation.
+  google.protobuf.Timestamp create_time = 6;
+
   // Deprecated fields.
   reserved 2;
 }
@@ -126,11 +131,13 @@ message TagPresentFilter {
   string tag = 1;
 }
 
+// Pool specfies a set of criteria that are used to select a subset of Tickets
+// that meet all the criteria.
 message Pool {
   // A developer-chosen human-readable name for this Pool.
   string name = 1;
 
-  // Set of Filters indicating the filtering criteria. Selected players must
+  // Set of Filters indicating the filtering criteria. Selected tickets must
   // match every Filter.
   repeated DoubleRangeFilter double_range_filters = 2;
 
@@ -138,6 +145,12 @@ message Pool {
 
   repeated TagPresentFilter tag_present_filters = 5;
 
+  // If specified, only Tickets created before the specified time are selected.
+  google.protobuf.Timestamp created_before = 6;
+
+  // If specified, only Tickets created after the specified time are selected.
+  google.protobuf.Timestamp created_after = 7;
+
   // Deprecated fields.
   reserved 3;
 }
diff --git a/api/query.swagger.json b/api/query.swagger.json
index af85d3c7..46db3005 100644
--- a/api/query.swagger.json
+++ b/api/query.swagger.json
@@ -143,7 +143,7 @@
           "items": {
             "$ref": "#/definitions/openmatchDoubleRangeFilter"
           },
-          "description": "Set of Filters indicating the filtering criteria. Selected players must\nmatch every Filter."
+          "description": "Set of Filters indicating the filtering criteria. Selected tickets must\nmatch every Filter."
         },
         "string_equals_filters": {
           "type": "array",
@@ -156,8 +156,19 @@
           "items": {
             "$ref": "#/definitions/openmatchTagPresentFilter"
           }
+        },
+        "created_before": {
+          "type": "string",
+          "format": "date-time",
+          "description": "If specified, only Tickets created before the specified time are selected."
+        },
+        "created_after": {
+          "type": "string",
+          "format": "date-time",
+          "description": "If specified, only Tickets created after the specified time are selected."
         }
-      }
+      },
+      "description": "Pool specfies a set of criteria that are used to select a subset of Tickets\nthat meet all the criteria."
     },
     "openmatchQueryTicketIdsRequest": {
       "type": "object",
@@ -272,6 +283,11 @@
             "$ref": "#/definitions/protobufAny"
           },
           "description": "Customized information not inspected by Open Match, to be used by the match\nmaking function, evaluator, and components making calls to Open Match.\nOptional, depending on the requirements of the connected systems."
+        },
+        "create_time": {
+          "type": "string",
+          "format": "date-time",
+          "description": "Create time represents the time at which this Ticket was created. It is\npopulated by Open Match at the time of Ticket creation."
         }
       },
       "description": "A Ticket is a basic matchmaking entity in Open Match. A Ticket represents either an\nindividual 'Player' or a 'Group' of players. Open Match will not interpret\nwhat the Ticket represents but just treat it as a matchmaking unit with a set\nof SearchFields. Open Match stores the Ticket in state storage and enables an\nAssignment to be associated with this Ticket."
diff --git a/internal/app/frontend/frontend_service.go b/internal/app/frontend/frontend_service.go
index ca0b8dfd..bb905b41 100644
--- a/internal/app/frontend/frontend_service.go
+++ b/internal/app/frontend/frontend_service.go
@@ -18,6 +18,7 @@ import (
 	"context"
 
 	"github.com/golang/protobuf/proto"
+	"github.com/golang/protobuf/ptypes"
 	"github.com/rs/xid"
 	"github.com/sirupsen/logrus"
 	"go.opencensus.io/trace"
@@ -59,6 +60,9 @@ func (s *frontendService) CreateTicket(ctx context.Context, req *pb.CreateTicket
 	if req.Ticket.Assignment != nil {
 		return nil, status.Errorf(codes.InvalidArgument, "tickets cannot be created with an assignment")
 	}
+	if req.Ticket.CreateTime != nil {
+		return nil, status.Errorf(codes.InvalidArgument, "tickets cannot be created with create time set")
+	}
 
 	return doCreateTicket(ctx, req, s.store)
 }
@@ -71,6 +75,7 @@ func doCreateTicket(ctx context.Context, req *pb.CreateTicketRequest, store stat
 	}
 
 	ticket.Id = xid.New().String()
+	ticket.CreateTime = ptypes.TimestampNow()
 	err := store.CreateTicket(ctx, ticket)
 	if err != nil {
 		logger.WithFields(logrus.Fields{
diff --git a/internal/app/query/query_service.go b/internal/app/query/query_service.go
index 14be90bb..0454682e 100644
--- a/internal/app/query/query_service.go
+++ b/internal/app/query/query_service.go
@@ -49,10 +49,15 @@ func (s *queryService) QueryTickets(req *pb.QueryTicketsRequest, responseServer
 		return status.Error(codes.InvalidArgument, ".pool is required")
 	}
 
+	pf, err := filter.NewPoolFilter(pool)
+	if err != nil {
+		return err
+	}
+
 	var results []*pb.Ticket
-	err := s.tc.request(responseServer.Context(), func(tickets map[string]*pb.Ticket) {
+	err = s.tc.request(responseServer.Context(), func(tickets map[string]*pb.Ticket) {
 		for _, ticket := range tickets {
-			if filter.InPool(ticket, pool) {
+			if pf.In(ticket) {
 				results = append(results, ticket)
 			}
 		}
@@ -86,10 +91,15 @@ func (s *queryService) QueryTicketIds(req *pb.QueryTicketIdsRequest, responseSer
 		return status.Error(codes.InvalidArgument, ".pool is required")
 	}
 
+	pf, err := filter.NewPoolFilter(pool)
+	if err != nil {
+		return err
+	}
+
 	var results []string
-	err := s.tc.request(responseServer.Context(), func(tickets map[string]*pb.Ticket) {
+	err = s.tc.request(responseServer.Context(), func(tickets map[string]*pb.Ticket) {
 		for id, ticket := range tickets {
-			if filter.InPool(ticket, pool) {
+			if pf.In(ticket) {
 				results = append(results, id)
 			}
 		}
diff --git a/internal/filter/filter.go b/internal/filter/filter.go
index cbe5bf01..8370c49d 100644
--- a/internal/filter/filter.go
+++ b/internal/filter/filter.go
@@ -18,18 +18,90 @@
 package filter
 
 import (
+	"time"
+
+	"github.com/golang/protobuf/ptypes"
+	"github.com/sirupsen/logrus"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
 	"open-match.dev/open-match/pkg/pb"
 )
 
 var emptySearchFields = &pb.SearchFields{}
 
-// InPool returns whether the ticket meets all the criteria of the pool.
-func InPool(ticket *pb.Ticket, pool *pb.Pool) bool {
+var (
+	logger = logrus.WithFields(logrus.Fields{
+		"app":       "openmatch",
+		"component": "filter",
+	})
+)
+
+// PoolFilter contains all the filtering criteria from a Pool that the Ticket
+// needs to meet to belong to that Pool.
+type PoolFilter struct {
+	DoubleRangeFilters  []*pb.DoubleRangeFilter
+	StringEqualsFilters []*pb.StringEqualsFilter
+	TagPresentFilters   []*pb.TagPresentFilter
+	CreatedBefore       time.Time
+	CreatedAfter        time.Time
+}
+
+// NewPoolFilter validates a Pool's filtering criteria and returns a PoolFilter.
+func NewPoolFilter(pool *pb.Pool) (*PoolFilter, error) {
+	var ca, cb time.Time
+	var err error
+
+	if pool.GetCreatedBefore() != nil {
+		if cb, err = ptypes.Timestamp(pool.GetCreatedBefore()); err != nil {
+			return nil, status.Error(codes.InvalidArgument, ".invalid created_before value")
+		}
+	}
+
+	if pool.GetCreatedAfter() != nil {
+		if ca, err = ptypes.Timestamp(pool.GetCreatedAfter()); err != nil {
+			return nil, status.Error(codes.InvalidArgument, ".invalid created_after value")
+		}
+	}
+
+	return &PoolFilter{
+		DoubleRangeFilters:  pool.GetDoubleRangeFilters(),
+		StringEqualsFilters: pool.GetStringEqualsFilters(),
+		TagPresentFilters:   pool.GetTagPresentFilters(),
+		CreatedBefore:       cb,
+		CreatedAfter:        ca,
+	}, nil
+}
+
+// In returns true if the Ticket meets all the criteria for this PoolFilter.
+func (pf *PoolFilter) In(ticket *pb.Ticket) bool {
 	s := ticket.GetSearchFields()
 	if s == nil {
 		s = emptySearchFields
 	}
-	for _, f := range pool.GetDoubleRangeFilters() {
+
+	if !pf.CreatedAfter.IsZero() || !pf.CreatedBefore.IsZero() {
+		// CreateTime is only populated by Open Match and hence expected to be valid.
+		if ct, err := ptypes.Timestamp(ticket.CreateTime); err == nil {
+			if !pf.CreatedAfter.IsZero() {
+				if !ct.After(pf.CreatedAfter) {
+					return false
+				}
+			}
+
+			if !pf.CreatedBefore.IsZero() {
+				if !ct.Before(pf.CreatedBefore) {
+					return false
+				}
+			}
+		} else {
+			logger.WithFields(logrus.Fields{
+				"error": err.Error(),
+				"id":    ticket.GetId(),
+			}).Error("failed to get time from Timestamp proto")
+		}
+	}
+
+	for _, f := range pf.DoubleRangeFilters {
 		v, ok := s.DoubleArgs[f.DoubleArg]
 		if !ok {
 			return false
@@ -40,7 +112,7 @@ func InPool(ticket *pb.Ticket, pool *pb.Pool) bool {
 		}
 	}
 
-	for _, f := range pool.GetStringEqualsFilters() {
+	for _, f := range pf.StringEqualsFilters {
 		v, ok := s.StringArgs[f.StringArg]
 		if !ok {
 			return false
@@ -51,7 +123,7 @@ func InPool(ticket *pb.Ticket, pool *pb.Pool) bool {
 	}
 
 outer:
-	for _, f := range pool.GetTagPresentFilters() {
+	for _, f := range pf.TagPresentFilters {
 		for _, v := range s.Tags {
 			if v == f.Tag {
 				continue outer
diff --git a/pkg/pb/messages.pb.go b/pkg/pb/messages.pb.go
index ed29a26f..364cea88 100644
--- a/pkg/pb/messages.pb.go
+++ b/pkg/pb/messages.pb.go
@@ -7,6 +7,7 @@ import (
 	fmt "fmt"
 	proto "github.com/golang/protobuf/proto"
 	any "github.com/golang/protobuf/ptypes/any"
+	timestamp "github.com/golang/protobuf/ptypes/timestamp"
 	_ "google.golang.org/genproto/googleapis/rpc/status"
 	math "math"
 )
@@ -39,10 +40,13 @@ type Ticket struct {
 	// Customized information not inspected by Open Match, to be used by the match
 	// making function, evaluator, and components making calls to Open Match.
 	// Optional, depending on the requirements of the connected systems.
-	Extensions           map[string]*any.Any `protobuf:"bytes,5,rep,name=extensions,proto3" json:"extensions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
-	XXX_NoUnkeyedLiteral struct{}            `json:"-"`
-	XXX_unrecognized     []byte              `json:"-"`
-	XXX_sizecache        int32               `json:"-"`
+	Extensions map[string]*any.Any `protobuf:"bytes,5,rep,name=extensions,proto3" json:"extensions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	// Create time represents the time at which this Ticket was created. It is
+	// populated by Open Match at the time of Ticket creation.
+	CreateTime           *timestamp.Timestamp `protobuf:"bytes,6,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}             `json:"-"`
+	XXX_unrecognized     []byte               `json:"-"`
+	XXX_sizecache        int32                `json:"-"`
 }
 
 func (m *Ticket) Reset()         { *m = Ticket{} }
@@ -98,6 +102,13 @@ func (m *Ticket) GetExtensions() map[string]*any.Any {
 	return nil
 }
 
+func (m *Ticket) GetCreateTime() *timestamp.Timestamp {
+	if m != nil {
+		return m.CreateTime
+	}
+	return nil
+}
+
 // Search fields are the fields which Open Match is aware of, and can be used
 // when specifying filters.
 type SearchFields struct {
@@ -386,17 +397,23 @@ func (m *TagPresentFilter) GetTag() string {
 	return ""
 }
 
+// Pool specfies a set of criteria that are used to select a subset of Tickets
+// that meet all the criteria.
 type Pool struct {
 	// A developer-chosen human-readable name for this Pool.
 	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
-	// Set of Filters indicating the filtering criteria. Selected players must
+	// Set of Filters indicating the filtering criteria. Selected tickets must
 	// match every Filter.
-	DoubleRangeFilters   []*DoubleRangeFilter  `protobuf:"bytes,2,rep,name=double_range_filters,json=doubleRangeFilters,proto3" json:"double_range_filters,omitempty"`
-	StringEqualsFilters  []*StringEqualsFilter `protobuf:"bytes,4,rep,name=string_equals_filters,json=stringEqualsFilters,proto3" json:"string_equals_filters,omitempty"`
-	TagPresentFilters    []*TagPresentFilter   `protobuf:"bytes,5,rep,name=tag_present_filters,json=tagPresentFilters,proto3" json:"tag_present_filters,omitempty"`
-	XXX_NoUnkeyedLiteral struct{}              `json:"-"`
-	XXX_unrecognized     []byte                `json:"-"`
-	XXX_sizecache        int32                 `json:"-"`
+	DoubleRangeFilters  []*DoubleRangeFilter  `protobuf:"bytes,2,rep,name=double_range_filters,json=doubleRangeFilters,proto3" json:"double_range_filters,omitempty"`
+	StringEqualsFilters []*StringEqualsFilter `protobuf:"bytes,4,rep,name=string_equals_filters,json=stringEqualsFilters,proto3" json:"string_equals_filters,omitempty"`
+	TagPresentFilters   []*TagPresentFilter   `protobuf:"bytes,5,rep,name=tag_present_filters,json=tagPresentFilters,proto3" json:"tag_present_filters,omitempty"`
+	// If specified, only Tickets created before the specified time are selected.
+	CreatedBefore *timestamp.Timestamp `protobuf:"bytes,6,opt,name=created_before,json=createdBefore,proto3" json:"created_before,omitempty"`
+	// If specified, only Tickets created after the specified time are selected.
+	CreatedAfter         *timestamp.Timestamp `protobuf:"bytes,7,opt,name=created_after,json=createdAfter,proto3" json:"created_after,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}             `json:"-"`
+	XXX_unrecognized     []byte               `json:"-"`
+	XXX_sizecache        int32                `json:"-"`
 }
 
 func (m *Pool) Reset()         { *m = Pool{} }
@@ -452,6 +469,20 @@ func (m *Pool) GetTagPresentFilters() []*TagPresentFilter {
 	return nil
 }
 
+func (m *Pool) GetCreatedBefore() *timestamp.Timestamp {
+	if m != nil {
+		return m.CreatedBefore
+	}
+	return nil
+}
+
+func (m *Pool) GetCreatedAfter() *timestamp.Timestamp {
+	if m != nil {
+		return m.CreatedAfter
+	}
+	return nil
+}
+
 // A MatchProfile is Open Match's representation of a Match specification. It is
 // used to indicate the criteria for selecting players for a match. A
 // MatchProfile is the input to the API to get matches and is passed to the
@@ -621,52 +652,57 @@ func init() {
 func init() { proto.RegisterFile("api/messages.proto", fileDescriptor_cb9fb1f207fd5b8c) }
 
 var fileDescriptor_cb9fb1f207fd5b8c = []byte{
-	// 747 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x55, 0xdd, 0x6e, 0xd3, 0x30,
-	0x14, 0x56, 0x7e, 0xfa, 0x77, 0xda, 0x6d, 0x99, 0xb7, 0x69, 0x5d, 0x61, 0xa8, 0x04, 0x26, 0x2a,
-	0x10, 0xa9, 0x34, 0x84, 0x84, 0x10, 0x48, 0x14, 0xd1, 0xc1, 0x86, 0x80, 0x91, 0x4d, 0x5c, 0x70,
-	0x53, 0xb9, 0x8d, 0x9b, 0x45, 0x4b, 0x9d, 0x10, 0xbb, 0xd3, 0xfa, 0x16, 0x3c, 0x07, 0xdc, 0x72,
-	0xcd, 0x53, 0xf0, 0x26, 0xbc, 0x00, 0x8a, 0x9d, 0xa6, 0x5e, 0x5b, 0xe0, 0x72, 0x77, 0xce, 0xf9,
-	0xf9, 0xce, 0x77, 0xbe, 0x73, 0xec, 0x00, 0xc2, 0x71, 0xd0, 0x1e, 0x11, 0xc6, 0xb0, 0x4f, 0x98,
-	0x13, 0x27, 0x11, 0x8f, 0x50, 0x25, 0x8a, 0x09, 0x1d, 0x61, 0x3e, 0x38, 0x6b, 0x6c, 0xfb, 0x51,
-	0xe4, 0x87, 0xa4, 0x9d, 0xc4, 0x83, 0x36, 0xe3, 0x98, 0x8f, 0xb3, 0x98, 0xc6, 0x4e, 0xe6, 0x10,
-	0x5f, 0xfd, 0xf1, 0xb0, 0x8d, 0xe9, 0x44, 0xba, 0xec, 0xef, 0x3a, 0x14, 0x4f, 0x83, 0xc1, 0x39,
-	0xe1, 0x68, 0x15, 0xf4, 0xc0, 0xab, 0x6b, 0x4d, 0xad, 0x55, 0x71, 0xf5, 0xc0, 0x43, 0x8f, 0x01,
-	0x30, 0x63, 0x81, 0x4f, 0x47, 0x84, 0xf2, 0xba, 0xd1, 0xd4, 0x5a, 0xd5, 0xfd, 0x2d, 0x27, 0x2f,
-	0xe7, 0x74, 0x72, 0xa7, 0xab, 0x04, 0xa2, 0x67, 0xb0, 0xc2, 0x08, 0x4e, 0x06, 0x67, 0xbd, 0x61,
-	0x40, 0x42, 0x8f, 0xd5, 0x4d, 0x91, 0xb9, 0xad, 0x64, 0x9e, 0x08, 0xff, 0x81, 0x70, 0xbb, 0x35,
-	0xa6, 0x7c, 0xa1, 0x0e, 0x00, 0xb9, 0xe4, 0x84, 0xb2, 0x20, 0xa2, 0xac, 0x5e, 0x68, 0x1a, 0xad,
-	0xea, 0xfe, 0x6d, 0x25, 0x55, 0x72, 0x75, 0xba, 0x79, 0x4c, 0x97, 0xf2, 0x64, 0xe2, 0x2a, 0x49,
-	0x8d, 0x13, 0x58, 0x9b, 0x73, 0x23, 0x0b, 0x8c, 0x73, 0x32, 0xc9, 0x7a, 0x4b, 0x8f, 0xe8, 0x3e,
-	0x14, 0x2e, 0x70, 0x38, 0x26, 0x75, 0x5d, 0xb0, 0xdb, 0x74, 0xa4, 0x44, 0xce, 0x54, 0x22, 0xa7,
-	0x43, 0x27, 0xae, 0x0c, 0x79, 0xaa, 0x3f, 0xd1, 0x8e, 0xcc, 0xb2, 0x6e, 0x19, 0xf6, 0x0f, 0x1d,
-	0x6a, 0x2a, 0x79, 0xf4, 0x06, 0xaa, 0x5e, 0x34, 0xee, 0x87, 0xa4, 0x87, 0x13, 0x9f, 0xd5, 0x35,
-	0xc1, 0xf7, 0xde, 0x5f, 0x5a, 0x75, 0x5e, 0x89, 0xd0, 0x4e, 0xe2, 0x4f, 0x59, 0x7b, 0xb9, 0x21,
-	0x45, 0x62, 0x3c, 0x09, 0xa8, 0x2f, 0x91, 0xf4, 0x7f, 0x23, 0x9d, 0x88, 0x50, 0x05, 0x89, 0xe5,
-	0x06, 0x84, 0xc0, 0xe4, 0xd8, 0x67, 0x75, 0xa3, 0x69, 0xb4, 0x2a, 0xae, 0x38, 0x37, 0x9e, 0xc3,
-	0xda, 0x5c, 0xf1, 0x25, 0x9a, 0x6c, 0xaa, 0x9a, 0x68, 0x4a, 0xf7, 0x69, 0xfa, 0x5c, 0xc5, 0xff,
-	0xa5, 0x57, 0x94, 0x74, 0xfb, 0x97, 0x06, 0x30, 0xdb, 0x16, 0x74, 0x0b, 0x60, 0x10, 0x51, 0x4a,
-	0x06, 0x3c, 0x88, 0x68, 0x86, 0xa0, 0x58, 0x50, 0xf7, 0xca, 0x0e, 0x98, 0x42, 0x89, 0xbd, 0xa5,
-	0x8b, 0x77, 0x4d, 0x7b, 0x70, 0x64, 0x96, 0x0d, 0xcb, 0xb4, 0x3f, 0xc1, 0xba, 0x14, 0xd5, 0xc5,
-	0xd4, 0x27, 0x07, 0x41, 0xc8, 0x49, 0x82, 0x76, 0x01, 0x66, 0x1b, 0x91, 0x55, 0xaa, 0xe4, 0x73,
-	0x4e, 0x19, 0x8c, 0xf0, 0x65, 0xa6, 0x70, 0x7a, 0x14, 0x96, 0x80, 0x8a, 0xfb, 0x95, 0x5a, 0x02,
-	0x6a, 0x1f, 0x02, 0x92, 0x6a, 0x77, 0xbf, 0x8c, 0x71, 0xc8, 0x66, 0xc0, 0xb3, 0x05, 0x99, 0x02,
-	0xe7, 0x63, 0x5f, 0xae, 0xbe, 0x7d, 0x17, 0xac, 0x53, 0xec, 0x1f, 0x27, 0x84, 0x11, 0xca, 0x33,
-	0x20, 0x0b, 0x0c, 0x8e, 0xa7, 0x08, 0xe9, 0xd1, 0xfe, 0xaa, 0x83, 0x79, 0x1c, 0x45, 0x61, 0xba,
-	0x3a, 0x14, 0x8f, 0x48, 0xe6, 0x13, 0x67, 0xf4, 0x1e, 0x36, 0xb3, 0x86, 0x92, 0xb4, 0xcd, 0xde,
-	0x50, 0xa0, 0x4c, 0x37, 0xf4, 0xa6, 0x32, 0x97, 0x05, 0x31, 0x5c, 0xe4, 0xcd, 0x9b, 0x18, 0xfa,
-	0x08, 0x5b, 0x59, 0x1f, 0x44, 0xb4, 0x97, 0x03, 0xca, 0x41, 0xef, 0xaa, 0x2b, 0xbf, 0xa0, 0x82,
-	0xbb, 0xc1, 0x16, 0x6c, 0x0c, 0xbd, 0x85, 0x0d, 0x8e, 0xfd, 0x5e, 0x2c, 0xdb, 0xcc, 0x01, 0xe5,
-	0xeb, 0x71, 0x43, 0x7d, 0x3d, 0xe6, 0xb4, 0x70, 0xd7, 0xf9, 0x9c, 0x85, 0x65, 0xb3, 0xfd, 0xad,
-	0x41, 0xed, 0x5d, 0x9a, 0x73, 0x9c, 0x44, 0xc3, 0x20, 0x24, 0x4b, 0xa5, 0xd9, 0x83, 0x42, 0x1c,
-	0x45, 0xa1, 0xbc, 0x6a, 0xd5, 0xfd, 0x35, 0xa5, 0x52, 0x2a, 0xa7, 0x2b, 0xbd, 0xe8, 0xf5, 0x92,
-	0x37, 0x4d, 0xbd, 0xd9, 0x6a, 0x9d, 0xeb, 0xdb, 0x68, 0xd3, 0x2a, 0xd8, 0x3f, 0x75, 0x28, 0x08,
-	0x36, 0x68, 0x07, 0xca, 0x82, 0x5c, 0x2f, 0xff, 0x25, 0x94, 0xc4, 0xf7, 0xa1, 0x87, 0xee, 0xc0,
-	0x8a, 0x74, 0xc5, 0x92, 0x72, 0xb6, 0x71, 0xb5, 0x91, 0x2a, 0xd7, 0x1e, 0xac, 0xca, 0xa0, 0xe1,
-	0x98, 0xca, 0x7b, 0x6e, 0x88, 0x28, 0x99, 0x7a, 0x90, 0x19, 0xd1, 0x03, 0x28, 0x71, 0xf1, 0xa2,
-	0x4f, 0xc7, 0xbf, 0xbe, 0xf0, 0xd6, 0xbb, 0xd3, 0x08, 0xf4, 0xe2, 0x8a, 0x8e, 0x25, 0x11, 0xdf,
-	0x9c, 0xd7, 0xf1, 0x3a, 0x04, 0x2c, 0x58, 0xc5, 0x23, 0xb3, 0x5c, 0xb4, 0x4a, 0x2f, 0x9d, 0xcf,
-	0xcd, 0x94, 0xcf, 0x43, 0x49, 0xc8, 0x23, 0x17, 0xed, 0xd9, 0x67, 0x3b, 0x3e, 0xf7, 0xdb, 0x71,
-	0xff, 0x9b, 0x5e, 0xf9, 0x10, 0x13, 0x2a, 0xc8, 0xf6, 0x8b, 0x02, 0xf4, 0xd1, 0x9f, 0x00, 0x00,
-	0x00, 0xff, 0xff, 0x57, 0x22, 0xdc, 0xde, 0xda, 0x07, 0x00, 0x00,
+	// 830 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x55, 0xdd, 0x6e, 0xe3, 0x44,
+	0x14, 0x56, 0x6c, 0x27, 0x69, 0x4e, 0xd2, 0xd6, 0x9d, 0xed, 0x6a, 0xbd, 0x81, 0x85, 0x60, 0xa8,
+	0xa8, 0x40, 0x38, 0x52, 0x11, 0x12, 0xe2, 0x47, 0x90, 0x15, 0x2d, 0x6c, 0x11, 0x50, 0xdc, 0x8a,
+	0x0b, 0x6e, 0xac, 0x49, 0x3c, 0xf1, 0x5a, 0xb5, 0xc7, 0xc6, 0x33, 0x59, 0x6d, 0xde, 0x83, 0xa7,
+	0xe0, 0x9a, 0x6b, 0x9e, 0x82, 0x87, 0xe0, 0x9e, 0x17, 0x40, 0xf3, 0x63, 0x67, 0xd6, 0x09, 0xbb,
+	0xdc, 0xa0, 0xde, 0xcd, 0x9c, 0x9f, 0x6f, 0xce, 0xf9, 0xce, 0xe7, 0x63, 0x40, 0xb8, 0x4c, 0xa7,
+	0x39, 0x61, 0x0c, 0x27, 0x84, 0x05, 0x65, 0x55, 0xf0, 0x02, 0x0d, 0x8a, 0x92, 0xd0, 0x1c, 0xf3,
+	0xc5, 0xd3, 0xf1, 0x83, 0xa4, 0x28, 0x92, 0x8c, 0x4c, 0xab, 0x72, 0x31, 0x65, 0x1c, 0xf3, 0x95,
+	0x8e, 0x19, 0x3f, 0xd4, 0x0e, 0x79, 0x9b, 0xaf, 0x96, 0x53, 0x4c, 0xd7, 0xda, 0xf5, 0x66, 0xdb,
+	0xc5, 0xd3, 0x9c, 0x30, 0x8e, 0xf3, 0x52, 0x05, 0xf8, 0x7f, 0x59, 0xd0, 0xbb, 0x49, 0x17, 0xb7,
+	0x84, 0xa3, 0x03, 0xb0, 0xd2, 0xd8, 0xeb, 0x4c, 0x3a, 0xa7, 0x83, 0xd0, 0x4a, 0x63, 0xf4, 0x11,
+	0x00, 0x66, 0x2c, 0x4d, 0x68, 0x4e, 0x28, 0xf7, 0xec, 0x49, 0xe7, 0x74, 0x78, 0x76, 0x3f, 0x68,
+	0xea, 0x09, 0x66, 0x8d, 0x33, 0x34, 0x02, 0xd1, 0x67, 0xb0, 0xcf, 0x08, 0xae, 0x16, 0x4f, 0xa3,
+	0x65, 0x4a, 0xb2, 0x98, 0x79, 0x8e, 0xcc, 0x7c, 0x60, 0x64, 0x5e, 0x4b, 0xff, 0x85, 0x74, 0x87,
+	0x23, 0x66, 0xdc, 0xd0, 0x0c, 0x80, 0x3c, 0xe7, 0x84, 0xb2, 0xb4, 0xa0, 0xcc, 0xeb, 0x4e, 0xec,
+	0xd3, 0xe1, 0xd9, 0x5b, 0x46, 0xaa, 0xaa, 0x35, 0x38, 0x6f, 0x62, 0xce, 0x29, 0xaf, 0xd6, 0xa1,
+	0x91, 0x84, 0x3e, 0x85, 0xe1, 0xa2, 0x22, 0x98, 0x93, 0x48, 0x34, 0xeb, 0xf5, 0xe4, 0xf3, 0xe3,
+	0x40, 0x31, 0x11, 0xd4, 0x4c, 0x04, 0x37, 0x35, 0x13, 0x21, 0xa8, 0x70, 0x61, 0x18, 0x5f, 0xc3,
+	0x61, 0x0b, 0x1b, 0xb9, 0x60, 0xdf, 0x92, 0xb5, 0x26, 0x46, 0x1c, 0xd1, 0x7b, 0xd0, 0x7d, 0x86,
+	0xb3, 0x15, 0xf1, 0x2c, 0x89, 0x7d, 0xbc, 0x85, 0x3d, 0xa3, 0xeb, 0x50, 0x85, 0x7c, 0x62, 0x7d,
+	0xdc, 0xb9, 0x74, 0xf6, 0x2c, 0xd7, 0xf6, 0x7f, 0xb7, 0x60, 0x64, 0x76, 0x8e, 0xbe, 0x81, 0x61,
+	0x5c, 0xac, 0xe6, 0x19, 0x89, 0x70, 0x95, 0x30, 0xaf, 0x23, 0x9b, 0x7d, 0xf7, 0x5f, 0x78, 0x0a,
+	0xbe, 0x92, 0xa1, 0xb3, 0x2a, 0xa9, 0x5b, 0x8e, 0x1b, 0x83, 0x40, 0x62, 0xbc, 0x4a, 0x69, 0xa2,
+	0x90, 0xac, 0x97, 0x23, 0x5d, 0xcb, 0x50, 0x03, 0x89, 0x35, 0x06, 0x84, 0xc0, 0xe1, 0x38, 0x61,
+	0x9e, 0x3d, 0xb1, 0x4f, 0x07, 0xa1, 0x3c, 0x8f, 0x3f, 0x87, 0xc3, 0xd6, 0xe3, 0x3b, 0x38, 0x39,
+	0x36, 0x39, 0xe9, 0x18, 0xdd, 0x8b, 0xf4, 0xd6, 0x8b, 0xaf, 0x4a, 0x1f, 0x18, 0xe9, 0xfe, 0x9f,
+	0x1d, 0x80, 0x8d, 0xd4, 0xd0, 0x1b, 0x00, 0x8b, 0x82, 0x52, 0xb2, 0xe0, 0x69, 0x41, 0x35, 0x82,
+	0x61, 0x41, 0xe7, 0x2f, 0x08, 0xc8, 0x91, 0x4c, 0x9c, 0xec, 0x54, 0xed, 0xcb, 0x44, 0xf4, 0x3f,
+	0xea, 0xe0, 0xd2, 0xd9, 0xb3, 0x5d, 0xc7, 0xff, 0x09, 0x8e, 0x14, 0xa9, 0x21, 0xa6, 0x09, 0xb9,
+	0x48, 0x33, 0x4e, 0x2a, 0xf4, 0x08, 0x60, 0xa3, 0x08, 0xfd, 0xd2, 0xa0, 0x99, 0xb3, 0xa8, 0x20,
+	0xc7, 0xcf, 0x35, 0xc3, 0xe2, 0x28, 0x2d, 0x29, 0x95, 0x1f, 0xa7, 0xb0, 0xa4, 0xd4, 0x7f, 0x02,
+	0x48, 0xb1, 0x7d, 0xfe, 0xcb, 0x0a, 0x67, 0x6c, 0x03, 0xbc, 0x11, 0x48, 0x0d, 0xdc, 0x8c, 0x7d,
+	0x37, 0xfb, 0xfe, 0x3b, 0xe0, 0xde, 0xe0, 0xe4, 0xaa, 0x22, 0x8c, 0x50, 0xae, 0x81, 0x5c, 0xb0,
+	0x39, 0xae, 0x11, 0xc4, 0xd1, 0xff, 0xd5, 0x06, 0xe7, 0xaa, 0x28, 0x32, 0x21, 0x1d, 0x8a, 0x73,
+	0xa2, 0x7d, 0xf2, 0x8c, 0xbe, 0x87, 0x63, 0xdd, 0x50, 0x25, 0xda, 0x8c, 0x96, 0x12, 0xa5, 0x56,
+	0xe8, 0xeb, 0xc6, 0x5c, 0xb6, 0xc8, 0x08, 0x51, 0xdc, 0x36, 0x31, 0xf4, 0x23, 0xdc, 0xd7, 0x7d,
+	0x10, 0xd9, 0x5e, 0x03, 0xa8, 0x06, 0xfd, 0xc8, 0x94, 0xfc, 0x16, 0x0b, 0xe1, 0x3d, 0xb6, 0x65,
+	0x63, 0xe8, 0x5b, 0xb8, 0xc7, 0x71, 0x12, 0x95, 0xaa, 0xcd, 0x06, 0x50, 0xad, 0x9e, 0xd7, 0xcc,
+	0xd5, 0xd3, 0xe2, 0x22, 0x3c, 0xe2, 0x2d, 0x8b, 0x58, 0x5f, 0x07, 0x6a, 0x99, 0xc4, 0xd1, 0x9c,
+	0x2c, 0x8b, 0xea, 0xbf, 0xac, 0x9f, 0x7d, 0x9d, 0xf1, 0x58, 0x26, 0xa0, 0x2f, 0xa0, 0x36, 0x44,
+	0x78, 0xc9, 0x49, 0xe5, 0xf5, 0x5f, 0x89, 0x30, 0xd2, 0x09, 0x33, 0x11, 0xaf, 0xf5, 0xf5, 0x77,
+	0x07, 0x46, 0xdf, 0x89, 0xba, 0xaf, 0xaa, 0x62, 0x99, 0x66, 0x64, 0xe7, 0x78, 0x4e, 0xa0, 0x5b,
+	0x16, 0x45, 0xa6, 0x3e, 0xf7, 0xe1, 0xd9, 0xa1, 0xd1, 0xad, 0x18, 0x69, 0xa8, 0xbc, 0xe8, 0xeb,
+	0x1d, 0x4b, 0xd9, 0xdc, 0x2e, 0xe6, 0x3b, 0x77, 0xf7, 0x55, 0x39, 0x6e, 0xd7, 0xff, 0xc3, 0x82,
+	0xae, 0xac, 0x06, 0x3d, 0x84, 0x3d, 0x59, 0x5c, 0xd4, 0xfc, 0xd3, 0xfa, 0xf2, 0xfe, 0x24, 0x46,
+	0x6f, 0xc3, 0xbe, 0x72, 0x95, 0xaa, 0x64, 0xad, 0xfa, 0x51, 0x6e, 0xd2, 0x75, 0x02, 0x07, 0x2a,
+	0x68, 0xb9, 0xa2, 0x6a, 0xd7, 0xd8, 0x32, 0x4a, 0xa5, 0x5e, 0x68, 0x23, 0x7a, 0x1f, 0xfa, 0x5c,
+	0xfe, 0x92, 0x6a, 0x09, 0x1e, 0x6d, 0xfd, 0xac, 0xc2, 0x3a, 0x02, 0x7d, 0xf9, 0x02, 0x8f, 0x7d,
+	0x19, 0x3f, 0x69, 0xf3, 0x78, 0x17, 0x04, 0x76, 0xdd, 0xde, 0xa5, 0xb3, 0xd7, 0x73, 0xfb, 0x8f,
+	0x83, 0x9f, 0x27, 0xa2, 0x9e, 0x0f, 0x54, 0x41, 0x31, 0x79, 0x36, 0xdd, 0x5c, 0xa7, 0xe5, 0x6d,
+	0x32, 0x2d, 0xe7, 0xbf, 0x59, 0x83, 0x1f, 0x4a, 0x42, 0x65, 0xb1, 0xf3, 0x9e, 0x04, 0xfd, 0xf0,
+	0x9f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7f, 0x53, 0xe9, 0x37, 0xbc, 0x08, 0x00, 0x00,
 }


Test patch:
diff --git a/internal/filter/filter_test.go b/internal/filter/filter_test.go
index 77a0f9b3..5a3ebf07 100644
--- a/internal/filter/filter_test.go
+++ b/internal/filter/filter_test.go
@@ -17,14 +17,25 @@ package filter
 import (
 	"testing"
 
+	"github.com/golang/protobuf/ptypes"
+	"github.com/golang/protobuf/ptypes/timestamp"
+	"github.com/stretchr/testify/assert"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
 	"open-match.dev/open-match/internal/filter/testcases"
+	"open-match.dev/open-match/pkg/pb"
 )
 
-func TestInPool(t *testing.T) {
+func TestMeetsCriteria(t *testing.T) {
 	for _, tc := range testcases.IncludedTestCases() {
 		tc := tc
 		t.Run(tc.Name, func(t *testing.T) {
-			if !InPool(tc.Ticket, tc.Pool) {
+			pf, err := NewPoolFilter(tc.Pool)
+			if err != nil {
+				t.Error("pool should be valid")
+			}
+			tc.Ticket.CreateTime = ptypes.TimestampNow()
+			if !pf.In(tc.Ticket) {
 				t.Error("ticket should be included in the pool")
 			}
 		})
@@ -33,9 +44,49 @@ func TestInPool(t *testing.T) {
 	for _, tc := range testcases.ExcludedTestCases() {
 		tc := tc
 		t.Run(tc.Name, func(t *testing.T) {
-			if InPool(tc.Ticket, tc.Pool) {
+			pf, err := NewPoolFilter(tc.Pool)
+			if err != nil {
+				t.Error("pool should be valid")
+			}
+			tc.Ticket.CreateTime = ptypes.TimestampNow()
+			if pf.In(tc.Ticket) {
 				t.Error("ticket should be excluded from the pool")
 			}
 		})
 	}
 }
+
+func TestValidPoolFilter(t *testing.T) {
+	for _, tc := range []struct {
+		name string
+		pool *pb.Pool
+		code codes.Code
+		msg  string
+	}{
+		{
+			"invalid create before",
+			&pb.Pool{
+				CreatedBefore: &timestamp.Timestamp{Nanos: -1},
+			},
+			codes.InvalidArgument,
+			".invalid created_before value",
+		},
+		{
+			"invalid create after",
+			&pb.Pool{
+				CreatedAfter: &timestamp.Timestamp{Nanos: -1},
+			},
+			codes.InvalidArgument,
+			".invalid created_after value",
+		},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			pf, err := NewPoolFilter(tc.pool)
+			assert.Nil(t, pf)
+			s := status.Convert(err)
+			assert.Equal(t, tc.code, s.Code())
+			assert.Equal(t, tc.msg, s.Message())
+		})
+	}
+}
diff --git a/internal/filter/testcases/testcases.go b/internal/filter/testcases/testcases.go
index 85aa603e..4d180411 100644
--- a/internal/filter/testcases/testcases.go
+++ b/internal/filter/testcases/testcases.go
@@ -18,7 +18,10 @@ package testcases
 import (
 	"fmt"
 	"math"
+	"time"
 
+	"github.com/golang/protobuf/ptypes"
+	tspb "github.com/golang/protobuf/ptypes/timestamp"
 	"open-match.dev/open-match/pkg/pb"
 )
 
@@ -32,6 +35,7 @@ type TestCase struct {
 // IncludedTestCases returns a list of test cases where using the given filter,
 // the ticket is included in the result.
 func IncludedTestCases() []TestCase {
+	now := time.Now()
 	return []TestCase{
 		{
 			"no filters or fields",
@@ -106,12 +110,41 @@ func IncludedTestCases() []TestCase {
 		},
 
 		multipleFilters(true, true, true),
+
+		{
+			"CreatedBefore simple positive",
+			&pb.Ticket{},
+			&pb.Pool{
+				CreatedBefore: timestamp(now.Add(time.Hour * 1)),
+			},
+		},
+		{
+			"CreatedAfter simple positive",
+			&pb.Ticket{},
+			&pb.Pool{
+				CreatedAfter: timestamp(now.Add(time.Hour * -1)),
+			},
+		},
+		{
+			"Between CreatedBefore and CreatedAfter positive",
+			&pb.Ticket{},
+			&pb.Pool{
+				CreatedBefore: timestamp(now.Add(time.Hour * 1)),
+				CreatedAfter:  timestamp(now.Add(time.Hour * -1)),
+			},
+		},
+		{
+			"No time search criteria positive",
+			&pb.Ticket{},
+			&pb.Pool{},
+		},
 	}
 }
 
 // ExcludedTestCases returns a list of test cases where using the given filter,
 // the ticket is NOT included in the result.
 func ExcludedTestCases() []TestCase {
+	now := time.Now()
 	return []TestCase{
 		{
 			"DoubleRange no SearchFields",
@@ -259,6 +292,37 @@ func ExcludedTestCases() []TestCase {
 			},
 		},
 
+		{
+			"CreatedBefore simple negative",
+			&pb.Ticket{},
+			&pb.Pool{
+				CreatedBefore: timestamp(now.Add(time.Hour * -1)),
+			},
+		},
+		{
+			"CreatedAfter simple negative",
+			&pb.Ticket{},
+			&pb.Pool{
+				CreatedAfter: timestamp(now.Add(time.Hour * 1)),
+			},
+		},
+		{
+			"Created before time range negative",
+			&pb.Ticket{},
+			&pb.Pool{
+				CreatedBefore: timestamp(now.Add(time.Hour * 2)),
+				CreatedAfter:  timestamp(now.Add(time.Hour * 1)),
+			},
+		},
+		{
+			"Created after time range negative",
+			&pb.Ticket{},
+			&pb.Pool{
+				CreatedBefore: timestamp(now.Add(time.Hour * -1)),
+				CreatedAfter:  timestamp(now.Add(time.Hour * -2)),
+			},
+		},
+
 		multipleFilters(false, true, true),
 		multipleFilters(true, false, true),
 		multipleFilters(true, true, false),
@@ -338,3 +402,12 @@ func multipleFilters(doubleRange, stringEquals, tagPresent bool) TestCase {
 		},
 	}
 }
+
+func timestamp(t time.Time) *tspb.Timestamp {
+	tsp, err := ptypes.TimestampProto(t)
+	if err != nil {
+		panic(err)
+	}
+
+	return tsp
+}
diff --git a/test/e2e/ticket_test.go b/test/e2e/ticket_test.go
index fa334cad..a9425171 100644
--- a/test/e2e/ticket_test.go
+++ b/test/e2e/ticket_test.go
@@ -22,6 +22,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/golang/protobuf/ptypes"
 	"github.com/stretchr/testify/assert"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
@@ -301,6 +302,16 @@ func TestCreateTicketErrors(t *testing.T) {
 			codes.InvalidArgument,
 			"tickets cannot be created with an assignment",
 		},
+		{
+			"already has create time",
+			&pb.CreateTicketRequest{
+				Ticket: &pb.Ticket{
+					CreateTime: ptypes.TimestampNow(),
+				},
+			},
+			codes.InvalidArgument,
+			"tickets cannot be created with create time set",
+		},
 	} {
 		tt := tt
 		t.Run(tt.name, func(t *testing.T) {