blob: 73e58736573a721f93e97d3799a9b7c6b6ba8551 [file] [log] [blame]
/*
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
)
type slotKey struct {
serviceID string
startSec int64
}
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
}
// AvailableSlotsFromBAL performs a maps booking batch availability lookup on all supplied availability slots. This function
// returns any errors when trying to call the BatchAvailabilityLookup RPC or returns all slots the aggregator marked as available.
func AvailableSlotsFromBAL(ctx context.Context, logger *log.Logger, av []*fpb.Availability, conn *utils.HTTPConnection) ([]*fpb.Availability, error) {
var slotMap = make(map[slotKey]*fpb.Availability)
for _, a := range av {
slotMap[slotKey{a.ServiceId, a.StartSec}] = a
}
reqPB, err := utils.BuildBatchAvailabilityLookupRequestFrom(av)
if err != nil {
return nil, 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 nil, fmt.Errorf("Could not convert pb3 to json: %v", reqPB)
}
httpResp, err := sendRequest(ctx, logger, "BatchAvailabilityLookup", req, conn)
if err != nil {
return nil, fmt.Errorf("invalid response. BatchAvailabilityLookup yielded error: %v", err)
}
var resp mpb.BatchAvailabilityLookupResponse
if err := jsonpb.UnmarshalString(httpResp, &resp); err != nil {
return nil, fmt.Errorf("BatchAvailabilityLookup: Could not parse HTTP response to pb3: %v", err)
}
if len(resp.GetSlotTimeAvailability()) != len(reqPB.GetSlotTime()) {
return nil, fmt.Errorf("invalid response. BatchAvailabilityLookup response.size and request.size differ: %v vs %v", len(resp.GetSlotTimeAvailability()), len(reqPB.GetSlotTime()))
}
diffCount := 0
var slots []*fpb.Availability
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 resp.GetSlotTimeAvailability()[i].GetAvailable() {
slots = append(slots, slotMap[slotKey{slotTimeResp.GetServiceId(), slotTimeResp.GetStartSec()}])
}
}
if diffCount > 0 {
return nil, fmt.Errorf("invalid response. Found %v diffs", diffCount)
}
return slots, 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, cmp.Comparer(proto.Equal)); 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)
}
// BookingStatus_CONFIRMED is the default case unless the slot is async, in which
// case the default is BookingStatus_PENDING_MERCHANT_CONFIRMATION.
wantStatus := mpb.BookingStatus_CONFIRMED
if b.GetSlot().GetConfirmationMode() == mpb.ConfirmationMode_CONFIRMATION_MODE_ASYNCHRONOUS {
wantStatus = mpb.BookingStatus_PENDING_MERCHANT_CONFIRMATION
}
if diff := cmp.Diff(resp.GetBookingStatus(), wantStatus); diff != "" {
return fmt.Errorf("invalid response. BookingStatus differs (-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, cmp.Comparer(proto.Equal)); 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
}