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

View File

@@ -0,0 +1,50 @@
package redisv2
import (
"encoding/json"
"github.com/pkg/errors"
)
type RedisBatchCommands struct {
Commands []Command
}
type Command struct {
Command string
Arguments []interface{}
}
func NewRedisBatchCommands() *RedisBatchCommands {
result := RedisBatchCommands{}
result.Commands = make([]Command, 0)
return &result
}
func (rbc *RedisBatchCommands) Add(command ...Command) {
rbc.Commands = append(rbc.Commands, command...)
}
func (rbc *RedisBatchCommands) AddPublish(key string, message interface{}) error {
data, err := json.Marshal(message)
if err != nil {
return errors.WithStack(err)
}
rbc.Add(Command{
Command: "PUBLISH",
Arguments: []interface{}{ChannelKey(key), string(data)},
})
return nil
}
func (rbc *RedisBatchCommands) IsEmpty() bool {
return len(rbc.Commands) == 0
}
func (rbc *RedisBatchCommands) Clear() {
rbc.Commands = make([]Command, 0)
}

790
pkg/redisv2/conn.go Normal file
View File

@@ -0,0 +1,790 @@
package redisv2
import (
"context"
"encoding/json"
"sync"
"time"
"fiskerinc.com/modules/logger"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
)
// This is a wrapper around a redis connection that does a bunch of
// different stuff
func NewClient(redisClient *redis.Client) (client *Connection) {
if redisClient == nil {
redisClient = NewConnection()
}
client = &Connection{
Client: redisClient,
}
return
}
// Client defines the function signatures associated with sending messages
//
// and setting/getting objects
type ClientInterface interface {
Close() error
Ping() error
GetClient() *redis.Client
SetClient(*redis.Client)
queueMessage(string, interface{}) error
publishMessage(string, interface{}) error
BatchQueueMessages(ids []string, messages []interface{}) error
BatchPublishMessages(ids []string, messages []interface{}) error
SafeQueueMessage(string, interface{}) error
SafePublishMessage(string, interface{}) error
// Simple redis operations
Set(string, interface{}) error
Get(string) (interface{}, error)
Delete([]string) error
GetMulti(ids []string) ([]interface{}, error)
// Sets
NewSet(string, interface{}, time.Duration) error
GetSet(string, interface{}) error
AddToSet(id string, data interface{}, expire time.Duration) error
// Use objects when you wish to access individual fields in future
SetObject(string, interface{}, time.Duration) error
SetObjectField(string, string, interface{}) error
SetObjects([]string, []interface{}, time.Duration) error
GetObject(string, interface{}) error
GetObjectField(string, string) (string, error)
GetObjectMap(string) (map[string]string, error)
GetObjectRaw(string) (map[string][]byte, error)
GetObjectsMulti([]string, []interface{}) error
GetObjectsMultiMap([]string) (map[string]map[string]string, error)
// General execution
Retrieve(command string, data interface{}) error
// Cache functions marshal/unmarshal any data type to redis
SetCache(string, interface{}, time.Duration) error
// Thread-safe variations
SafeSet(string, interface{}) error
SafeGet(string) (interface{}, error)
SafeDelete([]string) error
SafeNewSet(string, interface{}, time.Duration) error
SafeGetSet(string, interface{}) error
SafeSetObject(string, interface{}, time.Duration) error
SafeGetObject(string, interface{}) error
Execute(command ...interface{}) (interface{}, error)
SafeExecute(command ...interface{}) (interface{}, error)
ExecuteBatch(batch *RedisBatchCommands) ([]interface{}, error)
SafeExecuteBatch(batch *RedisBatchCommands) ([]interface{}, error)
Keys(pattern string) (keys []string, err error)
}
// Connection holds a client to redis
//
// The methods for connection are NOT thread safe.
type Connection struct {
*redis.Client
once sync.Once
mu sync.Mutex
}
// Retrieve implements ClientInterface.
func (*Connection) Retrieve(command string, data interface{}) error {
panic("unimplemented")
}
var _ ClientInterface = &Connection{}
func (c *Connection) GetClient() (client *redis.Client) {
return c.Client
}
func (c *Connection) SetClient(newClient *redis.Client) {
c.Client = newClient
}
// Close the client if it exists
func (c *Connection) Close() error {
if c.Client == nil {
return nil
}
err := c.Client.Close()
if err != nil {
return errors.WithStack(err)
}
c.Client = nil
return nil
}
func (c *Connection) do(commandName string, args ...interface{}) (reply interface{}, err error) {
p := append([]interface{}{commandName}, args...)
reply, err = c.Do(context.Background(), p...).Result()
if err != nil {
return reply, errors.WithStack(err)
}
return reply, nil
}
// func (c *Connection) send(commandName string, args ...interface{}) error {
// err := c.Client.Send(commandName, args...)
// if err != nil {
// return errors.WithStack(err)
// }
// return nil
// }
func (c *Connection) Ping() error {
return c.Client.Ping(context.Background()).Err()
}
// QueueMessage writes a message to the corresponding list
// follows the format "queue:<id>"
// Messages are guaranteed to be delivered upon websocket connection
func (c *Connection) queueMessage(id string, message interface{}) error {
data, err := json.Marshal(message)
if err != nil {
return errors.WithStack(err)
}
_, err = c.do("RPUSH", QueueKey(id), data)
if err != nil {
logger.At(logger.Error(), ChannelKey(id), "redis").
Str("msg", string(data)).Err(err).Send()
}
c.do("EXPIRE", QueueKey(id), 3600) // 3600 = 1hr
return err
}
// PublishMessage writes a message to the corresponding channel
// follows the format "channel:<id>"
// This is a fire and forget mechanism
func (c *Connection) publishMessage(id string, message interface{}) error {
data, err := json.Marshal(message)
if err != nil {
return errors.WithStack(err)
}
_, err = c.do("PUBLISH", ChannelKey(id), data)
if err != nil {
return err
}
logger.At(logger.Debug(), ChannelKey(id), "redis").
Str("msg", string(data)).
Msgf("sent redis msg to %s", id)
return nil
}
// BatchQueueMessages is the same as QueueMessage except performs a batch call
func (c *Connection) BatchQueueMessages(ids []string, messages []interface{}) error {
if len(ids) != len(messages) {
return errors.Errorf(
"mismatch number of ids and messages. have %d ids and %d messages",
len(ids),
len(messages),
)
}
batch := NewRedisBatchCommands()
for i := 0; i < len(ids); i++ {
data, err := json.Marshal(messages[i])
if err != nil {
return errors.WithStack(err)
}
batch.Add(Command{
Command: "RPUSH",
Arguments: []interface{}{QueueKey(ids[i]), data},
})
}
_, err := c.ExecuteBatch(batch)
if err != nil {
return err
}
return nil
}
// BatchPublishMessages is the same as PublishMessage except performs a batch call
func (c *Connection) BatchPublishMessages(ids []string, messages []interface{}) error {
if len(ids) != len(messages) {
return errors.Errorf(
"mismatch number of ids and messages. have %d ids and %d messages",
len(ids),
len(messages),
)
}
batch := NewRedisBatchCommands()
for i := 0; i < len(ids); i++ {
data, err := json.Marshal(messages[i])
if err != nil {
return errors.WithStack(err)
}
batch.Add(Command{
Command: "PUBLISH",
Arguments: []interface{}{ChannelKey(ids[i]), data},
})
}
_, err := c.ExecuteBatch(batch)
if err != nil {
return err
}
return nil
}
// SafeQueueMessage is the thread-safe implementation of QueueMessage
func (c *Connection) SafeQueueMessage(id string, message interface{}) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.queueMessage(id, message)
}
// SafePublishMessage is the thread-safe implementation of PublishMessage
func (c *Connection) SafePublishMessage(id string, message interface{}) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.publishMessage(id, message)
}
// Set replicates redis "SET"
func (c *Connection) Set(id string, data interface{}) error {
if _, err := c.do("SET", id, data); err != nil {
return err
}
return nil
}
// Get replicates redis "GET", must properly unpack interface returned
func (c *Connection) Get(id string) (interface{}, error) {
data, err := c.do("GET", id)
return data, err
}
// Delete removes all ids inputted
func (c *Connection) Delete(id []string) error {
numDeleted64, err := c.Del(context.Background(), id...).Result()
if err != nil {
return err
}
numDeleted := int(numDeleted64)
if numDeleted != len(id) {
return errors.Errorf(
"tried to delete %v (total: %v), however only %v were deleted",
id, len(id), numDeleted,
)
}
return nil
}
// Deprecated: NewSet adds items to a set in redis
func (c *Connection) NewSet(id string, data interface{}, expire time.Duration) error {
var err error
pipe := c.TxPipeline()
err = pipe.Del(context.Background(), id).Err()
if err != nil {
pipe.Discard()
return err
}
err = pipe.SAdd(context.Background(), id, data).Err()
if err != nil {
pipe.Discard()
return err
}
if expire > 0 {
err = pipe.Expire(context.Background(), id, expire).Err()
if err != nil {
pipe.Discard()
return err
}
}
_, err = pipe.Exec(context.Background())
err = errors.WithStack(err)
return err
}
// AddToSet adds item to a set in redis
func (c *Connection) AddToSet(id string, data interface{}, expire time.Duration) error {
var err error
pipe := c.TxPipeline()
err = pipe.SAdd(context.Background(), id, data).Err()
if err != nil {
pipe.Discard()
return err
}
if expire > 0 {
err = pipe.Expire(context.Background(), id, expire).Err()
if err != nil {
pipe.Discard()
return err
}
}
_, err = pipe.Exec(context.Background())
err = errors.WithStack(err)
return err
}
// Deprecated: GetSet retrieves items from a set in redis
func (c *Connection) GetSet(id string, data interface{}) (err error) {
res := c.SMembers(context.Background(), id)
if res.Err() != nil {
return err
}
err = res.ScanSlice(data)
return err
}
// Deprecated: SetObject assigns the hash key id to the data object
// data can be of any type. Expire is in seconds, use -1 for no expire
func (c *Connection) SetObject(id string, data interface{}, expire time.Duration) error {
var err error
if expire > 0 {
err = c.setExpiringObject(id, data, expire)
} else {
err = c.setPersistentObject(id, data)
}
return err
}
func (c *Connection) setPersistentObject(id string, data interface{}) error {
err := c.HSet(context.Background(), id, data).Err()
return err
}
func (c *Connection) setExpiringObject(id string, data interface{}, expire time.Duration) error {
var err error
pipe := c.TxPipeline()
err = pipe.HSet(context.Background(), id, data).Err()
if err != nil {
pipe.Discard()
return err
}
err = pipe.Expire(context.Background(), id, time.Duration(expire)).Err()
if err != nil {
pipe.Discard()
return err
}
_, err = pipe.Exec(context.Background())
return err
}
// Deprecated: SetObjectField sets a specific key of an object
func (c *Connection) SetObjectField(id string, key string, data interface{}) error {
err := c.Do(context.Background(), "HSET", id, key, data).Err()
return err
}
// Deprecated: SetObjects provides the same functionality as SetObject for multiple objects in one call to redis
func (c *Connection) SetObjects(ids []string, data []interface{}, expire time.Duration) error {
var err error
if len(ids) <= 0 || len(data) <= 0 || len(ids) != len(data) {
return errors.Errorf("invalid lengths entered, lengths must match and be > 0. ids length: %v data length: %v",
len(ids),
len(data),
)
}
pipe := c.TxPipeline()
for i := range ids {
err = pipe.Do(context.Background(), "HSET", ids[i], data[i]).Err()
if err != nil {
pipe.Discard()
return err
}
}
if expire > 0 {
for _, id := range ids {
err = pipe.Expire(context.Background(), id, time.Duration(expire)).Err()
if err != nil {
pipe.Discard()
return err
}
}
}
_, err = pipe.Exec(context.Background())
return err
}
// Deprecated: GetObject retrieves an object based off the hash key id
// and "unmarshals" it to struct pointer given
func (c *Connection) GetObject(id string, dest interface{}) error {
result := c.HGetAll(context.Background(), id)
err := result.Err()
if err != nil {
return err
}
err = result.Scan(&dest)
if err != nil {
return errors.WithStack(err)
}
return nil
}
// Deprecated: GetObjectMap retrieves an object based off the hash key id
// and returns it as a map[string]interface{}. GetObject()
// is preferred if you know the object being retrieved
func (c *Connection) GetObjectMap(id string) (map[string]string, error) {
result := c.HGetAll(context.Background(), id)
err := result.Err()
if result.Err() != nil {
return nil, err
}
object := result.Val()
return object, err
}
// Deprecated: GetObjectRaw retrieves an object based off the hash key id
// and returns it as a map[string][]byte. Use this method when you have
// a hash of objects that you want to unmarshal.
func (c *Connection) GetObjectRaw(id string) (map[string][]byte, error) {
var m map[string][]byte
result := c.HGetAll(context.Background(), id)
values, err := result.Result()
if err != nil {
return m, err
}
m = make(map[string][]byte, len(values))
for keyS, valueS := range values {
value := []byte(valueS)
m[keyS] = value
}
return m, nil
}
// Deprecated: GetObjectField retrieves the value associated with the id and key of a hash map
func (c *Connection) GetObjectField(id string, key string) (string, error) {
res := c.HGet(context.Background(), id, key)
value, err := res.Result()
return value, err
}
// Deprecated: GetObjectsMulti retrieves an array of objects based off
// the hash key ids given and returns the data as a
// []interface{}. Use this function if you know
// you need to get multiple objects from redis (one call to server).
func (c *Connection) GetObjectsMulti(ids []string, data []interface{}) error {
var err error
if len(ids) != len(data) {
return errors.Errorf(
"number of ids given %v does not match size of data slice %v",
len(ids),
len(data),
)
}
pipe := c.TxPipeline()
results := make([]*redis.MapStringStringCmd, 0, len(ids))
for _, id := range ids {
res := pipe.HGetAll(context.Background(), id)
err := res.Err()
if err != nil {
pipe.Discard()
return err
}
results = append(results, res)
}
_, err = pipe.Exec(context.Background())
if err != nil {
return err
}
for i, value := range results {
err = value.Scan(&data[i])
if err != nil {
err = errors.WithStack(err)
}
}
return nil
}
// Deprecated: GetObjectsMultiMap retrieves an array of objects based off
// the hash key ids given and returns the data as a
// map[string]interface{}. Use this function if you know
// you need to get multiple objects from redis (one call to server).
func (c *Connection) GetObjectsMultiMap(ids []string) (map[string]map[string]string, error) {
var err error
objects := make(map[string]map[string]string)
pipe := c.TxPipeline()
results := make([]*redis.MapStringStringCmd, 0, len(ids))
for _, id := range ids {
res := pipe.HGetAll(context.Background(), id)
err := res.Err()
if err != nil {
pipe.Discard()
return nil, err
}
results = append(results, res)
}
_, err = pipe.Exec(context.Background())
if err != nil {
return nil, err
}
for i, value := range results {
err = value.Err()
if err != nil {
return objects, errors.WithStack(err)
}
o := value.Val()
objects[ids[i]] = o
}
return objects, nil
}
func (c *Connection) makeKeys(ids []string) []interface{} {
keys := make([]interface{}, len(ids))
for i, id := range ids {
keys[i] = id
}
return keys
}
// Deprecated: GetMulti
func (c *Connection) GetMulti(ids []string) ([]interface{}, error) {
if len(ids) == 0 {
return nil, errors.WithStack(errors.New("cannot call redis MGET with no keys"))
}
res := c.MGet(context.Background(), ids...)
result, err := res.Result()
return result, err
}
// Deprecated: SetCache marshals object and inserts into redis based on key id
// sets expiration to expire time.Duration
func (c *Connection) SetCache(id string, data interface{}, expire time.Duration) error {
var err error
pipe := c.TxPipeline()
serialized, err := json.Marshal(data)
if err != nil {
return errors.WithStack(err)
}
err = pipe.Set(context.Background(), id, serialized, time.Duration(0)).Err()
if err != nil {
pipe.Discard()
return err
}
if expire > 0 {
err = pipe.Expire(context.Background(), id, time.Duration(expire)).Err()
if err != nil {
pipe.Discard()
return err
}
}
_, err = pipe.Exec(context.Background())
err = errors.WithStack(err)
return err
}
// Deprecated: SafeSet is the thread-safe version of Set()
func (c *Connection) SafeSet(id string, data interface{}) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.Set(id, data)
}
// Deprecated: SafeGet is the thread-safe version of Get()
func (c *Connection) SafeGet(id string) (interface{}, error) {
c.mu.Lock()
defer c.mu.Unlock()
return c.Get(id)
}
// Deprecated: SafeDelete is the thread-safe version of Delete()
func (c *Connection) SafeDelete(ids []string) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.Delete(ids)
}
// Deprecated: SafeNewSet is the thread-safe version of NewSet()
func (c *Connection) SafeNewSet(id string, data interface{}, expire time.Duration) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.NewSet(id, data, expire)
}
// Deprecated: SafeGetSet is the thread-safe version of GetSet()
func (c *Connection) SafeGetSet(id string, data interface{}) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.GetSet(id, data)
}
// Deprecated: SafeSetObject provides a thread-safe SetObject()
func (c *Connection) SafeSetObject(id string, data interface{}, expire time.Duration) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.SetObject(id, data, expire)
}
// Deprecated: SafeGetObject provides a thread-safe GetObject()
func (c *Connection) SafeGetObject(id string, data interface{}) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.GetObject(id, data)
}
// Execute provides an abstraction over redigo's Do method
// use this method over specialized methods within this library
func (c *Connection) Execute(command ...interface{}) (interface{}, error) {
var reply interface{}
if len(command) < 2 {
return reply, ErrInvalidCommand
}
reply, err := c.do(command[0].(string), command[1:]...)
if err != nil {
return reply, err
}
return reply, nil
}
// SafeExecute provides a thread-safe Execute method
func (c *Connection) SafeExecute(command ...interface{}) (interface{}, error) {
c.mu.Lock()
defer c.mu.Unlock()
return c.Execute(command)
}
// ExecuteBatch sends all commands stored to Redis
// removes all commands regardless of success or failure
func (c *Connection) ExecuteBatch(batch *RedisBatchCommands) ([]interface{}, error) {
if batch.IsEmpty() {
return nil, nil
}
return c.executeBatch(batch)
}
func (c *Connection) SafeExecuteBatch(batch *RedisBatchCommands) ([]interface{}, error) {
if batch.IsEmpty() {
return nil, nil
}
c.mu.Lock()
defer c.mu.Unlock()
return c.executeBatch(batch)
}
func (c *Connection) executeBatch(batch *RedisBatchCommands) ([]interface{}, error) {
var err error
defer batch.Clear()
pipe := c.TxPipeline()
responses := make([]interface{}, 0, len(batch.Commands))
for _, command := range batch.Commands {
keyword := command.Command
// There has to be a slightly nicer way to do this
commandList := []interface{}{keyword}
commandList = append(commandList, command.Arguments...)
res := pipe.Do(context.Background(), commandList...)
if res.Err() != nil {
pipe.Discard()
return responses, err
}
// This will yield no responses I think
responses = append(responses, res.Val())
}
_, err = pipe.Exec(context.Background())
return responses, err
}
func (c *Connection) Keys(pattern string) (keys []string, err error) {
keys, err = c.Keys(pattern)
return
}
// // redisArgs is a helper function to generate redis args
// func redisArgs(id string, data interface{}) redis.Args {
// return redis.Args{}.Add(id).AddFlat(data)
// }
// // redisArgsMulti is a helper function to generate redis args for hash map fields and arrays
// func redisArgsMulti(data ...interface{}) redis.Args {
// return redis.Args{}.Add(data...)
// }

