Initial cloud-services repo - gateway service + pkg modules
This commit is contained in:
593
pkg/tmobile/client.go
Normal file
593
pkg/tmobile/client.go
Normal file
@@ -0,0 +1,593 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/grpc/sms"
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/utils/randomvalues"
|
||||
"github.com/pkg/errors"
|
||||
errorsO "errors"
|
||||
|
||||
tmtg "fiskerinc.com/modules/tmobtokengen"
|
||||
)
|
||||
|
||||
const (
|
||||
failedToParseRequest = "failed to encode request: %v"
|
||||
payloadMsg = "payload: %s"
|
||||
contentType = "Content-Type"
|
||||
failedGeneratePod = "failed to generate pop token: %v"
|
||||
startingTMobileTimeout = time.Millisecond * 10
|
||||
maximumTMobileTimeout = time.Minute
|
||||
|
||||
Endpoint = "https://adn.t-mobile.com"
|
||||
)
|
||||
|
||||
var (
|
||||
fakeIDGenerator = randomvalues.NewNonCryptoGenerator("1234567890", 0)
|
||||
currentTMobileTimeout = startingTMobileTimeout
|
||||
)
|
||||
|
||||
type httpClienter interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type TMobClienter interface {
|
||||
AccessToken(ctx context.Context) (out *AccessTokenResponse, err error)
|
||||
SetAccessToken(accessToken string)
|
||||
SendSMS(ctx context.Context, in *SendSMSRequest) (out *SendSMSResponse, err error)
|
||||
Details(ctx context.Context, ID string) (out *SMSDetailsResponse, err error)
|
||||
ChangeRatePlan(context.Context, *ChangeRatePlanRequest) (*ChangeRatePlanResponse, error)
|
||||
CustomAttributes(context.Context, *CustomAtributesRequest) (*CustomAtributesResponse, error)
|
||||
GetProducts(context.Context, *sms.GetAvailableProductsRequest) (*sms.GetAvailableProductsResponse, error)
|
||||
DeviceDetails(context.Context, *DeviceDetailsRequest) (*DeviceDetailsResponse, error)
|
||||
ChangeDeviceStatus(ctx context.Context, cda ChangeDeviceActivation) (err error)
|
||||
|
||||
SetFilter(filter []string)
|
||||
}
|
||||
|
||||
type TMobClient struct {
|
||||
tg tmtg.Generator
|
||||
client httpClienter
|
||||
accessToken string
|
||||
baseURL *url.URL
|
||||
lock sync.RWMutex // Using a read-write mutex and anyone can send a sms at any time, but if the token is do for a renew, we should stop sending sms for a second
|
||||
toRefresh <-chan time.Time
|
||||
iccidFilter ICCIDFilter
|
||||
}
|
||||
|
||||
|
||||
var _ TMobClienter = &TMobClient{}
|
||||
|
||||
const (
|
||||
Authorization = "Authorization"
|
||||
XAuthorization = "X-Authorization"
|
||||
XAuthOriginator = "x-auth-originator"
|
||||
failedToDoRequest = "failed to do request"
|
||||
applicationJSON = "application/json"
|
||||
)
|
||||
|
||||
// baseURLstr: in the url to tmobile i.e. "https://core.saas.api.t-mobile.com"
|
||||
// tg: should have been initiated with the correct keys. I believe this service should do it itself given the correct input
|
||||
// timeout: The amount of time the http client will do a request before it gives up doing the request
|
||||
func NewTMobileClient(baseURLstr string, tg tmtg.Generator, timeout time.Duration) (*TMobClient, error) {
|
||||
u, err := url.Parse(baseURLstr)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to parse base URL")
|
||||
}
|
||||
|
||||
tmb := &TMobClient{
|
||||
baseURL: u,
|
||||
tg: tg,
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
},
|
||||
toRefresh: make(<-chan time.Time),
|
||||
}
|
||||
tmb.refresh(context.Background())
|
||||
|
||||
tmb.iccidFilter = InitFilter()
|
||||
return tmb, nil
|
||||
}
|
||||
|
||||
func (c *TMobClient) do(req *http.Request, out interface{}) error {
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(ErrDoRequest, "failed to do request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 300 {
|
||||
var body []byte
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(ErrReadResponseBody, "failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
toWrapErr := ErrBadStatusCode
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
toWrapErr = ErrAccessTokenExpired
|
||||
} else if resp.StatusCode == http.StatusBadGateway {
|
||||
toWrapErr = ErrBadGatewayCode
|
||||
} else if resp.StatusCode == http.StatusGatewayTimeout {
|
||||
toWrapErr = ErrBadGatewayCode
|
||||
}
|
||||
|
||||
return errors.WithMessagef(toWrapErr, "request failed with status code %d, body: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(out)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(ErrJSONMarshal, "failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Must call setAccessToken after calling this function to set the access token
|
||||
func (c *TMobClient) AccessToken(ctx context.Context) (out *AccessTokenResponse, err error) {
|
||||
path := "/oauth2/v1/tokens"
|
||||
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(c.tg.ClientSecretAsAuthVal()).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodPost)
|
||||
|
||||
token, err := c.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fullPath := c.baseURL.String() + path
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, nil)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrCreateRequest, "failed to create request")
|
||||
}
|
||||
|
||||
req.Header.Set(Authorization, c.tg.ClientSecretAsAuthVal())
|
||||
req.Header.Set(XAuthorization, token)
|
||||
|
||||
err = c.do(req, &out)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *TMobClient) SetAccessToken(accessToken string) {
|
||||
c.accessToken = "Bearer " + accessToken
|
||||
}
|
||||
|
||||
func (c *TMobClient) SendSMS(ctx context.Context, in *SendSMSRequest) (out *SendSMSResponse, err error) {
|
||||
if !c.iccidFilter.ShouldSend(in.ICCID) {
|
||||
out = &SendSMSResponse{
|
||||
SmsMessageID: fakeSMSID(),
|
||||
}
|
||||
return
|
||||
}
|
||||
path := "/eitcsr-iotcp-notifications-v2/prd02/iotcp/v2/notifications/sms/messages"
|
||||
|
||||
payload := new(bytes.Buffer)
|
||||
if err = json.NewEncoder(payload).Encode(in); err != nil {
|
||||
return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
|
||||
}
|
||||
|
||||
payloadStr := payload.String()
|
||||
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodPost).
|
||||
SetContentType(applicationJSON).
|
||||
SetBody(payloadStr)
|
||||
|
||||
logger.Debug().Msgf(payloadMsg, payloadStr)
|
||||
|
||||
token, err := c.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrTokenGen, "failed to generate token: %v", err)
|
||||
}
|
||||
|
||||
fullPath := c.baseURL.String() + path
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, bytes.NewBufferString(payloadStr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
req.Header.Set(Authorization, c.accessToken)
|
||||
req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
|
||||
req.Header.Set(XAuthorization, token)
|
||||
req.Header.Set(contentType, applicationJSON)
|
||||
|
||||
err = c.do(req, &out)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *TMobClient) Details(ctx context.Context, ID string) (out *SMSDetailsResponse, err error) {
|
||||
path := fmt.Sprintf("/eitcsr-iotcp-notifications-v2/prd02/iotcp/v2/notifications/sms/messages/%s", ID)
|
||||
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodGet)
|
||||
|
||||
token, err := c.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrTokenGen, "failed to generate token: %v", err)
|
||||
}
|
||||
|
||||
fullPath := c.baseURL.String() + path
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullPath, nil)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrCreateRequest, "failed to create request: %v", err)
|
||||
}
|
||||
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
req.Header.Set(Authorization, c.accessToken)
|
||||
req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
|
||||
req.Header.Set(XAuthorization, token)
|
||||
req.Header.Set(contentType, applicationJSON)
|
||||
|
||||
err = c.do(req, &out)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func InitTokenGen(pkVal, clientId, secret string) (*tmtg.PopTokenGenerator, error) {
|
||||
// No reason to take a variable and write it to a text file and then read it from the text file
|
||||
|
||||
tg, err := tmtg.NewTokenGenerator(
|
||||
clientId,
|
||||
secret,
|
||||
time.Minute*2,
|
||||
pkVal, // path to public key file
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to create token generator")
|
||||
}
|
||||
|
||||
return tg, nil
|
||||
}
|
||||
|
||||
func (c *TMobClient) ChangeRatePlan(ctx context.Context, in *ChangeRatePlanRequest) (*ChangeRatePlanResponse, error) {
|
||||
path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/change-rate-plan"
|
||||
fullPath := c.baseURL.String() + path
|
||||
|
||||
// The in body is so small, no use in using the encode method
|
||||
payload := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(payload).Encode(in); err != nil {
|
||||
return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
|
||||
}
|
||||
payloadStr := payload.String()
|
||||
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodPost).
|
||||
SetContentType(applicationJSON).
|
||||
SetBody(payloadStr)
|
||||
|
||||
logger.Debug().Msgf(payloadMsg, payloadStr)
|
||||
popToken, err := c.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
req.Header.Set(Authorization, c.accessToken)
|
||||
req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
|
||||
req.Header.Set(XAuthorization, popToken)
|
||||
req.Header.Set(contentType, applicationJSON)
|
||||
out := &ChangeRatePlanResponse{}
|
||||
|
||||
err = c.do(req, out)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *TMobClient) CustomAttributes(ctx context.Context, in *CustomAtributesRequest) (*CustomAtributesResponse, error) {
|
||||
path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/device-attributes"
|
||||
fullPath := c.baseURL.String() + path
|
||||
|
||||
payload := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(payload).Encode(in); err != nil {
|
||||
return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
|
||||
}
|
||||
payloadStr := payload.String()
|
||||
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodPost).
|
||||
SetContentType(applicationJSON).
|
||||
SetBody(payloadStr)
|
||||
|
||||
logger.Debug().Msgf(payloadMsg, payloadStr)
|
||||
popToken, err := c.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
req.Header.Set(Authorization, c.accessToken)
|
||||
req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
|
||||
req.Header.Set(XAuthorization, popToken)
|
||||
req.Header.Set(contentType, applicationJSON)
|
||||
out := &CustomAtributesResponse{}
|
||||
|
||||
err = c.do(req, out)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *TMobClient) GetProducts(ctx context.Context, in *sms.GetAvailableProductsRequest) (*sms.GetAvailableProductsResponse, error) {
|
||||
path := "/eitcsr-iot-product-service-v1/prd02/iotcp/v1/iot-product-service/products/query"
|
||||
fullPath := c.baseURL.String() + path
|
||||
|
||||
payload := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(payload).Encode(in); err != nil {
|
||||
return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
|
||||
}
|
||||
payloadStr := payload.String()
|
||||
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodPost).
|
||||
SetContentType(applicationJSON).
|
||||
SetBody(payloadStr)
|
||||
|
||||
logger.Debug().Msgf(payloadMsg, payloadStr)
|
||||
popToken, err := c.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
req.Header.Set(Authorization, c.accessToken)
|
||||
req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
|
||||
req.Header.Set(XAuthorization, popToken)
|
||||
req.Header.Set(contentType, applicationJSON)
|
||||
out := &sms.GetAvailableProductsResponse{}
|
||||
|
||||
err = c.do(req, &out.AvailableProducts)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *TMobClient) DeviceDetails(ctx context.Context, in *DeviceDetailsRequest) (*DeviceDetailsResponse, error) {
|
||||
path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/details"
|
||||
fullPath := c.baseURL.String() + path
|
||||
|
||||
payload := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(payload).Encode(in); err != nil {
|
||||
return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
|
||||
}
|
||||
payloadStr := payload.String()
|
||||
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodPost).
|
||||
SetContentType(applicationJSON).
|
||||
SetBody(payloadStr)
|
||||
|
||||
logger.Debug().Msgf(payloadMsg, payloadStr)
|
||||
popToken, err := c.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
req.Header.Set(Authorization, c.accessToken)
|
||||
req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
|
||||
req.Header.Set(XAuthorization, popToken)
|
||||
req.Header.Set(contentType, applicationJSON)
|
||||
out := &DeviceDetailsResponse{}
|
||||
|
||||
err = c.do(req, &out)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *TMobClient) refresh(ctx context.Context) {
|
||||
var expiresIn time.Duration
|
||||
|
||||
at, err := c.AccessToken(ctx)
|
||||
if err != nil {
|
||||
logger.Error().Msgf("failed to get access token: %v", err)
|
||||
expiresIn = currentTMobileTimeout
|
||||
currentTMobileTimeout = currentTMobileTimeout * currentTMobileTimeout
|
||||
if currentTMobileTimeout > maximumTMobileTimeout {
|
||||
currentTMobileTimeout = maximumTMobileTimeout
|
||||
}
|
||||
|
||||
} else {
|
||||
expiresIn = time.Duration(at.ExpiresIn) * time.Second
|
||||
expiresIn -= time.Minute * 1 // Expire 1 minute earlier than needed. Previous code was refreshing in half the time
|
||||
c.SetAccessToken(at.AccessToken)
|
||||
logger.Info().Msgf("Refreshed access token, expires in %s", expiresIn)
|
||||
// Reset the increasing timeout
|
||||
currentTMobileTimeout = startingTMobileTimeout
|
||||
}
|
||||
|
||||
time.AfterFunc(expiresIn, func() { c.refresh(context.Background()) })
|
||||
}
|
||||
|
||||
// SetFilter implements TMobClienter.
|
||||
func (c *TMobClient) SetFilter(filter []string) {
|
||||
c.iccidFilter = ICCIDFilter{
|
||||
filter: filter,
|
||||
}
|
||||
}
|
||||
|
||||
// SmsMessageID is a string consisting of numbers, will return a marker showing its not real
|
||||
func fakeSMSID() (fakeID string) {
|
||||
return fmt.Sprintf("FAKE_SMS_ID:%s", fakeIDGenerator.GetString(10))
|
||||
}
|
||||
|
||||
func IsRealSMSID(smsID string) (isReal bool) {
|
||||
return !strings.HasPrefix(smsID, "FAKE_SMS_ID:")
|
||||
}
|
||||
|
||||
|
||||
// HandleChangeDeviceStatus implements TMobClienter.
|
||||
func (c *TMobClient) ChangeDeviceStatus(ctx context.Context, cda ChangeDeviceActivation) (err error) {
|
||||
var fn func(ctx context.Context, in *ICCIDBody)(out *ICCIDBody, err error)
|
||||
if cda.Enabled {
|
||||
fn = c.handleServiceRestore
|
||||
}else {
|
||||
fn = c.handleServiceCancel
|
||||
}
|
||||
for _, iccid := range cda.ICCIDs {
|
||||
iccid = strings.TrimSuffix(strings.TrimSuffix(iccid, "F"), "f")
|
||||
temp := ICCIDBody{
|
||||
ICCID: iccid,
|
||||
}
|
||||
_, tErr := fn(ctx, &temp)
|
||||
if tErr != nil {
|
||||
err = errorsO.Join(err, tErr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// sets status as ACTIVATED
|
||||
func (c *TMobClient) handleServiceRestore(ctx context.Context, in *ICCIDBody)(out *ICCIDBody, err error){
|
||||
path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/service-restore"
|
||||
fullPath := c.baseURL.String() + path
|
||||
|
||||
payload := new(bytes.Buffer)
|
||||
err = json.NewEncoder(payload).Encode(in)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
|
||||
}
|
||||
payloadStr := payload.String()
|
||||
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodPost).
|
||||
SetContentType(applicationJSON).
|
||||
SetBody(payloadStr)
|
||||
popToken, err := c.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
req.Header.Set(Authorization, c.accessToken)
|
||||
req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
|
||||
req.Header.Set(XAuthorization, popToken)
|
||||
req.Header.Set(contentType, applicationJSON)
|
||||
out = &ICCIDBody{}
|
||||
|
||||
err = c.do(req, &out)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// sets status as DEACTIVATED
|
||||
func (c *TMobClient) handleServiceCancel(ctx context.Context, in *ICCIDBody)(out *ICCIDBody, err error){
|
||||
path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/service-suspend"
|
||||
fullPath := c.baseURL.String() + path
|
||||
|
||||
payload := new(bytes.Buffer)
|
||||
err = json.NewEncoder(payload).Encode(in)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
|
||||
}
|
||||
payloadStr := payload.String()
|
||||
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodPost).
|
||||
SetContentType(applicationJSON).
|
||||
SetBody(payloadStr)
|
||||
popToken, err := c.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
req.Header.Set(Authorization, c.accessToken)
|
||||
req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken))
|
||||
req.Header.Set(XAuthorization, popToken)
|
||||
req.Header.Set(contentType, applicationJSON)
|
||||
out = &ICCIDBody{}
|
||||
|
||||
err = c.do(req, &out)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
293
pkg/tmobile/client_it_test.go
Normal file
293
pkg/tmobile/client_it_test.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/grpc/sms"
|
||||
)
|
||||
|
||||
var CLIENT_ID = ""
|
||||
var SECRET = ""
|
||||
var PRIVATE_KEY = ""
|
||||
|
||||
func TestTMobileSendMessage(t *testing.T) {
|
||||
t.Skip()
|
||||
tg, err := InitTokenGen(
|
||||
PRIVATE_KEY,
|
||||
CLIENT_ID,
|
||||
SECRET,
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
client, err := NewTMobileClient(Endpoint, tg, time.Second*450)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*450)
|
||||
defer cancel()
|
||||
|
||||
o, err := client.AccessToken(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
client.SetAccessToken(o.AccessToken)
|
||||
|
||||
msg := SendSMSRequest{
|
||||
ICCID: "8901882000784166054",
|
||||
MessageText: "local_test",
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), time.Second*450)
|
||||
defer cancel()
|
||||
|
||||
out, err := client.SendSMS(ctx, &msg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Error(out.SmsMessageID)
|
||||
}
|
||||
|
||||
func TestTMobileCheckMessageStatus(t *testing.T) {
|
||||
t.Skip()
|
||||
messageID := "473007039"
|
||||
tg, err := InitTokenGen(
|
||||
PRIVATE_KEY,
|
||||
CLIENT_ID,
|
||||
SECRET,
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tmobc, err := NewTMobileClient(
|
||||
Endpoint,
|
||||
tg, time.Minute*2)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
o, err := tmobc.AccessToken(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tmobc.SetAccessToken(o.AccessToken)
|
||||
|
||||
failed := 0
|
||||
r := sync.WaitGroup{}
|
||||
for x := 0; x < 5; x++ {
|
||||
r.Add(1)
|
||||
go func(y int) {
|
||||
_, err := tmobc.Details(context.Background(), messageID)
|
||||
if err != nil {
|
||||
// t.Error(err)
|
||||
failed++
|
||||
t.Log(y, ",")
|
||||
}
|
||||
r.Done()
|
||||
}(x)
|
||||
}
|
||||
r.Wait()
|
||||
|
||||
t.Error(failed)
|
||||
}
|
||||
|
||||
func TestTMobileCustomAttributes(t *testing.T) {
|
||||
t.Skip()
|
||||
tg, err := InitTokenGen(
|
||||
PRIVATE_KEY,
|
||||
CLIENT_ID,
|
||||
SECRET,
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
client, err := NewTMobileClient(Endpoint, tg, time.Second*450)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*450)
|
||||
defer cancel()
|
||||
|
||||
o, err := client.AccessToken(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
client.SetAccessToken(o.AccessToken)
|
||||
|
||||
custumAttributes := CustomAtributesRequest{
|
||||
ICCID: "8901882000784166054",
|
||||
AccountCustom1: "US",
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), time.Second*450)
|
||||
defer cancel()
|
||||
out, err := client.CustomAttributes(ctx, &custumAttributes)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Log(out.ICCID)
|
||||
}
|
||||
|
||||
func TestTMobileDeviceDetails(t *testing.T) {
|
||||
t.Skip()
|
||||
tg, err := InitTokenGen(
|
||||
PRIVATE_KEY,
|
||||
CLIENT_ID,
|
||||
SECRET,
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
client, err := NewTMobileClient(Endpoint, tg, time.Second*450)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*450)
|
||||
defer cancel()
|
||||
|
||||
o, err := client.AccessToken(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
client.SetAccessToken(o.AccessToken)
|
||||
|
||||
deviceDetailsRequest := DeviceDetailsRequest{
|
||||
ICCID: "8901882000784166054",
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), time.Second*450)
|
||||
defer cancel()
|
||||
out, err := client.DeviceDetails(ctx, &deviceDetailsRequest)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Errorf("%+v", out)
|
||||
}
|
||||
|
||||
func TestTMobileGetProducts(t *testing.T) {
|
||||
t.Skip()
|
||||
tg, err := InitTokenGen(
|
||||
PRIVATE_KEY,
|
||||
CLIENT_ID,
|
||||
SECRET,
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
client, err := NewTMobileClient(Endpoint, tg, time.Second*450)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*450)
|
||||
defer cancel()
|
||||
|
||||
o, err := client.AccessToken(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
client.SetAccessToken(o.AccessToken)
|
||||
|
||||
getProductsRequest := sms.GetAvailableProductsRequest{
|
||||
AccountId: "500556839",
|
||||
ProductType: []string{"RATEPLAN"},
|
||||
ProductClassification: []string{"IOTCONNECTIVITY"},
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), time.Second*450)
|
||||
defer cancel()
|
||||
out, err := client.GetProducts(ctx, &getProductsRequest)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Errorf("%+v", out)
|
||||
}
|
||||
|
||||
func TestTMobileChangeRatePlan(t *testing.T) {
|
||||
t.Skip()
|
||||
tg, err := InitTokenGen(
|
||||
PRIVATE_KEY,
|
||||
CLIENT_ID,
|
||||
SECRET,
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
client, err := NewTMobileClient(Endpoint, tg, time.Second*450)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*450)
|
||||
defer cancel()
|
||||
|
||||
o, err := client.AccessToken(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
client.SetAccessToken(o.AccessToken)
|
||||
|
||||
changeRatePlan := ChangeRatePlanRequest{
|
||||
ICCID: "8901882000784166054",
|
||||
ProductId: "716f8a59-6d2c-4687-b927-990a46b847cc",
|
||||
AccountId: "500556839",
|
||||
}
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), time.Second*450)
|
||||
defer cancel()
|
||||
out, err := client.ChangeRatePlan(ctx, &changeRatePlan)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Error(out.ICCID)
|
||||
}
|
||||
|
||||
/* Available rate plans at Tmobile
|
||||
|
||||
{id:"64510d144707f865761ff0b0" productId:"03f30679-30a3-4d5c-ad49-be62ec26a886" shortDescription:"Fisker Intl3 5GB" status:"Published" effectiveDate:"2023-10-03T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker Intl3 5GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"64510d137953b71c0564d365" productId:"90111353-7f8c-4315-8175-2abe127f2c97" shortDescription:"Fisker Intl3 7GB" status:"Published" effectiveDate:"2023-10-03T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker Intl3 7GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"64510d137953b71c0564d366" productId:"71e97975-8b63-4c93-b2e4-60a48914408f" shortDescription:"Fisker Intl3 2GB" status:"Published" effectiveDate:"2023-10-03T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker Intl3 2GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"645056444707f865761ff0ae" productId:"9621b445-c5bd-4ef5-9ee7-415c8c6045b6" shortDescription:"Fisker Intl2 7GB" status:"Published" effectiveDate:"2023-10-03T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker Intl2 7GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"64504f37df187e2be859acde" productId:"6f56afbf-dd60-4210-9894-c7f3675f643d" shortDescription:"Fisker Intl2 2GB" status:"Published" effectiveDate:"2023-10-03T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker Intl2 2GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"64505643abfe5c3f44b7bb6a" productId:"ee5bb6bb-074d-47ba-b47d-907f95a77e9f" shortDescription:"Fisker Intl2 5GB" status:"Published" effectiveDate:"2023-10-03T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker Intl2 5GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"64504830abfe5c3f44b7bb68" productId:"92bf0610-098d-4ead-b38b-c8c4b2a1ede1" shortDescription:"Fisker Intl1 2GB" status:"Published" effectiveDate:"2023-08-21T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker Intl1 2GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"64e3a9fe3dc11817f3797090" productId:"716f8a59-6d2c-4687-b927-990a46b847cc" shortDescription:"Fisker USA 7GB" status:"Published" effectiveDate:"2023-08-21T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker USA 7GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"64e3a686117310400e90b45f" productId:"af6dc7f2-cb9e-466f-a24a-003da89c9573" shortDescription:"Fisker USA 2GB" status:"Published" effectiveDate:"2023-08-21T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker USA 2GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"64e3a684117310400e90b45e" productId:"74719a08-2d1f-44d2-ae76-fec6409cb0e0" shortDescription:"Fisker USA 5GB" status:"Published" effectiveDate:"2023-08-21T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker USA 5GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"aba2be04-df09-42ac-87ed-f5e53652cf9b" productId:"7290a552-cf8d-4a32-b829-ea8c62c83bda" shortDescription:"Fisker Testing 2GB" longDescription:"pub again to push to AHUB" status:"Published" effectiveDate:"2022-12-08T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker Testing 2GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"366bc7c0-c5f1-4e74-9748-a10910f42171" productId:"5bd33e0d-350c-45d3-bf97-8b501ac354d7" shortDescription:"Fisker Monaco 7GB" status:"Published" effectiveDate:"2023-05-09T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker Monaco 7GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"39284d65-5688-4324-91fb-95b1271efa44" productId:"2248a17c-483f-4732-b9f1-ce0929666464" shortDescription:"Fisker Monaco 5GB" status:"Published" effectiveDate:"2023-05-09T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker Monaco 5GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"1ed2d811-ec98-47ac-8338-1fad104f731d" productId:"e7f09cfe-4c80-4e90-92a9-a1fe92621e6e" shortDescription:"Fisker Monaco 2GB" status:"Published" effectiveDate:"2023-05-09T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker Monaco 2GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"5ff7d298-53af-46e0-8058-011eaf95e53a" productId:"6eda215c-cd11-4686-b2c7-3fc4ad5f0a1d" shortDescription:"Fisker Intl1 7GB" status:"Published" effectiveDate:"2023-05-09T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker Intl1 7GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
{id:"64609dda-4620-4057-b1a4-d8a9450ef404" productId:"164917cd-6bdc-44a7-a769-d4889775d6a4" shortDescription:"Fisker Intl1 5GB" status:"Published" effectiveDate:"2023-05-09T00:00:00Z" expirationDate:"9999-12-31T00:00:00Z" ratePlan:{ratePlanName:"Fisker Intl1 5GB" planType:"Monthly - Flexible Pool" planStatus:"Published"}}
|
||||
|
||||
*/
|
||||
368
pkg/tmobile/client_mini.go
Normal file
368
pkg/tmobile/client_mini.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
errorsO "errors"
|
||||
|
||||
"fiskerinc.com/modules/grpc/sms"
|
||||
tmtg "fiskerinc.com/modules/tmobtokengen"
|
||||
"fiskerinc.com/modules/utils/envtool"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type TMobileMiniClient struct {
|
||||
client httpClienter
|
||||
tg tmtg.Generator
|
||||
accessToken string
|
||||
baseURL string
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Meant to be used within 30 minutes, and then disposed of
|
||||
func NewTMobileMiniClient() (client *TMobileMiniClient, err error) {
|
||||
client = &TMobileMiniClient{}
|
||||
tg, err := InitTokenGen(
|
||||
envtool.GetEnv("TMOBILE_PRIVATE_KEY", ""),
|
||||
envtool.GetEnv("TMOBILE_CLIENT_ID", ""),
|
||||
envtool.GetEnv("TMOBILE_SECRET", ""),
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
client.tg = tg
|
||||
client.baseURL = Endpoint
|
||||
client.client = &http.Client{
|
||||
Timeout: time.Minute,
|
||||
}
|
||||
|
||||
acsToken, err := client.AccessToken(context.Background())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
client.SetAccessToken(acsToken.AccessToken)
|
||||
return
|
||||
}
|
||||
|
||||
// AccessToken implements TMobClienter.
|
||||
func (tmc *TMobileMiniClient) AccessToken(ctx context.Context) (out *AccessTokenResponse, err error) {
|
||||
path := "/oauth2/v1/tokens"
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(tmc.tg.ClientSecretAsAuthVal()).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodPost)
|
||||
|
||||
token, err := tmc.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fullPath := tmc.baseURL + path
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, nil)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrCreateRequest, "failed to create request")
|
||||
}
|
||||
|
||||
req.Header.Set(Authorization, tmc.tg.ClientSecretAsAuthVal())
|
||||
req.Header.Set(XAuthorization, token)
|
||||
|
||||
err = tmc.do(req, &out)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// HandleChangeDeviceStatus implements TMobClienter.
|
||||
func (tmc *TMobileMiniClient) ChangeDeviceStatus(ctx context.Context, cda ChangeDeviceActivation) (err error) {
|
||||
var fn func(ctx context.Context, in *ICCIDBody)(out *ICCIDBody, err error)
|
||||
if cda.Enabled {
|
||||
fn = tmc.handleServiceRestore
|
||||
}else {
|
||||
fn = tmc.handleServiceCancel
|
||||
}
|
||||
for _, iccid := range cda.ICCIDs {
|
||||
iccid = strings.TrimSuffix(strings.TrimSuffix(iccid, "F"), "f")
|
||||
temp := ICCIDBody{
|
||||
ICCID: iccid,
|
||||
}
|
||||
_, tErr := fn(ctx, &temp)
|
||||
if tErr != nil {
|
||||
err = errorsO.Join(err, tErr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ChangeRatePlan implements TMobClienter.
|
||||
func (tmc *TMobileMiniClient) ChangeRatePlan(context.Context, *ChangeRatePlanRequest) (*ChangeRatePlanResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// CustomAttributes implements TMobClienter.
|
||||
func (tmc *TMobileMiniClient) CustomAttributes(ctx context.Context, in *CustomAtributesRequest) (*CustomAtributesResponse, error) {
|
||||
path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/device-attributes"
|
||||
fullPath := tmc.baseURL + path
|
||||
|
||||
payload := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(payload).Encode(in); err != nil {
|
||||
return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
|
||||
}
|
||||
payloadStr := payload.String()
|
||||
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(tmc.accessToken).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodPost).
|
||||
SetContentType(applicationJSON).
|
||||
SetBody(payloadStr)
|
||||
|
||||
popToken, err := tmc.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set(Authorization, tmc.accessToken)
|
||||
req.Header.Set(XAuthOriginator, ToXAuthOriginator(tmc.accessToken))
|
||||
req.Header.Set(XAuthorization, popToken)
|
||||
req.Header.Set(contentType, applicationJSON)
|
||||
out := &CustomAtributesResponse{}
|
||||
|
||||
err = tmc.do(req, out)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Details implements TMobClienter.
|
||||
func (tmc *TMobileMiniClient) Details(ctx context.Context, iccid string) (out *SMSDetailsResponse, err error) {
|
||||
// Really not the way
|
||||
path := fmt.Sprintf("/eitcsr-iotcp-notifications-v2/prd02/iotcp/v2/notifications/sms/messages/%s", iccid)
|
||||
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(tmc.accessToken).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodGet)
|
||||
|
||||
token, err := tmc.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrTokenGen, "failed to generate token: %v", err)
|
||||
}
|
||||
|
||||
fullPath := tmc.baseURL + path
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullPath, nil)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrCreateRequest, "failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set(Authorization, tmc.accessToken)
|
||||
req.Header.Set(XAuthOriginator, ToXAuthOriginator(tmc.accessToken))
|
||||
req.Header.Set(XAuthorization, token)
|
||||
req.Header.Set(contentType, applicationJSON)
|
||||
|
||||
err = tmc.do(req, &out)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeviceDetails implements TMobClienter.
|
||||
func (tmc *TMobileMiniClient) DeviceDetails(ctx context.Context, in *DeviceDetailsRequest) (out *DeviceDetailsResponse, err error) {
|
||||
path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/details"
|
||||
fullPath := tmc.baseURL + path
|
||||
|
||||
payload := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(payload).Encode(in); err != nil {
|
||||
return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
|
||||
}
|
||||
payloadStr := payload.String()
|
||||
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(tmc.accessToken).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodPost).
|
||||
SetContentType(applicationJSON).
|
||||
SetBody(payloadStr)
|
||||
|
||||
popToken, err := tmc.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set(Authorization, tmc.accessToken)
|
||||
req.Header.Set(XAuthOriginator, ToXAuthOriginator(tmc.accessToken))
|
||||
req.Header.Set(XAuthorization, popToken)
|
||||
req.Header.Set(contentType, applicationJSON)
|
||||
out = &DeviceDetailsResponse{}
|
||||
|
||||
err = tmc.do(req, &out)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetProducts implements TMobClienter.
|
||||
func (tmc *TMobileMiniClient) GetProducts(context.Context, *sms.GetAvailableProductsRequest) (*sms.GetAvailableProductsResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// SendSMS implements TMobClienter.
|
||||
func (tmc *TMobileMiniClient) SendSMS(ctx context.Context, in *SendSMSRequest) (out *SendSMSResponse, err error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// SetAccessToken implements TMobClienter.
|
||||
// Possible one dumb piece of code
|
||||
func (tmc *TMobileMiniClient) SetAccessToken(accessToken string) {
|
||||
tmc.accessToken = "Bearer " + accessToken
|
||||
}
|
||||
|
||||
// SetFilter implements TMobClienter.
|
||||
func (tmc *TMobileMiniClient) SetFilter(filter []string) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
var _ TMobClienter = &TMobileMiniClient{}
|
||||
|
||||
func (c *TMobileMiniClient) do(req *http.Request, out interface{}) error {
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(ErrDoRequest, "failed to do request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 300 {
|
||||
var body []byte
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(ErrReadResponseBody, "failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
toWrapErr := ErrBadStatusCode
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
toWrapErr = ErrAccessTokenExpired
|
||||
} else if resp.StatusCode == http.StatusBadGateway {
|
||||
toWrapErr = ErrBadGatewayCode
|
||||
} else if resp.StatusCode == http.StatusGatewayTimeout {
|
||||
toWrapErr = ErrBadGatewayCode
|
||||
}
|
||||
|
||||
return errors.WithMessagef(toWrapErr, "request failed with status code %d, body: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(out)
|
||||
if err != nil {
|
||||
return errors.WithMessagef(ErrJSONMarshal, "failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// sets status as ACTIVATED
|
||||
func (tmc *TMobileMiniClient) handleServiceRestore(ctx context.Context, in *ICCIDBody)(out *ICCIDBody, err error){
|
||||
path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/service-restore"
|
||||
fullPath := tmc.baseURL + path
|
||||
|
||||
payload := new(bytes.Buffer)
|
||||
err = json.NewEncoder(payload).Encode(in)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
|
||||
}
|
||||
payloadStr := payload.String()
|
||||
fmt.Println(payloadStr)
|
||||
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(tmc.accessToken).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodPost).
|
||||
SetContentType(applicationJSON).
|
||||
SetBody(payloadStr)
|
||||
popToken, err := tmc.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set(Authorization, tmc.accessToken)
|
||||
req.Header.Set(XAuthOriginator, ToXAuthOriginator(tmc.accessToken))
|
||||
req.Header.Set(XAuthorization, popToken)
|
||||
req.Header.Set(contentType, applicationJSON)
|
||||
out = &ICCIDBody{}
|
||||
|
||||
err = tmc.do(req, &out)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// sets status as DEACTIVATED
|
||||
func (tmc *TMobileMiniClient) handleServiceCancel(ctx context.Context, in *ICCIDBody)(out *ICCIDBody, err error){
|
||||
path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/service-suspend"
|
||||
fullPath := tmc.baseURL + path
|
||||
|
||||
payload := new(bytes.Buffer)
|
||||
err = json.NewEncoder(payload).Encode(in)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err)
|
||||
}
|
||||
payloadStr := payload.String()
|
||||
|
||||
emap := make(tmtg.EHTSMap).SetAuthorization(tmc.accessToken).
|
||||
SetURI(path).
|
||||
SetHTTPMethod(http.MethodPost).
|
||||
SetContentType(applicationJSON).
|
||||
SetBody(payloadStr)
|
||||
popToken, err := tmc.tg.Generate(emap)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set(Authorization, tmc.accessToken)
|
||||
req.Header.Set(XAuthOriginator, ToXAuthOriginator(tmc.accessToken))
|
||||
req.Header.Set(XAuthorization, popToken)
|
||||
req.Header.Set(contentType, applicationJSON)
|
||||
out = &ICCIDBody{}
|
||||
|
||||
err = tmc.do(req, &out)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, failedToDoRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
156
pkg/tmobile/client_simulator.go
Normal file
156
pkg/tmobile/client_simulator.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/grpc/sms"
|
||||
)
|
||||
|
||||
// Want to make a client simulator that has a delay for each message, along with a chance of failure
|
||||
// For use with just await sms testing
|
||||
type tmobileSimulator struct {
|
||||
messages map[string]fakeMessage
|
||||
sy sync.Mutex
|
||||
count int
|
||||
FailPercentage int // 10 = 10% chance of failure
|
||||
AVGDeliveryTime time.Duration // lets do two seconds
|
||||
Deviation float64 // The number of seconds we can deviate by
|
||||
Rando *rand.Rand // Using a set seed so we can have consistent results with our random messages
|
||||
filter ICCIDFilter
|
||||
}
|
||||
|
||||
// ChangeDeviceStatus implements TMobClienter.
|
||||
func (ts *tmobileSimulator) ChangeDeviceStatus(ctx context.Context, cda ChangeDeviceActivation) (err error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
type fakeMessage struct {
|
||||
finishTime time.Time
|
||||
//status string // Set the time that the message will resolve, and then it will return this resolution. If blank, the message is not going to be delivered within 5 seconds
|
||||
}
|
||||
|
||||
// avgDeliveryTime is time in seconds
|
||||
func newTMboileSimulator() (tms tmobileSimulator) {
|
||||
tms.messages = make(map[string]fakeMessage)
|
||||
tms.Rando = rand.New(rand.NewSource(99))
|
||||
tms.FailPercentage = 5 // 5% chance of a message not being delivered, so 10,000 will have 500 failed
|
||||
tms.Deviation = 2 // How many seconds, maybe don't use this
|
||||
tms.AVGDeliveryTime = time.Second * 2 // On average take 2ish seconds to deliver the message
|
||||
return
|
||||
}
|
||||
|
||||
// AccessToken implements TMobClienter.
|
||||
func (*tmobileSimulator) AccessToken(ctx context.Context) (out *AccessTokenResponse, err error) {
|
||||
out = &AccessTokenResponse{}
|
||||
out.ExpiresIn = 500000
|
||||
return
|
||||
}
|
||||
|
||||
// ChangeRatePlan implements TMobClienter.
|
||||
func (*tmobileSimulator) ChangeRatePlan(context.Context, *ChangeRatePlanRequest) (*ChangeRatePlanResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// CustomAttributes implements TMobClienter.
|
||||
func (*tmobileSimulator) CustomAttributes(context.Context, *CustomAtributesRequest) (*CustomAtributesResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Details implements TMobClienter.
|
||||
func (ts *tmobileSimulator) Details(ctx context.Context, ID string) (out *SMSDetailsResponse, err error) {
|
||||
out = &SMSDetailsResponse{}
|
||||
msg := ts.messages[ID]
|
||||
if msg.finishTime.IsZero() {
|
||||
out.Status = "Pending"
|
||||
return
|
||||
}
|
||||
//-cpuprofile cpu.out
|
||||
if time.Now().After(msg.finishTime) {
|
||||
out.Status = "Delivered"
|
||||
} else {
|
||||
out.Status = "Pending"
|
||||
}
|
||||
|
||||
time.Sleep(time.Millisecond * 40)
|
||||
return
|
||||
}
|
||||
|
||||
// DeviceDetails implements TMobClienter.
|
||||
func (*tmobileSimulator) DeviceDetails(context.Context, *DeviceDetailsRequest) (*DeviceDetailsResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetProducts implements TMobClienter.
|
||||
func (*tmobileSimulator) GetProducts(context.Context, *sms.GetAvailableProductsRequest) (*sms.GetAvailableProductsResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// SendSMS implements TMobClienter.
|
||||
// Call this synchronously as we are using a non-locking map and non-locked index count
|
||||
func (ts *tmobileSimulator) SendSMS(ctx context.Context, in *SendSMSRequest) (out *SendSMSResponse, err error) {
|
||||
ts.sy.Lock()
|
||||
defer ts.sy.Unlock()
|
||||
out = &SendSMSResponse{}
|
||||
ts.messages[strconv.Itoa(ts.count)] = ts.randomDelivery()
|
||||
out.SmsMessageID = strconv.Itoa(ts.count)
|
||||
ts.count += 1
|
||||
return
|
||||
}
|
||||
|
||||
func (ts *tmobileSimulator) randomDelivery() (msg fakeMessage) {
|
||||
if ts.Rando.Intn(100) <= ts.FailPercentage {
|
||||
return
|
||||
}
|
||||
|
||||
// 0 -> .99 of deviation
|
||||
// 0:Deviation Value - 1/2 deviation = -.5 deviation -> .5 deviation
|
||||
// I think its okay that the time is only going to take longer, no real need for shorter messages
|
||||
ranomometer := (ts.Rando.Float64() * ts.Deviation)
|
||||
// Actually don't need the status for this test. We have the pending unless its delivered. WE don't really see the failed to delivery message
|
||||
msg.finishTime = time.Now().Add(ts.AVGDeliveryTime + time.Duration(ranomometer))
|
||||
return
|
||||
}
|
||||
|
||||
// SetAccessToken implements TMobClienter.
|
||||
func (*tmobileSimulator) SetAccessToken(accessToken string) {
|
||||
}
|
||||
|
||||
// SetFilter implements TMobClienter.
|
||||
func (ts *tmobileSimulator) SetFilter(filter []string) {
|
||||
ts.filter = ICCIDFilter{
|
||||
filter: filter,
|
||||
}
|
||||
}
|
||||
|
||||
var _ TMobClienter = new(tmobileSimulator)
|
||||
|
||||
// with the channel instead of default
|
||||
//BenchmarkSMSWrapper-16 1 5009644871 ns/op 3096536 B/op 45650 allocs/op
|
||||
// flat flat% sum% cum cum%
|
||||
// 50ms 31.25% 31.25% 50ms 31.25% runtime.kevent
|
||||
// 40ms 25.00% 56.25% 40ms 25.00% runtime.pthread_cond_wait
|
||||
// 20ms 12.50% 68.75% 20ms 12.50% runtime.pthread_cond_signal
|
||||
// 10ms 6.25% 75.00% 10ms 6.25% runtime.(*mspan).init
|
||||
// 10ms 6.25% 81.25% 20ms 12.50% runtime.mallocgc
|
||||
// 10ms 6.25% 87.50% 10ms 6.25% runtime.read
|
||||
// 10ms 6.25% 93.75% 10ms 6.25% runtime.usleep
|
||||
// 10ms 6.25% 100% 10ms 6.25% runtime.write1
|
||||
// 0 0% 100% 20ms 12.50% fiskerinc.com/modules/tmobile.(*SMSClient).SendSMS
|
||||
// 0 0% 100% 10ms 6.25% fiskerinc.com/modules/tmobile.(*tmobileSimulator).SendSMS
|
||||
// with default
|
||||
//BenchmarkSMSWrapper-16 1 5110808221 ns/op 2923960 B/op 43176 allocs/op no major difference
|
||||
// flat flat% sum% cum cum%
|
||||
// 50ms 45.45% 45.45% 50ms 45.45% runtime.pthread_cond_wait
|
||||
// 20ms 18.18% 63.64% 20ms 18.18% runtime.pthread_cond_signal
|
||||
// 10ms 9.09% 72.73% 10ms 9.09% runtime.arenaIndex (inline)
|
||||
// 10ms 9.09% 81.82% 10ms 9.09% runtime.kevent
|
||||
// 10ms 9.09% 90.91% 10ms 9.09% runtime.stackpoolalloc
|
||||
// 10ms 9.09% 100% 10ms 9.09% runtime.usleep
|
||||
// 0 0% 100% 10ms 9.09% fiskerinc.com/modules/tmobile.BenchmarkSMSWrapper
|
||||
// 0 0% 100% 10ms 9.09% runtime.copystack
|
||||
// 0 0% 100% 10ms 9.09% runtime.findObject
|
||||
// 0 0% 100% 60ms 54.55% runtime.findRunnable
|
||||
811
pkg/tmobile/client_test.go
Normal file
811
pkg/tmobile/client_test.go
Normal file
@@ -0,0 +1,811 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/grpc/sms"
|
||||
"fiskerinc.com/modules/tmobtokengen"
|
||||
)
|
||||
|
||||
type mockTG struct {
|
||||
wasSet chan tmobtokengen.EHTSMap
|
||||
}
|
||||
|
||||
func (c *mockTG) Generate(ehts tmobtokengen.EHTSMap) (string, error) {
|
||||
c.wasSet <- ehts
|
||||
|
||||
return "generatedPopToken", nil
|
||||
}
|
||||
|
||||
func (c *mockTG) ClientSecretAsAuthVal() string {
|
||||
return "clientSecret"
|
||||
}
|
||||
|
||||
type mockHTTPClientAT struct {
|
||||
wasSet chan *http.Request
|
||||
}
|
||||
|
||||
func (c *mockHTTPClientAT) Do(req *http.Request) (*http.Response, error) {
|
||||
c.wasSet <- req
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(strings.NewReader(mockATjson)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var mockATjson = `{
|
||||
"id_token": "generatedToken",
|
||||
"access_token": "generatedToken",
|
||||
"expires_in": 3600,
|
||||
"token_type": "type",
|
||||
"resource": "https://api.mob.com/",
|
||||
"refresh_token": "refreshToken",
|
||||
"scope": "scope"
|
||||
}`
|
||||
|
||||
var mockUrlStr = "https://api.mob.com"
|
||||
|
||||
var mockUrl *url.URL
|
||||
|
||||
func init() {
|
||||
mockUrl, _ = url.Parse(mockUrlStr)
|
||||
b, _ := json.Marshal(mockSmsDetailsResponse)
|
||||
mockSmsDetailsJsonRes = string(b)
|
||||
}
|
||||
|
||||
func TestTMobClient_AccessToken(t *testing.T) {
|
||||
mHTTP := &mockHTTPClientAT{wasSet: make(chan *http.Request, 1)}
|
||||
mTG := &mockTG{wasSet: make(chan tmobtokengen.EHTSMap, 1)}
|
||||
cl := &TMobClient{
|
||||
client: mHTTP,
|
||||
tg: mTG,
|
||||
baseURL: mockUrl,
|
||||
}
|
||||
|
||||
atr, err := cl.AccessToken(mockCtx)
|
||||
if err != nil {
|
||||
t.Errorf("AccessToken() should have succeeded: %v", err)
|
||||
}
|
||||
|
||||
if atr.AccessToken != "generatedToken" {
|
||||
t.Errorf("AccessToken() should have returned generated token")
|
||||
}
|
||||
|
||||
select {
|
||||
case ehts := <-mTG.wasSet:
|
||||
if ehts["Authorization"] != "clientSecret" {
|
||||
t.Errorf("AccessToken() should have set Authorization header")
|
||||
}
|
||||
if ehts["uri"] != "/oauth2/v1/tokens" {
|
||||
t.Errorf("AccessToken() ehts should have set uri")
|
||||
}
|
||||
if ehts["http-method"] != "POST" {
|
||||
t.Errorf("AccessToken() ehts should have set method")
|
||||
}
|
||||
if len(ehts) != 3 {
|
||||
t.Errorf("AccessToken() ehts should have set 3 keys")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("AccessToken() should have called tg.Generate()")
|
||||
}
|
||||
|
||||
select {
|
||||
case req := <-mHTTP.wasSet:
|
||||
if req.URL.String() != mockUrlStr+"/oauth2/v1/tokens" {
|
||||
t.Errorf("AccessToken() should have set URL")
|
||||
}
|
||||
if req.Method != "POST" {
|
||||
t.Errorf("AccessToken() should have set method")
|
||||
}
|
||||
if req.Header.Get("Authorization") != "clientSecret" {
|
||||
t.Errorf("AccessToken() should have set Authorization header")
|
||||
}
|
||||
if req.Header.Get("X-Authorization") != "generatedPopToken" {
|
||||
t.Errorf("AccessToken() should have set X-Authorization header")
|
||||
}
|
||||
|
||||
if len(req.Header) != 2 {
|
||||
t.Errorf("AccessToken() should have set 2 headers")
|
||||
}
|
||||
|
||||
if req.Body != nil {
|
||||
t.Errorf("AccessToken() should have no body")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("AccessToken() should have called http.Do()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewTMobileClient(t *testing.T) {
|
||||
cl, err := NewTMobileClient("http://localhost:8080", tmobtokengen.NewMockTokenGenerator{}, time.Second)
|
||||
if err != nil {
|
||||
t.Errorf("NewTMobileClient() should have succeeded: %v", err)
|
||||
}
|
||||
if cl == nil {
|
||||
t.Errorf("NewTMobileClient() should have returned non-nil")
|
||||
}
|
||||
|
||||
cl, err = NewTMobileClient("\thisisnotparsable", nil, 0)
|
||||
if err == nil {
|
||||
t.Errorf("NewTMobileClient() should have failed: %v", err)
|
||||
}
|
||||
if cl != nil {
|
||||
t.Errorf("NewTMobileClient() should have returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTMobClient_SetAccessToken(t *testing.T) {
|
||||
cl := &TMobClient{}
|
||||
cl.SetAccessToken("token")
|
||||
if cl.accessToken != "Bearer token" {
|
||||
t.Errorf("SetAccessToken() should have set access token")
|
||||
}
|
||||
}
|
||||
|
||||
type mockHTTPClientSendSMS struct {
|
||||
wasSet chan *http.Request
|
||||
}
|
||||
|
||||
func (c *mockHTTPClientSendSMS) Do(req *http.Request) (*http.Response, error) {
|
||||
c.wasSet <- req
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(strings.NewReader(mockSendSMSJsonRes)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
var mockSendSMSJsonRes = `{"smsMessageId": "1"}
|
||||
`
|
||||
|
||||
func TestTMobClient_SendSMS(t *testing.T) {
|
||||
mtg := &mockTG{wasSet: make(chan tmobtokengen.EHTSMap, 1)}
|
||||
mHTTP := &mockHTTPClientSendSMS{wasSet: make(chan *http.Request, 1)}
|
||||
|
||||
cl := &TMobClient{
|
||||
client: mHTTP,
|
||||
tg: mtg,
|
||||
baseURL: mockUrl,
|
||||
accessToken: "Bearer token",
|
||||
}
|
||||
|
||||
msrs := "{\"iccid\":\"12345678901234567890\",\"messageText\":\"Hello world\"}\n"
|
||||
res, err := cl.SendSMS(mockCtx, mockSmsRequest)
|
||||
if err != nil {
|
||||
t.Errorf("SendSMS() should have succeeded: %v", err)
|
||||
}
|
||||
|
||||
if res.SmsMessageID != "1" {
|
||||
t.Errorf("SendSMS() should have returned smsMessageId")
|
||||
}
|
||||
|
||||
select {
|
||||
case ehts := <-mtg.wasSet:
|
||||
if ehts["uri"] != "/eitcsr-iotcp-notifications-v2/prd02/iotcp/v2/notifications/sms/messages" {
|
||||
t.Errorf("SendSMS() ehts should have set uri")
|
||||
}
|
||||
if ehts["http-method"] != "POST" {
|
||||
t.Errorf("SendSMS() ehts should have set method")
|
||||
}
|
||||
if ehts["Content-Type"] != "application/json" {
|
||||
t.Errorf("SendSMS() ehts should have set Content-Type")
|
||||
}
|
||||
|
||||
if ehts["Authorization"] != "Bearer token" {
|
||||
t.Errorf("SendSMS() ehts should have set Authorization header")
|
||||
}
|
||||
|
||||
if ehts["body"] != msrs {
|
||||
t.Errorf("SendSMS() ehts should have set Body")
|
||||
}
|
||||
if len(ehts) != 5 {
|
||||
t.Errorf("SendSMS() ehts should have set 4 keys")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("SendSMS() should have called tg.Generate()")
|
||||
}
|
||||
|
||||
select {
|
||||
case req := <-mHTTP.wasSet:
|
||||
if req.URL.String() != mockUrlStr+"/eitcsr-iotcp-notifications-v2/prd02/iotcp/v2/notifications/sms/messages" {
|
||||
t.Errorf("SendSMS() should have set URL")
|
||||
}
|
||||
if req.Method != "POST" {
|
||||
t.Errorf("SendSMS() should have set method")
|
||||
}
|
||||
|
||||
if req.Header.Get("Authorization") != "Bearer token" {
|
||||
t.Errorf("SendSMS() should have set Authorization header")
|
||||
}
|
||||
|
||||
if req.Header.Get("X-Authorization") != "generatedPopToken" {
|
||||
t.Errorf("SendSMS() should have set X-Authorization header")
|
||||
}
|
||||
|
||||
if req.Header.Get(XAuthOriginator) != "token" {
|
||||
t.Errorf("SendSMS() should have set x-auth-originator header")
|
||||
}
|
||||
|
||||
if req.Header.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("SendSMS() should have set X-Authorization header")
|
||||
}
|
||||
|
||||
if len(req.Header) != 4 {
|
||||
t.Errorf("SendSMS() should have set 4 headers")
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Errorf("SendSMS() should have read body")
|
||||
}
|
||||
|
||||
if string(b) != msrs {
|
||||
t.Errorf("SendSMS() should have set body")
|
||||
}
|
||||
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("SendSMS() should have called http.Do()")
|
||||
}
|
||||
}
|
||||
|
||||
type mockHTTPClientDetails struct {
|
||||
wasSet chan *http.Request
|
||||
}
|
||||
|
||||
func (c *mockHTTPClientDetails) Do(req *http.Request) (*http.Response, error) {
|
||||
c.wasSet <- req
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(strings.NewReader(mockSmsDetailsJsonRes)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// inited above
|
||||
var mockSmsDetailsJsonRes string
|
||||
|
||||
func TestTMobClient_Details(t *testing.T) {
|
||||
mtg := &mockTG{wasSet: make(chan tmobtokengen.EHTSMap, 1)}
|
||||
mHTTP := &mockHTTPClientDetails{wasSet: make(chan *http.Request, 1)}
|
||||
|
||||
cl := &TMobClient{
|
||||
client: mHTTP,
|
||||
tg: mtg,
|
||||
baseURL: mockUrl,
|
||||
accessToken: "Bearer token",
|
||||
}
|
||||
|
||||
ID := "12345678901234567890"
|
||||
|
||||
res, err := cl.Details(mockCtx, ID)
|
||||
if err != nil {
|
||||
t.Errorf("Details() should have succeeded: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(mockSmsDetailsResponse, res) {
|
||||
t.Errorf("Details() got %v, want %v", res, mockSmsDetailsResponse)
|
||||
}
|
||||
|
||||
genPath := fmt.Sprintf("/eitcsr-iotcp-notifications-v2/prd02/iotcp/v2/notifications/sms/messages/%s", ID)
|
||||
select {
|
||||
case ehts := <-mtg.wasSet:
|
||||
if ehts["uri"] != genPath {
|
||||
t.Errorf("Details() ehts should have set uri")
|
||||
}
|
||||
if ehts["http-method"] != "GET" {
|
||||
t.Errorf("Details() ehts should have set method")
|
||||
}
|
||||
|
||||
if ehts["Authorization"] != "Bearer token" {
|
||||
t.Errorf("Details() ehts should have set Authorization header")
|
||||
}
|
||||
|
||||
if len(ehts) != 3 {
|
||||
t.Errorf("Details() ehts should have set 3 keys")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("Details() should have called tg.Generate()")
|
||||
}
|
||||
|
||||
select {
|
||||
case req := <-mHTTP.wasSet:
|
||||
if req.URL.String() != mockUrlStr+genPath {
|
||||
t.Errorf("Details() should have set URL")
|
||||
}
|
||||
if req.Method != "GET" {
|
||||
t.Errorf("Details() should have set method")
|
||||
}
|
||||
|
||||
if req.Header.Get("Authorization") != "Bearer token" {
|
||||
t.Errorf("Details() should have set Authorization header")
|
||||
}
|
||||
|
||||
if req.Header.Get("X-Authorization") != "generatedPopToken" {
|
||||
t.Errorf("Details() should have set X-Authorization header")
|
||||
}
|
||||
|
||||
if req.Header.Get(XAuthOriginator) != "token" {
|
||||
t.Errorf("Details() should have set x-auth-originator header")
|
||||
}
|
||||
|
||||
if req.Header.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("SendSMS() should have set X-Authorization header")
|
||||
}
|
||||
|
||||
if len(req.Header) != 4 {
|
||||
t.Errorf("Details() should have set 4 headers")
|
||||
}
|
||||
|
||||
if req.Body != nil {
|
||||
t.Errorf("Details() should have set nil body")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("Details() should have called http.Do()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitTokenGen(t *testing.T) {
|
||||
tg, err := InitTokenGen(mockPKCS8, "client_id", "secret")
|
||||
if err != nil {
|
||||
t.Errorf("InitTokenGen() error = %v", err)
|
||||
}
|
||||
if tg == nil {
|
||||
t.Errorf("InitTokenGen() tg is nil")
|
||||
}
|
||||
|
||||
tg, err = InitTokenGen("/tmp/thisFileDoesntExist", "client_id", "secret")
|
||||
if err == nil {
|
||||
t.Errorf("InitTokenGen() should have failed")
|
||||
}
|
||||
if tg != nil {
|
||||
t.Errorf("InitTokenGen() should have returned nil")
|
||||
}
|
||||
|
||||
tg, err = InitTokenGen(mockPKCS8Fail, "client_id", "secret")
|
||||
if err == nil {
|
||||
t.Errorf("InitTokenGen() should have failed")
|
||||
}
|
||||
if tg != nil {
|
||||
t.Errorf("InitTokenGen() should have returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
type mockHTTPClientCustomAttributes struct {
|
||||
wasSet chan *http.Request
|
||||
}
|
||||
|
||||
var mockCustomAttributesRequest = &CustomAtributesRequest{
|
||||
ICCID: "12345678901234567890",
|
||||
AccountCustom1: "US",
|
||||
}
|
||||
var mockCustomAttributesJsonRes = `{"iccid": "12345678901234567890"}`
|
||||
|
||||
func (c *mockHTTPClientCustomAttributes) Do(req *http.Request) (*http.Response, error) {
|
||||
c.wasSet <- req
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(strings.NewReader(mockCustomAttributesJsonRes)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestTMobClient_CustomAttributes(t *testing.T) {
|
||||
mtg := &mockTG{wasSet: make(chan tmobtokengen.EHTSMap, 1)}
|
||||
mHTTP := &mockHTTPClientCustomAttributes{wasSet: make(chan *http.Request, 1)}
|
||||
|
||||
cl := &TMobClient{
|
||||
client: mHTTP,
|
||||
tg: mtg,
|
||||
baseURL: mockUrl,
|
||||
accessToken: "Bearer token",
|
||||
}
|
||||
|
||||
msrs := "{\"iccid\":\"12345678901234567890\",\"accountCustom1\":\"US\"}\n"
|
||||
res, err := cl.CustomAttributes(mockCtx, mockCustomAttributesRequest)
|
||||
if err != nil {
|
||||
t.Errorf("CustomAttributes() should have succeeded: %v", err)
|
||||
}
|
||||
|
||||
if res.ICCID != "12345678901234567890" {
|
||||
t.Errorf("CustomAttributes() should have returned smsMessageId")
|
||||
}
|
||||
|
||||
select {
|
||||
case ehts := <-mtg.wasSet:
|
||||
if ehts["uri"] != "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/device-attributes" {
|
||||
t.Errorf("CustomAttributes() ehts should have set uri")
|
||||
}
|
||||
if ehts["http-method"] != "POST" {
|
||||
t.Errorf("CustomAttributes() ehts should have set method")
|
||||
}
|
||||
if ehts["Content-Type"] != "application/json" {
|
||||
t.Errorf("CustomAttributes() ehts should have set Content-Type")
|
||||
}
|
||||
|
||||
if ehts["Authorization"] != "Bearer token" {
|
||||
t.Errorf("CustomAttributes() ehts should have set Authorization header")
|
||||
}
|
||||
|
||||
if ehts["body"] != msrs {
|
||||
t.Errorf("CustomAttributes() ehts should have set Body")
|
||||
}
|
||||
if len(ehts) != 5 {
|
||||
t.Errorf("CustomAttributes() ehts should have set 4 keys")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("CustomAttributes() should have called tg.Generate()")
|
||||
}
|
||||
|
||||
select {
|
||||
case req := <-mHTTP.wasSet:
|
||||
if req.URL.String() != mockUrlStr+"/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/device-attributes" {
|
||||
t.Errorf("CustomAttributes() should have set URL")
|
||||
}
|
||||
if req.Method != "POST" {
|
||||
t.Errorf("CustomAttributes() should have set method")
|
||||
}
|
||||
|
||||
if req.Header.Get("Authorization") != "Bearer token" {
|
||||
t.Errorf("CustomAttributes() should have set Authorization header")
|
||||
}
|
||||
|
||||
if req.Header.Get("X-Authorization") != "generatedPopToken" {
|
||||
t.Errorf("CustomAttributes() should have set X-Authorization header")
|
||||
}
|
||||
|
||||
if req.Header.Get(XAuthOriginator) != "token" {
|
||||
t.Errorf("CustomAttributes() should have set x-auth-originator header")
|
||||
}
|
||||
|
||||
if req.Header.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("CustomAttributes() should have set X-Authorization header")
|
||||
}
|
||||
|
||||
if len(req.Header) != 4 {
|
||||
t.Errorf("CustomAttributes() should have set 4 headers")
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Errorf("CustomAttributes() should have read body")
|
||||
}
|
||||
|
||||
if string(b) != msrs {
|
||||
t.Errorf("CustomAttributes() should have set body")
|
||||
}
|
||||
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("CustomAttributes() should have called http.Do()")
|
||||
}
|
||||
}
|
||||
|
||||
type mockHTTPClientChangeRatePlan struct {
|
||||
wasSet chan *http.Request
|
||||
}
|
||||
|
||||
var mockChangeRatePlanRequest = &ChangeRatePlanRequest{
|
||||
ICCID: "12345678901234567890",
|
||||
ProductId: "product_1_with_rate_plan_1",
|
||||
}
|
||||
var mockChangeRatePlanRequestJsonRes = `{"iccid": "12345678901234567890"}`
|
||||
|
||||
func (c *mockHTTPClientChangeRatePlan) Do(req *http.Request) (*http.Response, error) {
|
||||
c.wasSet <- req
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(strings.NewReader(mockChangeRatePlanRequestJsonRes)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestTMobClient_ChangeRatePlan(t *testing.T) {
|
||||
mtg := &mockTG{wasSet: make(chan tmobtokengen.EHTSMap, 1)}
|
||||
mHTTP := &mockHTTPClientChangeRatePlan{wasSet: make(chan *http.Request, 1)}
|
||||
|
||||
cl := &TMobClient{
|
||||
client: mHTTP,
|
||||
tg: mtg,
|
||||
baseURL: mockUrl,
|
||||
accessToken: "Bearer token",
|
||||
}
|
||||
msrs := "{\"iccid\":\"12345678901234567890\",\"productId\":\"product_1_with_rate_plan_1\"}\n"
|
||||
res, err := cl.ChangeRatePlan(mockCtx, mockChangeRatePlanRequest)
|
||||
if err != nil {
|
||||
t.Errorf("ChangeRatePlan() should have succeeded: %v", err)
|
||||
}
|
||||
|
||||
if res.ICCID != "12345678901234567890" {
|
||||
t.Errorf("ChangeRatePlan() should have returned smsMessageId")
|
||||
}
|
||||
|
||||
select {
|
||||
case ehts := <-mtg.wasSet:
|
||||
if ehts["uri"] != "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/change-rate-plan" {
|
||||
t.Errorf("ChangeRatePlan() ehts should have set uri")
|
||||
}
|
||||
if ehts["http-method"] != "POST" {
|
||||
t.Errorf("ChangeRatePlan() ehts should have set method")
|
||||
}
|
||||
if ehts["Content-Type"] != "application/json" {
|
||||
t.Errorf("ChangeRatePlan() ehts should have set Content-Type")
|
||||
}
|
||||
|
||||
if ehts["Authorization"] != "Bearer token" {
|
||||
t.Errorf("ChangeRatePlan() ehts should have set Authorization header")
|
||||
}
|
||||
|
||||
if ehts["body"] != msrs {
|
||||
t.Errorf("ChangeRatePlan() ehts should have set Body")
|
||||
}
|
||||
if len(ehts) != 5 {
|
||||
t.Errorf("ChangeRatePlan() ehts should have set 4 keys")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("ChangeRatePlan() should have called tg.Generate()")
|
||||
}
|
||||
|
||||
select {
|
||||
case req := <-mHTTP.wasSet:
|
||||
if req.URL.String() != mockUrlStr+"/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/change-rate-plan" {
|
||||
t.Errorf("ChangeRatePlan() should have set URL")
|
||||
}
|
||||
if req.Method != "POST" {
|
||||
t.Errorf("ChangeRatePlan() should have set method")
|
||||
}
|
||||
|
||||
if req.Header.Get("Authorization") != "Bearer token" {
|
||||
t.Errorf("ChangeRatePlan() should have set Authorization header")
|
||||
}
|
||||
|
||||
if req.Header.Get("X-Authorization") != "generatedPopToken" {
|
||||
t.Errorf("ChangeRatePlan() should have set X-Authorization header")
|
||||
}
|
||||
|
||||
if req.Header.Get(XAuthOriginator) != "token" {
|
||||
t.Errorf("ChangeRatePlan() should have set x-auth-originator header")
|
||||
}
|
||||
|
||||
if req.Header.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("ChangeRatePlan() should have set X-Authorization header")
|
||||
}
|
||||
|
||||
if len(req.Header) != 4 {
|
||||
t.Errorf("ChangeRatePlan() should have set 4 headers")
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Errorf("ChangeRatePlan() should have read body")
|
||||
}
|
||||
|
||||
if string(b) != msrs {
|
||||
t.Errorf("ChangeRatePlan() should have set body")
|
||||
}
|
||||
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("ChangeRatePlan() should have called http.Do()")
|
||||
}
|
||||
}
|
||||
|
||||
type mockHTTPClientGetProducts struct {
|
||||
wasSet chan *http.Request
|
||||
}
|
||||
|
||||
var mockGetProductsRequest = &sms.GetAvailableProductsRequest{
|
||||
AccountId: "fisker_tmobile_1",
|
||||
ProductType: []string{"RATEPLAN"},
|
||||
ProductClassification: []string{"IOTCONNECTIVITY"},
|
||||
}
|
||||
|
||||
var mockGetProductsJsonRes = `[{"id": "1", "productId": "product_2","shortDescription": "short", "longDescription":"long", "status": "Published", "effectiveDate": "2021-02-17T19:49:00+00:00", "expirationDate": "", "ratePlan": {}}]`
|
||||
|
||||
func (c *mockHTTPClientGetProducts) Do(req *http.Request) (*http.Response, error) {
|
||||
c.wasSet <- req
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(strings.NewReader(mockGetProductsJsonRes)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestTMobClient_GetProducts(t *testing.T) {
|
||||
mtg := &mockTG{wasSet: make(chan tmobtokengen.EHTSMap, 1)}
|
||||
mHTTP := &mockHTTPClientGetProducts{wasSet: make(chan *http.Request, 1)}
|
||||
|
||||
cl := &TMobClient{
|
||||
client: mHTTP,
|
||||
tg: mtg,
|
||||
baseURL: mockUrl,
|
||||
accessToken: "Bearer token",
|
||||
}
|
||||
msrs := "{\"accountId\":\"fisker_tmobile_1\",\"productType\":[\"RATEPLAN\"],\"productClassification\":[\"IOTCONNECTIVITY\"]}\n"
|
||||
res, err := cl.GetProducts(mockCtx, mockGetProductsRequest)
|
||||
if err != nil {
|
||||
t.Errorf("GetProducts() should have succeeded: %v", err)
|
||||
}
|
||||
|
||||
if res.AvailableProducts[0].ProductId != "product_2" {
|
||||
t.Errorf("GetProducts() should have returned correct products")
|
||||
}
|
||||
|
||||
select {
|
||||
case ehts := <-mtg.wasSet:
|
||||
if ehts["uri"] != "/eitcsr-iot-product-service-v1/prd02/iotcp/v1/iot-product-service/products/query" {
|
||||
t.Errorf("GetProducts() ehts should have set uri")
|
||||
}
|
||||
if ehts["http-method"] != "POST" {
|
||||
t.Errorf("GetProducts() ehts should have set method")
|
||||
}
|
||||
if ehts["Content-Type"] != "application/json" {
|
||||
t.Errorf("GetProducts() ehts should have set Content-Type")
|
||||
}
|
||||
|
||||
if ehts["Authorization"] != "Bearer token" {
|
||||
t.Errorf("GetProducts() ehts should have set Authorization header")
|
||||
}
|
||||
if ehts["body"] != msrs {
|
||||
t.Errorf("GetProducts() ehts should have set Body")
|
||||
}
|
||||
if len(ehts) != 5 {
|
||||
t.Errorf("GetProducts() ehts should have set 4 keys")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("GetProducts() should have called tg.Generate()")
|
||||
}
|
||||
|
||||
select {
|
||||
case req := <-mHTTP.wasSet:
|
||||
if req.URL.String() != mockUrlStr+"/eitcsr-iot-product-service-v1/prd02/iotcp/v1/iot-product-service/products/query" {
|
||||
t.Errorf("GetProducts() should have set URL")
|
||||
}
|
||||
if req.Method != "POST" {
|
||||
t.Errorf("GetProducts() should have set method")
|
||||
}
|
||||
|
||||
if req.Header.Get("Authorization") != "Bearer token" {
|
||||
t.Errorf("GetProducts() should have set Authorization header")
|
||||
}
|
||||
|
||||
if req.Header.Get("X-Authorization") != "generatedPopToken" {
|
||||
t.Errorf("GetProducts() should have set X-Authorization header")
|
||||
}
|
||||
if req.Header.Get(XAuthOriginator) != "token" {
|
||||
t.Errorf("GetProducts() should have set x-auth-originator header")
|
||||
}
|
||||
|
||||
if req.Header.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("GetProducts() should have set X-Authorization header")
|
||||
}
|
||||
|
||||
if len(req.Header) != 4 {
|
||||
t.Errorf("GetProducts() should have set 4 headers")
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Errorf("GetProducts() should have read body")
|
||||
}
|
||||
|
||||
if string(b) != msrs {
|
||||
t.Errorf("GetProducts() should have set body")
|
||||
}
|
||||
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("GetProducts() should have called http.Do()")
|
||||
}
|
||||
}
|
||||
|
||||
type mockHTTPClientDeviceDetails struct {
|
||||
wasSet chan *http.Request
|
||||
}
|
||||
|
||||
var mockDeviceDetailsRequest = &DeviceDetailsRequest{
|
||||
ICCID: "1234567890",
|
||||
}
|
||||
|
||||
var mockDeviceDetailsJsonRes = `{"iccid": "1234567890", "ratePlan": "plan_1", "accountId": "account_1", "accountCustom1": "US", "status": "published"}`
|
||||
|
||||
func (c *mockHTTPClientDeviceDetails) Do(req *http.Request) (*http.Response, error) {
|
||||
c.wasSet <- req
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Body: io.NopCloser(strings.NewReader(mockDeviceDetailsJsonRes)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestTMobClient_DeviceDetails(t *testing.T) {
|
||||
mtg := &mockTG{wasSet: make(chan tmobtokengen.EHTSMap, 1)}
|
||||
mHTTP := &mockHTTPClientDeviceDetails{wasSet: make(chan *http.Request, 1)}
|
||||
|
||||
cl := &TMobClient{
|
||||
client: mHTTP,
|
||||
tg: mtg,
|
||||
baseURL: mockUrl,
|
||||
accessToken: "Bearer token",
|
||||
}
|
||||
msrs := "{\"iccid\":\"1234567890\"}\n"
|
||||
res, err := cl.DeviceDetails(mockCtx, mockDeviceDetailsRequest)
|
||||
if err != nil {
|
||||
t.Errorf("DeviceDetails() should have succeeded: %v", err)
|
||||
}
|
||||
|
||||
if res.ICCID != "1234567890" {
|
||||
t.Errorf("DeviceDetails() should have returned correct products")
|
||||
}
|
||||
|
||||
select {
|
||||
case ehts := <-mtg.wasSet:
|
||||
if ehts["uri"] != "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/details" {
|
||||
t.Errorf("DeviceDetails() ehts should have set uri")
|
||||
}
|
||||
if ehts["http-method"] != "POST" {
|
||||
t.Errorf("DeviceDetails() ehts should have set method")
|
||||
}
|
||||
if ehts["Content-Type"] != "application/json" {
|
||||
t.Errorf("DeviceDetails() ehts should have set Content-Type")
|
||||
}
|
||||
|
||||
if ehts["Authorization"] != "Bearer token" {
|
||||
t.Errorf("DeviceDetails() ehts should have set Authorization header")
|
||||
}
|
||||
if ehts["body"] != msrs {
|
||||
t.Errorf("DeviceDetails() ehts should have set Body")
|
||||
}
|
||||
if len(ehts) != 5 {
|
||||
t.Errorf("DeviceDetails() ehts should have set 4 keys")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("DeviceDetails() should have called tg.Generate()")
|
||||
}
|
||||
|
||||
select {
|
||||
case req := <-mHTTP.wasSet:
|
||||
if req.URL.String() != mockUrlStr+"/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/details" {
|
||||
t.Errorf("DeviceDetails() should have set URL")
|
||||
}
|
||||
if req.Method != "POST" {
|
||||
t.Errorf("DeviceDetails() should have set method")
|
||||
}
|
||||
|
||||
if req.Header.Get("Authorization") != "Bearer token" {
|
||||
t.Errorf("DeviceDetails() should have set Authorization header")
|
||||
}
|
||||
|
||||
if req.Header.Get("X-Authorization") != "generatedPopToken" {
|
||||
t.Errorf("DeviceDetails() should have set X-Authorization header")
|
||||
}
|
||||
|
||||
if req.Header.Get(XAuthOriginator) != "token" {
|
||||
t.Errorf("DeviceDetails() should have set x-auth-originator header")
|
||||
}
|
||||
|
||||
if req.Header.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("DeviceDetails() should have set X-Authorization header")
|
||||
}
|
||||
|
||||
if len(req.Header) != 4 {
|
||||
t.Errorf("DeviceDetails() should have set 4 headers")
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Errorf("DeviceDetails() should have read body")
|
||||
}
|
||||
|
||||
if string(b) != msrs {
|
||||
t.Errorf("DeviceDetails() should have set body")
|
||||
}
|
||||
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("DeviceDetails() should have called http.Do()")
|
||||
}
|
||||
}
|
||||
62
pkg/tmobile/errors.go
Normal file
62
pkg/tmobile/errors.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var ErrCreateRequest = errors.New("failed to create request")
|
||||
var ErrDoRequest = errors.New("failed to do request")
|
||||
var ErrReadResponseBody = errors.New("failed to read response body")
|
||||
var ErrBadStatusCode = errors.New("bad status code")
|
||||
var ErrBadGatewayCode = errors.New("bad gateway timeout")
|
||||
var ErrJSONMarshal = errors.New("json marshal & unmarshal error")
|
||||
var ErrTokenGen = errors.New("failed to generate token")
|
||||
var ErrBadMsgStatus = errors.New("bad message status")
|
||||
var ErrTimeoutSendingMessage = errors.New("timeout sending message")
|
||||
var ErrAccessTokenExpired = errors.New("access token expired")
|
||||
|
||||
func ErrorToGRPCError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrCreateRequest) {
|
||||
return status.Errorf(codes.Internal, "%s", err.Error())
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrDoRequest) {
|
||||
return status.Errorf(codes.Internal, "%s", err.Error())
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrReadResponseBody) {
|
||||
return status.Errorf(codes.Internal, "%s", err.Error())
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrBadStatusCode) {
|
||||
return status.Errorf(codes.Internal, "%s", err.Error())
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrBadGatewayCode) {
|
||||
return status.Errorf(codes.Internal, "%s", err.Error())
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrJSONMarshal) {
|
||||
return status.Errorf(codes.Internal, "%s", err.Error())
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrTokenGen) {
|
||||
return status.Errorf(codes.Internal, "%s", err.Error())
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrBadMsgStatus) {
|
||||
return status.Errorf(codes.Internal, "%s", err.Error())
|
||||
}
|
||||
|
||||
if errors.Is(err, ErrTimeoutSendingMessage) {
|
||||
return status.Errorf(codes.DeadlineExceeded, "%s", err.Error())
|
||||
}
|
||||
|
||||
return status.Errorf(codes.Unknown, "unknown error: %v", err)
|
||||
}
|
||||
105
pkg/tmobile/errors_test.go
Normal file
105
pkg/tmobile/errors_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func Test_errorToGRPCError(t *testing.T) {
|
||||
type args struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
args: args{
|
||||
err: nil,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "ErrCreateRequest",
|
||||
args: args{
|
||||
err: ErrCreateRequest,
|
||||
},
|
||||
wantErr: status.Errorf(codes.Internal, "failed to create request"),
|
||||
},
|
||||
{
|
||||
name: "ErrDoRequest",
|
||||
args: args{
|
||||
err: ErrDoRequest,
|
||||
},
|
||||
wantErr: status.Errorf(codes.Internal, "failed to do request"),
|
||||
},
|
||||
{
|
||||
name: "ErrReadResponseBody",
|
||||
args: args{
|
||||
err: ErrReadResponseBody,
|
||||
},
|
||||
wantErr: status.Errorf(codes.Internal, "failed to read response body"),
|
||||
},
|
||||
{
|
||||
name: "ErrBadStatusCode",
|
||||
args: args{
|
||||
err: ErrBadStatusCode,
|
||||
},
|
||||
wantErr: status.Errorf(codes.Internal, "bad status code"),
|
||||
},
|
||||
{
|
||||
name: "ErrJSONMarshal",
|
||||
args: args{
|
||||
err: ErrJSONMarshal,
|
||||
},
|
||||
wantErr: status.Errorf(codes.Internal, "json marshal & unmarshal error"),
|
||||
},
|
||||
{
|
||||
name: "ErrTokenGen",
|
||||
args: args{
|
||||
err: ErrTokenGen,
|
||||
},
|
||||
wantErr: status.Errorf(codes.Internal, "failed to generate token"),
|
||||
},
|
||||
{
|
||||
name: "ErrBadMsgStatus",
|
||||
args: args{
|
||||
err: ErrBadMsgStatus,
|
||||
},
|
||||
wantErr: status.Errorf(codes.Internal, "bad message status"),
|
||||
},
|
||||
{
|
||||
name: "ErrTimeoutSendingMessage",
|
||||
args: args{
|
||||
err: ErrTimeoutSendingMessage,
|
||||
},
|
||||
wantErr: status.Errorf(codes.DeadlineExceeded, "timeout sending message"),
|
||||
},
|
||||
|
||||
{
|
||||
name: "ErrUnknown",
|
||||
args: args{
|
||||
err: errors.New("unknown error"),
|
||||
},
|
||||
wantErr: status.Errorf(codes.Unknown, "unknown error: unknown error"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ErrorToGRPCError(tt.args.err)
|
||||
if err != nil {
|
||||
if err.Error() != tt.wantErr.Error() {
|
||||
t.Errorf("errorToGRPCError() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
} else if err != tt.wantErr {
|
||||
t.Errorf("errorToGRPCError() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
pkg/tmobile/iccid_filter.go
Normal file
50
pkg/tmobile/iccid_filter.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"fiskerinc.com/modules/utils/envtool"
|
||||
)
|
||||
|
||||
type ICCIDFilter struct {
|
||||
filter []string
|
||||
}
|
||||
|
||||
// InitFilter creates a filter that filters based on the following criteria
|
||||
// If the env variable SMS_ICCID_FILTER_LIST is empty
|
||||
// . then we will allow all ICCIDs through
|
||||
// If the filter is NOT empty, then only those specified ICCIDs will be sent
|
||||
// . all others will be automatically succeeded
|
||||
func InitFilter() (filter ICCIDFilter) {
|
||||
envVal := envtool.GetEnv("SMS_ICCID_FILTER_LIST", "")
|
||||
// No characters, then no need to create the filter
|
||||
if len(envVal) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
split := strings.Split(envVal, ",")
|
||||
for x := range split {
|
||||
split[x] = strings.TrimSpace(split[x])
|
||||
}
|
||||
filter.filter = split
|
||||
return
|
||||
}
|
||||
|
||||
// If send is false, then automatically return a success message
|
||||
func (filter ICCIDFilter) ShouldSend(iccid string) (send bool) {
|
||||
if len(filter.filter) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, id := range filter.filter {
|
||||
if id == iccid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (filter *ICCIDFilter) SetFilter(filterList []string) {
|
||||
filter.filter = filterList
|
||||
}
|
||||
44
pkg/tmobile/iccid_filter_test.go
Normal file
44
pkg/tmobile/iccid_filter_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package tmobile_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/tmobile"
|
||||
)
|
||||
|
||||
func TestICCIDFilter(t *testing.T){
|
||||
// If there is a filter, on the listed iccids should work
|
||||
err := os.Setenv("SMS_ICCID_FILTER_LIST", "iccid1,iccid2, iccid3")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
filter := tmobile.InitFilter()
|
||||
send := filter.ShouldSend("iccid2")
|
||||
if !send {
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
|
||||
send = filter.ShouldSend("iccid3")
|
||||
if !send {
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
|
||||
send = filter.ShouldSend("notInList")
|
||||
if send {
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
|
||||
// if the filter is empty, all should be sent
|
||||
filter.SetFilter([]string{})
|
||||
send = filter.ShouldSend("iccid2")
|
||||
if !send {
|
||||
t.Fail()
|
||||
return
|
||||
}
|
||||
}
|
||||
200
pkg/tmobile/mock_client_test.go
Normal file
200
pkg/tmobile/mock_client_test.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type MockClientAccTokResSucc struct {
|
||||
*TMobClient
|
||||
wasSet chan string
|
||||
n uint8
|
||||
iccidFilter ICCIDFilter
|
||||
}
|
||||
|
||||
var _ TMobClienter = &MockClientAccTokResSucc{}
|
||||
|
||||
func (mc *MockClientAccTokResSucc) Details(ctx context.Context, ID string) (out *SMSDetailsResponse, err error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
var mockAccessTokenResponse = &AccessTokenResponse{
|
||||
IdToken: "accessToken",
|
||||
AccessToken: "accessToken",
|
||||
ExpiresIn: 2,
|
||||
TokenType: "tokenType",
|
||||
Resource: "resource",
|
||||
Scope: "scope",
|
||||
}
|
||||
|
||||
func (mc *MockClientAccTokResSucc) AccessToken(
|
||||
ctx context.Context,
|
||||
) (out *AccessTokenResponse, err error) {
|
||||
matr := *mockAccessTokenResponse
|
||||
|
||||
matr.AccessToken = fmt.Sprintf("accessToken%d", mc.n)
|
||||
|
||||
if mc.n == 1 {
|
||||
matr.ExpiresIn = 6
|
||||
return &matr, nil
|
||||
}
|
||||
|
||||
mc.n++
|
||||
return &matr, nil
|
||||
}
|
||||
|
||||
func (mc *MockClientAccTokResSucc) SetAccessToken(
|
||||
accessToken string,
|
||||
) {
|
||||
mc.wasSet <- accessToken
|
||||
}
|
||||
|
||||
// SetFilter implements TMobClienter.
|
||||
func (mc *MockClientAccTokResSucc) SetFilter(filter []string) {
|
||||
mc.iccidFilter = ICCIDFilter{
|
||||
filter: filter,
|
||||
}
|
||||
}
|
||||
|
||||
type MockClientAccTokResFail struct {
|
||||
TMobClienter
|
||||
wasSet chan struct{}
|
||||
}
|
||||
|
||||
func (mc *MockClientAccTokResFail) AccessToken(
|
||||
ctx context.Context,
|
||||
) (out *AccessTokenResponse, err error) {
|
||||
mc.wasSet <- struct{}{}
|
||||
return nil, fmt.Errorf("failed to get access token")
|
||||
}
|
||||
|
||||
type MockClientSendSMSResFail struct {
|
||||
TMobClienter
|
||||
wasSet chan struct{}
|
||||
}
|
||||
|
||||
func (mc *MockClientSendSMSResFail) SendSMS(
|
||||
ctx context.Context,
|
||||
req *SendSMSRequest,
|
||||
) (out *SendSMSResponse, err error) {
|
||||
mc.wasSet <- struct{}{}
|
||||
return nil, fmt.Errorf("failed to send SMS")
|
||||
}
|
||||
|
||||
type MockClientSendSMSExpire struct {
|
||||
TMobClienter
|
||||
}
|
||||
|
||||
func (mc *MockClientSendSMSExpire) SendSMS(
|
||||
ctx context.Context,
|
||||
req *SendSMSRequest,
|
||||
) (out *SendSMSResponse, err error) {
|
||||
return &SendSMSResponse{
|
||||
SmsMessageID: "smsMessageID",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mc *MockClientSendSMSExpire) Details(
|
||||
ctx context.Context,
|
||||
ID string,
|
||||
) (out *SMSDetailsResponse, err error) {
|
||||
return &SMSDetailsResponse{
|
||||
SmsMsgID: "smsMessageID",
|
||||
Status: Pending,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MockClientSendSMSDetailsFailRes struct {
|
||||
TMobClienter
|
||||
wasSet chan struct{}
|
||||
}
|
||||
|
||||
func (mc *MockClientSendSMSDetailsFailRes) SendSMS(
|
||||
ctx context.Context,
|
||||
in *SendSMSRequest,
|
||||
) (out *SendSMSResponse, err error) {
|
||||
return &SendSMSResponse{
|
||||
SmsMessageID: "smsMessageID",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mc *MockClientSendSMSDetailsFailRes) Details(
|
||||
ctx context.Context,
|
||||
ID string,
|
||||
) (out *SMSDetailsResponse, err error) {
|
||||
mc.wasSet <- struct{}{}
|
||||
return nil, fmt.Errorf("failed to get SMS details")
|
||||
}
|
||||
|
||||
type MockClientSendSMSDetailsFailWithBadStatus struct {
|
||||
TMobClienter
|
||||
wasSet chan SmsDetailsStatus
|
||||
toFail []SmsDetailsStatus
|
||||
n uint8
|
||||
}
|
||||
|
||||
func (mc *MockClientSendSMSDetailsFailWithBadStatus) SendSMS(
|
||||
ctx context.Context,
|
||||
in *SendSMSRequest,
|
||||
) (out *SendSMSResponse, err error) {
|
||||
return &SendSMSResponse{
|
||||
SmsMessageID: "smsMessageID",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mc *MockClientSendSMSDetailsFailWithBadStatus) Details(
|
||||
ctx context.Context,
|
||||
ID string,
|
||||
) (out *SMSDetailsResponse, err error) {
|
||||
status := mc.toFail[mc.n]
|
||||
mc.wasSet <- status
|
||||
mc.n++
|
||||
|
||||
return &SMSDetailsResponse{
|
||||
SmsMsgID: "smsMessageID",
|
||||
Status: status,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MockClientSendSMSResSucc struct {
|
||||
TMobClienter
|
||||
wasSet chan SmsDetailsStatus
|
||||
n uint8
|
||||
}
|
||||
|
||||
func (mc *MockClientSendSMSResSucc) SendSMS(
|
||||
ctx context.Context,
|
||||
in *SendSMSRequest,
|
||||
) (out *SendSMSResponse, err error) {
|
||||
return &SendSMSResponse{
|
||||
SmsMessageID: "smsMessageID",
|
||||
}, nil
|
||||
}
|
||||
|
||||
var smsStatusInitialStages = []SmsDetailsStatus{
|
||||
Pending,
|
||||
CancelPending,
|
||||
}
|
||||
|
||||
var smsStatusSuccStages = append(smsStatusInitialStages, Delivered)
|
||||
|
||||
var smsStatusFailTypes = [][]SmsDetailsStatus{
|
||||
append(smsStatusInitialStages, Failed),
|
||||
append([]SmsDetailsStatus{}, Cancelled),
|
||||
append([]SmsDetailsStatus{}, Received),
|
||||
append([]SmsDetailsStatus{}, Unknown),
|
||||
append([]SmsDetailsStatus{}, CancelFailed),
|
||||
}
|
||||
|
||||
func (mc *MockClientSendSMSResSucc) Details(ctx context.Context, ID string) (out *SMSDetailsResponse, err error) {
|
||||
status := smsStatusSuccStages[mc.n]
|
||||
mc.wasSet <- status
|
||||
mc.n++
|
||||
|
||||
return &SMSDetailsResponse{
|
||||
SmsMsgID: "smsMessageID",
|
||||
Status: status,
|
||||
}, nil
|
||||
}
|
||||
|
||||
250
pkg/tmobile/model.go
Normal file
250
pkg/tmobile/model.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/grpc/sms"
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/utils/envtool"
|
||||
)
|
||||
|
||||
var FISKER_TMOBILE_ACCOUNT_ID = envtool.GetEnv("FISKER_TMOBILE_ACCOUNT_ID", "500556839")
|
||||
|
||||
type AccessTokenResponse struct {
|
||||
IdToken string `json:"id_token"`
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
Resource string `json:"resource"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
type SendSMSRequest struct {
|
||||
// An array of ICCIDs or MSISDNs that you want to send the message to. At least one item is required.
|
||||
// Please note the pluralized property name. Maximum length is 50.
|
||||
ICCID string `json:"iccid,omitempty" validate:"required,iccid,lte=50"`
|
||||
|
||||
MSISDN *string `json:"msisdn,omitempty" validate:"omitempty,lte=50"`
|
||||
|
||||
// If the API does not specify the data coding type, the default maximum message length is 320
|
||||
// characters. Messages that exceed 160 characters appear on two lines.
|
||||
MessageText string `json:"messageText,omitempty" validate:"required,lte=320"`
|
||||
|
||||
// (Optional) The type of message encoding used. Valid values are LITERAL (default) or BASE64.
|
||||
MessageEncoding *string `json:"messageEncoding,omitempty" validate:"omitempty,oneof=LITERAL BASE64"`
|
||||
|
||||
// (Optional) The type of data encoding used.
|
||||
// 0 - SMSC default alphabet; often GSM encoding
|
||||
// 1 - IA5/ASCII (but sometimes GSM depending on the SMSC implementation)
|
||||
// 3 - Latin 1 (ISO-8859-1)
|
||||
// 4 - Binary SMS
|
||||
// 8 - Unicode UCS2
|
||||
DataCoding *string `json:"dataCoding,omitempty" validate:"omitempty,oneof=0 1 3 4 8"`
|
||||
|
||||
// (Optional) The length of time the message is available before expiring.
|
||||
// The default value is 0.
|
||||
// 0 – 143 | (TPVP + 1) x 5 minutes | 5, 10, 15 minutes ... 11:55, 12:00 hours
|
||||
// 144 - 167 | (12 + (TPVP - 143) / 2) hours | 12:30, 13:00, ... 23:30, 24:00 hours
|
||||
// 168 - 196 | (TPVP - 166) days | 2, 3, 4, ... 30 days
|
||||
// 197 - 255 | (TPVP - 192) weeks | 5, 6, 7, ... 63 weeks
|
||||
TPVP *string `json:"tpvp,omitempty" validate:"omitempty,numeric,min=0,max=255"`
|
||||
|
||||
KafkaServiceTarget string `json:"kafkaServiceTarget,omitempty"`
|
||||
|
||||
await bool
|
||||
}
|
||||
|
||||
func (r *SendSMSRequest) Await() bool {
|
||||
return r.await
|
||||
}
|
||||
|
||||
func (r *SendSMSRequest) FromProtobuf(setReq *sms.SendSMSRequest) {
|
||||
r.ICCID = setReq.ICCID
|
||||
|
||||
r.ICCID = strings.TrimSuffix(strings.ToLower(r.ICCID), "f")
|
||||
|
||||
r.MessageText = setReq.MessageText
|
||||
if setReq.MessageEncoding != nil {
|
||||
encoding := setReq.MessageEncoding.String()
|
||||
r.MessageEncoding = &encoding
|
||||
}
|
||||
|
||||
if setReq.DataCoding != nil {
|
||||
dc := strconv.Itoa(int(setReq.DataCoding.Enum().Number()))
|
||||
r.DataCoding = &dc
|
||||
}
|
||||
|
||||
r.TPVP = setReq.Tpvp
|
||||
r.await = setReq.Await
|
||||
}
|
||||
|
||||
const tpvpUnit = 5 * time.Minute
|
||||
|
||||
var minAwait time.Duration
|
||||
|
||||
func init() {
|
||||
def := time.Second * 10
|
||||
ma, err := time.ParseDuration(envtool.GetEnv("SMS_MIN_AWAIT", "10s"))
|
||||
if err != nil {
|
||||
logger.Warn().Err(err).Msgf("SMS_MIN_AWAIT not set, setting default %v", def)
|
||||
ma = def
|
||||
}
|
||||
|
||||
minAwait = ma
|
||||
}
|
||||
|
||||
func (r *SendSMSRequest) ExpireIfNotDelivered() (exp time.Duration) {
|
||||
return time.Millisecond * 300
|
||||
/* if r.TPVP == nil {
|
||||
r.TPVP = new(string)
|
||||
*r.TPVP, exp = deftpvp()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
num, err := strconv.Atoi(*r.TPVP)
|
||||
if err != nil {
|
||||
*r.TPVP, exp = deftpvp()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if num > 255 {
|
||||
*r.TPVP, exp = maxtpvp()
|
||||
|
||||
return
|
||||
} else if num < 0 {
|
||||
*r.TPVP, exp = deftpvp()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return time.Duration(num) * tpvpUnit */
|
||||
}
|
||||
|
||||
type SendSMSResponse struct {
|
||||
SmsMessageID string `json:"smsMessageId"`
|
||||
}
|
||||
|
||||
type SmsDetailsStatus string
|
||||
|
||||
const (
|
||||
Received SmsDetailsStatus = "Received"
|
||||
Cancelled SmsDetailsStatus = "Cancelled"
|
||||
CancelFailed SmsDetailsStatus = "CancelFailed"
|
||||
CancelPending SmsDetailsStatus = "CancelPending"
|
||||
Delivered SmsDetailsStatus = "Delivered"
|
||||
Pending SmsDetailsStatus = "Pending"
|
||||
Failed SmsDetailsStatus = "Failed"
|
||||
Unknown SmsDetailsStatus = "Unknown"
|
||||
)
|
||||
|
||||
type SMSDetailsResponse struct {
|
||||
SmsMsgID string `json:"smsMsgId"`
|
||||
Status SmsDetailsStatus `json:"status"`
|
||||
MessageText string `json:"messageText"`
|
||||
SenderLogin string `json:"senderLogin"`
|
||||
SentTo string `json:"sentTo"`
|
||||
SentFrom string `json:"sentFrom"`
|
||||
MsgType string `json:"msgType"`
|
||||
DateSent string `json:"dateSent"`
|
||||
DateModified string `json:"dateModified"`
|
||||
ICCID string `json:"iccid"`
|
||||
}
|
||||
|
||||
func (r *SMSDetailsResponse) ToProtobuf() *sms.SMSDetailsResponse {
|
||||
status := sms.SMSDetailsResponse_Status(
|
||||
sms.SMSDetailsResponse_Status_value[string(r.Status)])
|
||||
|
||||
return &sms.SMSDetailsResponse{
|
||||
SmsMsgID: r.SmsMsgID,
|
||||
Status: status,
|
||||
MessageText: r.MessageText,
|
||||
SenderLogin: r.SenderLogin,
|
||||
SentTo: r.SentTo,
|
||||
SentFrom: r.SentFrom,
|
||||
MsgType: r.MsgType,
|
||||
DateSent: r.DateSent,
|
||||
DateModified: r.DateModified,
|
||||
ICCID: r.ICCID,
|
||||
}
|
||||
}
|
||||
|
||||
type ChangeRatePlanRequest struct {
|
||||
ICCID string `json:"iccid,omitempty" validate:"required,iccid,lte=50"`
|
||||
ProductId string `json:"productId,omitempty" validate:"required"`
|
||||
AccountId string `json:"accountId,omitempty" validate:"required"`
|
||||
}
|
||||
|
||||
func (r *ChangeRatePlanRequest) FromProtobuf(req *sms.ChangeRatePlanRequest) {
|
||||
r.ICCID = strings.TrimSuffix(strings.ToLower(req.ICCID), "f")
|
||||
r.ProductId = req.ProductId
|
||||
r.AccountId = req.AccountId
|
||||
}
|
||||
|
||||
type ChangeRatePlanResponse struct {
|
||||
ICCID string `json:"iccid,omitempty"`
|
||||
}
|
||||
|
||||
type CustomAtributesRequest struct {
|
||||
ICCID string `json:"iccid,omitempty" validate:"required,iccid,lte=50"`
|
||||
AccountCustom1 string `json:"accountCustom1,omitempty" validate:"required"`
|
||||
}
|
||||
|
||||
func (r *CustomAtributesRequest) FromProtobuf(req *sms.CustomAtributesRequest) {
|
||||
r.ICCID = strings.TrimSuffix(strings.ToLower(req.ICCID), "f")
|
||||
r.AccountCustom1 = req.AccountCustom1
|
||||
}
|
||||
|
||||
type CustomAtributesResponse struct {
|
||||
ICCID string `json:"iccid,omitempty"`
|
||||
}
|
||||
|
||||
type DeviceDetailsRequest struct {
|
||||
ICCID string `json:"iccid,omitempty" validate:"required,iccid,lte=50"`
|
||||
}
|
||||
|
||||
type ICCIDBody struct {
|
||||
ICCID string `json:"iccid,omitempty" validate:"required,iccid,lte=50"`
|
||||
}
|
||||
|
||||
type DeviceDetailsResponse struct {
|
||||
ICCID string `json:"iccid,omitempty"`
|
||||
RatePlan string `json:"ratePlan,omitempty"`
|
||||
AccountId string `json:"accountId,omitempty"`
|
||||
AccountCustom1 string `json:"accountCustom1,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
func (ddr *DeviceDetailsResponse) ToProtobuf() *sms.DeviceDetailsResponse {
|
||||
res := &sms.DeviceDetailsResponse{
|
||||
ICCID: ddr.ICCID,
|
||||
RatePlan: ddr.RatePlan,
|
||||
AccountId: ddr.AccountId,
|
||||
AccountCustom1: ddr.AccountCustom1,
|
||||
Status: ddr.Status,
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type SMSQueueResponse struct {
|
||||
SmsMsgID string `json:"smsMsgId"`
|
||||
SentSuccessful bool `json:"sentSuccessful"`
|
||||
}
|
||||
|
||||
func (sqr *SMSQueueResponse) ToProtobuf() *sms.SMSQueueResponse {
|
||||
res := &sms.SMSQueueResponse{
|
||||
SmsMsgID: sqr.SmsMsgID,
|
||||
SentSuccessful: sqr.SentSuccessful,
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
type ChangeDeviceActivation struct {
|
||||
ICCIDs []string
|
||||
Enabled bool
|
||||
}
|
||||
201
pkg/tmobile/model_test.go
Normal file
201
pkg/tmobile/model_test.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/grpc/sms"
|
||||
)
|
||||
|
||||
func TestSendSMSRequest_ExpireIfNotDelivered(t *testing.T) {
|
||||
t.Skip()
|
||||
req := &SendSMSRequest{}
|
||||
|
||||
expectedExpr := req.ExpireIfNotDelivered()
|
||||
|
||||
if *req.TPVP != "0" || expectedExpr != minAwait {
|
||||
t.Errorf("expected tpvp to be %s, got %s", tpvpUnit, *req.TPVP)
|
||||
t.Errorf("expected expire to be %s, got %s", tpvpUnit, expectedExpr)
|
||||
}
|
||||
|
||||
nan := "not-a-number"
|
||||
req.TPVP = &nan
|
||||
expectedExpr = req.ExpireIfNotDelivered()
|
||||
if *req.TPVP != "0" || expectedExpr != minAwait {
|
||||
t.Errorf("expected tpvp to be %s, got %s", tpvpUnit, *req.TPVP)
|
||||
t.Errorf("expected expire to be %s, got %s", tpvpUnit, expectedExpr)
|
||||
}
|
||||
|
||||
moreThan255 := "256"
|
||||
req.TPVP = &moreThan255
|
||||
expectedExpr = req.ExpireIfNotDelivered()
|
||||
if *req.TPVP != "255" || expectedExpr != tpvpUnit*255 {
|
||||
t.Errorf("expected tpvp to be %s, got %s", tpvpUnit*255, *req.TPVP)
|
||||
t.Errorf("expected expire to be %s, got %s", tpvpUnit*255, expectedExpr)
|
||||
}
|
||||
|
||||
lessThan0 := "-1"
|
||||
req.TPVP = &lessThan0
|
||||
expectedExpr = req.ExpireIfNotDelivered()
|
||||
if *req.TPVP != "0" || expectedExpr != minAwait {
|
||||
t.Errorf("expected tpvp to be %s, got %s", tpvpUnit, *req.TPVP)
|
||||
t.Errorf("expected expire to be %s, got %s", tpvpUnit, expectedExpr)
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().Unix())
|
||||
randInt := rand.Intn(254) + 1
|
||||
ris := strconv.Itoa(randInt)
|
||||
req.TPVP = &ris
|
||||
wantExpire := time.Duration(randInt) * tpvpUnit
|
||||
|
||||
expectedExpr = req.ExpireIfNotDelivered()
|
||||
if *req.TPVP != ris || expectedExpr != wantExpire {
|
||||
t.Errorf("expected tpvp to be %s, got %s", ris, *req.TPVP)
|
||||
t.Errorf("expected expire to be %s, got %s", wantExpire, expectedExpr)
|
||||
}
|
||||
}
|
||||
|
||||
var mockProtoSmsRequest = &sms.SendSMSRequest{
|
||||
ICCID: "12345678901234567890",
|
||||
MessageText: "Hello World",
|
||||
}
|
||||
|
||||
var mockProtoSmsRequestWithTrailingF = &sms.SendSMSRequest{
|
||||
ICCID: "12345678901234567890F",
|
||||
MessageText: "Hello World",
|
||||
}
|
||||
|
||||
func TestSendSMSRequest_FromProtobufWithTralingFAndWithout(t *testing.T) {
|
||||
iccid := "12345678901234567890"
|
||||
protoReq := mockProtoSmsRequestWithTrailingF
|
||||
|
||||
req := &SendSMSRequest{}
|
||||
req.FromProtobuf(protoReq)
|
||||
|
||||
if strings.HasSuffix(strings.ToLower(req.ICCID), "f") {
|
||||
t.Errorf("expected ICCID not to have trailing F, got %s", req.ICCID)
|
||||
}
|
||||
|
||||
if req.ICCID != iccid {
|
||||
t.Errorf("expected ICCID to be %s, got %s", iccid, req.ICCID)
|
||||
}
|
||||
|
||||
protoReq = mockProtoSmsRequest
|
||||
|
||||
req = &SendSMSRequest{}
|
||||
req.FromProtobuf(protoReq)
|
||||
|
||||
if req.ICCID != iccid {
|
||||
t.Errorf("expected ICCID to be %s, got %s", iccid, req.ICCID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendSMSRequest_FromProtobuf(t *testing.T) {
|
||||
encoding := sms.SendSMSRequest_LITERAL
|
||||
dataCoding := sms.SendSMSRequest_FOUR
|
||||
tpvp := "1"
|
||||
|
||||
mockProtoSmsRequest.MessageEncoding = &encoding
|
||||
mockProtoSmsRequest.DataCoding = &dataCoding
|
||||
mockProtoSmsRequest.Tpvp = &tpvp
|
||||
|
||||
protoReq := mockProtoSmsRequest
|
||||
|
||||
req := &SendSMSRequest{}
|
||||
req.FromProtobuf(protoReq)
|
||||
|
||||
if req.ICCID != protoReq.ICCID {
|
||||
t.Errorf("expected ICCID to be %s, got %s", protoReq.ICCID, req.ICCID)
|
||||
}
|
||||
|
||||
if req.MessageText != protoReq.MessageText {
|
||||
t.Errorf("expected MessageText to be %s, got %s", protoReq.MessageText, req.MessageText)
|
||||
}
|
||||
|
||||
if *req.MessageEncoding != protoReq.MessageEncoding.Enum().String() {
|
||||
t.Errorf("expected MessageEncoding to be %s, got %s", protoReq.MessageEncoding.Enum().String(), *req.MessageEncoding)
|
||||
}
|
||||
|
||||
if *req.DataCoding != strconv.Itoa(int(protoReq.DataCoding.Enum().Number())) {
|
||||
t.Errorf("expected DataCoding to be %d, got %s", protoReq.DataCoding.Enum().Number(), *req.DataCoding)
|
||||
}
|
||||
|
||||
if *req.TPVP != tpvp {
|
||||
t.Errorf("expected TPVP to be %s, got %s", tpvp, *req.TPVP)
|
||||
}
|
||||
}
|
||||
|
||||
var smsStatuses = map[string]struct{}{
|
||||
string(Received): {},
|
||||
string(Cancelled): {},
|
||||
string(CancelFailed): {},
|
||||
string(CancelPending): {},
|
||||
string(Delivered): {},
|
||||
string(Pending): {},
|
||||
string(Failed): {},
|
||||
string(Unknown): {},
|
||||
}
|
||||
|
||||
var mockSmsDetailsResponse = &SMSDetailsResponse{
|
||||
SmsMsgID: "12345678901234567890",
|
||||
Status: "some status",
|
||||
MessageText: "Hello World",
|
||||
SenderLogin: "login",
|
||||
SentTo: "someone",
|
||||
SentFrom: "someone else",
|
||||
MsgType: "type",
|
||||
DateSent: "date sent",
|
||||
DateModified: "date modified",
|
||||
ICCID: "12345678901234567890",
|
||||
}
|
||||
|
||||
func TestSMSDetailsResponse_AsProtobuf(t *testing.T) {
|
||||
req := mockSmsDetailsResponse
|
||||
|
||||
protoReq := req.ToProtobuf()
|
||||
|
||||
if protoReq.ICCID != req.ICCID {
|
||||
t.Errorf("expected ICCID to be %s, got %s", req.ICCID, protoReq.ICCID)
|
||||
}
|
||||
|
||||
if protoReq.SmsMsgID != req.SmsMsgID {
|
||||
t.Errorf("expected SmsMsgID to be %s, got %s", req.SmsMsgID, protoReq.SmsMsgID)
|
||||
}
|
||||
|
||||
if protoStatus, reqStatus := protoReq.Status.Enum().String(), string(req.Status); protoStatus != reqStatus {
|
||||
if _, ok := smsStatuses[reqStatus]; ok || protoStatus != "Unknown" {
|
||||
t.Errorf("expected Status to be %s, got %s", req.Status, protoReq.Status)
|
||||
}
|
||||
}
|
||||
|
||||
if protoReq.MessageText != req.MessageText {
|
||||
t.Errorf("expected MessageText to be %s, got %s", req.MessageText, protoReq.MessageText)
|
||||
}
|
||||
|
||||
if protoReq.SenderLogin != req.SenderLogin {
|
||||
t.Errorf("expected SenderLogin to be %s, got %s", req.SenderLogin, protoReq.SenderLogin)
|
||||
}
|
||||
|
||||
if protoReq.SentTo != req.SentTo {
|
||||
t.Errorf("expected SentTo to be %s, got %s", req.SentTo, protoReq.SentTo)
|
||||
}
|
||||
|
||||
if protoReq.SentFrom != req.SentFrom {
|
||||
t.Errorf("expected SentFrom to be %s, got %s", req.SentFrom, protoReq.SentFrom)
|
||||
}
|
||||
|
||||
if protoReq.MsgType != req.MsgType {
|
||||
t.Errorf("expected MsgType to be %s, got %s", req.MsgType, protoReq.MsgType)
|
||||
}
|
||||
|
||||
if protoReq.DateSent != req.DateSent {
|
||||
t.Errorf("expected DateSent to be %s, got %s", req.DateSent, protoReq.DateSent)
|
||||
}
|
||||
|
||||
if protoReq.DateModified != req.DateModified {
|
||||
t.Errorf("expected DateModified to be %s, got %s", req.DateModified, protoReq.DateModified)
|
||||
}
|
||||
}
|
||||
263
pkg/tmobile/queue.go
Normal file
263
pkg/tmobile/queue.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/grpc/kafka_grpc"
|
||||
|
||||
"fiskerinc.com/modules/kafka"
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/utils/envtool"
|
||||
"github.com/pkg/errors"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var (
|
||||
TMOBILE_MESSAGE_RETRY_MILLISECONDS int = envtool.GetEnvInt("TMOBILE_MESSAGE_RETRY_MILLISECONDS", 300)
|
||||
TMOBILE_MESSAGE_RETRY_ATTEMPTS int = envtool.GetEnvInt("TMOBILE_MESSAGE_RETRY_ATTEMPTS", 3) // Number of times we will try to send a message before giving up
|
||||
TMOBILE_MESSAGE_TIMEOUT_SECONDS int = envtool.GetEnvInt("TMOBILE_MESSAGE_TIMEOUT_SECONDS", 10)
|
||||
TMOBILE_MESSAGE_CHECK_MILLISECONDS int = envtool.GetEnvInt("TMOBILE_MESSAGE_CHECK_MILLISECONDS", 500) // About how often we want to check on a message
|
||||
|
||||
// If we need to send even more messages at one time, I would suggest using an external message queue from either redis or maybe even kafka
|
||||
TMOBILE_MESSAGE_QUEUE_SIZE int = envtool.GetEnvInt("TMOBILE_MESSAGE_QUEUE_SIZE", 5000) // Number of messages we can wait on at one time
|
||||
TMOBILE_READ_THREADS int = envtool.GetEnvInt("TMOBILE_READ_THREADS", 3) // Number of goroutines that will be used to check on tmboile messages
|
||||
)
|
||||
|
||||
// When we send an sms, we want to send it off, and then check on it every second to see if it has been delivered yet.
|
||||
// After it has been delivered, we send a message in kafka to say that that message is done, and continue on
|
||||
// This is for messages that we don't care if we deliver the message right away.
|
||||
// Like if we are waking the car up for an update, we don't we can wait for the car to wake up nicely
|
||||
// for remote commands, we want the car to be awoken and we return right away. Can maybe be its own faster queue
|
||||
|
||||
type RunningQueue struct {
|
||||
client TMobClienter
|
||||
messageQueues []chan messageTimeout // Need to lock and unlock this array
|
||||
toSendThread int // which thread we will try to send to
|
||||
kafka kafka.ProducerInterface
|
||||
}
|
||||
|
||||
type messageTimeout struct {
|
||||
MessageID string
|
||||
Timeout time.Time
|
||||
NextCheck time.Time
|
||||
}
|
||||
|
||||
func NewQueue(client TMobClienter, kafkaProducer kafka.ProducerInterface) (rq *RunningQueue) {
|
||||
rq = &RunningQueue{}
|
||||
rq.client = client
|
||||
rq.kafka = kafkaProducer
|
||||
|
||||
for x := 0; x < TMOBILE_READ_THREADS; x++ {
|
||||
rq.messageQueues = append(rq.messageQueues, make(chan messageTimeout, TMOBILE_MESSAGE_QUEUE_SIZE))
|
||||
go rq.queueChecker(x)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type SendSMS struct {
|
||||
Message string
|
||||
ICCID string // The target of our message
|
||||
CheckDelivery bool // Do we care if this message is delivered or not
|
||||
KafkaServiceTarget string // The kafka service that will get the sms delivery status
|
||||
}
|
||||
|
||||
// try to send message, if we fail to even send the message, that can be notified back right away
|
||||
// then we thread out of here to wait for actual message delivery checks
|
||||
func (rq *RunningQueue) SendSMS(ctx context.Context, req *SendSMSRequest) (messageID string, err error) {
|
||||
msg := SendSMS{
|
||||
Message: req.MessageText,
|
||||
ICCID: req.ICCID,
|
||||
CheckDelivery: req.await,
|
||||
KafkaServiceTarget: req.KafkaServiceTarget,
|
||||
}
|
||||
messageID, err = rq.sendSMS(msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If we don't want to await for the message delivery, we will skip checking on the message
|
||||
if !req.await {
|
||||
return
|
||||
}
|
||||
|
||||
queueMSG := messageTimeout{
|
||||
MessageID: messageID,
|
||||
Timeout: time.Now().Add(time.Second * time.Duration(TMOBILE_MESSAGE_TIMEOUT_SECONDS)),
|
||||
NextCheck: time.Now().Add(time.Millisecond * time.Duration(TMOBILE_MESSAGE_CHECK_MILLISECONDS)),
|
||||
}
|
||||
rq.addToQueue(queueMSG)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (rq *RunningQueue) addToQueue(msg messageTimeout) {
|
||||
rq.messageQueues[rq.toSendThread] <- msg
|
||||
|
||||
// Lightweight round robin, due to the multithreadedness, we might not send one message to each queue, but I am fine with that
|
||||
rq.toSendThread = (rq.toSendThread + 1) % TMOBILE_READ_THREADS
|
||||
}
|
||||
|
||||
// Send down the kafka line if this message has been successfully delivered or not
|
||||
// This should probably be moved outside of tmobile and into the sms service
|
||||
func (rq *RunningQueue) UpdateMessageStatusKafka(messageID string, status MessageStatusEnum) {
|
||||
// We want to use grpc, but the other attendant services would also need a conversion
|
||||
/* msgGRPC := kafka_grpc.SMSStatus{
|
||||
MessageID: messageID,
|
||||
Status: 0,
|
||||
}
|
||||
//grpcCanSignal := cansignal.ToGrpc(batch)
|
||||
grpcData, err := proto.Marshal(&msgGRPC)
|
||||
if err != nil {
|
||||
logger.Err(err).Msgf("failed to protobuff sms message id %s", messageID)
|
||||
return
|
||||
} */
|
||||
|
||||
msg := &kafka_grpc.GRPC_AttendantPayload_MessageStatus{
|
||||
MessageStatus: &kafka_grpc.MessageStatus{
|
||||
MessageId: messageID,
|
||||
Status: kafka_grpc.EmumStatus(kafka_grpc.EmumStatus_value[string(status)]),
|
||||
},
|
||||
}
|
||||
kafkaMSG := kafka_grpc.GRPC_AttendantPayload{
|
||||
Handler: "sms_delivery_status_manifest",
|
||||
Data: msg,
|
||||
}
|
||||
|
||||
binaryPayload, _ := proto.Marshal(&kafkaMSG)
|
||||
err := rq.kafka.ProduceBinary(kafka.AttendantServiceGRPCKafka, "", binaryPayload, nil)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Err(err).Msgf("failed to produce kafka message for sms id %s", messageID)
|
||||
}
|
||||
}
|
||||
|
||||
func (rq *RunningQueue) sendSMS(msg SendSMS) (messageID string, err error) {
|
||||
in := SendSMSRequest{
|
||||
ICCID: msg.ICCID,
|
||||
MessageText: msg.Message,
|
||||
}
|
||||
|
||||
var smr *SendSMSResponse
|
||||
// Giving 3 tries for a gateway failure
|
||||
// Sometimes we fail to deliver a message to tmobile for some reason, so we have to retry sending the message
|
||||
for x := 0; x < 3; x++ {
|
||||
smr, err = rq.client.SendSMS(context.Background(), &in)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrBadGatewayCode) {
|
||||
logger.Warn().Err(err).Msgf("ICCID %s gateway failure sending sms", in.ICCID)
|
||||
time.Sleep(time.Millisecond * time.Duration(TMOBILE_MESSAGE_RETRY_MILLISECONDS)) // 300 milliseconds seems to work best for me
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, ErrAccessTokenExpired) {
|
||||
logger.Err(err).Msgf("Access token for sms expired, should never happen")
|
||||
}
|
||||
return "", errors.WithMessagef(err, "failed to send SMS to ICCID: %s", in.ICCID)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if smr == nil {
|
||||
return "", errors.WithMessagef(err, "failed to send SMS to ICCID: %s", in.ICCID)
|
||||
}
|
||||
|
||||
messageID = smr.SmsMessageID
|
||||
return
|
||||
}
|
||||
|
||||
// Continually read from the queue and check the status of the given message
|
||||
func (rq *RunningQueue) queueChecker(index int) {
|
||||
messageQueue := rq.messageQueues[index]
|
||||
for msg := range messageQueue {
|
||||
// This will only happen when we are not sending messages
|
||||
if !time.Now().After(msg.NextCheck) {
|
||||
time.Sleep(time.Until(msg.NextCheck))
|
||||
}
|
||||
logger.Debug().Msgf("Checking msgID %s for delivery status", msg.MessageID)
|
||||
go func(msg messageTimeout) {
|
||||
delivered, err := rq.checkIfMessageDelivered(msg.MessageID)
|
||||
if err == nil {
|
||||
if delivered {
|
||||
rq.UpdateMessageStatusKafka(msg.MessageID, MESSAGE_STATUS_DELIVERED)
|
||||
return
|
||||
}
|
||||
// Not delivered, but not an error
|
||||
if time.Now().After(msg.Timeout) {
|
||||
rq.UpdateMessageStatusKafka(msg.MessageID, MESSAGE_STATUS_TIMEOUT)
|
||||
return
|
||||
}
|
||||
// If we get here, we need to check on the message again
|
||||
msg.NextCheck = time.Now().Add(time.Millisecond * time.Duration(TMOBILE_MESSAGE_CHECK_MILLISECONDS))
|
||||
rq.addToQueue(msg)
|
||||
} else {
|
||||
rq.UpdateMessageStatusKafka(msg.MessageID, MESSAGE_STATUS_FAILED)
|
||||
}
|
||||
}(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (rq *RunningQueue) checkIfMessageDelivered(msgID string) (delivered bool, err error) {
|
||||
if !IsRealSMSID(msgID) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var out *SMSDetailsResponse
|
||||
for x := 0; x < 3; x++ {
|
||||
out, err = rq.client.Details(context.Background(), msgID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrBadGatewayCode) {
|
||||
logger.Warn().Err(err).Msgf("MSG id %s gateway failure sending sms", msgID)
|
||||
time.Sleep(time.Millisecond * time.Duration(TMOBILE_MESSAGE_RETRY_MILLISECONDS))
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, ErrAccessTokenExpired) {
|
||||
logger.Err(err).Msgf("Access token for sms expired, should never happen")
|
||||
}
|
||||
|
||||
return false, errors.WithMessagef(err, "failed to check status of SMS: %s", msgID)
|
||||
}
|
||||
}
|
||||
|
||||
switch out.Status {
|
||||
case Pending, CancelPending:
|
||||
return false, nil
|
||||
case Delivered:
|
||||
return true, nil
|
||||
default:
|
||||
logger.Warn().Msgf("Fell through to default of sms status id: %s status: %s", msgID, out.Status)
|
||||
return false, errors.WithMessagef(
|
||||
ErrBadMsgStatus,
|
||||
"message with id %s failed with status %s",
|
||||
out.SmsMsgID,
|
||||
out.Status,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type MessageStatus struct {
|
||||
MessageID string `json:"MessageID"`
|
||||
Status MessageStatusEnum `json:"Status"`
|
||||
}
|
||||
|
||||
func GRPCToTMessage(payload *kafka_grpc.GRPC_AttendantPayload) []byte {
|
||||
if payload.Data == nil {
|
||||
return nil
|
||||
}
|
||||
data := payload.Data.(*kafka_grpc.GRPC_AttendantPayload_MessageStatus)
|
||||
msg := MessageStatus{
|
||||
MessageID: data.MessageStatus.GetMessageId(),
|
||||
Status: MessageStatusEnum(data.MessageStatus.GetStatus().String()),
|
||||
}
|
||||
bytes, _ := json.Marshal(msg)
|
||||
return bytes
|
||||
}
|
||||
|
||||
type MessageStatusEnum string
|
||||
|
||||
const (
|
||||
MESSAGE_STATUS_DELIVERED MessageStatusEnum = "DELIVERED"
|
||||
MESSAGE_STATUS_TIMEOUT MessageStatusEnum = "TIMEOUT"
|
||||
MESSAGE_STATUS_FAILED MessageStatusEnum = "FAILED"
|
||||
)
|
||||
97
pkg/tmobile/queue_test.go
Normal file
97
pkg/tmobile/queue_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestQueueIntegration(t *testing.T) {
|
||||
t.Skip()
|
||||
tg, err := InitTokenGen(
|
||||
"PRIVATE KEY",
|
||||
"ID",
|
||||
"PASSWORD",
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tmobc, err := NewTMobileClient(
|
||||
Endpoint,
|
||||
tg, time.Minute*2)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
client := NewQueue(tmobc, nil)
|
||||
|
||||
iccids := []string{"8901882000784161105", "8901882000784163135", "8901882000784161071",
|
||||
"8901882000784164976",
|
||||
"8901882000784163135",
|
||||
"8901882000787584451",
|
||||
"8901882000784166427",
|
||||
"8901882000784163671",
|
||||
"8901882000784163945",
|
||||
"8901882000784167342",
|
||||
"8901882000784166625",
|
||||
}
|
||||
|
||||
sentMessages := make([]string, 0)
|
||||
quickLock := sync.Mutex{}
|
||||
wg := sync.WaitGroup{}
|
||||
// Running test 10 times in a row to see if we get any failures
|
||||
for _, iccid := range iccids {
|
||||
wg.Add(1)
|
||||
go func(iccid string) {
|
||||
msg := SendSMSRequest{
|
||||
ICCID: iccid,
|
||||
MessageText: "newer_test",
|
||||
await: true,
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*45)
|
||||
defer cancel()
|
||||
id, err := client.SendSMS(ctx, &msg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
quickLock.Lock()
|
||||
sentMessages = append(sentMessages, id)
|
||||
quickLock.Unlock()
|
||||
wg.Done()
|
||||
}(iccid)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
time.Sleep(time.Second * 12)
|
||||
|
||||
for _, id := range sentMessages {
|
||||
out, err := tmobc.Details(context.Background(), id)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Log(out.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkQueue(b *testing.B) {
|
||||
sim := newTMboileSimulator()
|
||||
|
||||
wrap := NewQueue(&sim, nil)
|
||||
tt := sync.WaitGroup{}
|
||||
for x := 0; x < 1000; x++ {
|
||||
tt.Add(1)
|
||||
go func() {
|
||||
wrap.SendSMS(context.Background(), &SendSMSRequest{
|
||||
MessageText: ".",
|
||||
await: true,
|
||||
})
|
||||
tt.Done()
|
||||
}()
|
||||
}
|
||||
tt.Wait()
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
59
pkg/tmobile/util.go
Normal file
59
pkg/tmobile/util.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"fiskerinc.com/modules/logger"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const pfx = "-----BEGIN PRIVATE KEY-----"
|
||||
|
||||
func AsFileIfText(envVal string) (*os.File, error) {
|
||||
if strings.HasPrefix(envVal, pfx) {
|
||||
f, err := os.CreateTemp("/tmp", "pk*.pkcs8")
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to create temp file")
|
||||
}
|
||||
|
||||
_, err = f.WriteString(envVal)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to write to temp file")
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
if _, err := os.Stat(envVal); err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to stat file")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func pkPathVal(f *os.File, pkVal string) string {
|
||||
if f != nil {
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
return pkVal
|
||||
}
|
||||
|
||||
func tempFilCloseDelete(f *os.File) {
|
||||
if err := f.Close(); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to close temp file")
|
||||
}
|
||||
|
||||
if err := os.Remove(f.Name()); err != nil {
|
||||
logger.Warn().Err(err).Msg("failed to remove temp file")
|
||||
}
|
||||
}
|
||||
|
||||
func ToXAuthOriginator(accessToken string) string {
|
||||
arr := strings.Split(accessToken, "Bearer ")
|
||||
if len(arr) < 1 {
|
||||
return ""
|
||||
}
|
||||
return arr[1]
|
||||
}
|
||||
126
pkg/tmobile/util_test.go
Normal file
126
pkg/tmobile/util_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const mockPKCS8Fail = `-----BEGIN PRIVATE KEY-----
|
||||
unparseable
|
||||
`
|
||||
|
||||
const mockPKCS8 = `-----BEGIN PRIVATE KEY-----
|
||||
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDBQ0Wrn3iBE+3y
|
||||
sTGr+vxSX+wtq4hz1W9i6LqjV3O4DeA9hZ8Lj3PbFyLuIYXvlFsb6xXydCSHFg2j
|
||||
b8x1g3sUlh7+hDMh2ryVCDFJ05npCyVoxD05Ya9VUsHw1mjKWUt9+x2/sPYjDVzs
|
||||
zhqEwryacrkzmJlpCCpRnfmnfzL9PBPwr1tSkovPvzlzd+MC86Zu/4t7ZNR+UvFT
|
||||
I/S7SLnTIRFaHV0lUf6XYut3HNUotIVX9qFNaF+OenEMk85dgYGam/vReW0xwkx5
|
||||
QDMk/kE38lw9dB4DqhZGWnPw7NciH8+COOJ+JTmDls9WHCXiP9Fh/9ToHBmdD3LX
|
||||
o2KvrMMPAgMBAAECggEBAII9GIVoyWeLC66idMvmLwZAOEQqtaEB87dfCO+sroIZ
|
||||
b8Vl9+FtgfDibZq2orDqdF+jFD44wKj8VqKOY+XJfjdIV4jDhEXLR4zTYYvT+oOP
|
||||
DF3G6U9zIhpI1AO+Kg47EOHMSab11VmX1siKuFpBdaJLr70ymCes5f/siuKymKUI
|
||||
HMgz10exE9ypa0GPUzY1gtoIGRv2xsVoEy7wn29sJkMhhx4MMtfhtnSaLXjoKByW
|
||||
twOew3rNP4BajdmtGIQe2Z3qz/3dG1LG1jLe5AVoCPKZo5JlrzzjaRDbgv1ZZoze
|
||||
Ddi/RVqF0We4pPCNOdHSjhoWs61xDwdpCy24kUEooHECgYEA3002/N2zNQKgerjx
|
||||
3lkX8GbmHx42n1Q/2ihjiygR8RklAIgFCCfJNpAlHqKLGe4ZW7+llIWQOdA7NHOR
|
||||
DWS8StVlog7FwrgBA7dNh2zcmGVmEtP0vg29mhMBMM5IQ66Nsu+vXXhU4qy0CdWY
|
||||
BeadiHTX7YYA/0NsoMfcfIK0iNcCgYEA3ZACBqSiD8dnNnFVj66ut/zZRHX9bSO6
|
||||
dZ07htdOp9pttZlDzlOvA0QhH+qFxe+6h4NJ07Sf+Opu2PeRh90qYvmz0i2DKQHD
|
||||
CmmpaP6iITvvgOa48/sr7XG1k8stlNLa4cBRG6f35/qwr7ZIcU0N7lvnZYTSguXc
|
||||
+oohOatTuIkCgYEAgvYiHcNYauqTe+Yj1CekZpWyuOVbW65plGTDnMVvYFtC3EDp
|
||||
0pKi66E2Y/UoZ5jAvpJzZdu/bmi1kFmG5LgDxk/JP3YyfbS0w50plxc9eRNe/gPZ
|
||||
Me2VGVu0Aw+4ShmBeUQhMUx1XEu1e18Nvcg28+SzDtbcltjQSKtuoId3ohkCgYEA
|
||||
n2CjBHJTHbSb2z7lhGjsx+77v1J8zZCA5XAITPP+YaIvfw1UCEyMPXY5ucKzHfJX
|
||||
pHldlwt8c8r3l91mc2w1vGLQ5qTUj5/z1D6znZJlwDBkFb5iVydbrv831au3CzIu
|
||||
P2xfK9zE6Ludc5hVPiNmnQrBRnaoE38UWakZQ2mp3LkCgYAO9IZijrLWGTX77W3l
|
||||
ruIj2IHKEtb+27aFdZjoMfBpU+HOoWacnmBL3vL0gq8J7KwWuJa18cDLIaiIggHc
|
||||
fFjuF3lTk62dLro94yGr01rIRQnhtoBW5evutX85ukUQQo7E0ieABZP7V1Fqjvg+
|
||||
2oVGWBo1Wzar6CDovkH0yapPTA==
|
||||
-----END PRIVATE KEY-----`
|
||||
|
||||
func TestAsFileIfText(t *testing.T) {
|
||||
f, err := AsFileIfText(mockPKCS8)
|
||||
if err != nil {
|
||||
t.Errorf("failed to save into file: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
t.Errorf("file was nil")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
f2, err := AsFileIfText("/tmp/" + "definetly_not_a_file" + uuid.New().String())
|
||||
if err == nil {
|
||||
t.Errorf("failed to get error: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if f2 != nil {
|
||||
t.Errorf("file was not nil: %v", f2)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p := f.Name()
|
||||
|
||||
fp, err := AsFileIfText(p)
|
||||
if err != nil {
|
||||
t.Errorf("failed to stat file that exists: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if fp != nil {
|
||||
t.Errorf("file was not nil: %v", fp)
|
||||
|
||||
fp.Close()
|
||||
os.Remove(fp.Name())
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func Test_pkPathVal(t *testing.T) {
|
||||
f, err := os.CreateTemp("/tmp", "pk*.pkcs8")
|
||||
if err != nil {
|
||||
t.Errorf("failed to create temp file: %v", err)
|
||||
}
|
||||
|
||||
_, err = f.WriteString(mockPKCS8)
|
||||
if err != nil {
|
||||
t.Errorf("failed to write to temp file: %v", err)
|
||||
}
|
||||
|
||||
p := pkPathVal(f, "/tmp/somePath")
|
||||
if p != f.Name() {
|
||||
t.Errorf("pkPathVal returned wrong value: %v", p)
|
||||
f.Close()
|
||||
os.Remove(f.Name())
|
||||
}
|
||||
|
||||
p = pkPathVal(nil, "/tmp/somePath")
|
||||
if p != "/tmp/somePath" {
|
||||
t.Errorf("pkPathVal returned wrong value: %v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_tempFilCloseDelete(t *testing.T) {
|
||||
f, err := os.CreateTemp("/tmp", "pk*.pkcs8")
|
||||
if err != nil {
|
||||
t.Errorf("failed to create temp file: %v", err)
|
||||
}
|
||||
tempFilCloseDelete(f)
|
||||
_, err = os.Stat(f.Name())
|
||||
if err == nil {
|
||||
t.Errorf("file was not deleted: %v", err)
|
||||
}
|
||||
}
|
||||
150
pkg/tmobile/wrapper.go
Normal file
150
pkg/tmobile/wrapper.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/grpc/sms"
|
||||
"fiskerinc.com/modules/kafka"
|
||||
"fiskerinc.com/modules/logger"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// I want to move the check sms loop into a less thread intensive action
|
||||
// This is done in the following way
|
||||
// Have a single/limit a couple look at all the messages we need to see if they are successfully sent
|
||||
// If they are successfully sent then we notify something else that will properly return the status
|
||||
|
||||
// Currently the return is going to be the hardest part
|
||||
// Wrapper wraps the client with an auto-refresh and auth-token retry.
|
||||
|
||||
type SMSClientWrapper interface {
|
||||
Start(ctx context.Context)
|
||||
SendSMS(ctx context.Context, req *SendSMSRequest) (out *SMSDetailsResponse, err error)
|
||||
SendSMSQueue(ctx context.Context, req *SendSMSRequest) (out *SMSQueueResponse, err error)
|
||||
HandleChangeRatePlan(context.Context, *ChangeRatePlanRequest) (*ChangeRatePlanResponse, error)
|
||||
HandleCustomAttributes(context.Context, *CustomAtributesRequest) (*CustomAtributesResponse, error)
|
||||
HandleGetProducts(context.Context, *sms.GetAvailableProductsRequest) (*sms.GetAvailableProductsResponse, error)
|
||||
HandleDeviceDetails(context.Context, *DeviceDetailsRequest) (*DeviceDetailsResponse, error)
|
||||
HandleChangeDeviceStatus(ctx context.Context, cda ChangeDeviceActivation) (err error)
|
||||
}
|
||||
|
||||
type SMSClient struct {
|
||||
client TMobClienter
|
||||
queue *RunningQueue
|
||||
}
|
||||
|
||||
|
||||
func NewSMSClient(client TMobClienter, kafkaProducer kafka.ProducerInterface) *SMSClient {
|
||||
nc := &SMSClient{
|
||||
client: client,
|
||||
}
|
||||
nc.queue = NewQueue(client, kafkaProducer)
|
||||
return nc
|
||||
}
|
||||
|
||||
func (w *SMSClient) Start(ctx context.Context) {
|
||||
}
|
||||
|
||||
// If the SMS is not delivered, out will be null, and this will need to be checked
|
||||
func (w *SMSClient) SendSMS(ctx context.Context, req *SendSMSRequest) (out *SMSDetailsResponse, err error) {
|
||||
logger.Debug().Msgf("Sending SMS: %+v", req)
|
||||
|
||||
var smr *SendSMSResponse
|
||||
|
||||
// Giving 3 tries for a gateway failure
|
||||
for x := 0; x < 3; x++ {
|
||||
smr, err = w.client.SendSMS(ctx, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrBadGatewayCode) {
|
||||
logger.Warn().Err(err).Msgf("ICCID %s gateway failure sending sms", req.ICCID)
|
||||
time.Sleep(time.Millisecond * 300) // 300 milliseconds seems to work best for me
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, ErrAccessTokenExpired) {
|
||||
logger.Err(err).Msgf("Access token for sms expired, should never happen")
|
||||
}
|
||||
return nil, errors.WithMessagef(err, "failed to send SMS to ICCID: %s", req.ICCID)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if smr == nil {
|
||||
return nil, errors.WithMessagef(err, "failed to send SMS to ICCID: %s", req.ICCID)
|
||||
}
|
||||
|
||||
expDur := time.Second * 5
|
||||
logger.Debug().Msgf("SMS sent, expires in %d ms, id: %s currently %d", expDur.Milliseconds(), smr.SmsMessageID, time.Now().UnixMilli())
|
||||
|
||||
waitTill := time.After(expDur)
|
||||
checkTimer := time.NewTicker(time.Millisecond * 300)
|
||||
for {
|
||||
select {
|
||||
case <-waitTill:
|
||||
logger.Debug().Msgf("SMS with id %s not delivered after %d ms currently %d", smr.SmsMessageID, expDur.Milliseconds(), time.Now().UnixMilli())
|
||||
return nil, ErrTimeoutSendingMessage // Lots of messages seem to hit here
|
||||
case <-checkTimer.C:
|
||||
// Doing the wait before the first check, increases the chance of a first check success
|
||||
out, err = w.client.Details(ctx, smr.SmsMessageID)
|
||||
if err != nil {
|
||||
// Ignoring and retrying a bad gateway request
|
||||
if errors.Is(err, ErrBadGatewayCode) {
|
||||
logger.Warn().Err(err).Msgf("Bad gateway request from t-mobileICCID %s, Message ID: %s", req.ICCID, smr.SmsMessageID)
|
||||
continue
|
||||
}
|
||||
// If we get this error, t-mobile has rejected our details request
|
||||
logger.Err(err).Msgf("failed to get sms details with smsMsgID: %s to ICCID: %s", smr.SmsMessageID, req.ICCID)
|
||||
return nil, errors.WithMessagef(err, "failed to get sms details with smsMsgID: %s to ICCID: %s", smr.SmsMessageID, req.ICCID)
|
||||
}
|
||||
|
||||
if !req.await {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
switch out.Status {
|
||||
case Pending, CancelPending:
|
||||
logger.Debug().Msgf("MSG %s had Pending Status", smr.SmsMessageID)
|
||||
continue
|
||||
case Delivered:
|
||||
return out, nil
|
||||
default:
|
||||
logger.Warn().Msgf("Fell through to default of sms status id: %s status: %s", smr.SmsMessageID, out.Status)
|
||||
return out, errors.WithMessagef(
|
||||
ErrBadMsgStatus,
|
||||
"message with id %s failed with status %s",
|
||||
out.SmsMsgID,
|
||||
out.Status,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *SMSClient) SendSMSQueue(ctx context.Context, req *SendSMSRequest) (out *SMSQueueResponse, err error) {
|
||||
msgID, err := w.queue.SendSMS(ctx, req)
|
||||
out = &SMSQueueResponse{}
|
||||
out.SmsMsgID = msgID
|
||||
out.SentSuccessful = err == nil
|
||||
return
|
||||
}
|
||||
|
||||
func (w *SMSClient) HandleChangeRatePlan(ctx context.Context, req *ChangeRatePlanRequest) (*ChangeRatePlanResponse, error) {
|
||||
return w.client.ChangeRatePlan(ctx, req)
|
||||
}
|
||||
|
||||
func (w *SMSClient) HandleCustomAttributes(ctx context.Context, req *CustomAtributesRequest) (*CustomAtributesResponse, error) {
|
||||
return w.client.CustomAttributes(ctx, req)
|
||||
}
|
||||
|
||||
func (w *SMSClient) HandleGetProducts(ctx context.Context, req *sms.GetAvailableProductsRequest) (*sms.GetAvailableProductsResponse, error) {
|
||||
return w.client.GetProducts(ctx, req)
|
||||
}
|
||||
|
||||
func (w *SMSClient) HandleDeviceDetails(ctx context.Context, req *DeviceDetailsRequest) (*DeviceDetailsResponse, error) {
|
||||
return w.client.DeviceDetails(ctx, req)
|
||||
}
|
||||
|
||||
func (w *SMSClient) HandleChangeDeviceStatus(ctx context.Context, cda ChangeDeviceActivation) (err error) {
|
||||
return w.client.ChangeDeviceStatus(ctx, cda)
|
||||
}
|
||||
|
||||
373
pkg/tmobile/wrapper_test.go
Normal file
373
pkg/tmobile/wrapper_test.go
Normal file
@@ -0,0 +1,373 @@
|
||||
package tmobile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestSMSClient_Start(t *testing.T) {
|
||||
t.Skip()
|
||||
clSucc := MockClientAccTokResSucc{wasSet: make(chan string, 1)}
|
||||
smsc := NewSMSClient(&clSucc, nil)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
go smsc.Start(ctx)
|
||||
|
||||
t.Log("Waiting for FIRST access token to be set")
|
||||
|
||||
select {
|
||||
case gotAt := <-clSucc.wasSet:
|
||||
if gotAt != fmt.Sprintf("%s%d", mockAccessTokenResponse.AccessToken, 0) {
|
||||
t.Errorf("expected access token %s, got %s", mockAccessTokenResponse.AccessToken, gotAt)
|
||||
|
||||
close(clSucc.wasSet)
|
||||
cancel()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
t.Log("First access token set")
|
||||
case <-time.After(time.Second):
|
||||
t.Error("Access token was not set, cancelling test")
|
||||
|
||||
close(clSucc.wasSet)
|
||||
cancel()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
t.Log("Waiting for REFRESHED access token to be set")
|
||||
|
||||
select {
|
||||
case gotAt := <-clSucc.wasSet:
|
||||
if gotAt != fmt.Sprintf("%s%d", mockAccessTokenResponse.AccessToken, 1) {
|
||||
t.Errorf("expected access token %s, got %s", mockAccessTokenResponse.AccessToken, gotAt)
|
||||
|
||||
cancel()
|
||||
close(clSucc.wasSet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
t.Log("Refreshed access token set")
|
||||
case <-time.After(time.Second):
|
||||
t.Error("Access token was not set, cancelling test")
|
||||
|
||||
cancel()
|
||||
close(clSucc.wasSet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
t.Log("Waiting for CANCELLATION")
|
||||
cancel()
|
||||
|
||||
doneChan := ctx.Done()
|
||||
|
||||
if _, ok := <-doneChan; ok {
|
||||
t.Error("Context was not cancelled")
|
||||
|
||||
close(clSucc.wasSet)
|
||||
|
||||
return
|
||||
} else {
|
||||
t.Log("Context was cancelled")
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
if len(clSucc.wasSet) != 0 {
|
||||
t.Error("Access token was set after cancellation")
|
||||
|
||||
close(clSucc.wasSet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
t.Log("Success test finished")
|
||||
|
||||
clFail := MockClientAccTokResFail{wasSet: make(chan struct{}, 3)}
|
||||
smsc = NewSMSClient(&clFail, nil)
|
||||
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
go smsc.Start(ctx)
|
||||
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
if len(clFail.wasSet) <= 2 {
|
||||
t.Errorf("Access token was not set after failure: len(chan) = %d", len(clFail.wasSet))
|
||||
cancel()
|
||||
close(clFail.wasSet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
cancel()
|
||||
|
||||
t.Log("Test finished")
|
||||
}
|
||||
|
||||
var mockSmsRequest = &SendSMSRequest{
|
||||
ICCID: "12345678901234567890",
|
||||
MessageText: "Hello world",
|
||||
await: true,
|
||||
}
|
||||
|
||||
var mockCtx = context.Background()
|
||||
|
||||
func TestSMSClient_SendSMS(t *testing.T) {
|
||||
t.Skip()
|
||||
cl := MockClientSendSMSResSucc{wasSet: make(chan SmsDetailsStatus, 3)}
|
||||
smsc := NewSMSClient(&cl, nil)
|
||||
|
||||
t.Log("sending succ SMS")
|
||||
r, err := smsc.SendSMS(mockCtx, mockSmsRequest)
|
||||
if err != nil {
|
||||
t.Errorf("SendSMS failed: %v", err)
|
||||
|
||||
close(cl.wasSet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if r.Status != Delivered {
|
||||
t.Errorf("SendSMS returned status %s, expected %s", r.Status, Delivered)
|
||||
|
||||
close(cl.wasSet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
t.Log("getting succ SMS statuses")
|
||||
for i := 0; i < 3; i++ {
|
||||
select {
|
||||
case status := <-cl.wasSet:
|
||||
t.Logf("sms status was set: %s", status)
|
||||
case <-time.After(time.Second):
|
||||
t.Error("sms was not set")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSMSClient_SendSMS_FailOnSend(t *testing.T) {
|
||||
t.Skip()
|
||||
cl := MockClientSendSMSResFail{wasSet: make(chan struct{}, 1)}
|
||||
smsc := NewSMSClient(&cl, nil)
|
||||
|
||||
t.Log("sending fail SMS")
|
||||
out, err := smsc.SendSMS(mockCtx, mockSmsRequest)
|
||||
if !strings.HasPrefix(err.Error(), "failed to send SMS") {
|
||||
t.Error("SendSMS should have failed")
|
||||
|
||||
close(cl.wasSet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if out != nil {
|
||||
t.Error("SendSMS should have returned nil")
|
||||
|
||||
close(cl.wasSet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
t.Log("getting SendSMS failure")
|
||||
select {
|
||||
case <-cl.wasSet:
|
||||
t.Log("SendSMS was called")
|
||||
case <-time.After(time.Second):
|
||||
t.Error("SendSMS was not called")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestSMSClient_SendSMSFailOnDetailsStatus(t *testing.T) {
|
||||
t.Skip()
|
||||
for _, curType := range smsStatusFailTypes {
|
||||
cl := MockClientSendSMSDetailsFailWithBadStatus{wasSet: make(chan SmsDetailsStatus, len(curType)), toFail: curType}
|
||||
smsc := NewSMSClient(&cl, nil)
|
||||
|
||||
t.Logf("sending fail SMS with type %s", curType[len(curType)-1])
|
||||
|
||||
r, err := smsc.SendSMS(mockCtx, mockSmsRequest)
|
||||
if err == nil {
|
||||
t.Errorf("SendDetails should have failed: %v", err)
|
||||
|
||||
close(cl.wasSet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if r == nil {
|
||||
t.Error("SendDetails should have returned non-nil")
|
||||
|
||||
close(cl.wasSet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
t.Log("getting succ SMS statuses")
|
||||
for i := 0; i < len(curType); i++ {
|
||||
select {
|
||||
case status := <-cl.wasSet:
|
||||
t.Logf("sms status was set: %s", status)
|
||||
case <-time.After(time.Second):
|
||||
t.Error("sms was not set")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSMSClient_SendSMSFailOnDetails(t *testing.T) {
|
||||
// TODO fix the timing of this test, skipping for now
|
||||
t.Skip()
|
||||
|
||||
cl := &MockClientSendSMSDetailsFailRes{wasSet: make(chan struct{}, 1)}
|
||||
smsc := NewSMSClient(cl, nil)
|
||||
|
||||
r, err := smsc.SendSMS(mockCtx, mockSmsRequest)
|
||||
if !strings.HasPrefix(err.Error(), "failed to get sms details") {
|
||||
t.Errorf("SendDetails should have failed: %v", err)
|
||||
|
||||
close(cl.wasSet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if r != nil {
|
||||
t.Errorf("SendDetails should have returned nil: %v", r)
|
||||
|
||||
close(cl.wasSet)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-cl.wasSet:
|
||||
t.Log("SendDetails was called")
|
||||
case <-time.After(time.Second):
|
||||
t.Error("SendDetails was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSMSClient_SendSMSAwaitExpire(t *testing.T) {
|
||||
t.Skip()
|
||||
cl := &MockClientSendSMSExpire{}
|
||||
smsc := NewSMSClient(cl, nil)
|
||||
|
||||
r, err := smsc.SendSMS(mockCtx, mockSmsRequest)
|
||||
if !errors.Is(err, ErrTimeoutSendingMessage) {
|
||||
t.Errorf("SendSMS should have failed: %v", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if r != nil {
|
||||
t.Errorf("SendSMS should have returned nil: %v", r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type MockWrapperFail struct {
|
||||
SMSClientWrapper
|
||||
wasSet chan struct{}
|
||||
}
|
||||
|
||||
func (mw *MockWrapperFail) SendSMS(ctx context.Context, req *SendSMSRequest) (*SMSDetailsResponse, error) {
|
||||
mw.wasSet <- struct{}{}
|
||||
|
||||
return nil, ErrTimeoutSendingMessage
|
||||
}
|
||||
|
||||
type MockWrapper struct {
|
||||
SMSClientWrapper
|
||||
wasSet chan struct{}
|
||||
}
|
||||
|
||||
func (mw *MockWrapper) SendSMS(ctx context.Context, req *SendSMSRequest) (*SMSDetailsResponse, error) {
|
||||
mw.wasSet <- struct{}{}
|
||||
|
||||
return mockSmsDetailsResponse, nil
|
||||
}
|
||||
|
||||
func TestWrapperIntegrationTest(t *testing.T) {
|
||||
t.Skip()
|
||||
tg, err := InitTokenGen(
|
||||
"PRIVATE KEY",
|
||||
"ID",
|
||||
"PASSWORD",
|
||||
)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tmobc, err := NewTMobileClient(
|
||||
Endpoint,
|
||||
tg, time.Minute*2)
|
||||
|
||||
client := NewSMSClient(tmobc, nil)
|
||||
|
||||
iccids := []string{"8901882000784161105", "8901882000784163135", "8901882000784161071",
|
||||
"8901882000784164976",
|
||||
"8901882000784163135",
|
||||
"8901882000787584451",
|
||||
"8901882000784166427",
|
||||
"8901882000784163671",
|
||||
"8901882000784163945",
|
||||
"8901882000784167342",
|
||||
"8901882000784166625"}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
// Running test 10 times in a row to see if we get any failures
|
||||
for _, iccid := range iccids {
|
||||
wg.Add(1)
|
||||
go func(iccid string) {
|
||||
msg := SendSMSRequest{
|
||||
ICCID: iccid,
|
||||
MessageText: "newer_test",
|
||||
await: true,
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*45)
|
||||
defer cancel()
|
||||
_, err := client.SendSMS(ctx, &msg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}(iccid)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Want to benchmark and record the memory and cpu usage of having different ways to check if the sms was delivered
|
||||
func BenchmarkSMSWrapper(b *testing.B) {
|
||||
sim := newTMboileSimulator()
|
||||
|
||||
wrap := NewSMSClient(&sim, nil)
|
||||
tt := sync.WaitGroup{}
|
||||
for x := 0; x < 1000; x++ {
|
||||
tt.Add(1)
|
||||
go func() {
|
||||
wrap.SendSMS(context.Background(), &SendSMSRequest{
|
||||
MessageText: ".",
|
||||
await: true,
|
||||
})
|
||||
tt.Done()
|
||||
}()
|
||||
}
|
||||
tt.Wait()
|
||||
}
|
||||
Reference in New Issue
Block a user