Syncing between source head and git
diff --git a/api/api.go b/api/api.go index d419c40..61e0129 100644 --- a/api/api.go +++ b/api/api.go
@@ -1,5 +1,5 @@ /* -Copyright 2017 Google Inc. +Copyright 2019 Google Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import ( "bytes" + "context" "crypto/tls" "crypto/x509" "encoding/base64" @@ -33,33 +34,27 @@ "strings" "time" + epb "github.com/golang/protobuf/ptypes/empty" "github.com/golang/protobuf/jsonpb" "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/ptypes/empty" "github.com/google/go-cmp/cmp" + "github.com/maps-booking-v3/utils" fpb "github.com/maps-booking-v3/feeds" mpb "github.com/maps-booking-v3/v3" - wpb "github.com/maps-booking-v3/waitlist" + wpb "github.com/maps-booking-v3/v3waitlist" ) const ( - userID = "0" - firstName = "Jane" - lastName = "Doe" - telephone = "+1 800-789-7890" - email = "test@example.com" + userID = "0" + firstName = "Jane" + lastName = "Doe" + telephone = "+1 800-789-7890" + email = "test@example.com" + reqTimeout = 10 * time.Second ) -// HTTPConnection is a convenience struct for holding connection-related objects. -type HTTPConnection struct { - client *http.Client - credentials string - marshaler *jsonpb.Marshaler - baseURL string -} - func setupCertConfig(caFile string, fullServerName string) (*tls.Config, error) { if caFile == "" { return nil, nil @@ -78,9 +73,9 @@ }, nil } -// InitHTTPConnection creates and returns a new HTTPConnection object +// InitHTTPConnection creates and returns a new utils.HTTPConnection object // with a given server address and username/password. -func InitHTTPConnection(serverAddr string, credentialsFile string, caFile string, fullServerName string) (*HTTPConnection, error) { +func InitHTTPConnection(serverAddr string, credentialsFile string, caFile string, fullServerName string) (*utils.HTTPConnection, error) { // Set up username/password. var credentials string if credentialsFile != "" { @@ -98,24 +93,16 @@ if config != nil { protocol = "https" } - return &HTTPConnection{ - client: &http.Client{ - Timeout: 10 * time.Second, + return &utils.HTTPConnection{ + Client: &http.Client{ Transport: &http.Transport{TLSClientConfig: config}, }, - credentials: credentials, - marshaler: &jsonpb.Marshaler{OrigName: true}, - baseURL: protocol + "://" + serverAddr, + Credentials: credentials, + Marshaler: &jsonpb.Marshaler{OrigName: true}, + BaseURL: protocol + "://" + serverAddr, }, nil } -func (h HTTPConnection) getURL(rpcName string) string { - if rpcName != "" { - return h.baseURL + "/v3/" + rpcName - } - return h.baseURL -} - // Bookings is a convenience type for a booking array. type Bookings []*mpb.Booking @@ -132,32 +119,58 @@ } // HealthCheck performs a health check. -func HealthCheck(conn *HTTPConnection) error { - utils.LogFlow("Health Check", "Start") - defer utils.LogFlow("Health Check", "End") +func HealthCheck(ctx context.Context, logger *log.Logger, conn *utils.HTTPConnection) error { + utils.LogFlow(logger, "Health Check", "Start") + defer utils.LogFlow(logger, "Health Check", "End") - httpReq, err := http.NewRequest("GET", conn.getURL("HealthCheck"), nil) - httpReq.Header.Set("Authorization", conn.credentials) + fmt.Println(conn.GetURL("HealthCheck")) + httpReq, err := http.NewRequest("GET", conn.GetURL("HealthCheck"), nil) + reqCtx, cancel := context.WithTimeout(ctx, reqTimeout) + defer cancel() + httpReq = httpReq.WithContext(reqCtx) + httpReq.Header.Set("Authorization", conn.Credentials) // See if we get a response. - resp, err := conn.client.Do(httpReq) + resp, err := conn.Client.Do(httpReq) + if err := ctx.Err(); err != nil { + panic(fmt.Sprintf("Encountered context error: %s", err.Error())) + } if err != nil { return fmt.Errorf("Health check failed to connect to server: %v", err) } else if resp.StatusCode != 200 { return fmt.Errorf("Health check returned unhealthy status: %s", resp.Status) } - log.Printf("health check success! Got status: %s", resp.Status) + logger.Printf("health check success! Got status: %s", resp.Status) return nil } -// sendRequest sets up and sends the relevant HTTP request to the server and returns the HTTP response. -func sendRequest(rpcName string, req string, conn *HTTPConnection) (string, error) { - httpReq, err := http.NewRequest("POST", conn.getURL(rpcName), bytes.NewBuffer([]byte(req))) - httpReq.Header.Set("Content-Type", "application/json") - httpReq.Header.Set("Authorization", conn.credentials) - log.Printf("%v Request. Sent(unix): %s, Url: %v, Method: %v, Header: %v, Body: %v\n", rpcName, time.Now().UTC().Format(time.RFC850), httpReq.URL, httpReq.Method, httpReq.Header, httpReq.Body) +func getLogSafeHeader(header http.Header) http.Header { + logHeader := make(http.Header) + for k, v := range header { + if k == "Authorization" { + logHeader.Set("Authorization", "VALUE REDACTED.") + continue + } + logHeader[k] = v + } + return logHeader +} - httpResp, err := conn.client.Do(httpReq) +// sendRequest sets up and sends the relevant HTTP request to the server and returns the HTTP response. +func sendRequest(ctx context.Context, logger *log.Logger, rpcName string, req string, conn *utils.HTTPConnection) (string, error) { + httpReq, err := http.NewRequest("POST", conn.GetURL(rpcName), bytes.NewBuffer([]byte(req))) + reqCtx, cancel := context.WithTimeout(ctx, reqTimeout) + defer cancel() + httpReq = httpReq.WithContext(reqCtx) + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Authorization", conn.Credentials) + logHeader := getLogSafeHeader(httpReq.Header) + logger.Printf("%v Request. Sent(unix): %s, Url: %v, Method: %v, Header: %v, Body: %v\n", rpcName, time.Now().UTC().Format(time.RFC850), httpReq.URL, httpReq.Method, logHeader, httpReq.Body) + + httpResp, err := conn.Client.Do(httpReq) + if err := ctx.Err(); err != nil { + panic(fmt.Sprintf("Encountered context error: %s", err.Error())) + } if err != nil { return "", fmt.Errorf("invalid response. %s yielded error: %v", rpcName, err) } @@ -167,13 +180,13 @@ return "", fmt.Errorf("Could not read http response body") } bodyString := string(bodyBytes) - log.Printf("%v Response. Received(unix): %s, Response %v\n", rpcName, time.Now().UTC().Format(time.RFC850), bodyString) + logger.Printf("%v Response. Received(unix): %s, Response %v\n", rpcName, time.Now().UTC().Format(time.RFC850), bodyString) return bodyString, nil } // CheckAvailability performs a maps booking availability check on all supplied availability slots. This function // returns any errors when trying to call the CheckAvailability RPC. -func CheckAvailability(a *fpb.Availability, conn *HTTPConnection) error { +func CheckAvailability(ctx context.Context, logger *log.Logger, a *fpb.Availability, conn *utils.HTTPConnection) error { slot, err := utils.BuildSlotFrom(a) if err != nil { return fmt.Errorf("unable to build request for check availability flow. err: %v, availability record: %v", err, a.String()) @@ -181,11 +194,11 @@ reqPB := &mpb.CheckAvailabilityRequest{ Slot: slot, } - req, err := conn.marshaler.MarshalToString(reqPB) + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) } - httpResp, err := sendRequest("CheckAvailability", req, conn) + httpResp, err := sendRequest(ctx, logger, "CheckAvailability", req, conn) if err != nil { return fmt.Errorf("invalid response. CheckAvailability yielded error: %v", err) } @@ -207,16 +220,16 @@ // BatchAvailabilityLookup performs a maps booking batch availability lookup on all supplied availability slots. This function // returns any errors when trying to call the BatchAvailabilityLookup RPC. -func BatchAvailabilityLookup(av []*fpb.Availability, conn *HTTPConnection) error { +func BatchAvailabilityLookup(ctx context.Context, logger *log.Logger, av []*fpb.Availability, conn *utils.HTTPConnection) error { reqPB, err := utils.BuildBatchAvailabilityLookupRequestFrom(av) if err != nil { return fmt.Errorf("unable to build request for batch availability lookup flow. err: %v, availability records: %v", err, av) } - req, err := conn.marshaler.MarshalToString(reqPB) + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) } - httpResp, err := sendRequest("BatchAvailabilityLookup", req, conn) + httpResp, err := sendRequest(ctx, logger, "BatchAvailabilityLookup", req, conn) if err != nil { return fmt.Errorf("invalid response. BatchAvailabilityLookup yielded error: %v", err) } @@ -232,7 +245,7 @@ slotTimeReq := reqPB.GetSlotTime()[i] slotTimeResp := resp.GetSlotTimeAvailability()[i].GetSlotTime() if diff := cmp.Diff(slotTimeReq, slotTimeResp, cmp.Comparer(proto.Equal)); diff != "" { - log.Printf("Slot %v differs: req=%v, resp=%v", i, slotTimeReq, slotTimeResp) + logger.Printf("Slot %v differs: req=%v, resp=%v", i, slotTimeReq, slotTimeResp) diffCount++ } } @@ -243,7 +256,7 @@ } // CreateBooking attempts to create bookings from availability slots. -func CreateBooking(a *fpb.Availability, conn *HTTPConnection) (*mpb.Booking, error) { +func CreateBooking(ctx context.Context, logger *log.Logger, a *fpb.Availability, conn *utils.HTTPConnection) (*mpb.Booking, error) { slot, err := utils.BuildSlotFrom(a) if err != nil { return nil, fmt.Errorf("unable to build request for check availability flow. err: %v, availability record: %v", err, a.String()) @@ -265,12 +278,12 @@ }, IdempotencyToken: strconv.Itoa(gen.Intn(1000000)), } - req, err := conn.marshaler.MarshalToString(reqPB) + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return nil, fmt.Errorf("Could not convert pb3 to json: %v", reqPB) } - httpResp, err := sendRequest("CreateBooking", req, conn) + httpResp, err := sendRequest(ctx, logger, "CreateBooking", req, conn) if err != nil { return nil, fmt.Errorf("invalid response. CreateBooking yielded error: %v", err) } @@ -293,8 +306,8 @@ } // Perform idempotency test. - log.Printf("Idempotency check -- CreateBooking Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), reqPB.String()) - idemHTTPResp, err := sendRequest("CreateBooking", req, conn) + logger.Printf("Idempotency check -- CreateBooking Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), reqPB.String()) + idemHTTPResp, err := sendRequest(ctx, logger, "CreateBooking", req, conn) if err != nil { return nil, fmt.Errorf("invalid response. Idempotency check yielded error: %v", err) } @@ -311,15 +324,15 @@ } // ListBookings calls the maps booking ListBookings rpc and compares the return with all input bookings. -func ListBookings(tB Bookings, conn *HTTPConnection) (Bookings, error) { +func ListBookings(ctx context.Context, logger *log.Logger, tB Bookings, conn *utils.HTTPConnection) (Bookings, error) { reqPB := &mpb.ListBookingsRequest{ UserId: userID, } - req, err := conn.marshaler.MarshalToString(reqPB) + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return nil, fmt.Errorf("Could not convert pb3 to json: %v", reqPB) } - httpResp, err := sendRequest("ListBookings", req, conn) + httpResp, err := sendRequest(ctx, logger, "ListBookings", req, conn) if err != nil { return nil, fmt.Errorf("invalid response. ListBookings yielded error: %v. Abandoning all booking from this flow", err) } @@ -330,37 +343,37 @@ gB := Bookings(resp.GetBookings()) if len(tB) == 0 { - log.Printf("ListBookings returning %d found bookings", len(gB)) + logger.Printf("ListBookings returning %d found bookings", len(gB)) return gB, nil } if len(gB) != len(tB) { - log.Printf("ListBookings number of bookings differed unexpectedly. Got: %d, Want: %d.", len(gB), len(tB)) + logger.Printf("ListBookings number of bookings differed unexpectedly. Got: %d, Want: %d.", len(gB), len(tB)) } sort.Sort(gB) sort.Sort(tB) var out Bookings for i := 0; i < len(tB); i++ { if iE := utils.ValidateBooking(gB[i], tB[i]); iE != nil { - log.Printf("ListBookings invalid, %s, abandoning slot %d/%d", iE.Error(), i, len(tB)) + logger.Printf("ListBookings invalid, %s, abandoning slot %d/%d", iE.Error(), i, len(tB)) continue } out = append(out, tB[i]) } - log.Printf("ListBookings returning %d bookings", len(out)) + logger.Printf("ListBookings returning %d bookings", len(out)) return out, nil } // GetBookingStatus checks that all input bookings are in an acceptable state. -func GetBookingStatus(b *mpb.Booking, conn *HTTPConnection) error { +func GetBookingStatus(ctx context.Context, logger *log.Logger, b *mpb.Booking, conn *utils.HTTPConnection) error { reqPB := &mpb.GetBookingStatusRequest{ BookingId: b.GetBookingId(), } - req, err := conn.marshaler.MarshalToString(reqPB) + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) } - httpResp, err := sendRequest("GetBookingStatus", req, conn) + httpResp, err := sendRequest(ctx, logger, "GetBookingStatus", req, conn) if err != nil { return fmt.Errorf("invalid response. GetBookingStatus yielded error: %v", err) } @@ -377,18 +390,18 @@ } // CancelBooking is a clean up method that cancels all supplied bookings. -func CancelBooking(bookingID string, conn *HTTPConnection) error { +func CancelBooking(ctx context.Context, logger *log.Logger, bookingID string, conn *utils.HTTPConnection) error { reqPB := &mpb.UpdateBookingRequest{ Booking: &mpb.Booking{ BookingId: bookingID, Status: mpb.BookingStatus_CANCELED, }, } - req, err := conn.marshaler.MarshalToString(reqPB) + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) } - httpResp, err := sendRequest("UpdateBooking", req, conn) + httpResp, err := sendRequest(ctx, logger, "UpdateBooking", req, conn) if err != nil { return fmt.Errorf("invalid response. UpdateBooking yielded error: %v", err) } @@ -397,14 +410,20 @@ return fmt.Errorf("CancelBooking: Could not parse HTTP response to pb3: %v", err) } - if iE := utils.ValidateBooking(resp.GetBooking(), reqPB.GetBooking()); iE != nil { + // We only care that the correct booking was cancelled. The rest of the response content + // is not relevant. + compareBooking := &mpb.Booking{ + BookingId: resp.GetBooking().GetBookingId(), + Status: resp.GetBooking().GetStatus(), + } + if iE := utils.ValidateBooking(compareBooking, reqPB.GetBooking()); iE != nil { return fmt.Errorf("invalid response. UpdateBooking: %s", iE.Error()) } return nil } // Rescheduling will attempt to create a booking, update the booking, then cancel. -func Rescheduling(av []*fpb.Availability, conn *HTTPConnection) error { +func Rescheduling(ctx context.Context, logger *log.Logger, av []*fpb.Availability, conn *utils.HTTPConnection) error { var slots []*fpb.Availability for _, v := range utils.BuildMerchantServiceMap(av) { // Need at least two slots for reschedule. @@ -419,7 +438,7 @@ return errors.New("no suitable availability for rescheduling flow. exiting") } // Book first slot. - newBooking, err := CreateBooking(slots[0], conn) + newBooking, err := CreateBooking(ctx, logger, slots[0], conn) if err != nil { return fmt.Errorf("could not complete booking, abandoning rescheduling flow: %v", err) } @@ -436,11 +455,11 @@ }, }, } - req, err := conn.marshaler.MarshalToString(reqPB) + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return fmt.Errorf("Rescheduling UpdateBooking: Could not convert pb3 to json: %v", reqPB) } - updateHTTPResp, err := sendRequest("UpdateBooking", req, conn) + updateHTTPResp, err := sendRequest(ctx, logger, "UpdateBooking", req, conn) var resp mpb.CreateBookingResponse if err := jsonpb.UnmarshalString(updateHTTPResp, &resp); err != nil { return fmt.Errorf("Rescheduling UpdateBooking: Could not parse HTTP response to pb3: %v", err) @@ -453,21 +472,21 @@ if iE := utils.ValidateBooking(resp.GetBooking(), newBooking); iE != nil { return fmt.Errorf("invalid response. UpdateBooking: %s, abandoning slot 1/1", iE.Error()) } - return CancelBooking(resp.GetBooking().GetBookingId(), conn) + return CancelBooking(ctx, logger, resp.GetBooking().GetBookingId(), conn) } // CheckOrderFulfillability attempts to send a CheckOrderFulfillabilityRequest // to the connection endpoint and diff the results with what are expected. -func CheckOrderFulfillability(merchantID string, lineItems []*mpb.LineItem, conn *HTTPConnection) error { +func CheckOrderFulfillability(ctx context.Context, logger *log.Logger, merchantID string, lineItems []*mpb.LineItem, conn *utils.HTTPConnection) error { reqPB := &mpb.CheckOrderFulfillabilityRequest{ MerchantId: merchantID, Item: lineItems, } - req, err := conn.marshaler.MarshalToString(reqPB) + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) } - httpResp, err := sendRequest("CheckOrderFulfillability", req, conn) + httpResp, err := sendRequest(ctx, logger, "CheckOrderFulfillability", req, conn) if err != nil { return fmt.Errorf("invalid response. CheckOrderFulfillability yielded error: %v", err) } @@ -477,7 +496,6 @@ } orderFulfillability := resp.GetFulfillability() - // TODO(ccawdrey): Add validation cases for other OrderFulFillability enums. if diff := cmp.Diff(orderFulfillability.GetResult(), mpb.OrderFulfillability_CAN_FULFILL); diff != "" { return fmt.Errorf("invalid response. CheckOrderFulfillability.Fulfillability.OrderFulfillabilityResult differ (-got +want)\n%s", diff) } @@ -505,7 +523,7 @@ } // CreateOrder will attempt to build an order from a merchant id and array of line orders. -func CreateOrder(merchantID string, lineItems []*mpb.LineItem, conn *HTTPConnection) (*mpb.Order, error) { +func CreateOrder(ctx context.Context, logger *log.Logger, merchantID string, lineItems []*mpb.LineItem, conn *utils.HTTPConnection) (*mpb.Order, error) { gen := rand.New(rand.NewSource(time.Now().UnixNano())) reqOrder := &mpb.Order{ @@ -527,11 +545,11 @@ IdempotencyToken: strconv.Itoa(gen.Intn(1000000)), } - req, err := conn.marshaler.MarshalToString(reqPB) + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return nil, fmt.Errorf("Could not convert pb3 to json: %v", reqPB) } - httpResp, err := sendRequest("CreateOrder", req, conn) + httpResp, err := sendRequest(ctx, logger, "CreateOrder", req, conn) if err != nil { return nil, fmt.Errorf("invalid response. CreateOrder yielded error: %v", err) } @@ -549,8 +567,8 @@ } // Perform idempotency test. - log.Printf("Idempotency check") - idemHTTPResp, err := sendRequest("CreateOrder", req, conn) + logger.Printf("Idempotency check") + idemHTTPResp, err := sendRequest(ctx, logger, "CreateOrder", req, conn) if err != nil { return nil, fmt.Errorf("invalid response. Idempotency check yielded error: %v", err) } @@ -569,12 +587,12 @@ return resp.GetOrder(), nil } -func sendListOrdersRequest(reqPB *mpb.ListOrdersRequest, conn *HTTPConnection) (mpb.ListOrdersResponse, error) { - req, err := conn.marshaler.MarshalToString(reqPB) +func sendListOrdersRequest(ctx context.Context, logger *log.Logger, reqPB *mpb.ListOrdersRequest, conn *utils.HTTPConnection) (mpb.ListOrdersResponse, error) { + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return mpb.ListOrdersResponse{}, fmt.Errorf("Could not convert pb3 to json: %v", reqPB) } - httpResp, err := sendRequest("ListOrders", req, conn) + httpResp, err := sendRequest(ctx, logger, "ListOrders", req, conn) if err != nil { return mpb.ListOrdersResponse{}, fmt.Errorf("invalid response. ListOrders yielded error: %v", err) } @@ -588,7 +606,7 @@ // ListOrders first checks that the number and contents of the server's order state // are consistent with what the test client assumes is present. -func ListOrders(orders []*mpb.Order, conn *HTTPConnection) error { +func ListOrders(ctx context.Context, logger *log.Logger, orders []*mpb.Order, conn *utils.HTTPConnection) error { if len(orders) == 0 { return errors.New("at least one order must be present for ListOrders to succeed") } @@ -597,7 +615,7 @@ reqPB := &mpb.ListOrdersRequest{ Ids: &mpb.ListOrdersRequest_UserId{userID}, } - respUser, err := sendListOrdersRequest(reqPB, conn) + respUser, err := sendListOrdersRequest(ctx, logger, reqPB, conn) if err != nil { return err } @@ -613,7 +631,7 @@ } reqPB.Ids = &mpb.ListOrdersRequest_OrderIds_{&orderIDs} - respOrder, err := sendListOrdersRequest(reqPB, conn) + respOrder, err := sendListOrdersRequest(ctx, logger, reqPB, conn) if err != nil { return err } @@ -625,7 +643,7 @@ } // BatchGetWaitEstimates calls the partners API and verifies the returned WaitEstimates. -func BatchGetWaitEstimates(s *fpb.Service, conn *HTTPConnection) error { +func BatchGetWaitEstimates(ctx context.Context, logger *log.Logger, s *fpb.Service, conn *utils.HTTPConnection) error { rules := s.GetWaitlistRules() ps := make([]int32, rules.GetMaxPartySize()-rules.GetMinPartySize()) @@ -637,11 +655,11 @@ ServiceId: s.GetServiceId(), PartySize: ps, } - req, err := conn.marshaler.MarshalToString(reqPB) + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) } - httpResp, err := sendRequest("BatchGetWaitEstimates", req, conn) + httpResp, err := sendRequest(ctx, logger, "BatchGetWaitEstimates", req, conn) if err != nil { return fmt.Errorf("invalid response. BatchGetWaitEstimates yielded error: %v", err) } @@ -677,7 +695,7 @@ // CreateWaitlistEntry attempts to create waitlist entries from a service. // The max party size listed for the service's waitlist rules is used for // the waitlist entry's party size. -func CreateWaitlistEntry(s *fpb.Service, conn *HTTPConnection) (string, error) { +func CreateWaitlistEntry(ctx context.Context, logger *log.Logger, s *fpb.Service, conn *utils.HTTPConnection) (string, error) { gen := rand.New(rand.NewSource(time.Now().UnixNano())) reqPB := &wpb.CreateWaitlistEntryRequest{ @@ -696,12 +714,12 @@ if s.GetWaitlistRules().GetSupportsAdditionalRequest() { reqPB.AdditionalRequest = "test additional request" } - req, err := conn.marshaler.MarshalToString(reqPB) + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return "", fmt.Errorf("Could not convert pb3 to json: %v", reqPB) } - httpResp, err := sendRequest("CreateWaitlistEntry", req, conn) + httpResp, err := sendRequest(ctx, logger, "CreateWaitlistEntry", req, conn) if err != nil { return "", fmt.Errorf("invalid response. CreateWaitlistEntry yielded error: %v", err) } @@ -721,8 +739,8 @@ } // Perform idempotency test. - log.Printf("Idempotency check -- CreateWaitlistEntry Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), reqPB.String()) - idemHTTPResp, err := sendRequest("CreateWaitlistEntry", req, conn) + logger.Printf("Idempotency check -- CreateWaitlistEntry Request. Sent(unix): %s, Request %s", time.Now().UTC().Format(time.RFC850), reqPB.String()) + idemHTTPResp, err := sendRequest(ctx, logger, "CreateWaitlistEntry", req, conn) if err != nil { return "", fmt.Errorf("invalid response. Idempotency check yielded error: %v", err) } @@ -740,16 +758,16 @@ // GetWaitlistEntry retrieves and validates the booking for the specified // waitlist entry id. -func GetWaitlistEntry(id string, conn *HTTPConnection) (*wpb.WaitlistEntry, error) { +func GetWaitlistEntry(ctx context.Context, logger *log.Logger, id string, conn *utils.HTTPConnection) (*wpb.WaitlistEntry, error) { reqPB := &wpb.GetWaitlistEntryRequest{ WaitlistEntryId: id, } - req, err := conn.marshaler.MarshalToString(reqPB) + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return nil, fmt.Errorf("Could not convert pb3 to json: %v", reqPB) } - httpResp, err := sendRequest("GetWaitlistEntry", req, conn) + httpResp, err := sendRequest(ctx, logger, "GetWaitlistEntry", req, conn) if err != nil { return nil, fmt.Errorf("invalid response. GetWaitlistEntry yielded error: %v", err) } @@ -766,22 +784,22 @@ } // DeleteWaitlistEntry makes a request to delete the waitlist entry. -func DeleteWaitlistEntry(id string, conn *HTTPConnection) error { +func DeleteWaitlistEntry(ctx context.Context, logger *log.Logger, id string, conn *utils.HTTPConnection) error { reqPB := &wpb.DeleteWaitlistEntryRequest{ WaitlistEntryId: id, } - req, err := conn.marshaler.MarshalToString(reqPB) + req, err := conn.Marshaler.MarshalToString(reqPB) if err != nil { return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) } - httpResp, err := sendRequest("DeleteWaitlistEntry", req, conn) + httpResp, err := sendRequest(ctx, logger, "DeleteWaitlistEntry", req, conn) if err != nil { return fmt.Errorf("invalid response. DeleteWaitlistEntry yielded error: %v", err) } - var resp empty.Empty + var resp epb.Empty if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { return fmt.Errorf("DeleteWaitlistEntry: Could not parse HTTP response to pb3: %v. Body should be: {}", err) }
diff --git a/booking/bookingTests.go b/booking/bookingTests.go new file mode 100644 index 0000000..b44ec05 --- /dev/null +++ b/booking/bookingTests.go
@@ -0,0 +1,270 @@ +/* +Copyright 2019 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package booking contains test logic for booking related endpoints. +package booking + +import ( + "context" + "log" + + "github.com/maps-booking-v3/api" + "github.com/maps-booking-v3/utils" + + fpb "github.com/maps-booking-v3/feeds" + mpb "github.com/maps-booking-v3/v3" +) + +// GenerateBookings creates bookings from an availability feed. +func GenerateBookings(ctx context.Context, logger *log.Logger, av []*fpb.Availability, stats *utils.TestSummary, conn *utils.HTTPConnection, config *utils.Config) api.Bookings { + logger.Println("no previous bookings to use, acquiring new inventory") + utils.LogFlow(logger, "Generate Fresh Inventory", "Start") + defer utils.LogFlow(logger, "Generate Fresh Inventory", "End") + + var out api.Bookings + totalSlots := len(av) + for i, a := range av { + if config.BookingUseBal { + if err := api.BatchAvailabilityLookup(ctx, logger, []*fpb.Availability{a}, conn); err != nil { + logger.Printf("BAL error: %s. skipping slot %d/%d", err.Error(), i, totalSlots) + stats.BookingBatchAvailabilityLookupErrors++ + continue + } + stats.BookingBatchAvailabilityLookupSuccess++ + } else { + if err := api.CheckAvailability(ctx, logger, a, conn); err != nil { + logger.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots) + stats.BookingCheckAvailabilityErrors++ + continue + } + stats.BookingCheckAvailabilitySuccess++ + } + + booking, err := api.CreateBooking(ctx, logger, a, conn) + if err != nil { + logger.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots) + stats.BookingCreateBookingErrors++ + continue + } + out = append(out, booking) + stats.BookingCreateBookingSuccess++ + } + return out +} + +func logStats(stats *utils.TestSummary, logger *log.Logger, config *utils.Config) { + logger.Println("\n************* Begin Stats *************\n") + var totalErrors int + if config.BookingHealthFlow || config.BookingAllFlows { + if stats.BookingHealthCheckSuccess { + logger.Println("HealthCheck Succeeded") + } else { + totalErrors++ + logger.Println("HealthCheck Failed") + } + } + if config.BookingCheckFlow || config.BookingAllFlows { + if config.BookingUseBal { + totalErrors += stats.BookingBatchAvailabilityLookupErrors + logger.Printf("BatchAvailabilityLookup Errors: %d/%d", stats.BookingBatchAvailabilityLookupErrors, stats.BookingBatchAvailabilityLookupErrors+stats.BookingBatchAvailabilityLookupSuccess) + } else { + totalErrors += stats.BookingCheckAvailabilityErrors + logger.Printf("CheckAvailability Errors: %d/%d", stats.BookingCheckAvailabilityErrors, stats.BookingCheckAvailabilityErrors+stats.BookingCheckAvailabilitySuccess) + } + } + if config.BookingBookFlow || config.BookingAllFlows { + totalErrors += stats.BookingCreateBookingErrors + logger.Printf("CreateBooking Errors: %d/%d", stats.BookingCreateBookingErrors, stats.BookingCreateBookingErrors+stats.BookingCreateBookingSuccess) + } + if config.BookingListFlow || config.BookingAllFlows || config.BookingCancelAllBookings { + if stats.BookingListBookingsSuccess { + logger.Println("ListBookings Succeeded") + } else { + totalErrors++ + logger.Println("ListBookings Failed") + } + } + if config.BookingStatusFlow || config.BookingAllFlows { + totalErrors += stats.BookingGetBookingStatusErrors + logger.Printf("GetBookingStatus Errors: %d/%d", stats.BookingGetBookingStatusErrors, stats.BookingGetBookingStatusErrors+stats.BookingGetBookingStatusSuccess) + } + if config.BookingRescheduleFlow || config.BookingAllFlows { + if stats.BookingReschedulingSuccess { + logger.Println("Rescheduling Succeeded") + } else { + totalErrors++ + logger.Println("Rescheduling Failed") + } + } + + logger.Println("\n\n\n") + if totalErrors == 0 { + logger.Println("All Tests Pass!") + } else { + logger.Printf("Found %d Errors", totalErrors) + } + + logger.Println("\n************* End Stats *************\n") +} + +// RunTests runs booking tests. +func RunTests(ctx context.Context, logger *log.Logger, config *utils.Config, av []*fpb.Availability, avForRescheduling []*fpb.Availability, stats *utils.TestSummary) { + conn := config.Conn + // HealthCheck Flow + if config.BookingHealthFlow || config.BookingAllFlows { + stats.BookingHealthCheckSuccess = true + if err := api.HealthCheck(ctx, logger, conn); err != nil { + stats.BookingHealthCheckSuccess = false + logger.Println(err.Error()) + } + stats.BookingHealthCheckCompleted = true + if !config.BookingAllFlows && !config.BookingCheckFlow && !config.BookingBookFlow && + !config.BookingListFlow && !config.BookingStatusFlow && !config.BookingRescheduleFlow { + logStats(stats, logger, config) + return + } + } + if config.BookingCheckFlow || config.BookingAllFlows { + if config.BookingUseBal { + utils.LogFlow(logger, "Batch Availability Lookup", "Start") + for _, a := range utils.SplitAvailabilityByMerchant(av) { + if err := api.BatchAvailabilityLookup(ctx, logger, a, conn); err != nil { + logger.Printf("BatchAvailabilityLookup returned error: %v", err) + stats.BookingBatchAvailabilityLookupErrors++ + } else { + stats.BookingBatchAvailabilityLookupSuccess++ + } + } + utils.LogFlow(logger, "Batch Availability Lookup", "End") + stats.BookingBatchAvailabilityLookupCompleted = true + } else { + // AvailabilityCheck Flow + utils.LogFlow(logger, "Availability Check", "Start") + totalSlots := len(av) + + for i, a := range av { + if err := api.CheckAvailability(ctx, logger, a, conn); err != nil { + logger.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots) + stats.BookingCheckAvailabilityErrors++ + continue + } + stats.BookingCheckAvailabilitySuccess++ + } + utils.LogFlow(logger, "Availability Check", "End") + stats.BookingCheckAvailabilityCompleted = true + } + } + + // CreateBooking Flow. + var b []*mpb.Booking + if config.BookingBookFlow || config.BookingAllFlows { + utils.LogFlow(logger, "Booking", "Start") + totalSlots := len(av) + logger.Printf("total slots %d", totalSlots) + for i, a := range av { + logger.Printf("creating booking") + booking, err := api.CreateBooking(ctx, logger, a, conn) + if err != nil { + logger.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots) + stats.BookingCreateBookingErrors++ + continue + } + b = append(b, booking) + stats.BookingCreateBookingSuccess++ + } + utils.LogFlow(logger, "Booking", "End") + stats.BookingCreateBookingCompleted = true + } + // ListBookings Flow + if config.BookingListFlow || config.BookingAllFlows || config.BookingCancelAllBookings { + if len(b) == 0 && !config.BookingCancelAllBookings { + b = GenerateBookings(ctx, logger, av, stats, conn, config) + } + utils.LogFlow(logger, "List Bookings", "Start") + if len(b) > 0 || config.BookingCancelAllBookings { + var err error + b, err = api.ListBookings(ctx, logger, b, conn) + stats.BookingListBookingsSuccess = true + if err != nil { + stats.BookingListBookingsSuccess = false + logger.Println(err.Error()) + } + } else { + logger.Println("Could not create bookings to test ListBookings flow with.") + stats.BookingListBookingsSuccess = false + } + utils.LogFlow(logger, "List Bookings", "End") + stats.BookingListBookingsCompleted = true + } + + // GetBookingStatus Flow + if config.BookingStatusFlow || config.BookingAllFlows { + if len(b) == 0 { + b = GenerateBookings(ctx, logger, av, stats, conn, config) + } + + utils.LogFlow(logger, "BookingStatus", "Start") + totalBookings := len(b) + + if totalBookings > 0 { + j := 0 + for i, booking := range b { + if err := api.GetBookingStatus(ctx, logger, booking, conn); err != nil { + logger.Printf("%s. abandoning booking %d/%d", err.Error(), i, totalBookings) + stats.BookingGetBookingStatusErrors++ + continue + } + stats.BookingGetBookingStatusSuccess++ + b[j] = booking + j++ + } + b = b[:j] + } else { + logger.Println("Could not create bookings to test GetBookingStatus flow with.") + } + utils.LogFlow(logger, "BookingStatus", "End") + stats.BookingGetBookingStatusCompleted = true + } + // CancelBooking Flow + if len(b) > 0 { + utils.LogFlow(logger, "Cancel Booking", "Start") + for i, booking := range b { + if err := api.CancelBooking(ctx, logger, booking.GetBookingId(), conn); err != nil { + logger.Printf("%s. abandoning booking %d/%d", err.Error(), i, len(b)) + stats.BookingCancelBookingsErrors++ + continue + } + stats.BookingCancelBookingsSuccess++ + } + utils.LogFlow(logger, "Cancel Booking", "End") + stats.BookingCancelBookingsCompleted = true + } + + // Rescheduling is nuanced and can be isolated + // from the rest of the tests. + if config.BookingRescheduleFlow || config.BookingAllFlows { + utils.LogFlow(logger, "Rescheduling", "Start") + stats.BookingReschedulingSuccess = true + if err := api.Rescheduling(ctx, logger, avForRescheduling, conn); err != nil { + logger.Println(err.Error()) + stats.BookingReschedulingSuccess = false + } + utils.LogFlow(logger, "Rescheduling", "End") + stats.BookingReschedulingCompleted = true + } + + logStats(stats, logger, config) +}
diff --git a/order/orderTests.go b/order/orderTests.go new file mode 100644 index 0000000..23c689d --- /dev/null +++ b/order/orderTests.go
@@ -0,0 +1,155 @@ +/* +Copyright 2019 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package order contains test logic for order related endpoints. +package order + +import ( + "context" + "log" + + fpb "github.com/maps-booking-v3/feeds" + "github.com/maps-booking-v3/api" + mpb "github.com/maps-booking-v3/v3" + "github.com/maps-booking-v3/utils" +) + +func logStats(stats *utils.TestSummary, logger *log.Logger, config *utils.Config) { + logger.Println("\n************* Begin Stats *************\n") + var totalErrors int + if config.OrderHealthFlow || config.OrderAllFlows { + if stats.OrderHealthCheckSuccess { + logger.Println("HealthCheck Succeeded") + } else { + totalErrors++ + logger.Println("HealthCheck Failed") + } + stats.OrderHealthCheckCompleted = true + } + if config.OrderCheckFlow || config.OrderAllFlows { + totalErrors += stats.OrderCheckOrderFulfillabilityErrors + logger.Printf("CheckOrderFulfillability Errors: %d/%d", stats.OrderCheckOrderFulfillabilityErrors, stats.OrderCheckOrderFulfillabilityErrors+stats.OrderCheckOrderFulfillabilitySuccess) + } + if config.OrderOrderFlow || config.OrderAllFlows { + totalErrors += stats.OrderCreateOrderErrors + logger.Printf("CreateOrder Errors: %d/%d", stats.OrderCreateOrderErrors, stats.OrderCreateOrderErrors+stats.OrderCreateOrderSuccess) + } + if config.OrderAllFlows { + if stats.OrderListOrdersSuccess { + logger.Println("ListOrders Succeeded") + } else { + totalErrors++ + logger.Println("ListOrders Failed") + } + } + if config.OrderCheckFlow || config.OrderOrderFlow || config.OrderAllFlows { + logger.Printf("Total Slots Processed: %d", stats.OrderTotalSlotsProcessed) + } + + logger.Println("\n\n\n") + if totalErrors == 0 { + logger.Println("All Tests Pass!") + } else { + logger.Printf("Found %d Errors", totalErrors) + } + + logger.Println("\n************* End Stats *************\n") +} + +// RunTests runs order tests. +func RunTests(ctx context.Context, logger *log.Logger, config *utils.Config, av []*fpb.Availability, services []*fpb.Service, stats *utils.TestSummary) { + conn := config.Conn + // HealthCheck Flow + if config.OrderHealthFlow || config.OrderAllFlows { + stats.OrderHealthCheckSuccess = true + if err := api.HealthCheck(ctx, logger, conn); err != nil { + stats.OrderHealthCheckSuccess = false + logger.Println(err.Error()) + } + stats.OrderHealthCheckCompleted = true + if !config.OrderAllFlows && !config.OrderCheckFlow && !config.OrderOrderFlow { + logStats(stats, logger, config) + return + } + } + testInventory, err := utils.BuildLineItemMap(services, av) + if err != nil { + logger.Printf("Remaining tests cannot run due to error building line items: %s\n", err) + return + } + + // CheckOrderFulfillability Flow + if config.OrderCheckFlow || config.OrderAllFlows { + utils.LogFlow(logger, "CheckOrderFulfillability", "Start") + for _, value := range testInventory { + stats.OrderTotalSlotsProcessed += len(value) + } + + i := 0 + for merchantID, lineItems := range testInventory { + if err := api.CheckOrderFulfillability(ctx, logger, merchantID, lineItems, conn); err != nil { + logger.Printf("%s. skipping slots %d-%d/%d", err.Error(), i, i+len(lineItems)-1, stats.OrderTotalSlotsProcessed) + stats.OrderCheckOrderFulfillabilityErrors += len(lineItems) + delete(testInventory, merchantID) + i += len(lineItems) + continue + } + stats.OrderCheckOrderFulfillabilitySuccess += len(lineItems) + i += len(lineItems) + } + stats.OrderCheckOrderFulfillabilityCompleted = true + utils.LogFlow(logger, "CheckOrderFulfillability", "End") + } + // CreateOrder Flow. + var orders []*mpb.Order + if config.OrderOrderFlow || config.OrderAllFlows { + utils.LogFlow(logger, "CreateOrder", "Start") + if stats.OrderTotalSlotsProcessed == 0 { + for _, value := range testInventory { + stats.OrderTotalSlotsProcessed += len(value) + } + } + + i := 0 + for merchantID, lineItems := range testInventory { + order, err := api.CreateOrder(ctx, logger, merchantID, lineItems, conn) + if err != nil { + logger.Printf("%s. skipping slot %d-%d/%d", err.Error(), i, i+len(lineItems)-1, stats.OrderTotalSlotsProcessed) + stats.OrderCreateOrderErrors += len(lineItems) + delete(testInventory, merchantID) + i += len(lineItems) + continue + } + orders = append(orders, order) + stats.OrderCreateOrderSuccess += len(lineItems) + i += len(lineItems) + } + stats.OrderCreateOrderCompleted = true + utils.LogFlow(logger, "CreateOrder", "End") + } + // ListOrders Flow + if config.OrderAllFlows || config.OrderListFlow { + utils.LogFlow(logger, "ListOrders", "Start") + stats.OrderListOrdersSuccess = true + if err := api.ListOrders(ctx, logger, orders, conn); err != nil { + stats.OrderListOrdersSuccess = false + logger.Println(err.Error()) + } + stats.OrderListOrdersCompleted = true + utils.LogFlow(logger, "ListOrders", "End") + } + logStats(stats, logger, config) +}
diff --git a/proto/waitlist.proto b/proto/waitlist.proto index 218e868..d432edc 100644 --- a/proto/waitlist.proto +++ b/proto/waitlist.proto
@@ -100,6 +100,17 @@ EstimatedSeatTimeRange estimated_seat_time_range = 2; } +// The confirmation modes used when joining the waitlist. +enum WaitlistConfirmationMode { + // The confirmation mode was not specified. + // Synchronous confirmation will be assumed. + WAITLIST_CONFIRMATION_MODE_UNSPECIFIED = 0; + // Waitlist entries will be confirmed synchronously. + WAITLIST_CONFIRMATION_MODE_SYNCHRONOUS = 1; + // Waitlist entries will be confirmed asynchronously. + WAITLIST_CONFIRMATION_MODE_ASYNCHRONOUS = 2; +} + // The wait estimate for a particular party size, merchant and service. message WaitEstimate { // Required. The party size this wait estimate applies to. @@ -108,6 +119,11 @@ // Required. Contains fields measuring how long (in time or # of people) until // the user is ready to leave the waitlist and be seated. WaitLength wait_length = 2; + + // Required. Indicates whether waitlist entries for this wait estimate will be + // confirmed synchronously or asynchronously. An UNSPECIFIED value will be + // interpreted as synchronous. + WaitlistConfirmationMode waitlist_confirmation_mode = 3; } // CreateWaitlistEntry method @@ -186,6 +202,8 @@ // The waitlist entry was created and the user is currently waiting in the // waitlist. WAITING = 1; + // The waitlist entry is awaiting confirmation by the merchant. + PENDING_MERCHANT_CONFIRMATION = 8; // The waitlist entry has been canceled by the user. Cancellation for no-shows // should use the NO_SHOW state. CANCELED = 2;
diff --git a/testclient/.bookingClient.go.swp b/testclient/.bookingClient.go.swp deleted file mode 100644 index b3ae8ec..0000000 --- a/testclient/.bookingClient.go.swp +++ /dev/null Binary files differ
diff --git a/testclient/bookingClient.go b/testclient/bookingClient.go index eb23a84..8462716 100644 --- a/testclient/bookingClient.go +++ b/testclient/bookingClient.go
@@ -1,5 +1,5 @@ /* -Copyright 2017 Google Inc. +Copyright 2019 Google Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,29 +16,23 @@ package main import ( + "context" "flag" - "fmt" "log" - "os" - "path/filepath" - "time" "github.com/maps-booking-v3/api" - "github.com/maps-booking-v3/utils" - + "github.com/maps-booking-v3/booking" fpb "github.com/maps-booking-v3/feeds" - mpb "github.com/maps-booking-v3/v3" + "github.com/maps-booking-v3/utils" ) -const logFile = "http_test_client_log_" - var ( serverAddr = flag.String("server_addr", "example.com:80", "Your http server's address in the format of host:port") credentialsFile = flag.String("credentials_file", "", "File containing credentials for your server. Leave blank to bypass authentication. File should have exactly one line of the form 'username:password'.") testSlots = flag.Int("num_test_slots", 10, "Maximum number of slots to test from availability_feed. Slots will be selected randomly") allFlows = flag.Bool("all_tests", false, "Whether to test all endpoints.") healthFlow = flag.Bool("health_check_test", false, "Whether to test the Health endpoint.") - checkFlow = flag.Bool("check_availability_test", false, "Whether to test the CheckAvailability endpoint.") + checkFlow = flag.Bool("check_availability_test", false, "Whether to test availability lookup. Will use BatchAvailabilityLookup or CheckAvailability endpoint depending on value of the use_batch_availability_lookup flag.") bookFlow = flag.Bool("booking_test", false, "Whether to test the CreateBooking endpoint.") listFlow = flag.Bool("list_bookings_test", false, "Whether to test the ListBookings endpoint") statusFlow = flag.Bool("booking_status_test", false, "Whether to test the GetBookingStatus endpoint.") @@ -52,297 +46,61 @@ useBal = flag.Bool("use_batch_availability_lookup", false, "Whether to use the BatchAvailabilityLookup RPC (as opposed to the deprecated CheckAvailability)") ) -type counters struct { - TotalSlotsProcessed int - HealthCheckSuccess bool - BatchAvailabilityLookupErrors int - BatchAvailabilityLookupSuccess int - CheckAvailabilitySuccess int - CheckAvailabilityErrors int - CreateBookingSuccess int - CreateBookingErrors int - ListBookingsSuccess bool - GetBookingStatusSuccess int - GetBookingStatusErrors int - CancelBookingsSuccess int - CancelBookingsErrors int - ReschedulingSuccess bool -} - -// GenerateBookings creates bookings from an availability feed. -func GenerateBookings(av []*fpb.Availability, stats *counters, conn *api.HTTPConnection) api.Bookings { - log.Println("no previous bookings to use, acquiring new inventory") - utils.LogFlow("Generate Fresh Inventory", "Start") - defer utils.LogFlow("Generate Fresh Inventory", "End") - - var out api.Bookings - totalSlots := len(av) - for i, a := range av { - if *useBal { - if err := api.BatchAvailabilityLookup([]*fpb.Availability{a}, conn); err != nil { - log.Printf("BAL error: %s. skipping slot %d/%d", err.Error(), i, totalSlots) - stats.BatchAvailabilityLookupErrors++ - continue - } - stats.BatchAvailabilityLookupSuccess++ - } else { - if err := api.CheckAvailability(a, conn); err != nil { - log.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots) - stats.CheckAvailabilityErrors++ - continue - } - stats.CheckAvailabilitySuccess++ - } - - booking, err := api.CreateBooking(a, conn) - if err != nil { - log.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots) - stats.CreateBookingErrors++ - continue - } - out = append(out, booking) - stats.CreateBookingSuccess++ +func makeConfig(logger *log.Logger) *utils.Config { + conn, err := api.InitHTTPConnection(*serverAddr, *credentialsFile, *caFile, *fullServerName) + if err != nil { + logger.Fatalf("Failed to init http connection %v", err) } - return out -} - -func createLogFile() (*os.File, error) { - var err error - outPath := *outputDir - if outPath == "" { - outPath, err = os.Getwd() - if err != nil { - return nil, err - } - } - - now := time.Now().UTC() - nowString := fmt.Sprintf("%d-%02d-%02d_%02d-%02d-%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) - outFile := filepath.Join(outPath, fmt.Sprintf("%s%s", logFile, nowString)) - - return os.Create(outFile) -} - -func logStats(stats counters) { - log.Println("\n************* Begin Stats *************\n") - var totalErrors int - if *healthFlow || *allFlows { - if stats.HealthCheckSuccess { - log.Println("HealthCheck Succeeded") - } else { - totalErrors++ - log.Println("HealthCheck Failed") - } - } - if *useBal { - totalErrors += stats.BatchAvailabilityLookupErrors - log.Printf("BatchAvailabilityLookup Errors: %d/%d", stats.BatchAvailabilityLookupErrors, stats.BatchAvailabilityLookupErrors+stats.BatchAvailabilityLookupSuccess) - } else if *checkFlow || *allFlows { - totalErrors += stats.CheckAvailabilityErrors - log.Printf("CheckAvailability Errors: %d/%d", stats.CheckAvailabilityErrors, stats.CheckAvailabilityErrors+stats.CheckAvailabilitySuccess) - } - if *bookFlow || *allFlows { - totalErrors += stats.CreateBookingErrors - log.Printf("CreateBooking Errors: %d/%d", stats.CreateBookingErrors, stats.CreateBookingErrors+stats.CreateBookingSuccess) - } - if *listFlow || *allFlows || *cancelAllBookings { - if stats.ListBookingsSuccess { - log.Println("ListBookings Succeeded") - } else { - totalErrors++ - log.Println("ListBookings Failed") - } - } - if *statusFlow || *allFlows { - totalErrors += stats.GetBookingStatusErrors - log.Printf("GetBookingStatus Errors: %d/%d", stats.GetBookingStatusErrors, stats.GetBookingStatusErrors+stats.GetBookingStatusSuccess) - } - if *rescheduleFlow || *allFlows { - if stats.ReschedulingSuccess { - log.Println("Rescheduling Succeeded") - } else { - totalErrors++ - log.Println("Rescheduling Failed") - } - } - - log.Println("\n\n\n") - if totalErrors == 0 { - log.Println("All Tests Pass!") - } else { - log.Printf("Found %d Errors", totalErrors) - } - - log.Println("\n************* End Stats *************\n") - os.Exit(totalErrors) + return &utils.Config{Conn: conn, + BookingAllFlows: *allFlows, + BookingHealthFlow: *healthFlow, + BookingCheckFlow: *checkFlow, + BookingBookFlow: *bookFlow, + BookingListFlow: *listFlow, + BookingStatusFlow: *statusFlow, + BookingRescheduleFlow: *rescheduleFlow, + BookingCancelAllBookings: *cancelAllBookings, + BookingUseBal: *useBal} } func main() { flag.Parse() - var stats counters - if !*outputToTerminal { - // Set up logging before continuing with flows - f, err := createLogFile() - if err != nil { - log.Fatalf("Failed to create log file %v", err) - } - defer f.Close() - log.SetOutput(f) - } - - conn, err := api.InitHTTPConnection(*serverAddr, *credentialsFile, *caFile, *fullServerName) + logger, f, err := utils.MakeLogger(*outputToTerminal, *outputDir) if err != nil { - log.Fatalf("Failed to init http connection %v", err) + log.Fatal("Could not create logger: ", err) } + if f != nil { + defer f.Close() + } + + config := makeConfig(logger) // Health check doesn't affect the cancel booking flow so we let it through. if *cancelAllBookings && (*allFlows || *checkFlow || *bookFlow || *listFlow || *statusFlow || *rescheduleFlow) { - log.Fatal("cancel_all_bookings is not supported with other test flows") + logger.Fatal("cancel_all_bookings is not supported with other test flows") } - // HealthCheck Flow - if *healthFlow || *allFlows { - stats.HealthCheckSuccess = true - if err := api.HealthCheck(conn); err != nil { - stats.HealthCheckSuccess = false - log.Println(err.Error()) - } - if !*allFlows && !*checkFlow && !*bookFlow && - !*listFlow && !*statusFlow && !*rescheduleFlow { - logStats(stats) - } - } - + var summary utils.TestSummary var av []*fpb.Availability var avForRescheduling []*fpb.Availability - if !*cancelAllBookings { + + needAvailability := !*cancelAllBookings || *allFlows || *checkFlow || *bookFlow || *listFlow || *statusFlow || *rescheduleFlow + + if needAvailability { // Build availablility records. if *availabilityFeed == "" { - log.Fatal("please set availability_feed flag if you wish to test additional flows") + logger.Fatal("Please set availability_feed flag if you wish to test anything except health check.") } - av, err = utils.AvailabilityFrom(*availabilityFeed, *testSlots, false) + av, err = utils.AvailabilityFrom(logger, *availabilityFeed, *testSlots, false) if err != nil { - log.Fatalf("Failed to get availability: %v", err.Error()) + logger.Fatalf("Failed to get availability: %v", err.Error()) } - stats.TotalSlotsProcessed += len(av) - - avForRescheduling, err = utils.AvailabilityFrom(*availabilityFeed, *testSlots, true) + summary.BookingTotalSlotsProcessed += len(av) + avForRescheduling, err = utils.AvailabilityFrom(logger, *availabilityFeed, *testSlots, true) if err != nil { - log.Fatalf("Failed to get availability for rescheduling test: %v", err.Error()) + logger.Fatalf("Failed to get availability for rescheduling test: %v", err.Error()) } } - - if *useBal { - utils.LogFlow("Batch Availability Lookup", "Start") - for _, a := range utils.SplitAvailabilityByMerchant(av) { - if err = api.BatchAvailabilityLookup(a, conn); err != nil { - log.Printf("BatchAvailabilityLookup returned error: %v", err) - stats.BatchAvailabilityLookupErrors++ - } else { - stats.BatchAvailabilityLookupSuccess++ - } - } - utils.LogFlow("Batch Availability Lookup", "End") - } else if *checkFlow || *allFlows { - // AvailabilityCheck Flow - utils.LogFlow("Availability Check", "Start") - totalSlots := len(av) - - j := 0 - for i, a := range av { - if err = api.CheckAvailability(a, conn); err != nil { - log.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots) - stats.CheckAvailabilityErrors++ - continue - } - stats.CheckAvailabilitySuccess++ - av[j] = a - j++ - } - av = av[:j] - utils.LogFlow("Availability Check", "End") - } - // CreateBooking Flow. - var b []*mpb.Booking - if *bookFlow || *allFlows { - utils.LogFlow("Booking", "Start") - totalSlots := len(av) - for i, a := range av { - booking, err := api.CreateBooking(a, conn) - if err != nil { - log.Printf("%s. skipping slot %d/%d", err.Error(), i, totalSlots) - stats.CreateBookingErrors++ - continue - } - b = append(b, booking) - stats.CreateBookingSuccess++ - } - utils.LogFlow("Booking", "End") - } - // ListBookings Flow - if *listFlow || *allFlows || *cancelAllBookings { - if len(b) == 0 && !*cancelAllBookings { - b = GenerateBookings(av, &stats, conn) - } - utils.LogFlow("List Bookings", "Start") - b, err = api.ListBookings(b, conn) - stats.ListBookingsSuccess = true - if err != nil { - stats.ListBookingsSuccess = false - log.Println(err.Error()) - } - utils.LogFlow("List Bookings", "End") - } - - // GetBookingStatus Flow - if *statusFlow || *allFlows { - if len(b) == 0 { - b = GenerateBookings(av, &stats, conn) - } - - utils.LogFlow("BookingStatus", "Start") - totalBookings := len(b) - - j := 0 - for i, booking := range b { - if err = api.GetBookingStatus(booking, conn); err != nil { - log.Printf("%s. abandoning booking %d/%d", err.Error(), i, totalBookings) - stats.GetBookingStatusErrors++ - continue - } - stats.GetBookingStatusSuccess++ - b[j] = booking - j++ - } - b = b[:j] - utils.LogFlow("BookingStatus", "End") - } - // CancelBooking Flow - if len(b) > 0 { - utils.LogFlow("Cancel Booking", "Start") - for i, booking := range b { - if err = api.CancelBooking(booking.GetBookingId(), conn); err != nil { - log.Printf("%s. abandoning booking %d/%d", err.Error(), i, len(b)) - stats.CancelBookingsErrors++ - continue - } - stats.CancelBookingsSuccess++ - } - utils.LogFlow("Cancel Booking", "End") - } - - // Rescheduling is nuanced and can be isolated - // from the rest of the tests. - if *rescheduleFlow || *allFlows { - utils.LogFlow("Rescheduling", "Start") - stats.ReschedulingSuccess = true - if err = api.Rescheduling(avForRescheduling, conn); err != nil { - log.Println(err.Error()) - stats.ReschedulingSuccess = false - } - utils.LogFlow("Rescheduling", "End") - } - - logStats(stats) + booking.RunTests(context.Background(), logger, config, av, avForRescheduling, &summary) }
diff --git a/testclient/orderClient.go b/testclient/orderClient.go index 718ff23..a3ee63d 100644 --- a/testclient/orderClient.go +++ b/testclient/orderClient.go
@@ -1,5 +1,5 @@ /* -Copyright 2017 Google Inc. +Copyright 2019 Google Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,21 +16,17 @@ package main import ( + "context" "flag" - "fmt" "log" - "os" - "path/filepath" - "time" + + fpb "github.com/maps-booking-v3/feeds" "github.com/maps-booking-v3/api" + "github.com/maps-booking-v3/order" "github.com/maps-booking-v3/utils" - - mpb "github.com/maps-booking-v3/v3" ) -const logFile = "http_test_client_log_" - var ( serverAddr = flag.String("server_addr", "example.com:80", "Your http server's address in the format of host:port") credentialsFile = flag.String("credentials_file", "", "File containing credentials for your server. Leave blank to bypass authentication. File should have exactly one line of the form 'username:password'.") @@ -47,172 +43,48 @@ outputToTerminal = flag.Bool("output_to_terminal", false, "Output to terminal rather than a file.") ) -type counters struct { - TotalSlotsProcessed int - HealthCheckSuccess bool - CheckOrderFulfillabilitySuccess int - CheckOrderFulfillabilityErrors int - CreateOrderSuccess int - CreateOrderErrors int - ListOrdersSuccess bool -} - -func createLogFile() (*os.File, error) { - var err error - outPath := *outputDir - if outPath == "" { - outPath, err = os.Getwd() - if err != nil { - return nil, err - } +func makeConfig(logger *log.Logger) *utils.Config { + conn, err := api.InitHTTPConnection(*serverAddr, *credentialsFile, *caFile, *fullServerName) + if err != nil { + logger.Fatalf("Failed to init http connection %v", err) } - - now := time.Now().UTC() - nowString := fmt.Sprintf("%d-%02d-%02d_%02d-%02d-%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) - outFile := filepath.Join(outPath, fmt.Sprintf("%s%s", logFile, nowString)) - - return os.Create(outFile) -} - -func logStats(stats counters) { - log.Println("\n************* Begin Stats *************\n") - var totalErrors int - if *healthFlow || *allFlows { - if stats.HealthCheckSuccess { - log.Println("HealthCheck Succeeded") - } else { - totalErrors++ - log.Println("HealthCheck Failed") - } + return &utils.Config{Conn: conn, + OrderAllFlows: *allFlows, + OrderHealthFlow: *healthFlow, + OrderCheckFlow: *checkFlow, + OrderOrderFlow: *orderFlow, } - if *checkFlow || *allFlows { - totalErrors += stats.CheckOrderFulfillabilityErrors - log.Printf("CheckOrderFulfillability Errors: %d/%d", stats.CheckOrderFulfillabilityErrors, stats.CheckOrderFulfillabilityErrors+stats.CheckOrderFulfillabilitySuccess) - } - if *orderFlow || *allFlows { - totalErrors += stats.CreateOrderErrors - log.Printf("CreateOrder Errors: %d/%d", stats.CreateOrderErrors, stats.CreateOrderErrors+stats.CreateOrderSuccess) - } - if *allFlows { - if stats.ListOrdersSuccess { - log.Println("ListOrders Succeeded") - } else { - totalErrors++ - log.Println("ListOrders Failed") - } - } - if *checkFlow || *orderFlow || *allFlows { - log.Printf("Total Slots Processed: %d", stats.TotalSlotsProcessed) - } - - log.Println("\n\n\n") - if totalErrors == 0 { - log.Println("All Tests Pass!") - } else { - log.Printf("Found %d Errors", totalErrors) - } - - log.Println("\n************* End Stats *************\n") - os.Exit(totalErrors) } func main() { flag.Parse() - var stats counters - if !*outputToTerminal { - // Set up logging before continuing with flows - f, err := createLogFile() - if err != nil { - log.Fatalf("Failed to create log file %v", err) - } + logger, f, err := utils.MakeLogger(*outputToTerminal, *outputDir) + if err != nil { + log.Fatal("Could not create logger: ", err) + } + if f != nil { defer f.Close() - log.SetOutput(f) } - conn, err := api.InitHTTPConnection(*serverAddr, *credentialsFile, *caFile, *fullServerName) - if err != nil { - log.Fatalf("Failed to init http connection %v", err) - } - - // HealthCheck Flow - if *healthFlow || *allFlows { - stats.HealthCheckSuccess = true - if err := api.HealthCheck(conn); err != nil { - stats.HealthCheckSuccess = false - log.Println(err.Error()) - } - if !*allFlows && !*checkFlow && !*orderFlow { - logStats(stats) - } - } - - if *availabilityFeed == "" || *serviceFeed == "" { - log.Fatal("please set both availability_feed and service_feed flags") - } - - testInventory, err := utils.MerchantLineItemMapFrom(*serviceFeed, *availabilityFeed, *testSlots) - if err != nil { - log.Fatal(err.Error()) - } - - // CheckOrderFulfillability Flow - if *checkFlow || *allFlows { - utils.LogFlow("CheckOrderFulfillability", "Start") - for _, value := range testInventory { - stats.TotalSlotsProcessed += len(value) + config := makeConfig(logger) + var summary utils.TestSummary + var services []*fpb.Service + var av []*fpb.Availability + if config.OrderAllFlows || config.OrderCheckFlow || config.OrderOrderFlow { + if *availabilityFeed == "" || *serviceFeed == "" { + log.Fatal("please set both availability_feed and service_feed flags") } - i := 0 - for merchantID, lineItems := range testInventory { - if err = api.CheckOrderFulfillability(merchantID, lineItems, conn); err != nil { - log.Printf("%s. skipping slots %d-%d/%d", err.Error(), i, i+len(lineItems)-1, stats.TotalSlotsProcessed) - stats.CheckOrderFulfillabilityErrors += len(lineItems) - delete(testInventory, merchantID) - i += len(lineItems) - continue - } - stats.CheckOrderFulfillabilitySuccess += len(lineItems) - i += len(lineItems) - } - utils.LogFlow("CheckOrderFulfillability", "End") - } - // CreateOrder Flow. - var orders []*mpb.Order - if *orderFlow || *allFlows { - utils.LogFlow("CreateOrder", "Start") - if stats.TotalSlotsProcessed == 0 { - for _, value := range testInventory { - stats.TotalSlotsProcessed += len(value) - } + services, err = utils.ParseServiceFeed(*serviceFeed) + if err != nil { + log.Fatal(err.Error()) } - i := 0 - for merchantID, lineItems := range testInventory { - order, err := api.CreateOrder(merchantID, lineItems, conn) - if err != nil { - log.Printf("%s. skipping slot %d-%d/%d", err.Error(), i, i+len(lineItems)-1, stats.TotalSlotsProcessed) - stats.CreateOrderErrors += len(lineItems) - delete(testInventory, merchantID) - i += len(lineItems) - continue - } - orders = append(orders, order) - stats.CreateOrderSuccess += len(lineItems) - i += len(lineItems) + av, err = utils.AvailabilityFrom(logger, *availabilityFeed, *testSlots, false) + if err != nil { + log.Fatal(err.Error()) } - utils.LogFlow("CreateOrder", "End") } - // ListOrders Flow - if *allFlows { - utils.LogFlow("ListOrders", "Start") - stats.ListOrdersSuccess = true - if err = api.ListOrders(orders, conn); err != nil { - stats.ListOrdersSuccess = false - log.Println(err.Error()) - } - utils.LogFlow("ListOrders", "End") - } - - logStats(stats) + order.RunTests(context.Background(), logger, config, av, services, &summary) }
diff --git a/testclient/waitlistClient.go b/testclient/waitlistClient.go index 4f673e7..5260a74 100644 --- a/testclient/waitlistClient.go +++ b/testclient/waitlistClient.go
@@ -1,5 +1,5 @@ /* -Copyright 2017 Google Inc. +Copyright 2019 Google Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,21 +16,16 @@ package main import ( + "context" "flag" - "fmt" "log" - "os" - "path/filepath" - "time" "github.com/maps-booking-v3/api" - "github.com/maps-booking-v3/utils" - fpb "github.com/maps-booking-v3/feeds" + "github.com/maps-booking-v3/utils" + "github.com/maps-booking-v3/waitlist" ) -const logFile = "http_test_client_log_" - var ( serverAddr = flag.String("server_addr", "example.com:80", "Your http server's address in the format of host:port") credentialsFile = flag.String("credentials_file", "", "File containing credentials for your server. Leave blank to bypass authentication. File should have exactly one line of the form 'username:password'.") @@ -48,206 +43,64 @@ outputToTerminal = flag.Bool("output_to_terminal", false, "Output to terminal rather than a file.") ) -type counters struct { - TotalServicesProcessed int - HealthCheckSuccess bool - BatchGetWaitEstimatesSuccess int - BatchGetWaitEstimatesErrors int - CreateWaitlistEntrySuccess int - CreateWaitlistEntryErrors int - GetWaitlistEntrySuccess int - GetWaitlistEntryErrors int - DeleteWaitlistEntrySuccess int - DeleteWaitlistEntryErrors int -} - -// GenerateWaitlistEntries creates a waitlist entry for each provided service. -func GenerateWaitlistEntries(services []*fpb.Service, stats *counters, conn *api.HTTPConnection) []string { - log.Println("no previous waitlist entries to use, acquiring new inventory") - utils.LogFlow("Generate Fresh Entries", "Start") - defer utils.LogFlow("Generate Fresh Entries", "End") - - var out []string - totalServices := len(services) - for i, s := range services { - id, err := api.CreateWaitlistEntry(s, conn) - if err != nil { - log.Printf("%s. skipping waitlistEntry %d/%d, serviceID: %s", - err.Error(), i, totalServices, s.GetServiceId()) - stats.CreateWaitlistEntryErrors++ - continue - } - out = append(out, id) - stats.CreateWaitlistEntrySuccess++ +func makeConfig(logger *log.Logger) *utils.Config { + conn, err := api.InitHTTPConnection(*serverAddr, *credentialsFile, *caFile, *fullServerName) + if err != nil { + logger.Fatalf("Failed to init http connection %v", err) } - return out -} - -func createLogFile() (*os.File, error) { - var err error - outPath := *outputDir - if outPath == "" { - outPath, err = os.Getwd() - if err != nil { - return nil, err - } + return &utils.Config{Conn: conn, + WaitlistAllFlows: *allFlows, + WaitlistHealthFlow: *healthFlow, + WaitlistBatchGetWaitEstimatesFlow: *batchGetWaitEstimatesFlow, + WaitlistCreateWaitlistEntryFlow: *createWaitlistEntryFlow, + WaitlistGetWaitlistEntryFlow: *getWaitlistEntryFlow, + WaitlistDeleteWaitlistEntryFlow: *deleteWaitlistEntryFlow, } - - now := time.Now().UTC() - nowString := fmt.Sprintf("%d-%02d-%02d_%02d-%02d-%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) - outFile := filepath.Join(outPath, fmt.Sprintf("%s%s", logFile, nowString)) - - return os.Create(outFile) -} - -func logStats(stats counters) { - log.Println("\n************* Begin Stats *************\n") - var totalErrors int - if *healthFlow || *allFlows { - if stats.HealthCheckSuccess { - log.Println("HealthCheck Succeeded") - } else { - totalErrors++ - log.Println("HealthCheck Failed") - } - } - if *batchGetWaitEstimatesFlow || *allFlows { - totalErrors += stats.BatchGetWaitEstimatesErrors - log.Printf("BatchGetWaitEstimates Errors: %d/%d", stats.BatchGetWaitEstimatesErrors, stats.BatchGetWaitEstimatesErrors+stats.BatchGetWaitEstimatesSuccess) - } - if *createWaitlistEntryFlow || *allFlows { - totalErrors += stats.CreateWaitlistEntryErrors - log.Printf("CreateWaitlistEntry Errors: %d/%d", stats.CreateWaitlistEntryErrors, stats.CreateWaitlistEntryErrors+stats.CreateWaitlistEntrySuccess) - } - if *getWaitlistEntryFlow || *allFlows { - totalErrors += stats.GetWaitlistEntryErrors - log.Printf("GetWaitlistEntry Errors: %d/%d", stats.GetWaitlistEntryErrors, stats.GetWaitlistEntryErrors+stats.GetWaitlistEntrySuccess) - } - if *deleteWaitlistEntryFlow || *allFlows { - totalErrors += stats.DeleteWaitlistEntryErrors - log.Printf("DeleteWaitlistEntry Errors: %d/%d", stats.DeleteWaitlistEntryErrors, stats.DeleteWaitlistEntryErrors+stats.DeleteWaitlistEntrySuccess) - } - - log.Println("\n\n\n") - if totalErrors == 0 { - log.Println("All Tests Pass!") - } else { - log.Printf("Found %d Errors", totalErrors) - } - - log.Println("\n************* End Stats *************\n") - os.Exit(totalErrors) } func main() { flag.Parse() - var stats counters - if !*outputToTerminal { - // Set up logging before continuing with flows - f, err := createLogFile() - if err != nil { - log.Fatalf("Failed to create log file %v", err) - } - defer f.Close() - log.SetOutput(f) - } - - conn, err := api.InitHTTPConnection(*serverAddr, *credentialsFile, *caFile, *fullServerName) + logger, f, err := utils.MakeLogger(*outputToTerminal, *outputDir) if err != nil { - log.Fatalf("Failed to init http connection %v", err) + log.Fatal("Could not create logger: ", err) } + if f != nil { + defer f.Close() + } + config := makeConfig(logger) + var summary utils.TestSummary - // HealthCheck Flow - if *healthFlow || *allFlows { - stats.HealthCheckSuccess = true - if err := api.HealthCheck(conn); err != nil { - stats.HealthCheckSuccess = false - log.Println(err.Error()) - } - if !*allFlows && !*batchGetWaitEstimatesFlow && !*createWaitlistEntryFlow && - !*getWaitlistEntryFlow && !*deleteWaitlistEntryFlow { - logStats(stats) - } - } + needService := config.WaitlistAllFlows || config.WaitlistBatchGetWaitEstimatesFlow || config.WaitlistCreateWaitlistEntryFlow || + config.WaitlistGetWaitlistEntryFlow || config.WaitlistDeleteWaitlistEntryFlow // Build services. - if *serviceFeed == "" { - log.Fatal("please set service_feed flag if you wish to test additional flows") - } - - var services []*fpb.Service - services, err = utils.ParseServiceFeed(*serviceFeed) - if err != nil { - log.Fatalf("Failed to get services: %v", err.Error()) - } - // Remove services without waitlist rules. - waitlistServices := services[:0] - for _, s := range services { - if s.GetWaitlistRules() != nil { - waitlistServices = append(waitlistServices, s) + var reducedServices []*fpb.Service + if needService { + if *serviceFeed == "" { + logger.Fatal("please set service_feed flag if you wish to test additional flows") } - } - if len(waitlistServices) == 0 { - log.Fatal("no services have waitlist rules") - } - reducedServices := utils.ReduceServices(waitlistServices, *numTestServices) - stats.TotalServicesProcessed += len(reducedServices) - - // BatchGetWaitEstimates Flow - if *batchGetWaitEstimatesFlow || *allFlows { - utils.LogFlow("BatchGetWaitEstimates", "Start") - - for i, s := range reducedServices { - if err = api.BatchGetWaitEstimates(s, conn); err != nil { - log.Printf("%s. BatchGerWaitEstimates failed for service %d/%d. Service_id:", - err.Error(), i, stats.TotalServicesProcessed, s.GetServiceId()) - stats.BatchGetWaitEstimatesErrors++ - continue + var services []*fpb.Service + services, err = utils.ParseServiceFeed(*serviceFeed) + if err != nil { + logger.Fatalf("Failed to get services: %v", err.Error()) + } + // Remove services without waitlist rules. + waitlistServices := services[:0] + for _, s := range services { + if s.GetWaitlistRules() != nil { + waitlistServices = append(waitlistServices, s) } - stats.BatchGetWaitEstimatesSuccess++ } - utils.LogFlow("BatchGetWaitEstimates", "End") - } - // CreateWaitlistEntry Flow. - var ids []string - if *createWaitlistEntryFlow || *getWaitlistEntryFlow || - *deleteWaitlistEntryFlow || *allFlows { - utils.LogFlow("CreateWaitlistEntry", "Start") - ids = GenerateWaitlistEntries(reducedServices, &stats, conn) - utils.LogFlow("CreateWaitlistEntry", "End") - } - // GetWaitlistEntry Flow - if *getWaitlistEntryFlow || *allFlows { - utils.LogFlow("GetWaitlistEntry", "Start") - for _, id := range ids { - if _, err = api.GetWaitlistEntry(id, conn); err != nil { - log.Printf("%s. get waitlist entry failed for waitlist entry id: %s", - err.Error(), id) - stats.GetWaitlistEntryErrors++ - continue - } - stats.GetWaitlistEntrySuccess++ + + if len(waitlistServices) == 0 { + logger.Fatal("no services have waitlist rules") } - utils.LogFlow("GetWaitlistEntry", "End") + reducedServices = utils.ReduceServices(logger, waitlistServices, *numTestServices) + } else { + reducedServices = []*fpb.Service{} } - - // DeleteWaitlistentry Flow - if *deleteWaitlistEntryFlow || *allFlows { - utils.LogFlow("DeleteWaitlistEntry", "Start") - - for _, id := range ids { - if err = api.DeleteWaitlistEntry(id, conn); err != nil { - log.Printf("%s. Delete waitlist entry failed for waitlist entry id: %s", - err.Error(), id) - stats.DeleteWaitlistEntryErrors++ - continue - } - stats.DeleteWaitlistEntrySuccess++ - } - utils.LogFlow("DeleteWaitlistEntry", "End") - } - - logStats(stats) + summary.WaitlistTotalServicesProcessed += len(reducedServices) + waitlist.RunTests(context.Background(), logger, config, reducedServices, &summary) }
diff --git a/utils/structs.go b/utils/structs.go new file mode 100644 index 0000000..689c35e --- /dev/null +++ b/utils/structs.go
@@ -0,0 +1,152 @@ +/* +Copyright 2019 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import ( + "net/http" + + "github.com/golang/protobuf/jsonpb" +) + +// Config holds all test configuration options. +type Config struct { + BookingAllFlows bool + BookingHealthFlow bool + BookingCheckFlow bool + BookingBookFlow bool + BookingListFlow bool + BookingStatusFlow bool + BookingRescheduleFlow bool + BookingCancelAllBookings bool + BookingUseBal bool + + OrderAllFlows bool + OrderHealthFlow bool + OrderCheckFlow bool + OrderOrderFlow bool + OrderListFlow bool + + WaitlistAllFlows bool + WaitlistHealthFlow bool + WaitlistBatchGetWaitEstimatesFlow bool + WaitlistCreateWaitlistEntryFlow bool + WaitlistGetWaitlistEntryFlow bool + WaitlistDeleteWaitlistEntryFlow bool + + Conn *HTTPConnection +} + +// ShouldTestBookings determines whether any booking tests need to be run. +func (c *Config) ShouldTestBookings() bool { + return c.BookingAllFlows || + c.BookingHealthFlow || + c.BookingCheckFlow || + c.BookingBookFlow || + c.BookingListFlow || + c.BookingStatusFlow || + c.BookingRescheduleFlow || + c.BookingCancelAllBookings +} + +// ShouldTestOrders determines whether any orders tests need to be run. +func (c *Config) ShouldTestOrders() bool { + return c.OrderAllFlows || + c.OrderHealthFlow || + c.OrderCheckFlow || + c.OrderOrderFlow || + c.OrderListFlow +} + +// ShouldTestWaitlists determines whether any waitlist tests need to be run. +func (c *Config) ShouldTestWaitlists() bool { + return c.WaitlistAllFlows || + c.WaitlistHealthFlow || + c.WaitlistBatchGetWaitEstimatesFlow || + c.WaitlistCreateWaitlistEntryFlow || + c.WaitlistGetWaitlistEntryFlow || + c.WaitlistDeleteWaitlistEntryFlow +} + +// TestSummary contains a summary of all test results. +type TestSummary struct { + BookingTotalSlotsProcessed int + BookingHealthCheckSuccess bool + BookingHealthCheckCompleted bool + BookingBatchAvailabilityLookupErrors int + BookingBatchAvailabilityLookupSuccess int + BookingBatchAvailabilityLookupCompleted bool + BookingCheckAvailabilitySuccess int + BookingCheckAvailabilityErrors int + BookingCheckAvailabilityCompleted bool + BookingCreateBookingSuccess int + BookingCreateBookingErrors int + BookingCreateBookingCompleted bool + BookingListBookingsSuccess bool + BookingListBookingsCompleted bool + BookingGetBookingStatusSuccess int + BookingGetBookingStatusErrors int + BookingGetBookingStatusCompleted bool + BookingCancelBookingsSuccess int + BookingCancelBookingsErrors int + BookingCancelBookingsCompleted bool + BookingReschedulingSuccess bool + BookingReschedulingCompleted bool + + OrderTotalSlotsProcessed int + OrderHealthCheckSuccess bool + OrderHealthCheckCompleted bool + OrderCheckOrderFulfillabilitySuccess int + OrderCheckOrderFulfillabilityErrors int + OrderCheckOrderFulfillabilityCompleted bool + OrderCreateOrderSuccess int + OrderCreateOrderErrors int + OrderCreateOrderCompleted bool + OrderListOrdersSuccess bool + OrderListOrdersCompleted bool + + WaitlistTotalServicesProcessed int + WaitlistHealthCheckSuccess bool + WaitlistHealthCheckCompleted bool + WaitlistBatchGetWaitEstimatesSuccess int + WaitlistBatchGetWaitEstimatesErrors int + WaitlistBatchGetWaitEstimatesCompleted bool + WaitlistCreateWaitlistEntrySuccess int + WaitlistCreateWaitlistEntryErrors int + WaitlistCreateWaitlistEntryCompleted bool + WaitlistGetWaitlistEntrySuccess int + WaitlistGetWaitlistEntryErrors int + WaitlistGetWaitlistEntryCompleted bool + WaitlistDeleteWaitlistEntrySuccess int + WaitlistDeleteWaitlistEntryErrors int + WaitlistDeleteWaitlistEntryCompleted bool +} + +// HTTPConnection is a convenience struct for holding connection-related objects. +type HTTPConnection struct { + Client *http.Client + Credentials string + Marshaler *jsonpb.Marshaler + BaseURL string +} + +// GetURL computes the URL for an RPC. +func (h HTTPConnection) GetURL(rpcName string) string { + if rpcName != "" { + return h.BaseURL + "/v3/" + rpcName + } + return h.BaseURL +}
diff --git a/utils/utils.go b/utils/utils.go index c5e102f..03984b2 100644 --- a/utils/utils.go +++ b/utils/utils.go
@@ -1,5 +1,5 @@ /* -Copyright 2017 Google Inc. +Copyright 2019 Google Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,25 +18,32 @@ package utils import ( - "crypto/md5" + "crypto/sha256" "errors" "fmt" "io/ioutil" "log" "math/rand" + "os" "path" + "path/filepath" "sort" "strings" + "time" "github.com/golang/protobuf/jsonpb" "github.com/golang/protobuf/proto" "github.com/google/go-cmp/cmp" + fpb "github.com/maps-booking-v3/feeds" + mpb "github.com/maps-booking-v3/v3" - wpb "github.com/maps-booking-v3/waitlist" + wpb "github.com/maps-booking-v3/v3waitlist" ) +const logFile = "http_test_client_log_" + // SlotKey is a struct representing a unique service. type SlotKey struct { MerchantID string @@ -45,13 +52,45 @@ RoomID string } +func createLogFile(outPath string) (*os.File, error) { + var err error + if outPath == "" { + outPath, err = os.Getwd() + if err != nil { + return nil, err + } + } + + now := time.Now().UTC() + nowString := fmt.Sprintf("%d-%02d-%02d_%02d-%02d-%02d", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second()) + outFile := filepath.Join(outPath, fmt.Sprintf("%s%s", logFile, nowString)) + + return os.Create(outFile) +} + +// MakeLogger creates a logger that either logs to terminal or to a file. If outputToTerminal +// is true, outputDir will be ignored. +func MakeLogger(outputToTerminal bool, outputDir string) (*log.Logger, *os.File, error) { + var logger *log.Logger + if outputToTerminal { + logger = log.New(os.Stderr, "", log.Flags()) + return logger, nil, nil + } + f, err := createLogFile(outputDir) + if err != nil { + return nil, nil, err + } + logger = log.New(f, "", log.Flags()) + return logger, f, nil +} + // LogFlow is a convenience function for logging common flows.. -func LogFlow(f string, status string) { - log.Println(strings.Join([]string{"\n##########\n", status, f, "Flow", "\n##########"}, " ")) +func LogFlow(logger *log.Logger, f string, status string) { + logger.Println(strings.Join([]string{"\n##########\n", status, f, "Flow", "\n##########"}, " ")) } // ReduceServices randomly selects and returns numTestServices from the provided services. -func ReduceServices(allServices []*fpb.Service, numTestServices int) []*fpb.Service { +func ReduceServices(logger *log.Logger, allServices []*fpb.Service, numTestServices int) []*fpb.Service { reducedServices := make([]*fpb.Service, 0, numTestServices) if len(allServices) <= numTestServices { @@ -61,7 +100,7 @@ reducedServices = append(reducedServices, allServices[n]) } } - log.Printf("Selected %d services out of a possible %d", len(reducedServices), len(allServices)) + logger.Printf("Selected %d services out of a possible %d", len(reducedServices), len(allServices)) return reducedServices } @@ -152,13 +191,8 @@ return lineItem } -// MerchantLineItemMapFrom attempts to build a collection of LineItems from a service and availability feed. -func MerchantLineItemMapFrom(serviceFeed, availabilityFeed string, testSlots int) (map[string][]*mpb.LineItem, error) { - services, err := ParseServiceFeed(serviceFeed) - if err != nil { - return nil, err - } - +// BuildLineItemMap creats a collection of LineItems from slices of services and availabilities. +func BuildLineItemMap(services []*fpb.Service, availabilities []*fpb.Availability) (map[string][]*mpb.LineItem, error) { feedHasTicketType := false serviceTicketTypeMap := make(map[string][]*fpb.TicketType) for _, service := range services { @@ -178,12 +212,7 @@ } if !feedHasTicketType { - return nil, errors.New("no valid ticket types found in service feed, please update service feed and retry") - } - - availabilities, err := AvailabilityFrom(availabilityFeed, testSlots, false) - if err != nil { - return nil, err + return nil, errors.New("no valid ticket types found in services, please update services and retry") } merchantLineItemMap := make(map[string][]*mpb.LineItem) @@ -198,14 +227,16 @@ merchantLineItemMap[availability.GetMerchantId()] = append(merchantLineItemMap[availability.GetMerchantId()], lineItem) } } - + if len(merchantLineItemMap) == 0 { + return nil, errors.New("no availability slots with ticket type IDs matching those in the service were found") + } return merchantLineItemMap, nil } // AvailabilityFrom parses the file specified in availabilityFeed, returning a random permutation of availability data, maximum entries specified in testSlots -func AvailabilityFrom(availabilityFeed string, testSlots int, forRescheduling bool) ([]*fpb.Availability, error) { - LogFlow("Parse Input Feed", "Start") - defer LogFlow("Parse Input Feed", "End") +func AvailabilityFrom(logger *log.Logger, availabilityFeed string, testSlots int, forRescheduling bool) ([]*fpb.Availability, error) { + LogFlow(logger, "Parse Input Feed", "Start") + defer LogFlow(logger, "Parse Input Feed", "End") var feed fpb.AvailabilityFeed content, err := ioutil.ReadFile(availabilityFeed) @@ -248,16 +279,40 @@ } } } - log.Printf("Selected %d slots out of a possible %d", len(finalAvailability), len(rawAvailability)) + logger.Printf("Selected %d slots out of a possible %d", len(finalAvailability), len(rawAvailability)) return finalAvailability, nil } +func slotDiff(got, want *mpb.Slot) string { + // CONFIRMATION_MODE_SYNCHRONOUS and CONFIRMATION_MODE_UNSPECIFIED should be treated as equivalent. + var gotConfirmationMode, wantConfirmationMode mpb.ConfirmationMode + var gotComp, wantComp mpb.Slot + if got != nil { + gotComp = *got + gotConfirmationMode = got.GetConfirmationMode() + gotComp.ConfirmationMode = mpb.ConfirmationMode_CONFIRMATION_MODE_UNSPECIFIED + } + if want != nil { + wantComp = *want + wantConfirmationMode = want.GetConfirmationMode() + wantComp.ConfirmationMode = mpb.ConfirmationMode_CONFIRMATION_MODE_UNSPECIFIED + } + if diff := cmp.Diff(gotComp, wantComp, cmp.Comparer(proto.Equal)); diff != "" { + return diff + } + if (wantConfirmationMode == mpb.ConfirmationMode_CONFIRMATION_MODE_ASYNCHRONOUS) != + (gotConfirmationMode == mpb.ConfirmationMode_CONFIRMATION_MODE_ASYNCHRONOUS) { + return cmp.Diff(gotConfirmationMode, wantConfirmationMode) + } + return "" +} + // ValidateBooking performs granular comparisons between all got and want Bookings. func ValidateBooking(got, want *mpb.Booking) error { if got.GetBookingId() == "" { return errors.New("booking_id is empty") } - if diff := cmp.Diff(got.GetSlot(), want.GetSlot(), cmp.Comparer(proto.Equal)); diff != "" { + if diff := slotDiff(got.GetSlot(), want.GetSlot()); diff != "" { return fmt.Errorf("slots differ (-got +want)\n%s", diff) } // UserId is the only required field for the partner to return. @@ -362,7 +417,7 @@ for _, ticket := range l.GetTickets() { uID = append(uID, ticket.GetTicketId()) } - return fmt.Sprintf("%x", md5.Sum([]byte(strings.Join(uID, `|`)))) + return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(uID, `|`)))) } // Orders is a convenience type for an Orders array @@ -500,20 +555,20 @@ } waitLength := waitEstimate.GetWaitLength() if waitLength == nil { - return errors.New("wait estimate not specified") + return errors.New("wait length not specified") } if waitLength.GetPartiesAheadCount() < 0 { return errors.New("parties ahead count < 0") } estimatedSeatTimeRange := waitLength.GetEstimatedSeatTimeRange() if estimatedSeatTimeRange == nil { - return errors.New("estimated seat time range not specified") + return nil } if estimatedSeatTimeRange.GetStartSeconds() <= 0 { - return errors.New("start seconds <= 0") + return errors.New("estimated seat time range provided and start seconds <= 0") } if estimatedSeatTimeRange.GetEndSeconds() <= 0 { - return errors.New("end seconds <= 0") + return errors.New("estimated seat time range provided and end seconds <= 0") } return nil }
diff --git a/waitlist/waitlist.pb.go b/v3waitlist/v3waitlist.pb.go similarity index 100% rename from waitlist/waitlist.pb.go rename to v3waitlist/v3waitlist.pb.go
diff --git a/waitlist/waitlistTests.go b/waitlist/waitlistTests.go new file mode 100644 index 0000000..d67c6ef --- /dev/null +++ b/waitlist/waitlistTests.go
@@ -0,0 +1,165 @@ +/* +Copyright 2019 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package waitlist contains test logic for waitlist related endpoints. +package waitlist + +import ( + "context" + "log" + + fpb "github.com/maps-booking-v3/feeds" + "github.com/maps-booking-v3/api" + "github.com/maps-booking-v3/utils" +) + +func logStats(stats *utils.TestSummary, logger *log.Logger, config *utils.Config) { + logger.Println("\n************* Begin Stats *************\n") + var totalErrors int + if config.WaitlistHealthFlow || config.WaitlistAllFlows { + if stats.WaitlistHealthCheckSuccess { + logger.Println("HealthCheck Succeeded") + } else { + totalErrors++ + logger.Println("HealthCheck Failed") + } + } + if config.WaitlistBatchGetWaitEstimatesFlow || config.WaitlistAllFlows { + totalErrors += stats.WaitlistBatchGetWaitEstimatesErrors + logger.Printf("BatchGetWaitEstimates Errors: %d/%d", stats.WaitlistBatchGetWaitEstimatesErrors, stats.WaitlistBatchGetWaitEstimatesErrors+stats.WaitlistBatchGetWaitEstimatesSuccess) + } + if config.WaitlistCreateWaitlistEntryFlow || config.WaitlistAllFlows { + totalErrors += stats.WaitlistCreateWaitlistEntryErrors + logger.Printf("CreateWaitlistEntry Errors: %d/%d", stats.WaitlistCreateWaitlistEntryErrors, stats.WaitlistCreateWaitlistEntryErrors+stats.WaitlistCreateWaitlistEntrySuccess) + } + if config.WaitlistGetWaitlistEntryFlow || config.WaitlistAllFlows { + totalErrors += stats.WaitlistGetWaitlistEntryErrors + logger.Printf("GetWaitlistEntry Errors: %d/%d", stats.WaitlistGetWaitlistEntryErrors, stats.WaitlistGetWaitlistEntryErrors+stats.WaitlistGetWaitlistEntrySuccess) + } + if config.WaitlistDeleteWaitlistEntryFlow || config.WaitlistAllFlows { + totalErrors += stats.WaitlistDeleteWaitlistEntryErrors + logger.Printf("DeleteWaitlistEntry Errors: %d/%d", stats.WaitlistDeleteWaitlistEntryErrors, stats.WaitlistDeleteWaitlistEntryErrors+stats.WaitlistDeleteWaitlistEntrySuccess) + } + + logger.Println("\n\n\n") + if totalErrors == 0 { + logger.Println("All Tests Pass!") + } else { + logger.Printf("Found %d Errors", totalErrors) + } + + logger.Println("\n************* End Stats *************\n") +} + +// generateWaitlistEntries creates a waitlist entry for each provided service. +func generateWaitlistEntries(ctx context.Context, logger *log.Logger, services []*fpb.Service, stats *utils.TestSummary, conn *utils.HTTPConnection) []string { + logger.Println("no previous waitlist entries to use, acquiring new inventory") + utils.LogFlow(logger, "Generate Fresh Entries", "Start") + defer utils.LogFlow(logger, "Generate Fresh Entries", "End") + + var out []string + totalServices := len(services) + for i, s := range services { + id, err := api.CreateWaitlistEntry(ctx, logger, s, conn) + if err != nil { + logger.Printf("%s. skipping waitlistEntry %d/%d, serviceID: %s", + err.Error(), i, totalServices, s.GetServiceId()) + stats.WaitlistCreateWaitlistEntryErrors++ + continue + } + out = append(out, id) + stats.WaitlistCreateWaitlistEntrySuccess++ + } + return out +} + +// RunTests runs waitlist tests. +func RunTests(ctx context.Context, logger *log.Logger, config *utils.Config, reducedServices []*fpb.Service, stats *utils.TestSummary) { + // HealthCheck Flow + conn := config.Conn + if config.WaitlistHealthFlow || config.WaitlistAllFlows { + stats.WaitlistHealthCheckSuccess = true + if err := api.HealthCheck(ctx, logger, conn); err != nil { + stats.WaitlistHealthCheckSuccess = false + logger.Println(err.Error()) + } + stats.WaitlistHealthCheckCompleted = true + if !config.WaitlistAllFlows && !config.WaitlistBatchGetWaitEstimatesFlow && !config.WaitlistCreateWaitlistEntryFlow && + !config.WaitlistGetWaitlistEntryFlow && !config.WaitlistDeleteWaitlistEntryFlow { + logStats(stats, logger, config) + return + } + } + + // BatchGetWaitEstimates Flow + if config.WaitlistBatchGetWaitEstimatesFlow || config.WaitlistAllFlows { + utils.LogFlow(logger, "BatchGetWaitEstimates", "Start") + + for i, s := range reducedServices { + if err := api.BatchGetWaitEstimates(ctx, logger, s, conn); err != nil { + logger.Printf("%s. BatchGerWaitEstimates failed for service %d/%d. Service_id: %s", + err.Error(), i, stats.WaitlistTotalServicesProcessed, s.GetServiceId()) + stats.WaitlistBatchGetWaitEstimatesErrors++ + continue + } + stats.WaitlistBatchGetWaitEstimatesSuccess++ + } + stats.WaitlistBatchGetWaitEstimatesCompleted = true + utils.LogFlow(logger, "BatchGetWaitEstimates", "End") + } + // CreateWaitlistEntry Flow. + var ids []string + if config.WaitlistCreateWaitlistEntryFlow || config.WaitlistGetWaitlistEntryFlow || + config.WaitlistDeleteWaitlistEntryFlow || config.WaitlistAllFlows { + utils.LogFlow(logger, "CreateWaitlistEntry", "Start") + ids = generateWaitlistEntries(ctx, logger, reducedServices, stats, conn) + stats.WaitlistCreateWaitlistEntryCompleted = true + utils.LogFlow(logger, "CreateWaitlistEntry", "End") + } + // GetWaitlistEntry Flow + if config.WaitlistGetWaitlistEntryFlow || config.WaitlistAllFlows { + utils.LogFlow(logger, "GetWaitlistEntry", "Start") + for _, id := range ids { + if _, err := api.GetWaitlistEntry(ctx, logger, id, conn); err != nil { + logger.Printf("%s. get waitlist entry failed for waitlist entry id: %s", + err.Error(), id) + stats.WaitlistGetWaitlistEntryErrors++ + continue + } + stats.WaitlistGetWaitlistEntrySuccess++ + } + stats.WaitlistGetWaitlistEntryCompleted = true + utils.LogFlow(logger, "GetWaitlistEntry", "End") + } + + // DeleteWaitlistentry Flow + if config.WaitlistDeleteWaitlistEntryFlow || config.WaitlistAllFlows { + utils.LogFlow(logger, "DeleteWaitlistEntry", "Start") + + for _, id := range ids { + if err := api.DeleteWaitlistEntry(ctx, logger, id, conn); err != nil { + logger.Printf("%s. Delete waitlist entry failed for waitlist entry id: %s", + err.Error(), id) + stats.WaitlistDeleteWaitlistEntryErrors++ + continue + } + stats.WaitlistDeleteWaitlistEntrySuccess++ + } + stats.WaitlistDeleteWaitlistEntryCompleted = true + utils.LogFlow(logger, "DeleteWaitlistEntry", "End") + } + logStats(stats, logger, config) +}