29
pkg/redisv2/connection.go Normal file
View File

@@ -0,0 +1,29 @@
package redisv2
import (
"fmt"
"fiskerinc.com/modules/utils/envtool"
"github.com/redis/go-redis/v9"
)
// connection vars
var ()
// Actual connection to the redis
func NewConnection() (redisClient *redis.Client) {
host := envtool.GetEnv("REDIS_HOST", "localhost")
port := envtool.GetEnv("REDIS_PORT", "6379")
username := envtool.GetEnv("REDIS_USERNAME", "default")
password := envtool.GetEnv("REDIS_PASSWORD", "REPLACE_ME")
addr := fmt.Sprintf("%v:%v", host, port)
rdb := redis.NewClient(&redis.Options{
Addr: addr,
Username: username,
Password: password,
MaxActiveConns: envtool.GetEnvInt("REDIS_MAXACTIVECONN", 10),
PoolSize: envtool.GetEnvInt("REDIS_POOLSIZE", 10),
})
return rdb
}

14
pkg/redisv2/errors.go Normal file
View File

@@ -0,0 +1,14 @@
package redisv2
import (
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
)
var ErrHangingCommands = errors.New("commands left over in redis client")
var ErrInvalidCommand = errors.New("invalid command entered")
var ErrInvalidResults = errors.New("invalid results returned in redis")
var ErrNilObject = errors.New("not found in redis")
var ErrNil = redis.Nil

