Initial cloud-services repo - gateway service + pkg modules

This commit is contained in:
Chris Rai
2026-01-30 23:14:52 -05:00
commit fbb820d7b3
1037 changed files with 171318 additions and 0 deletions

593
pkg/tmobile/client.go Normal file
View 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
}

View 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
View 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
}

View 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
View 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
View 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
View 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)
}
})
}
}

View 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
}

View 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
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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()
}