| /* | |
| 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 api contains validation wrappers over BookingService endpoints. | |
| package api | |
| import ( | |
| "bytes" | |
| "context" | |
| "crypto/tls" | |
| "crypto/x509" | |
| "encoding/base64" | |
| "errors" | |
| "fmt" | |
| "io/ioutil" | |
| "log" | |
| "math/rand" | |
| "net/http" | |
| "sort" | |
| "strconv" | |
| "strings" | |
| "time" | |
| epb "github.com/golang/protobuf/ptypes/empty" | |
| "github.com/golang/protobuf/jsonpb" | |
| "github.com/golang/protobuf/proto" | |
| "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/v3waitlist" | |
| ) | |
| const ( | |
| userID = "0" | |
| firstName = "Jane" | |
| lastName = "Doe" | |
| telephone = "+1 800-789-7890" | |
| email = "test@example.com" | |
| reqTimeout = 10 * time.Second | |
| ) | |
| func setupCertConfig(caFile string, fullServerName string) (*tls.Config, error) { | |
| if caFile == "" { | |
| return nil, nil | |
| } | |
| b, err := ioutil.ReadFile(caFile) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to read root certificates file: %v", err) | |
| } | |
| cp := x509.NewCertPool() | |
| if !cp.AppendCertsFromPEM(b) { | |
| return nil, errors.New("failed to parse root certificates, please check your roots file (ca_file flag) and try again") | |
| } | |
| return &tls.Config{ | |
| RootCAs: cp, | |
| ServerName: fullServerName, | |
| }, nil | |
| } | |
| // 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) (*utils.HTTPConnection, error) { | |
| // Set up username/password. | |
| var credentials string | |
| if credentialsFile != "" { | |
| data, err := ioutil.ReadFile(credentialsFile) | |
| if err != nil { | |
| return nil, err | |
| } | |
| credentials = "Basic " + base64.StdEncoding.EncodeToString([]byte(strings.Replace(string(data), "\n", "", -1))) | |
| } | |
| config, err := setupCertConfig(caFile, fullServerName) | |
| if err != nil { | |
| return nil, err | |
| } | |
| protocol := "http" | |
| if config != nil { | |
| protocol = "https" | |
| } | |
| return &utils.HTTPConnection{ | |
| Client: &http.Client{ | |
| Transport: &http.Transport{TLSClientConfig: config}, | |
| }, | |
| Credentials: credentials, | |
| Marshaler: &jsonpb.Marshaler{OrigName: true}, | |
| BaseURL: protocol + "://" + serverAddr, | |
| }, nil | |
| } | |
| // Bookings is a convenience type for a booking array. | |
| type Bookings []*mpb.Booking | |
| func (b Bookings) Len() int { | |
| return len(b) | |
| } | |
| func (b Bookings) Less(i, j int) bool { | |
| return b[i].GetBookingId() < b[j].GetBookingId() | |
| } | |
| func (b Bookings) Swap(i, j int) { | |
| b[i], b[j] = b[j], b[i] | |
| } | |
| // HealthCheck performs a health check. | |
| 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") | |
| 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) | |
| 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) | |
| } | |
| logger.Printf("health check success! Got status: %s", resp.Status) | |
| return nil | |
| } | |
| 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 | |
| } | |
| // 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) | |
| } | |
| defer httpResp.Body.Close() | |
| bodyBytes, err := ioutil.ReadAll(httpResp.Body) | |
| if err != nil { | |
| return "", fmt.Errorf("Could not read http response body") | |
| } | |
| bodyString := string(bodyBytes) | |
| 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(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()) | |
| } | |
| reqPB := &mpb.CheckAvailabilityRequest{ | |
| Slot: slot, | |
| } | |
| req, err := conn.Marshaler.MarshalToString(reqPB) | |
| if err != nil { | |
| return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) | |
| } | |
| httpResp, err := sendRequest(ctx, logger, "CheckAvailability", req, conn) | |
| if err != nil { | |
| return fmt.Errorf("invalid response. CheckAvailability yielded error: %v", err) | |
| } | |
| var resp mpb.CheckAvailabilityResponse | |
| if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | |
| return fmt.Errorf("CheckAvailability: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| if diff := cmp.Diff(resp.GetSlot(), slot, cmp.Comparer(proto.Equal)); diff != "" { | |
| return fmt.Errorf("invalid response. CheckAvailability slots differ (-got +want)\n%s", diff) | |
| } | |
| if resp.GetCountAvailable() == 0 { | |
| return errors.New("no count available in response") | |
| } | |
| return nil | |
| } | |
| // 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(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) | |
| if err != nil { | |
| return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) | |
| } | |
| httpResp, err := sendRequest(ctx, logger, "BatchAvailabilityLookup", req, conn) | |
| if err != nil { | |
| return fmt.Errorf("invalid response. BatchAvailabilityLookup yielded error: %v", err) | |
| } | |
| var resp mpb.BatchAvailabilityLookupResponse | |
| if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | |
| return fmt.Errorf("BatchAvailabilityLookup: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| if len(resp.GetSlotTimeAvailability()) != len(reqPB.GetSlotTime()) { | |
| return fmt.Errorf("invalid response. BatchAvailabilityLookup response.size and request.size differ: %v vs %v", len(resp.GetSlotTimeAvailability()), len(reqPB.GetSlotTime())) | |
| } | |
| diffCount := 0 | |
| for i := 0; i < len(reqPB.GetSlotTime()); i++ { | |
| slotTimeReq := reqPB.GetSlotTime()[i] | |
| slotTimeResp := resp.GetSlotTimeAvailability()[i].GetSlotTime() | |
| if diff := cmp.Diff(slotTimeReq, slotTimeResp, cmp.Comparer(proto.Equal)); diff != "" { | |
| logger.Printf("Slot %v differs: req=%v, resp=%v", i, slotTimeReq, slotTimeResp) | |
| diffCount++ | |
| } | |
| } | |
| if diffCount > 0 { | |
| return fmt.Errorf("invalid response. Found %v diffs", diffCount) | |
| } | |
| return nil | |
| } | |
| // CreateBooking attempts to create bookings from availability slots. | |
| 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()) | |
| } | |
| gen := rand.New(rand.NewSource(time.Now().UnixNano())) | |
| // Lease currently unsupported. | |
| reqPB := &mpb.CreateBookingRequest{ | |
| Slot: slot, | |
| UserInformation: &mpb.UserInformation{ | |
| UserId: userID, | |
| GivenName: firstName, | |
| FamilyName: lastName, | |
| Telephone: telephone, | |
| Email: email, | |
| }, | |
| PaymentInformation: &mpb.PaymentInformation{ | |
| PrepaymentStatus: mpb.PrepaymentStatus_PREPAYMENT_NOT_PROVIDED, | |
| }, | |
| IdempotencyToken: strconv.Itoa(gen.Intn(1000000)), | |
| } | |
| req, err := conn.Marshaler.MarshalToString(reqPB) | |
| if err != nil { | |
| return nil, fmt.Errorf("Could not convert pb3 to json: %v", reqPB) | |
| } | |
| httpResp, err := sendRequest(ctx, logger, "CreateBooking", req, conn) | |
| if err != nil { | |
| return nil, fmt.Errorf("invalid response. CreateBooking yielded error: %v", err) | |
| } | |
| var resp mpb.CreateBookingResponse | |
| if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | |
| return nil, fmt.Errorf("CreateBooking: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| if resp.GetBookingFailure() != nil { | |
| return nil, fmt.Errorf("invalid response. CreateBooking failed with booking failure %v", resp.GetBookingFailure()) | |
| } | |
| b := resp.GetBooking() | |
| if iE := utils.ValidateBooking(b, &mpb.Booking{ | |
| Slot: reqPB.GetSlot(), | |
| UserInformation: reqPB.GetUserInformation(), | |
| PaymentInformation: reqPB.GetPaymentInformation(), | |
| }); iE != nil { | |
| return nil, fmt.Errorf("invalid response. CreateBooking invalid: %s", iE.Error()) | |
| } | |
| // Perform idempotency test. | |
| 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) | |
| } | |
| var idemResp mpb.CreateBookingResponse | |
| if err := jsonpb.UnmarshalString(idemHTTPResp, &idemResp); err != nil { | |
| return nil, fmt.Errorf("CreateBooking idem: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| if diff := cmp.Diff(idemResp, resp); diff != "" { | |
| return b, fmt.Errorf("Idempotency check invalid (-got +want)\n%s", diff) | |
| } | |
| return b, nil | |
| } | |
| // ListBookings calls the maps booking ListBookings rpc and compares the return with all input bookings. | |
| 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) | |
| if err != nil { | |
| return nil, fmt.Errorf("Could not convert pb3 to json: %v", reqPB) | |
| } | |
| 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) | |
| } | |
| var resp mpb.ListBookingsResponse | |
| if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | |
| return nil, fmt.Errorf("ListBookings: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| gB := Bookings(resp.GetBookings()) | |
| if len(tB) == 0 { | |
| logger.Printf("ListBookings returning %d found bookings", len(gB)) | |
| return gB, nil | |
| } | |
| if 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 { | |
| logger.Printf("ListBookings invalid, %s, abandoning slot %d/%d", iE.Error(), i, len(tB)) | |
| continue | |
| } | |
| out = append(out, tB[i]) | |
| } | |
| logger.Printf("ListBookings returning %d bookings", len(out)) | |
| return out, nil | |
| } | |
| // GetBookingStatus checks that all input bookings are in an acceptable state. | |
| 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) | |
| if err != nil { | |
| return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) | |
| } | |
| httpResp, err := sendRequest(ctx, logger, "GetBookingStatus", req, conn) | |
| if err != nil { | |
| return fmt.Errorf("invalid response. GetBookingStatus yielded error: %v", err) | |
| } | |
| var resp mpb.GetBookingStatusResponse | |
| if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | |
| return fmt.Errorf("GetBookingsStatus: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| if diff := cmp.Diff(resp.GetBookingStatus(), mpb.BookingStatus_CONFIRMED); diff != "" { | |
| return fmt.Errorf("invalid response. BookingStatus differ (-got +want)\n%s", diff) | |
| } | |
| return nil | |
| } | |
| // CancelBooking is a clean up method that cancels all supplied bookings. | |
| 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) | |
| if err != nil { | |
| return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) | |
| } | |
| httpResp, err := sendRequest(ctx, logger, "UpdateBooking", req, conn) | |
| if err != nil { | |
| return fmt.Errorf("invalid response. UpdateBooking yielded error: %v", err) | |
| } | |
| var resp mpb.UpdateBookingResponse | |
| if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | |
| return fmt.Errorf("CancelBooking: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| // 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(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. | |
| if len(v) <= 1 { | |
| continue | |
| } | |
| slots = v | |
| break | |
| } | |
| if len(slots) == 0 { | |
| return errors.New("no suitable availability for rescheduling flow. exiting") | |
| } | |
| // Book first slot. | |
| newBooking, err := CreateBooking(ctx, logger, slots[0], conn) | |
| if err != nil { | |
| return fmt.Errorf("could not complete booking, abandoning rescheduling flow: %v", err) | |
| } | |
| // New slot. | |
| lastAvailability := slots[len(slots)-1] | |
| reqPB := &mpb.UpdateBookingRequest{ | |
| Booking: &mpb.Booking{ | |
| BookingId: newBooking.GetBookingId(), | |
| Slot: &mpb.Slot{ | |
| StartSec: lastAvailability.GetStartSec(), | |
| DurationSec: lastAvailability.GetDurationSec(), | |
| }, | |
| }, | |
| } | |
| 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(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) | |
| } | |
| // Update slot before performing diff. | |
| newBooking.GetSlot().StartSec = lastAvailability.GetStartSec() | |
| newBooking.GetSlot().DurationSec = lastAvailability.GetDurationSec() | |
| if iE := utils.ValidateBooking(resp.GetBooking(), newBooking); iE != nil { | |
| return fmt.Errorf("invalid response. UpdateBooking: %s, abandoning slot 1/1", iE.Error()) | |
| } | |
| 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(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) | |
| if err != nil { | |
| return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) | |
| } | |
| httpResp, err := sendRequest(ctx, logger, "CheckOrderFulfillability", req, conn) | |
| if err != nil { | |
| return fmt.Errorf("invalid response. CheckOrderFulfillability yielded error: %v", err) | |
| } | |
| var resp mpb.CheckOrderFulfillabilityResponse | |
| if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | |
| return fmt.Errorf("CheckOrderFulfillability: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| orderFulfillability := resp.GetFulfillability() | |
| 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) | |
| } | |
| if orderFulfillability.GetUnfulfillableReason() != "" { | |
| return errors.New("invalid response. CheckOrderFulfillability.UnfulfillableReason should be empty") | |
| } | |
| var respLineItems []*mpb.LineItem | |
| for _, lineItemFulfillability := range orderFulfillability.GetItemFulfillability() { | |
| if diff := cmp.Diff(lineItemFulfillability.GetResult(), mpb.LineItemFulfillability_CAN_FULFILL); diff != "" { | |
| return fmt.Errorf("invalid response. CheckOrderFulfillability.Fulfillability.ItemFulfillability.Result for LineItem %v -- differ (-got +want)\n%s", lineItemFulfillability.GetItem(), diff) | |
| } | |
| if lineItemFulfillability.GetUnfulfillableReason() != "" { | |
| return errors.New("invalid response. CheckOrderFulfillability.Fulfillability.ItemFulfillability.UnfulfillableReason should be empty") | |
| } | |
| respLineItems = append(respLineItems, lineItemFulfillability.GetItem()) | |
| } | |
| if err = utils.ValidateLineItems(respLineItems, lineItems, false); err != nil { | |
| return fmt.Errorf("invalid response. CheckOrderFulfillability %v", err) | |
| } | |
| return nil | |
| } | |
| // CreateOrder will attempt to build an order from a merchant id and array of line orders. | |
| 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{ | |
| UserInformation: &mpb.UserInformation{ | |
| UserId: userID, | |
| GivenName: firstName, | |
| FamilyName: lastName, | |
| Telephone: telephone, | |
| Email: email, | |
| }, | |
| PaymentInformation: &mpb.PaymentInformation{ | |
| PrepaymentStatus: mpb.PrepaymentStatus_PREPAYMENT_NOT_PROVIDED, | |
| }, | |
| MerchantId: merchantID, | |
| Item: lineItems, | |
| } | |
| reqPB := &mpb.CreateOrderRequest{ | |
| Order: reqOrder, | |
| IdempotencyToken: strconv.Itoa(gen.Intn(1000000)), | |
| } | |
| req, err := conn.Marshaler.MarshalToString(reqPB) | |
| if err != nil { | |
| return nil, fmt.Errorf("Could not convert pb3 to json: %v", reqPB) | |
| } | |
| httpResp, err := sendRequest(ctx, logger, "CreateOrder", req, conn) | |
| if err != nil { | |
| return nil, fmt.Errorf("invalid response. CreateOrder yielded error: %v", err) | |
| } | |
| var resp mpb.CreateOrderResponse | |
| if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | |
| return nil, fmt.Errorf("CreateOrder: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| if resp.GetOrderFailure() != nil { | |
| return nil, fmt.Errorf("invalid response. CreateOrder contains OrderFailure for request %v", reqPB) | |
| } | |
| if err = utils.ValidateOrder(*resp.GetOrder(), *reqOrder); err != nil { | |
| return nil, fmt.Errorf("invalid response. CreateOrder %v", err) | |
| } | |
| // Perform idempotency test. | |
| 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) | |
| } | |
| var idemResp mpb.CreateOrderResponse | |
| if err := jsonpb.UnmarshalString(idemHTTPResp, &idemResp); err != nil { | |
| return nil, fmt.Errorf("CreateOrder idem: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| if idemResp.GetOrderFailure() != nil { | |
| return nil, errors.New("Idempotency check invalid. CreateOrder contains OrderFailure") | |
| } | |
| if err = utils.ValidateOrder(*idemResp.GetOrder(), *resp.GetOrder()); err != nil { | |
| return nil, fmt.Errorf("Idempotency check invalid %v", err) | |
| } | |
| return resp.GetOrder(), nil | |
| } | |
| 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(ctx, logger, "ListOrders", req, conn) | |
| if err != nil { | |
| return mpb.ListOrdersResponse{}, fmt.Errorf("invalid response. ListOrders yielded error: %v", err) | |
| } | |
| var resp mpb.ListOrdersResponse | |
| if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | |
| return resp, fmt.Errorf("ListOrders: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| return resp, nil | |
| } | |
| // 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(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") | |
| } | |
| // UserId check first. | |
| reqPB := &mpb.ListOrdersRequest{ | |
| Ids: &mpb.ListOrdersRequest_UserId{userID}, | |
| } | |
| respUser, err := sendListOrdersRequest(ctx, logger, reqPB, conn) | |
| if err != nil { | |
| return err | |
| } | |
| if err = utils.ValidateOrders(respUser.GetOrder(), orders); err != nil { | |
| return fmt.Errorf("invalid response. ListOrders %v for request %v", err, reqPB) | |
| } | |
| // Still here? OrderId check. | |
| reqPB.Reset() | |
| var orderIDs mpb.ListOrdersRequest_OrderIds | |
| for _, order := range orders { | |
| orderIDs.OrderId = append(orderIDs.OrderId, order.GetOrderId()) | |
| } | |
| reqPB.Ids = &mpb.ListOrdersRequest_OrderIds_{&orderIDs} | |
| respOrder, err := sendListOrdersRequest(ctx, logger, reqPB, conn) | |
| if err != nil { | |
| return err | |
| } | |
| if err = utils.ValidateOrders(respOrder.GetOrder(), orders); err != nil { | |
| return fmt.Errorf("invalid response. ListOrders %v for request %v", err, reqPB) | |
| } | |
| return nil | |
| } | |
| // BatchGetWaitEstimates calls the partners API and verifies the returned WaitEstimates. | |
| 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()) | |
| for i := range ps { | |
| ps[i] = rules.GetMinPartySize() + int32(i) | |
| } | |
| reqPB := &wpb.BatchGetWaitEstimatesRequest{ | |
| MerchantId: s.GetMerchantId(), | |
| ServiceId: s.GetServiceId(), | |
| PartySize: ps, | |
| } | |
| req, err := conn.Marshaler.MarshalToString(reqPB) | |
| if err != nil { | |
| return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) | |
| } | |
| httpResp, err := sendRequest(ctx, logger, "BatchGetWaitEstimates", req, conn) | |
| if err != nil { | |
| return fmt.Errorf("invalid response. BatchGetWaitEstimates yielded error: %v", err) | |
| } | |
| var resp wpb.BatchGetWaitEstimatesResponse | |
| if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | |
| return fmt.Errorf("BatchGetWaitEstimates: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| if resp.GetWaitlistStatus() == wpb.WaitlistStatus_WAITLIST_STATUS_UNSPECIFIED { | |
| return errors.New("BatchGetWaitEstimates: waitlist status was not specified in response") | |
| } else if resp.GetWaitlistStatus() != wpb.WaitlistStatus_OPEN { | |
| // Waitlist is closed. Wait estimate should not be provided. | |
| if len(resp.GetWaitEstimate()) > 0 { | |
| return errors.New("BatchGetWaitEstimates: wait estimate must not be provided when waitlist is closed") | |
| } | |
| } | |
| // Waitlist status is OPEN | |
| for _, we := range resp.GetWaitEstimate() { | |
| if err := utils.ValidateWaitEstimate(we); err != nil { | |
| return fmt.Errorf("BatchGetWaitEstimates invalid, %s, service id: %s", | |
| err.Error(), s.GetServiceId()) | |
| } else if we.GetPartySize() > rules.GetMaxPartySize() || | |
| we.GetPartySize() < rules.GetMinPartySize() { | |
| return fmt.Errorf("batchGetWaitEstimates: Party size outside of min/max for service id: %s."+ | |
| "returned party size: %d, min party size: %d, max party size: %d", | |
| s.GetServiceId(), we.GetPartySize(), rules.GetMinPartySize(), rules.GetMaxPartySize()) | |
| } | |
| } | |
| return nil | |
| } | |
| // 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(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{ | |
| MerchantId: s.GetMerchantId(), | |
| ServiceId: s.GetServiceId(), | |
| PartySize: s.GetWaitlistRules().GetMaxPartySize(), | |
| UserInformation: &wpb.UserInformation{ | |
| UserId: userID, | |
| GivenName: firstName, | |
| FamilyName: lastName, | |
| Telephone: telephone, | |
| Email: email, | |
| }, | |
| IdempotencyToken: strconv.Itoa(gen.Intn(1000000)), | |
| } | |
| if s.GetWaitlistRules().GetSupportsAdditionalRequest() { | |
| reqPB.AdditionalRequest = "test additional request" | |
| } | |
| req, err := conn.Marshaler.MarshalToString(reqPB) | |
| if err != nil { | |
| return "", fmt.Errorf("Could not convert pb3 to json: %v", reqPB) | |
| } | |
| httpResp, err := sendRequest(ctx, logger, "CreateWaitlistEntry", req, conn) | |
| if err != nil { | |
| return "", fmt.Errorf("invalid response. CreateWaitlistEntry yielded error: %v", err) | |
| } | |
| var resp wpb.CreateWaitlistEntryResponse | |
| if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | |
| return "", fmt.Errorf("CreateWaitlistEntry: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| if resp.GetWaitlistBusinessLogicFailure() != nil { | |
| return "", fmt.Errorf("invalid response. CreateWaitlistEntry failed with business logic failure %v", | |
| resp.GetWaitlistBusinessLogicFailure()) | |
| } | |
| if resp.GetWaitlistEntryId() == "" { | |
| return "", fmt.Errorf("invalid response. CreateWaitlistEntry missing waitlist entry id for service: %s", | |
| s.GetServiceId()) | |
| } | |
| // Perform idempotency test. | |
| 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) | |
| } | |
| var idemResp wpb.CreateWaitlistEntryResponse | |
| if err := jsonpb.UnmarshalString(idemHTTPResp, &idemResp); err != nil { | |
| return "", fmt.Errorf("CreateWaitlistEntry idem: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| if diff := cmp.Diff(idemResp, resp); diff != "" { | |
| return "", fmt.Errorf("Idempotency check invalid (-got +want)\n%s for service: %s", diff, s.GetServiceId()) | |
| } | |
| return resp.GetWaitlistEntryId(), nil | |
| } | |
| // GetWaitlistEntry retrieves and validates the booking for the specified | |
| // waitlist entry id. | |
| 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) | |
| if err != nil { | |
| return nil, fmt.Errorf("Could not convert pb3 to json: %v", reqPB) | |
| } | |
| httpResp, err := sendRequest(ctx, logger, "GetWaitlistEntry", req, conn) | |
| if err != nil { | |
| return nil, fmt.Errorf("invalid response. GetWaitlistEntry yielded error: %v", err) | |
| } | |
| var resp wpb.GetWaitlistEntryResponse | |
| if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil { | |
| return nil, fmt.Errorf("GetWaitlistEntry: Could not parse HTTP response to pb3: %v", err) | |
| } | |
| if iE := utils.ValidateWaitlistEntry(resp.GetWaitlistEntry()); iE != nil { | |
| return nil, fmt.Errorf("invalid response. GetWaitlistEntry: %s", iE.Error()) | |
| } | |
| return resp.GetWaitlistEntry(), nil | |
| } | |
| // DeleteWaitlistEntry makes a request to delete the waitlist entry. | |
| 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) | |
| if err != nil { | |
| return fmt.Errorf("Could not convert pb3 to json: %v", reqPB) | |
| } | |
| httpResp, err := sendRequest(ctx, logger, "DeleteWaitlistEntry", req, conn) | |
| if err != nil { | |
| return fmt.Errorf("invalid response. DeleteWaitlistEntry yielded error: %v", err) | |
| } | |
| 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) | |
| } | |
| return nil | |
| } |