163
pkg/redisv2/keys.go Normal file
View File

@@ -0,0 +1,163 @@
package redisv2
import (
"fmt"
"runtime/debug"
"fiskerinc.com/modules/logger"
"github.com/google/uuid"
)
const (
channelPrefix = "channel"
SupersetAccTokenKey = "superset:access_token"
)
// ChannelKey provides hash value for redis channel
func ChannelKey(id string) string {
return fmt.Sprintf("%s:%s", channelPrefix, id)
}
// ParseChannelKey returns id from hash value
func ParseChannelKey(key string) string {
return key[len(channelPrefix)+1:]
}
const queuePrefix = "queue"
// QueueKey provides hash value for redis queue
func QueueKey(id string) string {
if id == "" {
stack := debug.Stack()
logger.Warn().Str("Queue", queuePrefix).Str("ID", id).Str("Stack", string(stack)).Msg("Creating Redis Queue Key Empty")
}
return fmt.Sprintf("%s:%s", queuePrefix, id)
}
// ParseQueueKey returns id from hash value
func ParseQueueKey(key string) string {
return key[len(queuePrefix)+1:]
}
func CarConfigKey(vin string) string {
return fmt.Sprintf("car:%s:config", vin)
}
func CarLogFilter(vin string) string {
return fmt.Sprintf("car:%s:state:log", vin)
}
// WindowsHashKey provides hash key lookup string for windows state
func WindowsHashKey(vin string) string {
return fmt.Sprintf("car:%s:state:windows", vin)
}
// CarStateHashKey provides hash key structure for car state
//
// car state values such as windows, locks, etc. are stored as
// keys in the redis hash map
func CarStateHashKey(vin string) string {
return fmt.Sprintf("car:%s:state", vin)
}
// CarAlerts returns key of expiring cache of sent car alerts.
func CarAlerts(vin string) string {
return fmt.Sprintf("car:%s:alerts", vin)
}
// CANParseSignalWarnings returns key of expiring cache of unknown signals.
func CANParseSignalWarnings(version string) string {
return fmt.Sprintf("can:version:%s:signals:warn", version)
}
// CarToDriverKey provides hash key lookup for driver associated with car
func CarToDriverKey(vin string, id string) string {
return fmt.Sprintf("car:%s:driver:%s", vin, id)
}
// CarToAllDriversKey provides hash key lookup for drivers associated with car
func CarToAllDriversKey(vin string) string {
return fmt.Sprintf("car:%s:drivers", vin)
}
// CarSessionsKey provides a set of all cars in session
func CarSessionsKey() string {
return "cars:sessions"
}
func CarLocationsKey() string {
return "cars:locations"
}
// CarUpdateStatusHashKey provides hash key lookup string for UpdateStatus
func CarUpdateStatusHashKey(carupdateid int64) string {
return fmt.Sprintf("carupdate:%v", carupdateid)
}
// CarUpdateStatusHMIHashKey provides hash key lookup string for UpdateStatus
func CarUpdateStatusTBOXHashKey(carupdateid int64) string {
return fmt.Sprintf("carupdatetbox:%v", carupdateid)
}
// CarUpdateStatusHMIHashKey provides hash key lookup string for UpdateStatus
func CarUpdateStatusHMIHashKey(carupdateid int64) string {
return fmt.Sprintf("carupdatehmi:%v", carupdateid)
}
func DriverToVINsKey(id string) string {
return fmt.Sprintf("driver:%s:cars", id)
}
// HMISessionsKey provides a set of all HMIs in session
func HMISessionsKey() string {
return "hmi:sessions"
}
// HMIManySessionsKey provides a set of all sessions cloud believes are open
func HMIManySessionsKey(vin string) string {
return fmt.Sprintf("hmi:%s:many-sessions", vin)
}
// HMISessionKey provides hash key lookup for HMI session key
func HMISessionKey(vin string) string {
return fmt.Sprintf("hmi:%s:session", vin)
}
func HMISaltKey(vin string) string {
return fmt.Sprintf("hmi:%s:salt", vin)
}
// FileIDEncryptionParamsKey provides hash key lookup string for file encryption parameters
func FileIDEncryptionParamsKey(fileid string) string {
return fmt.Sprintf("fileid:%s", fileid)
}
// MobileSessionsKey provides a set of all mobiles in session
func MobileSessionsKey() string {
return "mobile:sessions"
}
// APITokenKey provides hash key lookup string
func APITokenKey(key string) string {
return fmt.Sprintf("apikey:%s", key)
}
func SubscriptionTypeListKey(subtypeID uuid.UUID) string {
return fmt.Sprintf("subscriptiontypes:%s", subtypeID.String())
}
// TimezoneQuadKey provides hash key lookup string
func TimezoneQuadKey(quadkey string, zoom int) string {
trim := min(len(quadkey), zoom)
return fmt.Sprintf("timezone:%s", quadkey[:trim])
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

218
pkg/redisv2/mock/conn.go Normal file
View File

@@ -0,0 +1,218 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: pool.go
// Package mock_redis is a generated GoMock package.
package mock_redis
import (
reflect "reflect"
time "time"
gomock "github.com/golang/mock/gomock"
redis "github.com/gomodule/redigo/redis"
)
// MockPool is a mock of Pool interface.
type MockPool struct {
ctrl *gomock.Controller
recorder *MockPoolMockRecorder
}
// MockPoolMockRecorder is the mock recorder for MockPool.
type MockPoolMockRecorder struct {
mock *MockPool
}
// NewMockPool creates a new mock instance.
func NewMockPool(ctrl *gomock.Controller) *MockPool {
mock := &MockPool{ctrl: ctrl}
mock.recorder = &MockPoolMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPool) EXPECT() *MockPoolMockRecorder {
return m.recorder
}
// Get mocks base method.
func (m *MockPool) Get() redis.Conn {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get")
ret0, _ := ret[0].(redis.Conn)
return ret0
}
func (m *MockPool) Stats() redis.PoolStats {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Stats")
ret0, _ := ret[0].(redis.PoolStats)
return ret0
}
func (m *MockPool) ActiveCount() int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ActiveCount")
ret0, _ := ret[0].(int)
return ret0
}
// Get indicates an expected call of Get.
func (mr *MockPoolMockRecorder) Get() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPool)(nil).Get))
}
// MockConn is a mock of Conn interface.
type MockConn struct {
ctrl *gomock.Controller
recorder *MockConnMockRecorder
}
// MockConnMockRecorder is the mock recorder for MockConn.
type MockConnMockRecorder struct {
mock *MockConn
}
// NewMockConn creates a new mock instance.
func NewMockConn(ctrl *gomock.Controller) *MockConn {
mock := &MockConn{ctrl: ctrl}
mock.recorder = &MockConnMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockConn) EXPECT() *MockConnMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockConn) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockConnMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockConn)(nil).Close))
}
// Do mocks base method.
func (m *MockConn) Do(arg0 string, arg1 ...interface{}) (interface{}, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Do", varargs...)
ret0, _ := ret[0].(interface{})
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Do indicates an expected call of Do.
func (mr *MockConnMockRecorder) Do(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockConn)(nil).Do), varargs...)
}
// DoWithTimeout mocks base method.
func (m *MockConn) DoWithTimeout(arg0 time.Duration, arg1 string, arg2 ...interface{}) (interface{}, error) {
m.ctrl.T.Helper()
varargs := []interface{}{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DoWithTimeout", varargs...)
ret0, _ := ret[0].(interface{})
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DoWithTimeout indicates an expected call of DoWithTimeout.
func (mr *MockConnMockRecorder) DoWithTimeout(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0, arg1}, arg2...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoWithTimeout", reflect.TypeOf((*MockConn)(nil).DoWithTimeout), varargs...)
}
// Err mocks base method.
func (m *MockConn) Err() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Err")
ret0, _ := ret[0].(error)
return ret0
}
// Err indicates an expected call of Err.
func (mr *MockConnMockRecorder) Err() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Err", reflect.TypeOf((*MockConn)(nil).Err))
}
// Flush mocks base method.
func (m *MockConn) Flush() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Flush")
ret0, _ := ret[0].(error)
return ret0
}
// Flush indicates an expected call of Flush.
func (mr *MockConnMockRecorder) Flush() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Flush", reflect.TypeOf((*MockConn)(nil).Flush))
}
// Receive mocks base method.
func (m *MockConn) Receive() (interface{}, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Receive")
ret0, _ := ret[0].(interface{})
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Receive indicates an expected call of Receive.
func (mr *MockConnMockRecorder) Receive() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Receive", reflect.TypeOf((*MockConn)(nil).Receive))
}
// ReceiveWithTimeout mocks base method.
func (m *MockConn) ReceiveWithTimeout(arg0 time.Duration) (interface{}, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReceiveWithTimeout", arg0)
ret0, _ := ret[0].(interface{})
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReceiveWithTimeout indicates an expected call of ReceiveWithTimeout.
func (mr *MockConnMockRecorder) ReceiveWithTimeout(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceiveWithTimeout", reflect.TypeOf((*MockConn)(nil).ReceiveWithTimeout), arg0)
}
// Send mocks base method.
func (m *MockConn) Send(arg0 string, arg1 ...interface{}) error {
m.ctrl.T.Helper()
varargs := []interface{}{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Send", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Send indicates an expected call of Send.
func (mr *MockConnMockRecorder) Send(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockConn)(nil).Send), varargs...)
}

View File

@@ -0,0 +1,29 @@
package tester
import (
"encoding/json"
"fmt"
)
type ExpiringCache struct {
Value interface{}
Expires int
}
// get string value for comparison
func (e *ExpiringCache) StringValue() (string, error) {
switch e.Value.(type) {
case string:
return e.Value.(string), nil
default:
data, err := json.Marshal(&e.Value)
if err != nil {
return "", err
}
return string(data), nil
}
}
func (e *ExpiringCache) String() string {
return fmt.Sprintf("%s, expires %d", e.Value, e.Expires)
}

View File

@@ -0,0 +1,662 @@
package tester
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"sync"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/redis"
"github.com/pkg/errors"
)
const (
errInsufficientArgs = "insufficient number of args"
errBadSetValue = "bad set value"
errBadExpireValue = "bad expire value"
errExpectedKey = "expected key to be string"
errExpectedMessage = "expected message to be []byte"
)
func NewRedisMock() *MockRedis {
redis.MockRedisConnection()
conn := redis.GetMockPool().Get()
mockRedis := &MockRedis{}
mockRedis.SetConn(conn)
mockRedis.Reset()
return mockRedis
}
type MockRedis struct {
redis.Connection
mu sync.Mutex
// HGET results for key and field
HGETResults map[string]map[string]interface{}
// HGETALL results array for key
HGETALLResults map[string][]interface{}
// SMSMEMBER results for key and member
SISMEMBEResults map[string]map[string]interface{}
// Results for get commands (GET, EXISTS, SMEMBERS) and key
GetCommandResult map[string]map[string]interface{}
ExecuteResults interface{}
GetResults interface{}
GetCacheResults string
GetSetResults string
RetrieveResult string
Error error
PublishedMessages map[string]interface{}
GetObjectResults map[string]string
GetObjectRawResults map[string][]byte
GetMultiResults []interface{}
SetValues map[string]ExpiringCache
ExecutedCommands []interface{}
Closed bool
}
func (m *MockRedis) Delete(id ...interface{}) error {
return m.processDelCommand(append([]interface{}{"DEL"}, id...))
}
func (m *MockRedis) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
m.Closed = true
return m.Error
}
func (m *MockRedis) Execute(command ...interface{}) (interface{}, error) {
_, _ = m.executeBatch(&redis.RedisBatchCommands{Commands: [][]interface{}{command}})
return m.ExecuteResults, m.Error
}
func (m *MockRedis) ExecuteBatch(batch *redis.RedisBatchCommands) (interface{}, error) {
if m.Error != nil {
return nil, m.Error
}
if batch.IsEmpty() {
return nil, nil
}
return m.executeBatch(batch)
}
func (c *MockRedis) SafeQueueMessage(id string, message interface{}) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.QueueMessage(id, message)
}
// SafeQueueMessage is the thread-safe implementation of QueueMessage
func (c *MockRedis) SafePublishMessage(id string, message interface{}) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.PublishMessage(id, message)
}
func (m *MockRedis) SetObjectField(key, field string, value interface{}) error {
return m.processHSetCommand([]interface{}{"HSET", key, field, value})
}
func (m *MockRedis) GetObjectField(string, string) (string, error) {
return "", nil
}
func (m *MockRedis) SafeExecuteBatch(batch *redis.RedisBatchCommands) (interface{}, error) {
if m.Error != nil {
return nil, m.Error
}
if batch.IsEmpty() {
return nil, nil
}
m.mu.Lock()
defer m.mu.Unlock()
return m.executeBatch(batch)
}
func (m *MockRedis) executeBatch(batch *redis.RedisBatchCommands) (interface{}, error) {
if m.Error != nil {
return nil, m.Error
}
var err error
var val interface{}
var vals []interface{}
results := []interface{}{}
defer batch.Clear()
for _, command := range batch.Commands {
m.ExecutedCommands = append(m.ExecutedCommands, command)
val = int64(0)
switch command[0] {
case "DEL":
err = m.processDelCommand(command)
case "GET", "EXISTS", "SMEMBERS":
val, err = m.processGetCommand(command)
case "EXPIRE", "EXPIREAT":
err = m.processExpireCommand(command)
case "HGETALL":
vals, err = m.processHGetAllCommand(command)
if err == nil {
results = append(results, vals)
continue
}
case "HGET":
val, err = m.processHGetCommand(command)
case "HSET":
err = m.processHSetCommand(command)
case "PUBLISH":
err = m.processPublishCommand(command)
case "SET":
err = m.processSetCommand(command)
case "RPUSH":
err = m.processQueueCommand(command)
case "SISMEMBER":
val, err = m.processSISMemberCommand(command)
case "SADD":
err = m.processSADDCommand(command)
}
if err == nil {
results = append(results, val)
}
}
return results, err
}
func (m *MockRedis) processGetCommand(command []interface{}) (interface{}, error) {
if len(command) != 2 {
return nil, errors.New(errInsufficientArgs)
}
return m.getMapMapResult(m.GetCommandResult, command), nil
}
func (m *MockRedis) processSetCommand(command []interface{}) error {
cache := ExpiringCache{}
if len(command) < 3 {
return errors.New(errInsufficientArgs)
} else {
data, ok := command[2].([]byte)
if !ok {
return errors.New(errBadSetValue)
}
cache.Value = string(data)
}
if len(command) == 5 && command[3] == "EX" {
expires, ok := command[4].(int)
if !ok {
return errors.New(errBadExpireValue)
}
cache.Expires = expires
}
key := fmt.Sprintf("%v", command[1])
m.SetValues[key] = cache
return nil
}
func (m *MockRedis) getMapMapResult(mmap map[string]map[string]interface{}, command []interface{}) interface{} {
if mmap == nil {
return nil
}
value, ok := mmap[command[0].(string)]
if !ok {
return nil
}
result, ok := value[command[1].(string)]
if ok {
return result
}
return nil
}
func (m *MockRedis) processHGetCommand(command []interface{}) (interface{}, error) {
if len(command) != 3 {
return nil, errors.New("HGET incorrect number of parameters")
}
return m.getMapMapResult(m.HGETResults, command[1:]), nil
}
func (m *MockRedis) processHSetCommand(command []interface{}) error {
cache := ExpiringCache{}
numArgs := len(command)
if numArgs < 4 || numArgs%2 != 0 {
return errors.New(errInsufficientArgs)
} else {
obj, expire := m.getValueCache(command[1].(string))
for i, value := range command[2:] {
if i%2 == 0 {
key, ok := value.(string)
if !ok {
return errors.New(errExpectedKey)
}
obj[key] = command[i+3]
}
}
data, err := json.Marshal(obj)
if err != nil {
return err
}
cache.Value = string(data)
cache.Expires = expire
}
key := fmt.Sprintf("%v", command[1])
m.SetValues[key] = cache
return nil
}
func (m *MockRedis) getValueCache(key string) (map[string]interface{}, int) {
obj := map[string]interface{}{}
if cache, ok := m.SetValues[key]; ok {
data := cache.Value.(string)
err := json.Unmarshal([]byte(data), &obj)
if err != nil {
panic(fmt.Sprintf("getValueCache %s %v", key, err))
}
return obj, cache.Expires
}
return obj, 0
}
func (m *MockRedis) processExpireCommand(command []interface{}) error {
if len(command) != 3 {
return errors.New(errInsufficientArgs)
}
key := fmt.Sprintf("%v", command[1])
cache, ok := m.SetValues[key]
if ok {
expires, err := strconv.Atoi(fmt.Sprint(command[2]))
if err == nil {
cache.Expires = expires
m.SetValues[key] = cache
} else {
return errors.New(errBadExpireValue)
}
}
return nil
}
func (m *MockRedis) processSISMemberCommand(command []interface{}) (interface{}, error) {
if len(command) != 3 {
return nil, errors.New(errInsufficientArgs)
}
return m.getMapMapResult(m.SISMEMBEResults, command[1:]), nil
}
func (m *MockRedis) processHGetAllCommand(command []interface{}) ([]interface{}, error) {
if len(command) != 2 {
return nil, errors.New(errInsufficientArgs)
}
if m.HGETALLResults == nil {
return nil, nil
}
values, ok := m.HGETALLResults[command[1].(string)]
if !ok {
return nil, nil
}
return values, nil
}
func (m *MockRedis) processQueueCommand(command []interface{}) error {
if len(command) != 3 {
return errors.New(errInsufficientArgs)
}
return m.QueueMessage(command[1].(string), command[2])
}
func (m *MockRedis) processPublishCommand(command []interface{}) error {
if len(command) != 3 {
return errors.New(errInsufficientArgs)
}
// Publish message is passed in as []byte, but mock PublishMessage expects object
data, ok := command[2].([]byte)
if !ok {
return errors.New(errExpectedMessage)
}
msg := map[string]interface{}{}
err := json.Unmarshal(data, &msg)
if err != nil {
return errors.WithStack(err)
}
return m.PublishMessage(command[1].(string), msg)
}
func (m *MockRedis) processDelCommand(command []interface{}) error {
if len(command) != 2 {
return errors.New(errInsufficientArgs)
}
key := command[1].(string)
cache := ExpiringCache{Value: "DELETED"}
m.SetValues[key] = cache
return nil
}
func (m *MockRedis) processSADDCommand(command []interface{}) error {
key := command[1].(string)
cache := ExpiringCache{Value: command[2:]}
m.SetValues[key] = cache
return nil
}
func (m *MockRedis) Get(string) (interface{}, error) {
return m.GetResults, m.Error
}
func (m *MockRedis) GetCache(id string, data interface{}, expire int) error {
err := json.Unmarshal([]byte(m.GetCacheResults), data)
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (m *MockRedis) GetSet(id string, data interface{}) error {
if m.Error != nil {
return m.Error
}
err := json.Unmarshal([]byte(m.GetSetResults), data)
if err != nil {
return err
}
return nil
}
func (m *MockRedis) PublishMessage(id string, msg interface{}) error {
if m.Error != nil {
return m.Error
}
if m.PublishedMessages == nil {
m.PublishedMessages = map[string]interface{}{}
}
// In the real thing, you message is converted to JSON and sent out, so further changes
// to the struct do not change redis, but because we are keeping the struct, any internal pointer
// can still be affected
msg, _ = BeginDeepCopy(msg)
// trim prefix for publishing and queueing of message
key := strings.Replace(strings.Replace(id, "channel:", "", 1), "queue:", "", 1)
m.PublishedMessages[key] = msg
return nil
}
func (m *MockRedis) QueueMessage(id string, msg interface{}) error {
return m.PublishMessage(id, msg)
}
func (m *MockRedis) BatchPublishMessages(ids []string, messages []interface{}) error {
if len(ids) != len(messages) {
return errors.Errorf(
"mismatch number of ids and messages. have %d ids and %d messages",
len(ids),
len(messages),
)
}
for i := 0; i < len(ids); i++ {
err := m.SafePublishMessage(ids[i], messages[i])
if err != nil {
return err
}
}
return nil
}
func (m *MockRedis) BatchQueueMessages(ids []string, messages []interface{}) error {
if len(ids) != len(messages) {
return errors.Errorf(
"mismatch number of ids and messages. have %d ids and %d messages",
len(ids),
len(messages),
)
}
for i := 0; i < len(ids); i++ {
err := m.QueueMessage(ids[i], messages[i])
if err != nil {
return err
}
}
return nil
}
func (m *MockRedis) Retrieve(id string, data interface{}) error {
if m.Error != nil {
return m.Error
}
err := json.Unmarshal([]byte(m.RetrieveResult), data)
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (m *MockRedis) Set(key string, value interface{}) error {
if m.Error != nil {
return m.Error
}
m.SetValues[key] = ExpiringCache{
Value: value,
}
return nil
}
func (m *MockRedis) SetCache(key string, value interface{}, expires int) error {
if m.Error != nil {
return m.Error
}
m.SetValues[key] = ExpiringCache{Value: value, Expires: expires}
return nil
}
func (m *MockRedis) GetObject(key string, obj interface{}) error {
if m.Error != nil {
return m.Error
}
data, ok := m.GetObjectResults[key]
if !ok {
return redis.ErrNilObject
}
err := json.Unmarshal([]byte(data), obj)
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (m *MockRedis) SetObject(key string, value interface{}, expires int) error {
return m.SetCache(key, value, expires)
}
func (m *MockRedis) GetMulti(ids []string) ([]interface{}, error) {
return m.GetMultiResults, m.Error
}
// Test helper methods
func (m *MockRedis) HasMessage(id string, msg string) (string, bool) {
var compare string
if value, ok := m.PublishedMessages[id]; ok {
if compare, ok = m.isByteSlice(value); !ok {
if compare, ok = m.getJSON(value); !ok {
return "", false
}
}
if compare == msg {
return compare, true
}
}
return compare, false
}
func (m *MockRedis) getJSON(value interface{}) (string, bool) {
result, err := json.Marshal(value)
if err != nil {
return "", false
}
return string(result), true
}
func (m *MockRedis) isByteSlice(value interface{}) (string, bool) {
// convert the []byte message into string
if data, ok := value.([]byte); ok {
return string(data), true
}
return "", false
}
func (m *MockRedis) FetchCache(id string) (value ExpiringCache, ok bool) {
value, ok = m.SetValues[id]
return
}
func (m *MockRedis) NewSet(id string, value interface{}, expires int) error {
if m.Error != nil {
return m.Error
}
m.SetValues[id] = ExpiringCache{Value: value, Expires: expires}
return nil
}
func (m *MockRedis) Ping() error {
return m.Error
}
func (m *MockRedis) GetObjectRaw(string) (map[string][]byte, error) {
if m.Error != nil {
return nil, m.Error
}
return m.GetObjectRawResults, nil
}
func (m *MockRedis) Reset() {
m.ExecuteResults = nil
m.GetResults = nil
m.Error = nil
m.PublishedMessages = map[string]interface{}{}
m.SetValues = map[string]ExpiringCache{}
m.ExecutedCommands = []interface{}{}
m.Closed = false
}
// We attempt to type convert our interfaces so we can copy them.
// If we try to do the json.Marshal copy without doing this, then we get map[string]interface{}
// which requires code changes to multiple different areas to get working
func BeginDeepCopy(original interface{}) (copy interface{}, err error) {
switch original.(type) {
case common.Message:
copy, err = deepCopyMessage(original.(common.Message))
default:
err = errors.New("no match")
}
if err != nil {
return original, err
}
return
}
// If we want to deep copy other data structs, just need to add their type in here
func deepCopyMessage(original common.Message) (copy common.Message, err error) {
copy = original
switch original.Data.(type) {
case ExampleDeepItem:
data := original.Data.(ExampleDeepItem)
copy.Data, err = copyObject(data)
case common.UpdateManifest:
data := original.Data.(common.UpdateManifest)
copy.Data, err = copyObject(data)
case common.CarUpdate:
data := original.Data.(common.CarUpdate)
copy.Data, err = copyObject(data)
default:
err = errors.New("no result")
}
if err != nil {
return original, err
}
return
}
// If this is used on an interface, it makes it a map[string]interface
// so your object needs to be type casted first
func copyObject[V any](original V) (copy V, err error) {
b, err := json.Marshal(original)
if err != nil {
return original, err
}
err = json.Unmarshal(b, &copy)
if err != nil {
return original, err
}
return
}
type ExampleDeepItem struct {
Title string
NestedObject []*NestedDeepItem
}
type NestedDeepItem struct {
ID int
Description *string
}

View File

@@ -0,0 +1,56 @@
package tester
import (
"sync"
"fiskerinc.com/modules/redis"
)
func NewMockClientPool(args ...interface{}) redis.ClientPoolInterface {
result := &MockClientPool{}
for i := range args {
if pool, ok := args[i].(redis.Pool); ok {
result.pool = pool
} else if client, ok := args[i].(redis.Client); ok {
result.client = client
}
}
return result
}
type MockClientPool struct {
once sync.Once
oncePool sync.Once
client redis.Client
pool redis.Pool
}
func (f *MockClientPool) getClient() redis.Client {
f.once.Do(func() {
if f.client == nil {
f.client = NewRedisMock()
}
})
return f.client
}
func (f *MockClientPool) GetPool() redis.Pool {
f.oncePool.Do(func() {
if f.pool == nil {
f.pool = redis.GetMockPool()
}
})
return f.pool
}
func (f *MockClientPool) SetPool(pool redis.Pool) {
f.pool = pool
}
func (f *MockClientPool) GetFromPool() redis.Client {
return f.getClient()
}

View File

@@ -0,0 +1,58 @@
package tester
import (
"testing"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/utils/elptr"
"github.com/stretchr/testify/assert"
)
func TestMockClientPublishMessage(t *testing.T) {
testItem := ExampleDeepItem{}
testItem.Title = "testItem"
testItem.NestedObject = append(testItem.NestedObject,
elptr.ElPtr(NestedDeepItem{ID: 0, Description: elptr.ElPtr("zero")}),
elptr.ElPtr(NestedDeepItem{ID: 1, Description: elptr.ElPtr("one")}),
)
redisMock := NewRedisMock()
redisMock.SafePublishMessage("0", common.Message{
Handler: "first-handler",
Data: testItem,
})
// Now we do some changes to the testItem
testItem.NestedObject[0].ID = 25
testItem.NestedObject[0].Description = elptr.ElPtr("not zero")
testItem.NestedObject = testItem.NestedObject[:1]
redisMock.PublishMessage("1", common.Message{
Handler: "second-handler",
Data: testItem,
})
expectedItem1 := ExampleDeepItem{}
expectedItem1.Title = "testItem"
expectedItem1.NestedObject = append(expectedItem1.NestedObject,
elptr.ElPtr(NestedDeepItem{ID: 0, Description: elptr.ElPtr("zero")}),
elptr.ElPtr(NestedDeepItem{ID: 1, Description: elptr.ElPtr("one")}),
)
expectedItem2 := ExampleDeepItem{}
expectedItem2.Title = "testItem"
expectedItem2.NestedObject = append(expectedItem2.NestedObject,
elptr.ElPtr(NestedDeepItem{ID: 25, Description: elptr.ElPtr("not zero")}),
)
expectedMessage := map[string]interface{}{
"0": common.Message{
Handler: "first-handler",
Data: expectedItem1,
},
"1": common.Message{
Handler: "second-handler",
Data: expectedItem2,
},
}
assert.Equal(t, expectedMessage, redisMock.PublishedMessages)
}

View File

@@ -0,0 +1,13 @@
package tester
import "fiskerinc.com/modules/cache"
type MockVehiclesCache struct{}
func (m *MockVehiclesCache) Set(key cache.VehiclesTTLParams, value *cache.VehiclesTTLResult) error {
return nil
}
func (m *MockVehiclesCache) Get(key cache.VehiclesTTLParams) (*cache.VehiclesTTLResult, error) {
return nil, nil
}

View File

@@ -0,0 +1,113 @@
package tester
import (
"fmt"
"regexp"
"testing"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/testhelper"
)
type ExpiringCacheResult struct {
Value string
Expires int
RegexCompare *regexp.Regexp
}
type RedisTestCase struct {
Device common.Device
DeviceKey string
PayloadData string
ExpectedError string
ExpectedMessages map[string]string
ExpectedCaches map[string]ExpiringCacheResult
MockRedisError error
MockRedisGet interface{}
MockRedisGetCache string
MockRedisRetrieve string
MockRedisGetSet string
MockRedisGetMulti []interface{}
Setup func()
}
func (tc *RedisTestCase) SetupRedis(mockRedis *MockRedis) {
if mockRedis != nil {
mockRedis.Error = tc.MockRedisError
mockRedis.GetCacheResults = tc.MockRedisGetCache
mockRedis.GetResults = tc.MockRedisGet
mockRedis.GetSetResults = tc.MockRedisGetSet
mockRedis.RetrieveResult = tc.MockRedisRetrieve
mockRedis.GetMultiResults = tc.MockRedisGetMulti
if tc.Setup != nil {
tc.Setup()
}
}
}
func (tc *RedisTestCase) CheckHandlerError(t *testing.T, name string, err error) {
if err != nil && err.Error() != tc.ExpectedError {
t.Errorf(testhelper.TestErrorTemplate, name, tc.ExpectedError, err.Error())
} else if err == nil && tc.ExpectedError != "" {
t.Errorf(testhelper.TestErrorTemplate, name, tc.ExpectedError, err)
}
}
func (tc *RedisTestCase) Validate(t *testing.T, name string, mock *MockRedis) {
tc.checkRedisMessages(t, name, mock)
tc.checkRedisCache(t, name, mock)
}
func (tc *RedisTestCase) checkRedisMessages(t *testing.T, name string, mock *MockRedis) {
for key, msg := range tc.ExpectedMessages {
if compare, ok := mock.HasMessage(key, msg); !ok {
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("%s CheckRedisMessages %s", name, key), msg, compare)
}
}
for key := range mock.PublishedMessages {
if _, ok := tc.ExpectedMessages[key]; !ok {
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("%s unexpected message %s", name, key), nil, fmt.Sprintf("%s = %s", key, mock.PublishedMessages[key]))
}
}
}
func (tc *RedisTestCase) checkRedisCache(t *testing.T, name string, mock *MockRedis) {
for key, expected := range tc.ExpectedCaches {
tc.checkCacheValues(t, name, mock, key, expected)
}
for key := range mock.SetValues {
if _, ok := tc.ExpectedCaches[key]; !ok {
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("%s unexpected cache %s", name, key), nil, fmt.Sprintf("%s = %v", key, mock.SetValues[key]))
}
}
}
func (tc *RedisTestCase) getCacheValue(mock *MockRedis, cache ExpiringCache) (string, error) {
return cache.StringValue()
}
func (tc *RedisTestCase) checkCacheValues(t *testing.T, test string, mock *MockRedis, key string, expected ExpiringCacheResult) {
name := fmt.Sprintf("%s checkCacheValues %s", test, key)
if cached, ok := mock.FetchCache(key); ok {
value, err := tc.getCacheValue(mock, cached)
if err != nil {
t.Error(err)
}
if expected.RegexCompare != nil {
if !expected.RegexCompare.Match([]byte(value)) {
t.Errorf(testhelper.TestErrorTemplate, name, expected.RegexCompare.String(), value)
}
} else if value != expected.Value {
t.Errorf(testhelper.TestErrorTemplate, name, expected.Value, value)
}
if expected.Expires != cached.Expires {
t.Errorf(testhelper.TestErrorTemplate, name, expected.Expires, cached.Expires)
}
} else {
t.Errorf(testhelper.TestErrorTemplate, name, fmt.Sprintf("Has Cache %s", key), nil)
}
}

32
pkg/redisv2/tool.go Normal file
View File

@@ -0,0 +1,32 @@
package redisv2
import (
"errors"
"fmt"
)
var ErrUnEvenResults = errors.New("uneven number of results in interface array")
// Except an even number of results in the array
func InterfaceArrayToStringMap(array []interface{})(result map[string]string, err error){
if len(array) %2 != 0 {
err = ErrUnEvenResults
return
}
result = make(map[string]string)
for x := 0; x < len(array); x += 2 {
key, ok := array[x].(string)
if !ok {
err =errors.Join(err, fmt.Errorf("failed to convert key %v to string", array[x]))
continue
}
value, ok := array[x+1].(string)
if !ok {
err =errors.Join(err, fmt.Errorf("failed to convert value %v to string", array[x+1]))
continue
}
result[key] = value
}
return
}