Add depot, attendant, jetfire, optimus, ota services with kustomize overlays

This commit is contained in:
Chris Rai
2026-01-31 15:35:07 -05:00
parent a0ec642ca1
commit 9a5cb2f547
404 changed files with 38817 additions and 16 deletions

View File

@@ -0,0 +1,25 @@
ARG BASE_IMAGE=cloud_base_go
FROM ${BASE_IMAGE} as builder-go
WORKDIR /build/attendant
COPY ./attendant/go.mod ./attendant/go.sum ./
RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \
&& go mod download
COPY ./attendant ./
RUN go mod edit -replace fiskerinc.com/modules=../fiskerinc.com/modules \
&& go build -tags musl
FROM alpine:3.17
RUN apk add --no-cache librdkafka --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \
&& apk add --no-cache ca-certificates
COPY ./modules_go/logger/log_config .
COPY --from=builder-go /build/attendant/attendant .
ENV LOG_CONFIG=log_config
EXPOSE 8077
CMD ./attendant

View File

@@ -0,0 +1,518 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
s "github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/grpc/sms"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/manifestsender"
"github.com/fiskerinc/cloud-services/pkg/redis"
vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig"
"github.com/fiskerinc/cloud-services/pkg/hwversion"
"github.com/go-pg/pg/v10"
r "github.com/gomodule/redigo/redis"
"github.com/pkg/errors"
)
const redisObjectExpire = 3600
const (
PackageDownloadStart = "package_download_start"
PackageDownloadComplete = "package_download_complete"
PackageInstallStart = "package_install_start"
PackageInstallComplete = "package_install_complete"
InstallError = "install_error"
)
var RepeatedStatus = errors.New("RepeatedStatus")
// CarUpdateProgress takes in a car update message and saves it to our database
// This includes setting the status of a car update, and telling the car and SAP that the update is done
func NewCarUpdateProgress(clientPool redis.ClientPoolInterface, ka *services.KeepAwake, db *services.DB, device common.Device) CarUpdateProgressInterface {
if device == common.TRex {
return &CarUpdateProgress{
RedisClientPool: clientPool,
DB: db,
ka: ka,
}
}
if device == common.HMI {
return &HMICarUpdateProgress{
conf: services.GetVehicleConfig(),
sms: services.GetSMSClient(),
ka: ka,
CarUpdateProgress: CarUpdateProgress{
RedisClientPool: clientPool,
DB: db,
ka: ka,
},
}
}
return nil
}
type CarUpdateProgressInterface interface {
Process(vin string, data []byte) error
ProcessStatus(vin string, status common.CarUpdateProgress) error
Dispose()
}
type CarUpdateProgress struct {
RedisClientPool redis.ClientPoolInterface
DB *services.DB
ka *services.KeepAwake
}
func (cu *CarUpdateProgress) Process(vin string, data []byte) error {
var status common.CarUpdateProgress
err := json.Unmarshal(data, &status)
if err != nil {
return err
}
return cu.ProcessStatus(vin, status)
}
func (cu *CarUpdateProgress) ProcessStatus(vin string, status common.CarUpdateProgress) (err error) {
if cu.transformDBCarUpdateProgress(&status) {
err = cu.logStatusDB(status)
// If the error is the repeated, we can just exit early
if err != nil {
if errors.Is(err, queries.RepeatedStatus) {
err = nil
}
return
}
}
cu.cancelTheCANAwake(vin, status)
batch := redis.NewRedisBatchCommands()
cu.transformRedisCarUpdateProgress(&status)
cu.BatchCacheRedis(batch, redis.CarUpdateStatusHashKey(status.CarUpdateID), &status)
client := cu.RedisClientPool.GetFromPool()
defer client.Close()
_, err = client.ExecuteBatch(batch)
if err != nil {
return err
}
// do not send car update status for internal cloud statuses
if cu.isInternalStatus(status) {
return nil
}
msg := cu.getMessage(&status)
err = cu.publishStatusHMI(vin, &msg)
if err != nil {
return err
}
msgMobile := cu.getMessageForMobile(&status, vin)
err = cu.publishStatusMobile(vin, &msgMobile)
if err != nil {
return err
}
err = cu.onUpdateManifestComplete(&status, vin)
return err
}
// We will try and cancel sending CAN status stuff
func (cu *CarUpdateProgress) cancelTheCANAwake(vin string, status common.CarUpdateProgress) {
switch status.Status {
case s.DownloadFailed, s.InstallFailed, s.ManifestCancelAccepted, s.ManifestCancelRejected,
s.ManifestError, s.ManifestRejected, s.ManifestValidationFailed, s.RequirementsFailed, s.ManifestCanceled:
logger.Info().Msgf("canceling CAN Awake for %s because %s", vin, status.Status)
cu.ka.RemoveKeepAwakeMessage(vin)
}
}
func (cu *CarUpdateProgress) isInternalStatus(status common.CarUpdateProgress) bool {
return status.Status == s.Sent || status.Status == s.Pending
}
func (cu *CarUpdateProgress) transformDBCarUpdateProgress(status *common.CarUpdateProgress) bool {
switch status.Status {
case s.DownloadStarted:
if status.PackageCurrent == 0 {
status.Status = PackageDownloadStart
}
return true
case s.DownloadCompleted:
if status.PackageCurrent == status.PackageTotal {
status.Status = PackageDownloadComplete
}
return true
case s.InstallStarted:
if status.InstalledFiles == 0 && status.TotalFiles > 0 {
status.Status = PackageInstallStart
}
return true
case s.InstallSucceeded:
if status.InstalledFiles == status.TotalFiles && status.TotalFiles > 0 {
status.Status = PackageInstallComplete
}
return true
case InstallError:
status.Status = s.InstallFailed
return true
case s.Installing:
return false
case s.Downloading:
// these status updates do not need to be saved in the database
return false
}
return true
}
func (cu *CarUpdateProgress) transformRedisCarUpdateProgress(status *common.CarUpdateProgress) {
switch status.Status {
case s.DownloadStarted, s.DownloadCompleted, PackageDownloadStart:
status.Status = s.Downloading
case s.InstallStarted, s.InstallSucceeded, PackageInstallStart:
status.Status = s.Installing
}
}
func (cu *CarUpdateProgress) logStatusDB(status common.CarUpdateProgress) (err error) {
// If we are one of these status's we want to ignore, then we need to do some extra database steps, otherwise insert normally
carUpdate := common.CarUpdate{
ID: status.CarUpdateID,
Status: status.Status,
ErrorCode: status.ErrorCode,
Info: strings.TrimSpace(fmt.Sprintf("%s %s", status.ECU, status.Info)),
}
if _, ok := s.NoRepeatUpdateStatus[status.Status]; ok {
_, err = cu.DB.GetCarUpdates().UpdateStatusIfNotRepeat(&carUpdate)
return
}
_, err = cu.DB.GetCarUpdates().UpdateStatus(&carUpdate)
return err
}
func (cu *CarUpdateProgress) GetCache(key string) (*common.CarUpdateProgress, error) {
client := cu.RedisClientPool.GetFromPool()
defer client.Close()
status := common.CarUpdateProgress{}
err := client.GetObject(key, &status)
return &status, err
}
func (cu *CarUpdateProgress) BatchCacheRedis(batch *redis.RedisBatchCommands, key string, status *common.CarUpdateProgress) {
batch.Add(r.Args{}.Add("HSET").Add(key).AddFlat(status)...)
batch.Add("EXPIRE", key, redisObjectExpire)
}
func (cu *CarUpdateProgress) getMessage(status *common.CarUpdateProgress) common.Message {
return common.Message{
Handler: "car_update_status",
Data: status,
}
}
func (cu *CarUpdateProgress) getMessageForMobile(status *common.CarUpdateProgress, vin string) common.Message {
type mobileData struct {
VIN string `json:"vin"`
*common.CarUpdateProgress
}
return common.Message{
Handler: "car_update_status",
Data: mobileData{vin, status},
}
}
func (cu *CarUpdateProgress) publishStatusHMI(vin string, msg *common.Message) error {
client := cu.RedisClientPool.GetFromPool()
defer client.Close()
// redis publish to HMI
hmiKey := common.HMI.Key(vin)
// Add VIN
err := client.SafePublishMessage(hmiKey, msg)
return err
}
func (cu *CarUpdateProgress) publishStatusMobile(vin string, msg *common.Message) error {
drivers := cache.NewDriversCache(cu.RedisClientPool, cu.DB.GetCars())
// redis publish to mobile devices
driverIDs, err := drivers.RetrieveDriverIDs(vin)
if err != nil {
return err
}
// Change thos for loop to isntead create a batch and execute it all at once
client := cu.RedisClientPool.GetFromPool()
defer client.Close()
for _, d := range driverIDs {
mobileKey := common.Mobile.Key(d)
err = client.SafePublishMessage(mobileKey, msg)
if err != nil {
return err
}
}
return nil
}
func (cu *CarUpdateProgress) Dispose() {
cu.DB = nil
}
type HMICarUpdateProgress struct {
conf vconfig.ConfigServiceInterface
sms sms.SMSServiceClient
ka *services.KeepAwake
CarUpdateProgress
}
func (h *HMICarUpdateProgress) Process(vin string, data []byte) error {
var status common.CarUpdateProgress
err := json.Unmarshal(data, &status)
if err != nil {
return err
}
if h.downloadComplete(&status) {
// stop calling the sendKeepAwakeMessage
h.ka.RemoveKeepAwakeMessage(vin)
_, err = h.sendManifestToTRex(vin, &status)
if err != nil {
return err
}
h.logStatusDB(common.CarUpdateProgress{
CarUpdateID: status.CarUpdateID,
Status: s.Sent,
Info: "TBOX",
})
}
return h.ProcessStatus(vin, status)
}
func (h *HMICarUpdateProgress) downloadComplete(status *common.CarUpdateProgress) bool {
return status.Status == s.DownloadCompleted
}
func (h *HMICarUpdateProgress) getManifest(status *common.CarUpdateProgress) (*common.UpdateManifest, error) {
update := common.CarUpdate{ID: status.CarUpdateID}
err := h.DB.GetCarUpdates().Load(&update)
if err != nil {
return nil, err
}
update.UpdateManifest.CarUpdateID = status.CarUpdateID
return update.UpdateManifest, nil
}
func (h *HMICarUpdateProgress) sendManifestToTRex(vin string, status *common.CarUpdateProgress) (msgID string, err error) {
logger.Info().Msgf("HMICarUpdateProgress sendManifestToTRex car_update_id %d", status.CarUpdateID)
manifest, err := h.getManifest(status)
if err != nil {
return
}
if !manifest.HasSelfDownload() {
logger.Error().Msgf("%s download_completed for non-self-download manifest", vin)
return
}
err = hwversion.SetHWVersion(manifest, vin, services.GetDB().GetCars())
if err != nil {
// An error here is very unexpected. The hw versioning should have been confirmed earlier before ICC was updated
err = errors.WithStack(err)
logger.Err(err).Str("VIN", vin).Int64("UpdateID", status.CarUpdateID).Msg("failed to set hw versions for a manifest after ICC complete update")
err = nil
}
manifest.SortECUs()
manifest.FilterCompatibleECUs(vin)
// This code is going to be removed by mny other PR so not going to mess with it for now
client := h.RedisClientPool.GetFromPool()
defer client.Close()
trex := manifestsender.NewTBOXManifestSender(client, h.conf, h.DB, h.sms, nil)
defer trex.Close()
msgID, err = trex.ProcessSoftwareUpdate(vin, manifest, services.GetDB().GetCarConfigData())
return
}
func (h *HMICarUpdateProgress) GetRedisHashKey(status *common.CarUpdateProgress) string {
return redis.CarUpdateStatusHMIHashKey(status.CarUpdateID)
}
// Car Update Done
func (cu *CarUpdateProgress) onUpdateManifestComplete(status *common.CarUpdateProgress, vin string) (err error) {
success := false
final := false
submitSAP := false
switch status.Status {
case s.ManifestSucceeded:
success = true
submitSAP = true
final = true
case s.ManifestCanceled, s.ManifestError, s.ManifestRejected:
success = false
submitSAP = true
final = true
case s.DownloadFailed, s.ManifestCancelPending, s.RollbackSucceeded, s.RollbackFailed, s.CleanupSucceeded:
final = true
default:
return nil
}
carUpdatesDB := cu.DB.GetCarUpdates()
carUpdate, err := carUpdatesDB.SelectByID(status.CarUpdateID)
if err != nil {
err = errors.WithStack(err)
return
}
if carUpdate != nil {
// Notify car user of in progress update through FOA API
fs := services.GetFoaService()
foaResp, err := fs.OtaUpdateStatus(vin, carUpdate, status)
if err != nil || (foaResp != nil && foaResp.StatusCode != http.StatusOK) {
bodyBytes, _ := io.ReadAll(foaResp.Body)
bodyString := string(bodyBytes)
logger.Err(err).Msgf("notify FOA for update manifest %d final state %s for %s failed with http status %d and message %s", carUpdate.UpdateManifestID, status.Status, vin, foaResp.StatusCode, bodyString)
err = nil
}
}
logger.Info().Msgf("Manifest update completed for %s with status of %s", vin, status.Status)
if submitSAP {
logger.Info().Msg("SAP: No Longer Submit Updates")
// sap := services.GetSapService()
// err = sap.SubmitResult(vin, success)
// if err != nil {
// requestBody := struct {
// VIN string
// Success bool
// CarUpdateProgress common.CarUpdateProgress
// }{VIN: vin, Success: success, CarUpdateProgress: *status}
// logger.Err(err).Interface("body", requestBody).Msgf("failed to call sap submit result")
// err = nil
// }
}
if success {
// If we are successful, we want to possibly update the cars sums version
// Need to pull the manifest to check it has a sums version, and then update the car
err = cu.updateCarsSUMSVersion(status)
if err != nil {
logger.Err(err).Msgf("failed to update car sums version for manifest with CarUpdateID %d", status.CarUpdateID)
err = nil
}
// Send the read_ecu_versions remote command so that the ECU data is updated in postgres ASAP
client := services.RedisClientPool().GetFromPool()
defer client.Close()
err = client.SafePublishMessage(
common.TRex.Key(vin),
common.Message{
Handler: "read_ecu_versions",
Data: common.RemoteReadVersionsCommandArgs{
ECUName: "*",
},
},
)
if err != nil {
logger.Err(err).Msgf("failed to send read_ecu_versions command to vin %s", vin)
err = nil
}
}
if final {
// if the manifest is in a final state
// then delete the redundant requirements_await rows from car_update_statuses, to avoid overcrowding the table
err = cu.truncateRequirementsAwaitForUpdate(status)
if err != nil {
logger.Err(err).Msgf("failed to delete redundant requirements_await rows from car_update_statuses for manifest with CarUpdateID %d", status.CarUpdateID)
err = nil
}
}
return err
}
func (cu *CarUpdateProgress) truncateRequirementsAwaitForUpdate(status *common.CarUpdateProgress) error {
logger.Info().Msgf("Manifest with CarUpdateID %d successful with status %s. Deleting redundant requirements_await rows from car_update_statuses", status.CarUpdateID, status.Status)
_, err := cu.DB.GetCarUpdates().TruncateRequirementsAwaitForUpdate(status.CarUpdateID)
if err != nil && !errors.Is(err, pg.ErrNoRows) {
return err
}
return nil
}
// Find the car update, and it gives the update manifest
// If the manifest has a sums version, apply it to the car
func (cu *CarUpdateProgress) updateCarsSUMSVersion(status *common.CarUpdateProgress) (err error) {
carUpdatesDB := cu.DB.GetCarUpdates()
carUpdate, err := carUpdatesDB.SelectByID(status.CarUpdateID)
if err != nil {
err = errors.WithStack(err)
return
}
if carUpdate.UpdateManifest == nil {
err = errors.New("failed to pull car updates update manifest")
return
}
um := carUpdate.UpdateManifest
// So if we have have a sums version we want to update
if um.SUMS == "" {
return
}
carsDB := cu.DB.GetCars()
filter := common.Car{
VIN: carUpdate.VIN,
}
cars, err := carsDB.Select(&filter, nil)
if err != nil {
err = errors.WithStack(err)
return
}
if len(cars) != 1 {
err = fmt.Errorf("did not receive only one car, received: %d", len(cars))
err = errors.WithStack(err)
return
}
car := cars[0]
car.SUMSVersion = um.SUMS
_, err = carsDB.Update(&car)
if err != nil {
err = errors.WithStack(err)
}
return
}

View File

@@ -0,0 +1,184 @@
package controllers
import (
"github.com/fiskerinc/cloud-services/services/attendant/services"
"encoding/base64"
"encoding/json"
"errors"
"sync"
"time"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
)
type DTCEntry struct {
CreatedAt time.Time `bson:"created_at"`
VIN string `bson:"vin"`
ECU string `json:"ecu" bson:"ecu"`
DTC uint64 `json:"dtc" bson:"dtc"`
Status uint8 `json:"status" bson:"status"`
Timestamp time.Time `json:"timestamp" bson:"timestamp"`
Speed uint16 `json:"speed" bson:"speed"`
Mileage uint32 `json:"mileage" bson:"mileage"`
Voltage uint16 `json:"voltage" bson:"voltage"`
SnapshotBase64 string `json:"snapshot,omitempty" bson:"snapshot,omitempty"`
}
func GRPCToDTCEntry(payload *kafka_grpc.GRPC_AttendantPayload) []byte {
if payload.Data == nil {
return nil
}
data := payload.Data.(*kafka_grpc.GRPC_AttendantPayload_DtcEntry)
if data == nil {
return nil
}
dtc := &DTCEntry{
VIN: data.DtcEntry.Vin,
ECU: data.DtcEntry.Ecu,
CreatedAt: milliToDate(data.DtcEntry.CreatedAt),
DTC: data.DtcEntry.Dtc,
Status: uint8(data.DtcEntry.Status),
Timestamp: milliToDate(data.DtcEntry.Timestamp),
Speed: uint16(data.DtcEntry.Speed),
Mileage: data.DtcEntry.Mileage,
Voltage: uint16(data.DtcEntry.Volt),
SnapshotBase64: data.DtcEntry.SnapshotBase64,
}
bytes, _ := json.Marshal(dtc)
return bytes
}
func milliToDate(timestamp int64) time.Time {
seconds := timestamp / 1000
nanoseconds := (timestamp % 1000) * int64(time.Millisecond)
return time.Unix(seconds, nanoseconds)
}
const SNAPSHOT_LEN = 21
func (entry *DTCEntry) ParseSnapshot() error {
data, errc := base64.StdEncoding.DecodeString(entry.SnapshotBase64)
if errc != nil {
return errc
}
payloadLen := len(data) - 2
if payloadLen < SNAPSHOT_LEN {
logger.Debug().Msgf("DTC snapshot payload is too small. Required length=%d, actual=%d, ECU=%s\n", SNAPSHOT_LEN, len(data), entry.ECU)
return errors.New("Snapshot too small")
}
// Our "Basic Diagnostic" spec defines 4 mandatory DIDs that must be
// stored inside DTCStapshotRecord:
// 1. EF F6 - Timestamp (6 bytes).
// 2. EF F7 - Vehicle Speed (2 bytes).
// 3. EF F8 - Milage (idk, our spec doesn't specify exact number.
// Empirically it's 4 bytes (same as "ICC_0x531::TotMilg_ODO").
// 4. EF F9 - Battery Voltage (2 bytes).
///entry.Speed = int16(speed.ToPhysical(float64(uint16(decodedBytes[0])<<8 | uint16(decodedBytes[1]))))
//entry.Voltage = int16(voltage.ToPhysical(float64(uint16(decodedBytes[0])<<8 | uint16(decodedBytes[1]))))
for idx := 2; idx < len(data)-1; {
if data[idx] != 0xEF {
idx++
continue
}
if data[idx+1] == 0xF6 && idx+7 < len(data) {
idx += 2
entry.Timestamp, idx = consumeTimestamp(data, idx)
} else if data[idx+1] == 0xF7 && idx+3 < len(data) {
idx += 2
entry.Speed, idx = consumeSpeed(data, idx)
} else if data[idx+1] == 0xF8 && idx+5 < len(data) {
idx += 2
entry.Mileage, idx = consumeMileage(data, idx)
} else if data[idx+1] == 0xF9 && idx+3 < len(data) {
idx += 2
entry.Voltage, idx = consumeVoltage(data, idx)
} else {
idx++
}
}
return nil
}
var onceTimestamp sync.Once
var (
day *descriptor.Signal
hr *descriptor.Signal
mins *descriptor.Signal
yr *descriptor.Signal
month *descriptor.Signal
sec *descriptor.Signal
)
func consumeTimestamp(data []byte, idx int) (res time.Time, idx_ret int) {
onceTimestamp.Do(func() {
day, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Day")
hr, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Hr")
mins, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Mins")
yr, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Yr")
month, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Mth")
sec, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Sec")
})
res = time.Date(int(yr.ToPhysical(float64(data[idx]))),
time.Month(month.ToPhysical(float64(data[idx+1]))),
int(day.ToPhysical(float64(data[idx+2]))),
int(hr.ToPhysical(float64(data[idx+3]))),
int(mins.ToPhysical(float64(data[idx+4]))),
int(sec.ToPhysical(float64(data[idx+5]))), 0, time.UTC)
idx_ret = idx + 6
return res, idx_ret
}
var onceSpeed sync.Once
var speed *descriptor.Signal
func consumeSpeed(data []byte, idx int) (res uint16, idx_ret int) {
onceSpeed.Do(func() {
speed, _ = services.GetDBC().Signal(0x318, "ESP_VehSpd")
})
res = uint16(speed.ToPhysical(float64(uint16(data[idx])<<8 | uint16(data[idx+1]))))
idx_ret = idx + 2
return res, idx_ret
}
var onceMileage sync.Once
var mileage *descriptor.Signal
func consumeMileage(data []byte, idx int) (res uint32, idx_ret int) {
onceMileage.Do(func() {
mileage, _ = services.GetDBC().Signal(0x531, "ICC_TotMilg_ODO")
})
res = uint32(mileage.ToPhysical(
float64(
uint64(data[idx])<<24 |
uint64(data[idx+1])<<16 |
uint64(data[idx+2])<<8 | uint64(data[idx+3]))))
idx_ret = idx + 4
return res, idx_ret
}
var onceVoltage sync.Once
var voltage *descriptor.Signal
func consumeVoltage(data []byte, idx int) (res uint16, idx_ret int) {
onceVoltage.Do(func() {
voltage, _ = services.GetDBC().Signal(0x507, "VCU_BattVolt")
})
res = uint16(voltage.ToPhysical(float64(uint16(data[idx])<<8 | uint16(data[idx+1]))))
idx_ret = idx + 2
return res, idx_ret
}

View File

@@ -0,0 +1,60 @@
package controllers_test
import (
"github.com/fiskerinc/cloud-services/services/attendant/controllers"
"encoding/json"
"fmt"
"testing"
)
func TestParsing(t *testing.T) {
ecc := []byte(`{"ecu":"ECC","dtc":1719200,"status":9,"snapshot":"AQTv9ucIARIKNO/3AADv+AAAAADv+TLI"}`)
icc := []byte(`{"ecu":"ICC","dtc":14302599,"status":9,"snapshot":"AQUFAAHv+AAAAADv+S2W7/cAAO/2B+cIChAB"}`)
gw := []byte(`{"ecu":"GW","dtc":10718486,"status":8,"snapshot":"AQTv+QLA7/cAAO/2CAcHFAMs7/gAAAAA"}`)
mcu := []byte(`{"ecu":"MCU","dtc":14123795,"status":47,"snapshot":"AQTv9ggHCRcBD+/3AADv+AAAAADv+TLI"}`)
check := func(ecu []byte) {
var entry controllers.DTCEntry
err := json.Unmarshal(ecu, &entry)
if err != nil {
t.Error(err)
}
entry.ParseSnapshot()
if entry.Mileage != 0 {
t.Errorf("Incorrect Mileage %d", entry.Mileage)
}
fmt.Println(entry.Timestamp.Unix())
switch entry.ECU {
case "ECC":
if entry.Timestamp.Unix() != 8730871852 {
t.Errorf("Incorrect Timestamp %d", entry.Timestamp.Unix())
}
if entry.Voltage != 13 {
t.Errorf("Incorrect Voltage %d", entry.Voltage)
}
case "ICC":
if entry.Timestamp.Unix() != 1670580961 {
t.Errorf("Incorrect Timestamp %d", entry.Timestamp.Unix())
}
if entry.Voltage != 11 {
t.Errorf("Incorrect Voltage %d", entry.Voltage)
}
case "GW":
if entry.Timestamp.Unix() != 1691525024 {
t.Errorf("Incorrect Timestamp %d", entry.Timestamp.Unix())
}
case "MCU":
if entry.Timestamp.Unix() != 1691708475 {
t.Errorf("Incorrect Timestamp %d", entry.Timestamp.Unix())
}
}
}
check(ecc)
check(icc)
check(gw)
check(mcu)
}

View File

@@ -0,0 +1,81 @@
package controllers
import (
"encoding/json"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/pkg/errors"
)
// Mega ducky repeat of /handlers/get_file_keys. Need a code re-org
func GetFileKeys(db *services.DB, device common.Device, id string, data []byte) error {
logger.Debug().Msgf("GetFileKeys %v %s", device, id)
var err error
var req *common.FileKeysRequest
client := services.RedisClientPool().GetFromPool()
defer client.Close()
req, err = parseGetFileKeysRequest(data)
if err != nil {
notifyFileKeysGeneralError(client, device, id, err)
return err
}
keys, err := cache.RetrieveFileEncryptionParams(client, db.GetFileKeys(), req.FileIDs)
if err != nil {
notifyFileKeysGeneralError(client, device, id, err)
return err
}
err = client.SafePublishMessage(device.Key(id), common.Message{
Handler: "filekeys",
Data: keys,
})
if err != nil {
return err
}
logger.Debug().Msgf("GetFileKeys sent %v %s", device, id)
return nil
}
func parseGetFileKeysRequest(data []byte) (*common.FileKeysRequest, error) {
var status common.FileKeysRequest
err := json.Unmarshal(data, &status)
if err != nil {
return nil, errors.WithStack(err)
}
err = validator.ValidateStruct(status)
if err != nil {
return &status, errors.WithStack(err)
}
return &status, nil
}
func notifyFileKeysGeneralError(client redis.Client, device common.Device, id string, err error) {
e := client.SafePublishMessage(device.Key(id), common.Message{
Handler: "filekeys",
Data: []common.FileKeyResponse{
{
FileID: "0",
Error: err.Error(),
},
},
})
if e != nil {
logger.Error().Err(errors.WithStack(e)).Send()
}
}

View File

@@ -0,0 +1,59 @@
package controllers
import (
"time"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/health"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/pkg/errors"
)
var mismatchTypeError = errors.New("mismatch type error")
func HealthCheck() {
redis := health.NewRedisHealth(services.RedisClientPool())
server := health.HealthCheckServer{}
err := server.Serve([]health.Config{
{
Name: "db",
Check: health.NewPostgresCheck(services.GetDB().GetDBClient().GetConn()),
Timeout: time.Second * 1,
},
{
Name: "redis",
Check: redis.Check,
Timeout: time.Second * 1,
},
{
Name: "kafka",
Check: health.NewKafkaMultiCheck(getKafkaConsumer),
Timeout: time.Second * 1,
Vital: true,
},
})
if err != nil {
logger.Error().Err(err).Send()
}
}
func getKafkaConsumer() (connections []health.KafkaConnCheckInterface, err error) {
client, oldClient, err := services.GetKafkaConsumer()
if err != nil {
return connections, err
}
conn, ok := client.(health.KafkaConnCheckInterface)
if !ok {
return nil, errors.WithStack(mismatchTypeError)
}
connections = append(connections, conn)
oldConn, ok := oldClient.(health.KafkaConnCheckInterface)
if !ok {
return connections, errors.WithStack(mismatchTypeError)
}
connections = append(connections, oldConn)
return connections, nil
}

View File

@@ -0,0 +1,403 @@
package controllers
import (
"encoding/json"
"fmt"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/carcommand"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/common/manifestfingerprintparams"
"github.com/fiskerinc/cloud-services/pkg/grpc/sms"
"github.com/fiskerinc/cloud-services/pkg/hwversion"
"github.com/fiskerinc/cloud-services/pkg/kafka"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/manifestsender"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/tmobile"
uhelpers "github.com/fiskerinc/cloud-services/pkg/usecase_helpers"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"github.com/fiskerinc/cloud-services/pkg/utils/randomvalues"
"github.com/fiskerinc/cloud-services/pkg/utils/whereami"
"github.com/fiskerinc/cloud-services/pkg/validator"
vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig"
"google.golang.org/protobuf/proto"
"sync"
"time"
"github.com/pkg/errors"
)
// 0100084000000101012200010101010001010101000000000000000000ff7eff7f000101010101000101010100010001010101000101010100000000000000000000000000010101000100000100010101000201010101000101020101000101010200010101010101010101000101000100010001010101010101010101010000000000000100010101020101010101010000000000000000ffffff00010102010102020001010000000000000000000000000000000000000000000000000000000000000000000100202310010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6
var defaultVOD string = envtool.GetEnv("DEFAULT_VOD", "00FF111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111114A")
var fileKeyBypass map[int64]string
var FileKeyBypassManifest_US map[int64]string = map[int64]string{
1189: "fe4f19fbc940ed58",
1380: "6a44fd71c940716d",
}
var FileKeyBypassManifest_EU map[int64]string = map[int64]string{
893: "fe4f19fbc940ed58",
1002: "6a44fd71c940716d",
1012: "d70e1d32c940a221",
}
func init() {
if whereami.Environment == whereami.PRODUCTION_EU {
fileKeyBypass = FileKeyBypassManifest_EU
} else {
fileKeyBypass = FileKeyBypassManifest_US
}
}
func NewManifestSender(
r redis.ClientPoolInterface,
db *services.DB,
conf vconfig.ConfigServiceInterface,
device common.Device,
ka *services.KeepAwake,
seed int64, //This seed is used only for testing
) *ManifestSender {
randomGenerator := randomvalues.NewNonCryptoGenerator("ABCDEFGHIJKLMNOPQRSTUVWXYZ", seed)
return &ManifestSender{
Redis: r,
db: db,
conf: conf,
Device: device,
sms: services.GetSMSClient(),
ka: ka,
gen: randomGenerator,
}
}
type ManifestSender struct {
Redis redis.ClientPoolInterface
db *services.DB
conf vconfig.ConfigServiceInterface
sms sms.SMSServiceClient
Device common.Device
ka *services.KeepAwake
gen randomvalues.NonCryptoGenerator
}
func (m *ManifestSender) Process(id string, data []byte) error {
// id string parameter is unused except in the following log
// the data might not be used either, just for its single carUpdateID
u, err := m.parseRequest(data)
if err != nil {
logger.Err(err).Msgf("Manifest sender, unable to process update %s", id)
return err
}
carUpdate := common.CarUpdate{ID: u.CarUpdateID}
err = m.loadCarUpdate(&carUpdate)
if err != nil {
return err
}
logger.Info().Msgf("ManifestSender car_update_id %d", carUpdate.ID)
carUpdate.UpdateManifest.CarUpdateID = carUpdate.ID
helper := uhelpers.NewECUKeys(m.db.GetECCKeys())
err = helper.AddECUECCKeys(carUpdate.UpdateManifest)
if err != nil {
logger.Err(err).Msgf("Unable to decrypt keys for %d", carUpdate.ID)
return err //comment out for local debugging
}
// If we fail to deliver the sms, then we don't believe the car has awoken
// then set the manifest as failed
err = m.send(carUpdate.VIN, carUpdate.UpdateManifest)
if err != nil {
update := NewCarUpdateProgress(m.Redis, m.ka, m.db, common.TRex)
err = update.ProcessStatus(carUpdate.VIN, common.CarUpdateProgress{
CarUpdateID: u.CarUpdateID,
Status: carupdatestatus.ManifestCanceled,
Info: " Underlying error: " + err.Error(),
})
}
return err
}
func (m *ManifestSender) destination(manifest *common.UpdateManifest) string {
if manifest.HasSelfDownload() {
return "ICC"
} else {
return "ICC/TBOX"
}
}
func (m *ManifestSender) parseRequest(data []byte) (*common.UpdateManifest, error) {
var req common.UpdateManifest
err := json.Unmarshal(data, &req)
if err != nil {
return nil, errors.WithStack(err)
}
err = validator.ValidateIDField(req.CarUpdateID)
if err != nil {
return &req, errors.WithStack(err)
}
return &req, nil
}
func (m *ManifestSender) loadCarUpdate(cu *common.CarUpdate) error {
err := m.db.GetCarUpdates().Load(cu)
if err != nil {
return errors.WithStack(err)
}
if cu.UpdateManifest == nil {
return nil
}
return nil
}
func (m *ManifestSender) queueManifest(vin string, manifest *common.UpdateManifest) error {
loggedManifest := manifest.Copy()
loggedManifest.RemoveECCKeysFromECUs()
logger.At(logger.Info(), common.HMI.Key(vin), "update").Interface("manifest.json", loggedManifest).Msgf("HMI ManifestSender sent %v %s %d", common.HMI, vin, manifest.CarUpdateID)
client := m.Redis.GetFromPool()
defer client.Close()
err := client.SafeQueueMessage(common.HMI.Key(vin), common.Message{
Handler: "update_manifest",
Data: manifest,
})
if err != nil {
logger.Err(err).Msgf("failed to queue manifest in redis vin %s ", vin)
}
return err
}
func (m *ManifestSender) send(vin string, manifest *common.UpdateManifest) error {
updateManifestID := manifest.ID
manifest.TransformECUNames()
err := hwversion.SetHWVersion(manifest, vin, m.db.GetCars())
if err != nil {
logger.Err(err).Msgf("manifest sender failed at SetHwVersion for vin %s", vin)
return err //comment out for local debugging
}
manifest.SortECUs()
manifest.RemoveOriginalS19HexFiles()
manifest.RemoveOriginalS19HexFilesRollbacks()
manifest.FilterCompatibleECUs(vin)
err = uhelpers.PopulateECUsCurrentVersion(m.db.GetCars(), vin, manifest.ECUs)
if err != nil {
logger.Err(err).Msgf("manifest sender failed at PupulateECUsCurrentVersion for vin %s", vin)
return err
}
fpparams := manifestfingerprintparams.GetFPParams()
manifest.GenerateFingerprint(fpparams.CurTime(), fpparams.ManifestSerial())
hmiManifest := manifest.Copy()
cds, err := m.addVOD(hmiManifest, vin)
if err != nil {
logger.Err(err).Msgf("manifest sender failed at adding the vod for vin %s", vin)
return err // comment out for local debugging
}
err = validator.ValidateField(hmiManifest.SUMS, "sums_version")
if err == nil {
err = hmiManifest.AddSUMSToVOD()
if err != nil {
logger.Err(err).Msgf("manifest sender failed at AddSUMSToVOD for vin %s", vin)
return err
}
} else {
logger.Err(err).Msgf("manifest sender failed at Validation for vin %s", vin)
return err
}
hmiManifest.Scrub(common.HMI)
client := m.Redis.GetFromPool()
defer client.Close()
// If HasSelfDownload, we are not yet ready to send to t box, so we wake the car if the update is forced
if manifest.HasSelfDownload() && m.sms != nil {
// The function that calls this one handles canceling the manifest
var msgID string
res, err := carcommand.QueueSMSWakeUp(vin, true, client, m.db.GetCars(), m.sms)
if err != nil || res == nil || !res.SentSuccessful {
logger.Err(err).Msgf("Attendant:manifest sender failed at sendSMSWakeUp for vin %s", vin)
msgID = m.gen.GetString(10)
defer m.selfKafkaCallback(msgID)
} else {
msgID = res.SmsMsgID
}
err = m.ka.SendFirstKeepAwakeMessage(vin)
if err != nil {
logger.Err(err).Msgf("failed to SendFirstKeepAwakeMessage on msgID %s", msgID)
}
m.queueManifest(vin, hmiManifest)
fileID, ok := fileKeyBypass[updateManifestID]
if ok {
logger.Info().Str("VIN", vin).Msg("Queueing SendFileKeys")
time.AfterFunc(time.Second*3, func() { sendFileKeys(vin, fileID) })
}
// Do not send manifest to TBOX, wait until ICC has finished downloading
return err
}
trex := manifestsender.NewTBOXManifestSender(client, m.conf, m.db, services.GetSMSClient(), cds)
defer trex.Close()
//smsID, err := trex.ProcessSoftwareUpdate(vin, manifest)
_, err = trex.ProcessSoftwareUpdate(vin, manifest, services.GetDB().GetCarConfigData())
logger.Debug().Msgf("Send HMI manifest to %s ", vin)
m.queueManifest(vin, hmiManifest)
// We managed to send an sms, so we want to continue with an sms delivered check
// if smsID != "" {
// err = m.setManifestCache(smsID, ManifestCachedPoint{
// VIN: vin,
// ManifestID: manifest.ID,
// Destination: m.destination(manifest),
// })
// if err != nil {
// return err
// }
// }
return err
}
// So this flow of continue will be activated at two different times, possible multiple times.
// 1) When a new car_update comes through and the .hasdownload is true, and then again when the manifest is being sent to the tbox
func (m *ManifestSender) ContinueTBOXSend(id string, data []byte) (err error) {
// After a text has been succesfully delivered, we can now continue the update
// Check the status and make sure that is is success, otherwise fail and set the update status as failed
var msgStatus tmobile.MessageStatus
err = json.Unmarshal(data, &msgStatus)
if err != nil {
return
}
return nil
}
func (m *ManifestSender) setManifestCache(msgID string, cache ManifestCachedPoint) (err error) {
client := m.Redis.GetFromPool()
defer client.Close()
return client.Set(msgIDToRedisKey(msgID), cache)
}
type ManifestCachedPoint struct {
VIN string
ManifestID int64
Destination string
}
func msgIDToRedisKey(msgID string) (redisKey string) {
return fmt.Sprintf("manifest_tbox_send_cache:%s", msgID)
}
func (m *ManifestSender) addVOD(manifest *common.UpdateManifest, vin string) (map[string]string, error) {
cds, err := uhelpers.GetCDS(m.conf, services.GetDB().GetCarConfigData(), vin)
if err != nil {
return nil, err
}
if vod, ok := cds["VOD"]; ok && vod != "" {
manifest.VOD = vod
} else {
manifest.VOD = defaultVOD
}
return cds, nil
}
// When car is a virtual trex, we are going to produce to the kafka message queue as if the sms service did it
func (m *ManifestSender) selfKafkaCallback(messageID string) {
producer, err := services.GetKafkaProducer()
if err != nil {
logger.Err(err).Send()
return
}
messageStatus := &kafka_grpc.GRPC_AttendantPayload_MessageStatus{
MessageStatus: &kafka_grpc.MessageStatus{
MessageId: messageID,
Status: kafka_grpc.EmumStatus_DELIVERED,
},
}
kafkaMSG := kafka_grpc.GRPC_AttendantPayload{
Handler: "sms_delivery_status_manifest",
Data: messageStatus,
}
binaryPayload, _ := proto.Marshal(&kafkaMSG)
err = producer.ProduceBinary(kafka.AttendantServiceGRPCKafka, "4:Service", binaryPayload, nil)
if err != nil {
err = errors.WithStack(err)
logger.Err(err).Msgf("failed to produce kafka message for sms id %s", messageID)
}
}
func (m *ManifestSender) Release() {
m.Redis = nil
m.db = nil
m.conf = nil
}
var fpParams FingerprintParamer
var fpParamsOnce sync.Once
func SetFPParams(fpp FingerprintParamer) {
fpParams = fpp
}
func GetFPParams() FingerprintParamer {
fpParamsOnce.Do(func() {
if fpParams == nil {
fpParams = &fingerprintParams{
serialNum: envtool.GetEnv("OTA_MANIFEST_SERIAL", "00000000000000000"),
}
}
})
return fpParams
}
type fingerprintParams struct {
serialNum string
}
func (p *fingerprintParams) ManifestSerial() string {
return p.serialNum
}
func (p *fingerprintParams) CurTime() time.Time {
return time.Now().UTC()
}
type FingerprintParamer interface {
ManifestSerial() string
CurTime() time.Time
}
// Mega hack mode
func sendFileKeys(vin string, fileID string) {
logger.Info().Str("VIN", vin).Msg("Sending SendFileKeys")
db := services.GetDB()
err := GetFileKeys(db, common.HMI, vin, []byte(`{"file_ids": ["`+fileID+`"]}`))
if err != nil {
logger.Err(err).Str("VIN", vin).Str("file ID", fileID).Msg("Failed to SendFileKeys")
}
}

115
services/attendant/go.mod Normal file
View File

@@ -0,0 +1,115 @@
module github.com/fiskerinc/cloud-services/services/attendant
go 1.25
toolchain go1.25.0
require (
github.com/fiskerinc/cloud-services/pkg v0.0.0-00010101000000-000000000000
github.com/fiskerinc/cloud-services/pkg/can-go v0.0.0-00010101000000-000000000000
github.com/pkg/errors v0.9.1
)
require (
github.com/go-pg/pg/v10 v10.11.1
github.com/gomodule/redigo v1.8.9
github.com/jinzhu/copier v0.3.5
github.com/stretchr/testify v1.10.0
google.golang.org/grpc v1.67.3
google.golang.org/protobuf v1.36.1
)
require (
github.com/DataDog/appsec-internal-go v1.4.0 // indirect
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 // indirect
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 // indirect
github.com/DataDog/datadog-go/v5 v5.3.0 // indirect
github.com/DataDog/go-libddwaf/v2 v2.2.3 // indirect
github.com/DataDog/go-tuf v1.0.2-0.5.2 // indirect
github.com/DataDog/sketches-go v1.4.2 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect
github.com/albenik/bcd v0.0.0-20170831201648-635201416bc7 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.5.2 // indirect
github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-jose/go-jose/v4 v4.1.0 // indirect
github.com/go-pg/zerochecker v0.2.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.15.1 // indirect
github.com/golang/mock v1.7.0-rc.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/schema v1.2.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/jeremywohl/flatten v1.0.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.25.0 // indirect
github.com/outcaste-io/ristretto v0.2.3 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/redis/go-redis/v9 v9.5.1 // indirect
github.com/rs/zerolog v1.29.1 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/twmb/franz-go v1.20.6 // indirect
github.com/twmb/franz-go/pkg/kadm v1.17.2 // indirect
github.com/twmb/franz-go/pkg/kmsg v1.12.0 // indirect
github.com/vmihailenco/bufpool v0.1.11 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser v0.1.2 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
gopkg.in/DataDog/dd-trace-go.v1 v1.60.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect
mellium.im/sasl v0.3.1 // indirect
)
replace (
github.com/fiskerinc/cloud-services/pkg => ../../pkg
github.com/fiskerinc/cloud-services/pkg/can-go => ../../pkg/can-go
)

483
services/attendant/go.sum Normal file
View File

@@ -0,0 +1,483 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/appsec-internal-go v1.4.0 h1:KFI8ElxkJOgpw+cUm9TXK/jh5EZvRaWM07sXlxGg9Ck=
github.com/DataDog/appsec-internal-go v1.4.0/go.mod h1:ONW8aV6R7Thgb4g0bB9ZQCm+oRgyz5eWiW7XoQ19wIc=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8=
github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0/go.mod h1:HzySONXnAgSmIQfL6gOv9hWprKJkx8CicuXuUbmgWfo=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 h1:5nE6N3JSs2IG3xzMthNFhXfOaXlrsdgqmJ73lndFf8c=
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1/go.mod h1:Vc+snp0Bey4MrrJyiV2tVxxJb6BmLomPvN1RgAvjGaQ=
github.com/DataDog/datadog-go/v5 v5.3.0 h1:2q2qjFOb3RwAZNU+ez27ZVDwErJv5/VpbBPprz7Z+s8=
github.com/DataDog/datadog-go/v5 v5.3.0/go.mod h1:XRDJk1pTc00gm+ZDiBKsjh7oOOtJfYfglVCmFb8C2+Q=
github.com/DataDog/go-libddwaf/v2 v2.2.3 h1:LpKE8AYhVrEhlmlw6FGD41udtDf7zW/aMdLNbCXpegQ=
github.com/DataDog/go-libddwaf/v2 v2.2.3/go.mod h1:8nX0SYJMB62+fbwYmx5J7zuCGEjiC/RxAo3+AuYJuFE=
github.com/DataDog/go-tuf v1.0.2-0.5.2 h1:EeZr937eKAWPxJ26IykAdWA4A0jQXJgkhUjqEI/w7+I=
github.com/DataDog/go-tuf v1.0.2-0.5.2/go.mod h1:zBcq6f654iVqmkk8n2Cx81E1JnNTMOAx1UEO/wZR+P0=
github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4=
github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM=
github.com/DataDog/sketches-go v1.4.2 h1:gppNudE9d19cQ98RYABOetxIhpTCl4m7CnbRZjvVA/o=
github.com/DataDog/sketches-go v1.4.2/go.mod h1:xJIXldczJyyjnbDop7ZZcLxJdV3+7Kra7H1KMgpgkLk=
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ReneKroon/ttlcache/v2 v2.11.0 h1:OvlcYFYi941SBN3v9dsDcC2N8vRxyHcCmJb3Vl4QMoM=
github.com/ReneKroon/ttlcache/v2 v2.11.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY=
github.com/albenik/bcd v0.0.0-20170831201648-635201416bc7 h1:m3Ayfs5OcAlIMEdLIQKubBsVLGee4YMUr14+d1256WE=
github.com/albenik/bcd v0.0.0-20170831201648-635201416bc7/go.mod h1:QIAMbrwsnQZ2ES3G26RubSrDB5SPyzsp9Hts5NJdTrI=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 h1:icCHutJouWlQREayFwCc7lxDAhws08td+W3/gdqgZts=
github.com/confluentinc/confluent-kafka-go/v2 v2.3.0/go.mod h1:/VTy8iEpe6mD9pkCH5BhijlUl8ulUXymKv1Qig5Rgb8=
github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXTLTM2Pg=
github.com/containerd/containerd v1.7.0/go.mod h1:QfR7Efgb/6X2BDpTPJRvPTYDE9rsF0FsXX9J8sIs/sc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v23.0.4+incompatible h1:Kd3Bh9V/rO+XpTP/BLqM+gx8z7+Yb0AA2Ibj+nNo4ek=
github.com/docker/docker v23.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/ebitengine/purego v0.5.2 h1:r2MQEtkGzZ4LRtFZVAg5bjYKnUbxxloaeuGxH0t7qfs=
github.com/ebitengine/purego v0.5.2/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/elliotchance/orderedmap/v2 v2.2.0 h1:7/2iwO98kYT4XkOjA9mBEIwvi4KpGB4cyHeOFOnj4Vk=
github.com/elliotchance/orderedmap/v2 v2.2.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/go-jose/go-jose/v4 v4.1.0 h1:cYSYxd3pw5zd2FSXk2vGdn9igQU2PS8MuxrCOCl0FdY=
github.com/go-jose/go-jose/v4 v4.1.0/go.mod h1:GG/vqmYm3Von2nYiB2vGTXzdoNKE5tix5tuc6iAd+sw=
github.com/go-pg/pg/v10 v10.11.1 h1:vYwbFpqoMpTDphnzIPshPPepdy3VpzD8qo29OFKp4vo=
github.com/go-pg/pg/v10 v10.11.1/go.mod h1:ExJWndhDNNftBdw1Ow83xqpSf4WMSJK8urmXD5VXS1I=
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=
github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 h1:E/LAvt58di64hlYjx7AsNS6C/ysHWYo+2qPCZKTQhRo=
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs=
github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo=
github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=
github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8=
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
github.com/opencontainers/runc v1.1.6 h1:XbhB8IfG/EsnhNvZtNdLB0GBw92GYEFvKlhaJk9jUgA=
github.com/opencontainers/runc v1.1.6/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0=
github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8=
github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA=
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg=
github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI=
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/testcontainers/testcontainers-go v0.14.0 h1:h0D5GaYG9mhOWr2qHdEKDXpkce/VlvaYOCzTRi6UBi8=
github.com/testcontainers/testcontainers-go v0.14.0/go.mod h1:hSRGJ1G8Q5Bw2gXgPulJOLlEBaYJHeBSOkQM5JLG+JQ=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/twmb/franz-go v1.20.6 h1:TpQTt4QcixJ1cHEmQGPOERvTzo99s8jAutmS7rbSD6w=
github.com/twmb/franz-go v1.20.6/go.mod h1:u+FzH2sInp7b9HNVv2cZN8AxdXy6y/AQ1Bkptu4c0FM=
github.com/twmb/franz-go/pkg/kadm v1.17.2 h1:g5f1sAxnTkYC6G96pV5u715HWhxd66hWaDZUAQ8xHY8=
github.com/twmb/franz-go/pkg/kadm v1.17.2/go.mod h1:ST55zUB+sUS+0y+GcKY/Tf1XxgVilaFpB9I19UubLmU=
github.com/twmb/franz-go/pkg/kmsg v1.12.0 h1:CbatD7ers1KzDNgJqPbKOq0Bz/WLBdsTH75wgzeVaPc=
github.com/twmb/franz-go/pkg/kmsg v1.12.0/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY=
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
go4.org/intern v0.0.0-20230525184215-6c62f75575cb h1:ae7kzL5Cfdmcecbh22ll7lYP3iuUdnfnhiPcSaDgH/8=
go4.org/intern v0.0.0-20230525184215-6c62f75575cb/go.mod h1:Ycrt6raEcnF5FTsLiLKkhBTO6DPX3RCUCUVnks3gFJU=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 h1:lGdhQUN/cnWdSH3291CUuxSEqc+AsGTiDxPP3r2J0l4=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/DataDog/dd-trace-go.v1 v1.60.1 h1:Sqkq62MxQW/RD+sgZsQuUdHWHyXI4JS5x0lxlxrv2Hk=
gopkg.in/DataDog/dd-trace-go.v1 v1.60.1/go.mod h1:6aArYrAHjnuaofJ3lKuSRQbhrBx1LcSpiEYCIScJE5Y=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/gotraceui v0.2.0 h1:dmNsfQ9Vl3GwbiVD7Z8d/osC6WtGGrasyrC2suc4ZIQ=
honnef.co/go/gotraceui v0.2.0/go.mod h1:qHo4/W75cA3bX0QQoSvDjbJa4R8mAyyFjbWAj63XElc=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a h1:1XCVEdxrvL6c0TGOhecLuB7U9zYNdxZEjvOqJreKZiM=
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a/go.mod h1:e83i32mAQOW1LAqEIweALsuK2Uw4mhQadA5r7b0Wobo=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=

View File

@@ -0,0 +1,26 @@
package handlers
import (
"github.com/fiskerinc/cloud-services/services/attendant/controllers"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/pkg/errors"
)
func CarUpdateProgressStatus(db *services.DB, ka *services.KeepAwake, device common.Device, id string, data []byte) error {
logger.Debug().Msgf("CarUpdateProgressStatus %v %s", device, id)
clientPool := services.RedisClientPool()
handler := controllers.NewCarUpdateProgress(clientPool, ka, db, device)
if handler == nil {
return errors.Errorf("NewCarUpdateProgress cannot handle device %v", device)
}
defer handler.Dispose()
err := handler.Process(id, data)
return err
}

View File

@@ -0,0 +1,389 @@
package handlers_test
import (
"fmt"
"testing"
"github.com/fiskerinc/cloud-services/services/attendant/handlers"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestCarUpdateProgressFunctional(t *testing.T) {
t.Skip()
testVIN := "WBSEH93466B798124"
conn := tester.NewRedisMock()
db := services.GetDB()
manifest, carUpdateID, err := setupCarUpdateProgressFunc(db, testVIN)
if err != nil {
panic(err)
}
defer func() {
if carUpdateID > 0 {
db.GetCarUpdates().Delete(&common.CarUpdate{ID: carUpdateID})
conn.Delete(redis.CarUpdateStatusTBOXHashKey(carUpdateID), redis.CarUpdateStatusHMIHashKey(carUpdateID))
}
if manifest != nil && manifest.ID > 0 {
db.GetUpdateManifests().Delete(manifest)
}
}()
ka := services.NewKeepAwakeService()
ka.SetService(&services.MockKeepAwakeImplementation{})
type testCase struct {
Name string
Device common.Device
Payload string
ExpectedMsg string
ExpectInstalled int
ExpectInstallTotal int
ExpectDBStatus string
ExpectDownloadCurrent uint64
ExpectDownloadTotal uint64
ExpectErrorCode int
}
tests := []testCase{
{
Name: "manifest_received",
Device: common.TRex,
Payload: fmt.Sprintf(`{
"car_update_id": %d,
"msg": "manifest_received"
}`, carUpdateID),
ExpectedMsg: "manifest_received",
ExpectDBStatus: "manifest_received",
},
{
Name: "install_approval_await",
Device: common.TRex,
Payload: fmt.Sprintf(`{
"car_update_id": %d,
"msg": "install_approval_await"
}`, carUpdateID),
ExpectedMsg: "install_approval_await",
ExpectDBStatus: "install_approval_await",
},
{
Name: "other error",
Device: common.TRex,
Payload: fmt.Sprintf(`{
"car_update_id": %d,
"ecu": "TEST",
"installed": 3,
"total_files": 10,
"msg": "other error",
"err": -100
}`, carUpdateID),
ExpectedMsg: "other error",
ExpectDBStatus: "other error",
ExpectInstalled: 3,
ExpectInstallTotal: 10,
ExpectErrorCode: -100,
},
{
Name: "download_start",
Device: common.TRex,
Payload: fmt.Sprintf(`{
"car_update_id": %d,
"ecu": "TEST",
"file_current": 0,
"file_total": 100,
"package_current": 0,
"package_total": 100,
"msg": "download_start",
"err": 0
}`, carUpdateID),
ExpectedMsg: "downloading",
ExpectDBStatus: "package_download_start",
ExpectDownloadCurrent: 0,
ExpectDownloadTotal: 100,
},
{
Name: "downloading",
Device: common.TRex,
Payload: fmt.Sprintf(`{
"car_update_id": %d,
"ecu": "TEST",
"file_current": 0,
"file_total": 100,
"package_current": 30,
"package_total": 100,
"msg": "downloading",
"err": 0
}`, carUpdateID),
ExpectedMsg: "downloading",
ExpectDBStatus: "package_download_start",
ExpectDownloadCurrent: 30,
ExpectDownloadTotal: 100,
},
{
Name: "download_complete",
Device: common.TRex,
Payload: fmt.Sprintf(`{
"car_update_id": %d,
"ecu": "TEST",
"file_current": 100,
"file_total": 100,
"package_current": 900,
"package_total": 1000,
"msg": "download_complete",
"err": 0
}`, carUpdateID),
ExpectedMsg: "downloading",
ExpectDBStatus: "package_download_start",
ExpectDownloadCurrent: 900,
ExpectDownloadTotal: 1000,
},
{
Name: "package_download_complete",
Device: common.TRex,
Payload: fmt.Sprintf(`{
"car_update_id": %d,
"ecu": "TEST",
"file_current": 100,
"file_total": 100,
"package_current": 1000,
"package_total": 1000,
"msg": "download_complete",
"err": 0
}`, carUpdateID),
ExpectedMsg: "package_download_complete",
ExpectDBStatus: "package_download_complete",
ExpectDownloadCurrent: 1000,
ExpectDownloadTotal: 1000,
},
{
Name: "download_error",
Device: common.TRex,
Payload: fmt.Sprintf(`{
"car_update_id": %d,
"ecu": "TEST",
"file_current": 0,
"file_total": 100,
"package_current": 0,
"package_total": 1000,
"msg": "download_error",
"err": 0
}`, carUpdateID),
ExpectedMsg: "download_error",
ExpectDBStatus: "download_error",
ExpectDownloadCurrent: 0,
ExpectDownloadTotal: 1000,
},
{
Name: "install_start",
Device: common.TRex,
Payload: fmt.Sprintf(`{
"car_update_id": %d,
"ecu": "TEST",
"installed": 0,
"total_files": 10,
"msg": "install_start",
"err": 0
}`, carUpdateID),
ExpectedMsg: "installing",
ExpectDBStatus: "package_install_start",
ExpectInstalled: 0,
ExpectInstallTotal: 10,
},
{
Name: "installing",
Device: common.TRex,
Payload: fmt.Sprintf(`{
"car_update_id": %d,
"ecu": "TEST",
"installed": 2,
"total_files": 10,
"msg": "installing",
"err": 0
}`, carUpdateID),
ExpectedMsg: "installing",
ExpectDBStatus: "package_install_start",
ExpectInstalled: 2,
ExpectInstallTotal: 10,
},
{
Name: "install_complete",
Device: common.TRex,
Payload: fmt.Sprintf(`{
"car_update_id": %d,
"ecu": "TEST",
"installed": 9,
"total_files": 10,
"msg": "install_complete",
"err": 0
}`, carUpdateID),
ExpectedMsg: "installing",
ExpectDBStatus: "package_install_start",
ExpectInstalled: 9,
ExpectInstallTotal: 10,
},
{
Name: "package_install_complete",
Device: common.TRex,
Payload: fmt.Sprintf(`{
"car_update_id": %d,
"ecu": "TEST",
"installed": 10,
"total_files": 10,
"msg": "install_complete",
"err": 0
}`, carUpdateID),
ExpectedMsg: "package_install_complete",
ExpectDBStatus: "package_install_complete",
ExpectInstalled: 10,
ExpectInstallTotal: 10,
},
{
Name: "install_error",
Device: common.TRex,
Payload: fmt.Sprintf(`{
"car_update_id": %d,
"ecu": "TEST",
"installed": 3,
"total_files": 10,
"msg": "install_error",
"err": 0
}`, carUpdateID),
ExpectedMsg: "install_error",
ExpectDBStatus: "install_error",
ExpectInstalled: 3,
ExpectInstallTotal: 10,
},
{
Name: "package_download_complete",
Device: common.HMI,
Payload: fmt.Sprintf(`{
"car_update_id":%d,
"ecu":"ICC",
"file_current":null,
"file_total":null,
"package_current":920639485,
"package_total":920639485,
"installed":null,
"total_files":null,
"msg":"package_download_complete",
"err":null
}`, carUpdateID),
ExpectedMsg: "package_download_complete",
ExpectDBStatus: "package_download_complete",
ExpectInstalled: 3,
ExpectInstallTotal: 10,
ExpectDownloadCurrent: 920639485,
ExpectDownloadTotal: 920639485,
},
}
keys := make([]string, 1)
statuses := make([]interface{}, 1)
for _, test := range tests {
err := handlers.CarUpdateProgressStatus(db, ka, test.Device, testVIN, []byte(test.Payload))
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s output error", test.Device, test.Name), nil, err)
}
status := common.CarUpdateProgress{}
keys[0] = redis.CarUpdateStatusHashKey(carUpdateID)
statuses[0] = &status
err = conn.GetObjectsMulti(keys, statuses)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "GetObjectsMulti", nil, err)
return
}
if status.Status != test.ExpectedMsg {
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s Status", test.Device, test.Name), test.ExpectedMsg, status.Status)
}
if status.InstalledFiles != test.ExpectInstalled {
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s InstalledFiles", test.Device, test.Name), test.ExpectInstalled, status.InstalledFiles)
}
if status.TotalFiles != test.ExpectInstallTotal {
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s TotalFiles", test.Device, test.Name), test.ExpectInstallTotal, status.TotalFiles)
}
if status.PackageCurrent != test.ExpectDownloadCurrent {
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s PackageCurrent", test.Device, test.Name), test.ExpectDownloadCurrent, status.PackageCurrent)
}
if status.PackageTotal != test.ExpectDownloadTotal {
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s PackageTotal", test.Device, test.Name), test.ExpectDownloadTotal, status.PackageTotal)
}
if status.ErrorCode != test.ExpectErrorCode {
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s ErrorCode", test.Device, test.Name), test.ExpectErrorCode, status.ErrorCode)
}
cu, err := db.GetCarUpdates().SelectByID(carUpdateID)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "Get from DB", nil, err)
return
}
if cu.Status != test.ExpectDBStatus {
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s DB Status", test.Device, test.Name), test.ExpectDBStatus, cu.Status)
}
if cu.ErrorCode != test.ExpectErrorCode {
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("[%v] %s DB ErrorCode", test.Device, test.Name), test.ExpectErrorCode, cu.ErrorCode)
}
}
}
func setupCarUpdateProgressFunc(db *services.DB, vin string) (*common.UpdateManifest, int64, error) {
_, err := db.GetCars().SelectOrInsert(&common.Car{
VIN: vin,
Model: "Ocean",
Year: 2022,
Trim: "Sport",
})
if err != nil {
return nil, 0, err
}
manifest := common.UpdateManifest{
Name: "TestCarUpdateProgressFunctional",
Version: "1000",
Description: "For TestCarUpdateProgressFunctional",
ReleaseNotes: "http://releasenotes.com",
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
BodyType: "truck",
}
_, err = db.GetUpdateManifests().Insert(&manifest)
if err != nil {
return nil, 0, err
}
_, err = db.GetUpdateManifests().ECUInsert(&common.UpdateManifestECU{
UpdateManifestID: manifest.ID,
ECU: "ICC",
Version: "ICCVERSION",
})
if err != nil {
return nil, 0, err
}
_, err = db.GetUpdateManifests().ECUInsert(&common.UpdateManifestECU{
UpdateManifestID: manifest.ID,
ECU: "ADAS",
Version: "ADASVERSION",
})
if err != nil {
return nil, 0, err
}
carupdate := common.CarUpdate{
VIN: vin,
UpdateManifestID: manifest.ID,
}
_, err = db.GetCarUpdates().Insert(&carupdate)
if err != nil {
return nil, 0, err
}
return &manifest, carupdate.ID, nil
}

View File

@@ -0,0 +1,776 @@
package handlers_test
import (
"errors"
"fmt"
"net/http"
"strings"
"testing"
"time"
"github.com/fiskerinc/cloud-services/services/attendant/controllers"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/manifestfingerprintparams"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig"
)
var (
schemaToTRex = "file://" + th.GetSchemaDirPath() + "/trex/RXMessage.json"
schemaToHMI = "file://" + th.GetSchemaDirPath() + "/hmi/RXMessage.json"
)
func TestCarUpdateProgress(t *testing.T) {
testGetSetResult := `["valid-cognito-id-1","valid-cognito-id-2"]`
testVIN := "JH4KA7680RC01"
mobile1Key := "3:valid-cognito-id-1"
mobile2Key := "3:valid-cognito-id-2"
hmiKey := "2:JH4KA7680RC01"
trexKey := common.TRex.Key(testVIN)
carupdateKey := "carupdate:297"
var bhex common.BinaryHex
expectedExpire := 3600
bhex = []byte("test")
fingerprintTime, _ := time.Parse("02/01/06", "19/01/24")
fpp := manifestfingerprintparams.MockFingerprintParamer{
ManifestSerialValue: "00000000000000000",
Time: fingerprintTime,
}
manifestfingerprintparams.SetFPParams(&fpp)
manifest := common.UpdateManifest{
ID: 1,
Name: "test",
Version: "MANIFEST_VERSION",
SUMS: "2023.10.01.00.E",
Description: "description",
ReleaseNotes: "http://releasenotes.com",
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
BodyType: "truck",
RollbackEnabled: true,
Type: "standard",
ECUs: []*common.UpdateManifestECU{
{
ECU: "ICC",
Version: "version",
HWVersions: []string{"hardware_version"},
Mode: "D",
SelfDownload: true,
Files: []*common.UpdateManifestFile{
{
FileID: "fileid",
URL: "http://download.com",
Filename: "filename.bin",
FileSize: 10000,
FileType: common.Software,
WriteRegionID: 2222,
WriteRegion: common.MemoryRegion{
ID: 2000,
Offset: 10000,
Length: 20,
},
DBModelBase: th.Timestamp,
},
},
DBModelBase: th.Timestamp,
},
{
ECU: "ADAS",
Version: "version",
HWVersions: []string{"hardware_version"},
Mode: "A",
InstallPriority: 10,
Files: []*common.UpdateManifestFile{
{
FileID: "fileid",
URL: "http://download.com",
Filename: "adas.bin",
FileSize: 9999,
FileType: common.Software,
WriteRegionID: 9999,
WriteRegion: common.MemoryRegion{
ID: 8888,
Offset: 8888,
Length: 8888,
},
DBModelBase: th.Timestamp,
},
},
ECCKeys: &common.ECCKeys{
ECU: "ADAS",
PrivKey1: &bhex,
PrivKey2: &bhex,
PrivKey3: &bhex,
PubKey1: &bhex,
PubKey2: &bhex,
PubKey3: &bhex,
},
DBModelBase: th.Timestamp,
},
{
ECU: "ECUA",
Version: "version",
HWVersions: []string{"hardware_version"},
Mode: "A",
InstallPriority: 5,
Files: []*common.UpdateManifestFile{
{
FileID: "fileid",
URL: "http://download.com",
Filename: "adas.bin",
FileSize: 9999,
FileType: common.Software,
WriteRegionID: 9999,
WriteRegion: common.MemoryRegion{
ID: 8888,
Offset: 8888,
Length: 8888,
},
DBModelBase: th.Timestamp,
},
{
FileID: "SHOULD_NOT_BE_IN_UPDATE",
URL: "http://download.com/SHOULD_NOT_BE_IN_UPDATE.bin",
FileSize: 1000,
Checksum: "AAAAAAA",
FileType: common.Calibration,
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
Parsed: elptr.ElPtr(false),
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
DBModelBase: th.Timestamp,
},
{
FileID: "MUST_BE_IN_UPDATE",
URL: "http://download.com/MUST_BE_IN_UPDATE.bin",
FileSize: 1000,
Checksum: "AAAAAAA",
FileType: common.Calibration,
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
Parsed: elptr.ElPtr(true),
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
DBModelBase: th.Timestamp,
},
},
ECCKeys: &common.ECCKeys{
ECU: "ECUA",
PrivKey1: &bhex,
PrivKey2: &bhex,
PrivKey3: &bhex,
PubKey1: &bhex,
PubKey2: &bhex,
PubKey3: &bhex,
},
DBModelBase: th.Timestamp,
},
},
DBModelBase: th.Timestamp,
}
ecuaRollback := []*common.UpdateManifestECU{
{
ID: 100,
UpdateManifestID: 200,
ECU: "ECUA",
Version: "VERSIONOLD",
HWVersions: []string{"hardware_version"},
Mode: "A",
DBModelBase: th.Timestamp,
Files: []*common.UpdateManifestFile{
{
FileID: "FILEIDOLD",
UpdateManifestECUID: 1001,
Filename: "FILENAMEOLD",
URL: "URLOLD",
FileType: common.Software,
WriteRegionID: 700,
WriteRegion: common.MemoryRegion{
Offset: 701,
Length: 702,
},
FileSize: 240,
DBModelBase: th.Timestamp,
},
},
ECCKeys: &common.ECCKeys{
ECU: "ECUA",
PrivKey1: &bhex,
PrivKey2: &bhex,
PrivKey3: &bhex,
PubKey1: &bhex,
PubKey2: &bhex,
PubKey3: &bhex,
},
},
}
mockDB := &services.DB{}
mockCars := &mocks.MockCars{}
mockCarUpdates := &mocks.MockCarUpdates{
SelectCarUpdateResponse: &common.CarUpdate{
UpdateManifestID: 816,
UpdateManifest: &common.UpdateManifest{
ID: 816,
},
},
}
mockManifests := &mocks.MockUpdateManifests{
ECUUpdatesMock: func(man *common.UpdateManifestECU, vin string) ([]*common.UpdateManifestECU, error) {
if man.ECU == "ECUA" {
return ecuaRollback, nil
}
return nil, nil
},
}
mockKeys := &mocks.MockEccKeys{
MockListResponse: []common.ECCKeys{
{
ECU: "PDU",
PrivKey1: &bhex,
PrivKey2: &bhex,
PrivKey3: &bhex,
PubKey1: &bhex,
PubKey2: &bhex,
PubKey3: &bhex,
},
{
ECU: "TBOX",
PrivKey1: &bhex,
PrivKey2: &bhex,
PrivKey3: &bhex,
PubKey1: &bhex,
PubKey2: &bhex,
PubKey3: &bhex,
},
},
}
mockDB.SetCars(mockCars)
mockDB.SetCarUpdates(mockCarUpdates)
mockDB.SetECCKeys(mockKeys)
mockDB.SetManifests(mockManifests)
mockFoa := FoaServiceMock{}
services.SetFoaService(&mockFoa)
mockRedis := tester.NewRedisMock()
mockKeepAwake := services.NewKeepAwakeService()
services.SetRedisClientPool(tester.NewMockClientPool(mockRedis))
mockSap := vconfig.SAPServiceMock{GetConfigurationMock: func(vin string) (common.SAPResponse, error) {
return common.SAPResponse{
ModelYear: 2023,
ModelType: "Ocean",
VersionDuringModelYear: "1",
Features: []common.SAPFeature{
{
FamilyCode: "FamilyCode1",
FeatureCode: "FeatureCode1",
},
{
FamilyCode: "FamilyCode2",
FeatureCode: "FeatureCode2",
},
},
}, nil
}}
mockConf := vconfig.ConfigMock{GetVODCDSCodingDataMock: func(request common.VODCDSRequest) (map[string]string, error) {
return map[string]string{
"ECUA": "config",
"VOD": "00a62299027600000101012200010100010001010101000000000000000000fffeffff000101010101010101010100010001010101000101010100000000000000000000000000010101000100000100010101000201010101000101020101000101010200010101010101010101000101010100010001010101010101010201010000000000000100000101ff00000001010200000000000003ffffffff0000000201010200000100000000000000000000000000000000000000000000000000000000000000000001202310010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}, nil
}}
services.SetSapService(mockSap)
services.SetVehicleConfig(mockConf)
schemaTesterHMI := th.NewSchemaTestHelper(t, schemaToHMI)
schemaTesterTRex := th.NewSchemaTestHelper(t, schemaToTRex)
tests := []AttendentRouteTestCase{
{
Name: "[HMI] install_error",
RedisTestCase: tester.RedisTestCase{
Device: common.HMI,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":5,"total_files":10,"msg":"install_error","err":0}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":5,"status":"install_failed","total_files":10,"total_size":0}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_failed","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_failed","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_failed","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[HMI] download_completed",
RedisTestCase: tester.RedisTestCase{
Device: common.HMI,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"ICC","file_current":null,"file_total":null,"package_current":920639485,"package_total":920639485,"installed":null,"total_files":null,"msg":"download_completed","err":null}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":920639485,"ecu":"ICC","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"package_download_complete","total_files":0,"total_size":920639485}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
trexKey: `{"handler":"update_manifest","data":{"ecu_updates":[{"name":"ICC","version":"version","hw_version":"hardware_version","self_download":true},{"name":"ECUA","version":"version","hw_version":"hardware_version","configuration":"config","files":[{"file_id":"fileid","url":"http://download.com","file_size":9999,"type":"software","write_region":{"offset":8888,"length":8888}},{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}}],"rollback":[{"version":"VERSIONOLD","files":[{"file_id":"FILEIDOLD","url":"URLOLD","file_size":240,"type":"software","write_region":{"offset":701,"length":702}}]}],"ecc_keys":{"level_1":"74657374","level_2":"74657374","level_3":"74657374"}},{"name":"ADAS","version":"version","hw_version":"hardware_version","files":[{"file_id":"fileid","url":"http://download.com","file_size":9999,"type":"software","write_region":{"offset":8888,"length":8888}}],"ecc_keys":{"level_1":"74657374","level_2":"74657374","level_3":"74657374"}}],"fingerprint":"240119FISKER00000000000000000","car_update_id":297,"rollback":true,"type":"standard","vod":"01012299027600000101012200010100010001010101000000000000000000fffeffff000101010101010101010100010001010101000101010100000000000000000000000000010101000100000100010101000201010101000101020101000101010200010101010101010101000101010100010001010101010101010201010000000000000100000101ff00000001010200000000000003ffffffff00000002010102000001000000000000000000000000000000000000000000000000000000000000000000012023100100010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021","update_duration":30}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":920639485,"package_total":920639485,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ICC","msg":"package_download_complete","err":0}}`,
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":920639485,"package_total":920639485,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ICC","msg":"package_download_complete","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":920639485,"package_total":920639485,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ICC","msg":"package_download_complete","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
MockLoadManifest: &manifest,
},
{
Name: "[HMI] manifest_succeeded",
RedisTestCase: tester.RedisTestCase{
Device: common.HMI,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"ICC","file_current":null,"file_total":null,"package_current":920639485,"package_total":920639485,"installed":null,"total_files":null,"msg":"manifest_succeeded","err":null}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":920639485,"ecu":"ICC","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"manifest_succeeded","total_files":0,"total_size":920639485}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":920639485,"package_total":920639485,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ICC","msg":"manifest_succeeded","err":0}}`,
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":920639485,"package_total":920639485,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ICC","msg":"manifest_succeeded","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":920639485,"package_total":920639485,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ICC","msg":"manifest_succeeded","err":0}}`,
"1:JH4KA7680RC01": `{"handler":"read_ecu_versions","data":{"ecu_name":"*"}}`,
},
MockRedisGetSet: testGetSetResult,
},
SelectCarUpdate: &common.CarUpdate{
UpdateManifest: &validUpdateManifest,
},
},
{
Name: "[TREX] manifest_received",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"msg":"manifest_received","err":-6,"extra_info":""}`,
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"manifest_received","err":-6}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"manifest_received","err":-6}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"manifest_received","err":-6}}`,
},
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"","errorcode":-6,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"manifest_received","total_files":0,"total_size":0}`,
Expires: expectedExpire,
},
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] manifest_accepted",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"msg":"manifest_accepted","err":-7,"extra_info":""}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"","errorcode":-7,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"manifest_accepted","total_files":0,"total_size":0}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"manifest_accepted","err":-7}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"manifest_accepted","err":-7}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"manifest_accepted","err":-7}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] download_started",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"msg":"download_started","err":-14,"extra_info":""}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"","errorcode":-14,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"downloading","total_files":0,"total_size":0}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"downloading","err":-14}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"downloading","err":-14}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"downloading","err":-14}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] downloading",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"ADAS","file_current":1048576,"file_total":1264672,"package_current":1048576,"package_total":2529856,"msg":"downloading","err":0}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":1048576,"ecu":"ADAS","errorcode":0,"file_size":1048576,"file_total":1264672,"id":297,"installed":0,"status":"downloading","total_files":0,"total_size":2529856}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1048576,"file_total":1264672,"package_current":1048576,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ADAS","msg":"downloading","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1048576,"file_total":1264672,"package_current":1048576,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ADAS","msg":"downloading","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":1048576,"file_total":1264672,"package_current":1048576,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ADAS","msg":"downloading","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] download_completed ECU 1",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"ADAS","file_current":1264672,"file_total":1264672,"package_current":1264672,"package_total":2529856,"msg":"download_completed","err":0}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":1264672,"ecu":"ADAS","errorcode":0,"file_size":1264672,"file_total":1264672,"id":297,"installed":0,"status":"downloading","total_files":0,"total_size":2529856}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1264672,"file_total":1264672,"package_current":1264672,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ADAS","msg":"downloading","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1264672,"file_total":1264672,"package_current":1264672,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ADAS","msg":"downloading","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":1264672,"file_total":1264672,"package_current":1264672,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"ADAS","msg":"downloading","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] download_started ECU 2",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"EKS","file_current":0,"file_total":1265184,"package_current":1264672,"package_total":2529856,"msg":"download_started","err":0}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":1264672,"ecu":"EKS","errorcode":0,"file_size":0,"file_total":1265184,"id":297,"installed":0,"status":"downloading","total_files":0,"total_size":2529856}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":1265184,"package_current":1264672,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"downloading","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":1265184,"package_current":1264672,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"downloading","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":1265184,"package_current":1264672,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"downloading","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] downloading ECU 2",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"EKS","file_current":1048576,"file_total":1265184,"package_current":2313248,"package_total":2529856,"msg":"downloading","err":0}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":2313248,"ecu":"EKS","errorcode":0,"file_size":1048576,"file_total":1265184,"id":297,"installed":0,"status":"downloading","total_files":0,"total_size":2529856}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1048576,"file_total":1265184,"package_current":2313248,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"downloading","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1048576,"file_total":1265184,"package_current":2313248,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"downloading","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":1048576,"file_total":1265184,"package_current":2313248,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"downloading","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] download_completed ECU 2",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"EKS","file_current":1265184,"file_total":1265184,"package_current":2529856,"package_total":2529856,"msg":"download_completed","err":0}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":2529856,"ecu":"EKS","errorcode":0,"file_size":1265184,"file_total":1265184,"id":297,"installed":0,"status":"package_download_complete","total_files":0,"total_size":2529856}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1265184,"file_total":1265184,"package_current":2529856,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"package_download_complete","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":1265184,"file_total":1265184,"package_current":2529856,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"package_download_complete","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":1265184,"file_total":1265184,"package_current":2529856,"package_total":2529856,"installed":0,"total_files":0,"car_update_id":297,"ecu":"EKS","msg":"package_download_complete","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] package_download_complete",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"msg":"download_completed","err":-15,"extra_info":""}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"","errorcode":-15,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"package_download_complete","total_files":0,"total_size":0}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"package_download_complete","err":-15}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"package_download_complete","err":-15}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"package_download_complete","err":-15}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] download_failed",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"TEST","file_current":0,"file_total":100,"package_current":0,"package_total":1000,"msg":"download_failed","err":0}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":100,"id":297,"installed":0,"status":"download_failed","total_files":0,"total_size":1000}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":100,"package_current":0,"package_total":1000,"installed":0,"total_files":0,"car_update_id":297,"ecu":"TEST","msg":"download_failed","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":100,"package_current":0,"package_total":1000,"installed":0,"total_files":0,"car_update_id":297,"ecu":"TEST","msg":"download_failed","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":100,"package_current":0,"package_total":1000,"installed":0,"total_files":0,"car_update_id":297,"ecu":"TEST","msg":"download_failed","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] install_started",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":0,"total_files":10,"msg":"install_started","err":0}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"installing","total_files":10,"total_size":0}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"installing","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"installing","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"installing","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] installing",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":5,"total_files":10,"msg":"installing","err":0}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":5,"status":"installing","total_files":10,"total_size":0}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"installing","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"installing","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"installing","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] install_succeeded ECU",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":10,"total_files":10,"msg":"install_succeeded"}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":10,"status":"package_install_complete","total_files":10,"total_size":0}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"package_install_complete","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"package_install_complete","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"package_install_complete","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] package_install_complete",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"msg":"install_succeeded","err":0}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":0,"status":"installing","total_files":0,"total_size":0}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"installing","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"installing","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":297,"ecu":"","msg":"installing","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] install_failed",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":5,"total_files":10,"msg":"install_failed","err":0}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":5,"status":"install_failed","total_files":10,"total_size":0}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_failed","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_failed","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_failed","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] requirements_failed",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":5,"total_files":10,"msg":"requirements_failed","err":0}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":5,"status":"requirements_failed","total_files":10,"total_size":0}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"requirements_failed","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"requirements_failed","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":5,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"requirements_failed","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] install_scheduled ECU",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":10,"total_files":10,"msg":"install_scheduled"}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":10,"status":"install_scheduled","total_files":10,"total_size":0}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_scheduled","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_scheduled","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"install_scheduled","err":0}}`,
},
MockRedisGetSet: testGetSetResult,
},
},
{
Name: "[TREX] manifest_succeeded",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":297,"ecu":"TEST","installed":10,"total_files":10,"msg":"manifest_succeeded"}`,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
carupdateKey: {
Value: `{"current_size":0,"ecu":"TEST","errorcode":0,"file_size":0,"file_total":0,"id":297,"installed":10,"status":"manifest_succeeded","total_files":10,"total_size":0}`,
Expires: expectedExpire,
},
},
ExpectedMessages: map[string]string{
mobile1Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"manifest_succeeded","err":0}}`,
mobile2Key: `{"handler":"car_update_status","data":{"vin":"JH4KA7680RC01","file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"manifest_succeeded","err":0}}`,
hmiKey: `{"handler":"car_update_status","data":{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":10,"total_files":10,"car_update_id":297,"ecu":"TEST","msg":"manifest_succeeded","err":0}}`,
"1:JH4KA7680RC01": `{"handler":"read_ecu_versions","data":{"ecu_name":"*"}}`,
},
MockRedisGetSet: testGetSetResult,
},
SelectCarUpdate: &common.CarUpdate{
UpdateManifest: &validUpdateManifest,
},
},
}
for i := range tests {
mockRedis.Reset()
test := &tests[i]
test.SetupRedis(mockRedis)
test.SetupDB(mockCars, mockCarUpdates, test)
redisPool := tester.NewMockClientPool(mockRedis)
handler := controllers.NewCarUpdateProgress(redisPool, mockKeepAwake, mockDB, test.Device)
if handler == nil {
t.Error(errors.New("NewCarUpdateProgress cannot handle device %v"))
continue
}
err := handler.Process(test.DeviceKey, []byte(test.PayloadData))
test.CheckHandlerError(t, test.Name, err)
test.Validate(t, test.Name, mockRedis)
for key, m := range test.RedisTestCase.ExpectedMessages {
name := fmt.Sprintf("%s %s", test.Name, key)
if strings.Contains(key, "1:") {
schemaTesterTRex.ValidateSchemaObject(name, []byte(m))
} else if strings.Contains(key, "2:") {
schemaTesterHMI.ValidateSchemaObject(name, []byte(m))
}
}
}
}
type FoaServiceMock struct{}
func (f *FoaServiceMock) OtaUpdateStatus(vin string, carUpdate *common.CarUpdate, status *common.CarUpdateProgress) (*http.Response, error) {
return &http.Response{StatusCode: 200}, nil
}

View File

@@ -0,0 +1,74 @@
package handlers
import (
"encoding/json"
"errors"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/common"
fv "github.com/fiskerinc/cloud-services/pkg/flashpackversion"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/validator"
)
func UpdateCarState(db *services.DB, vin string, data []byte) error {
logger.Debug().Msgf("UpdateCarState %s", vin)
var update common.CarStateUpdate
err := json.Unmarshal(data, &update)
if err != nil {
return err
}
err = validator.ValidateStruct(&update)
if err != nil {
logger.Err(err).Interface("CarStateUpdate", update).Str("CarStateUpdateByteValue as string", string(data)).Send()
return err
}
return processUpdateCarStateECUs(db, vin, update.ECUs)
}
func processUpdateCarStateECUs(db *services.DB, vin string, ecus map[string]common.CarECU) error {
cache := services.GetCarEcuCache()
insert := []common.CarECU{}
errs := []error{}
for name, ecu := range ecus {
ecu.VIN = vin
ecu.ECU = name
err := validator.ValidateStruct(&ecu)
if err != nil {
logger.Error().Msgf("invalid CarECU %v", err)
errs = append(errs, err)
continue
}
if !cache.Exists(ecu.CacheKey(), ecu.HashValues()) {
insert = append(insert, ecu)
}
}
if len(insert) == 0 {
return combineErrors(errs)
}
err := fv.InsertCarECUsAndUpdateFlashpackVersion(db.GetCars(), db.GetCarVersionsLog(), vin, insert)
if err != nil {
errs = append(errs, err)
}
return combineErrors(errs)
}
func combineErrors(errs []error) error {
if len(errs) == 0 {
return nil
}
errString := errs[0].Error()
for i := 1; i < len(errs); i++ {
errString += " " + errs[i].Error()
}
return errors.New(errString)
}

View File

@@ -0,0 +1,113 @@
package handlers_test
import (
"testing"
"github.com/fiskerinc/cloud-services/services/attendant/handlers"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig"
)
func TestUpdateCarState(t *testing.T) {
setupDBMock()
setupRedisMock()
mockSap := vconfig.SAPServiceMock{}
services.SetSapService(mockSap)
type testCase struct {
Name string
VIN string
Payload string
ExpectedErr string
}
tests := []testCase{
{
Name: "Empty ECU",
VIN: "JH4KA7680RC011845",
Payload: `{"ecus": {}}`,
ExpectedErr: "Key: 'CarStateUpdate.ECUs' Error:Field validation for 'ECUs' failed on the 'min' tag",
},
{
Name: "Bad VIN and ECU",
VIN: "JH4KA7680RC01",
Payload: `{
"ecus": {
"ECU1": {
"serial_number": "AAAAA",
"hw_version": "2000",
"boot_loader_version": "3000",
"fingerprint": "0xffffffffffffffffff",
"config": "0x024000941f9fffbfffe2dd9bff5860000007dfff091fff7f",
"vendor": "A021E00029"
}
}
}`,
ExpectedErr: "Key: 'CarECU.VIN' Error:Field validation for 'VIN' failed on the 'vin' tag",
},
{
Name: "Good ECUs with/without software version",
VIN: "JH4KA7680RC011845",
Payload: `{
"ecus": {
"ECU1": {
"sw_version": "1000",
"serial_number": "AAAAA",
"hw_version": "2000",
"boot_loader_version": "3000",
"fingerprint": "0xffffffffffffffffff",
"config": "0x024000941f9fffbfffe2dd9bff5860000007dfff091fff7f",
"vendor": "A021E00029"
},
"ECU2": {
"serial_number": "BBBBBBB",
"hw_version": "2001",
"boot_loader_version": "3001",
"fingerprint": "0xffffffffffffffffff",
"config": "0x024000941f9fffbfffe2dd9bff5860000007dfff091fff7f",
"vendor": "A021E00029"
}
}
}`,
ExpectedErr: "",
},
{
Name: "bad ECU followed by a Good ECU",
VIN: "JH4KA7680RC011845",
Payload: `{
"ecus": {
"ECU1": {
"serial_number": "AAAAA",
"hw_version": "2000",
"boot_loader_version": "3000",
"fingerprint": "0xffffffffffffffffff",
"config": "0x024000941f9fffbfffe2dd9bff5860000007dfff091fff7f",
"vendor": "A021E00029",
"epoch_usec": "ABC"
},
"ECU2": {
"serial_number": "BBBBBBB",
"hw_version": "2001",
"boot_loader_version": "3001",
"fingerprint": "0xffffffffffffffffff",
"config": "0x024000941f9fffbfffe2dd9bff5860000007dfff091fff7f",
"vendor": "A021E00029"
}
}
}`,
ExpectedErr: "json: cannot unmarshal string into Go struct field CarECU.ecus.epoch_usec of type int64",
},
}
for _, test := range tests {
err := handlers.UpdateCarState(mockDB, test.VIN, []byte(test.Payload))
if err != nil && test.ExpectedErr != err.Error() {
t.Errorf(testhelper.TestErrorTemplate, test.Name, test.ExpectedErr, err.Error())
} else if err == nil && test.ExpectedErr != "" {
t.Errorf(testhelper.TestErrorTemplate, test.Name, test.ExpectedErr, err)
}
}
}

View File

@@ -0,0 +1,94 @@
package handlers
import (
"github.com/fiskerinc/cloud-services/services/attendant/services"
"encoding/json"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/pkg/errors"
)
func GetAllEccKeys(db *services.DB, id string, data []byte) error {
logger.Debug().Msgf("GetAllEccKeys %v %s", common.TRex, id)
var err error
var eccKeys []common.ECCKeys
req, err := parseGetAllEccKeysRequest(data)
if err != nil {
return err
}
if req.CarUpdateID == 0 {
eccKeys, err = db.GetECCKeys().SelectAllPrivateKeysByVIN(id)
} else {
eccKeys, err = db.GetECCKeys().SelectAllPrivateKeysByCarUpdateID(req.CarUpdateID)
}
if err != nil {
return err
}
if eccKeys == nil {
eccKeys = make([]common.ECCKeys, 0)
}
eccKeys = replaceECU(eccKeys)
client := services.RedisClientPool().GetFromPool()
defer client.Close()
err = client.SafePublishMessage(common.TRex.Key(id), common.Message{
Handler: "ecc_keys",
Data: eccKeys,
})
if err != nil {
return err
}
logger.Debug().Msgf("GetAllEccKeys sent %v %s", common.TRex, id)
return nil
}
func parseGetAllEccKeysRequest(data []byte) (common.CarUpdateRequest, error) {
var req common.CarUpdateRequest
if len(data) == 0 {
return req, nil
}
err := json.Unmarshal(data, &req)
if err != nil {
return req, errors.WithStack(err)
}
return req, nil
}
func replaceECU(eccKeys []common.ECCKeys) []common.ECCKeys {
ecuReplacements := common.ECUReplacement()
for i := 0; i < len(eccKeys); i++ {
ecu := eccKeys[i].ECU
if replacement, exist := ecuReplacements[ecu]; exist {
eccKeys[i].ECU = replacement
}
}
return eccKeys
}
/*
func notifyEccKeysGeneralError(client redis.Client, device common.Device, id string, err error) {
e := client.PublishMessage(device.Key(id), common.Message{
Handler: "ecc_keys",
Data: []struct{ Error string }{
{
Error: err.Error(),
},
},
})
if e != nil {
logger.Error().Err(errors.WithStack(e)).Send()
}
}
*/

View File

@@ -0,0 +1,111 @@
package handlers_test
import (
"github.com/fiskerinc/cloud-services/services/attendant/handlers"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/pkg/errors"
)
func TestGetAllEccKeys(t *testing.T) {
testVIN := "JH4KA7680RC01"
trexKey := "1:JH4KA7680RC01"
pk11 := common.NewBinaryHex([]byte("testprivkey11"))
pk12 := common.NewBinaryHex([]byte("testprivkey12"))
pk13 := common.NewBinaryHex([]byte("testprivkey13"))
pk21 := common.NewBinaryHex([]byte("testprivkey21"))
pk22 := common.NewBinaryHex([]byte("testprivkey22"))
pk23 := common.NewBinaryHex([]byte("testprivkey23"))
testDBQuery := []common.ECCKeys{
{
ECU: "testecu1",
PrivKey1: &pk11,
PrivKey2: &pk12,
PrivKey3: &pk13,
},
{
ECU: "testecu2",
PrivKey1: &pk21,
PrivKey2: &pk22,
PrivKey3: &pk23,
},
{
ECU: "PDU",
PrivKey1: &pk21,
PrivKey2: &pk22,
PrivKey3: &pk23,
},
}
mockRedis := tester.NewRedisMock()
services.SetRedisClientPool(tester.NewMockClientPool(mockRedis))
mockEccKeys := &mocks.MockEccKeys{}
mockDB := &services.DB{}
mockDB.SetECCKeys(mockEccKeys)
tests := []AttendentRouteTestCase{
{
Name: "[TREX] From DB, no car update id",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
ExpectedMessages: map[string]string{
trexKey: `{"handler":"ecc_keys","data":[{"ecu":"testecu1","level_1":"74657374707269766b65793131","level_2":"74657374707269766b65793132","level_3":"74657374707269766b65793133"},{"ecu":"testecu2","level_1":"74657374707269766b65793231","level_2":"74657374707269766b65793232","level_3":"74657374707269766b65793233"},{"ecu":"OBC","level_1":"74657374707269766b65793231","level_2":"74657374707269766b65793232","level_3":"74657374707269766b65793233"}]}`,
},
},
MockEccKeysSelect: testDBQuery,
},
{
Name: "[TREX] From DB, with car update id",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
ExpectedMessages: map[string]string{
trexKey: `{"handler":"ecc_keys","data":[{"ecu":"testecu1","level_1":"74657374707269766b65793131","level_2":"74657374707269766b65793132","level_3":"74657374707269766b65793133"},{"ecu":"testecu2","level_1":"74657374707269766b65793231","level_2":"74657374707269766b65793232","level_3":"74657374707269766b65793233"},{"ecu":"OBC","level_1":"74657374707269766b65793231","level_2":"74657374707269766b65793232","level_3":"74657374707269766b65793233"}]}`,
},
PayloadData: `{"car_update_id":10000}`,
},
MockEccKeysSelect: testDBQuery,
},
{
Name: "Error",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
ExpectedError: "something went wrong",
},
MockEccKeysSelect: nil,
},
}
schemaTester := testhelper.NewSchemaTestHelper(t, schemaToTRex)
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
mockRedis.Reset()
test.SetupRedis(mockRedis)
mockEccKeys.MockListResponse = test.MockEccKeysSelect
if test.Name == "Error" {
mockEccKeys.Error = errors.New("something went wrong")
} else {
mockEccKeys.Error = nil
}
err := handlers.GetAllEccKeys(mockDB, test.DeviceKey, []byte(test.PayloadData))
test.CheckHandlerError(t, test.Name, err)
test.Validate(t, test.Name, mockRedis)
for _, m := range test.ExpectedMessages {
schemaTester.ValidateSchemaObject(test.Name, []byte(m))
}
})
}
}

View File

@@ -0,0 +1,80 @@
package handlers
import (
"encoding/json"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/pkg/errors"
)
func GetFileKeys(db *services.DB, device common.Device, id string, data []byte) error {
logger.Debug().Msgf("GetFileKeys %v %s", device, id)
var err error
var req *common.FileKeysRequest
client := services.RedisClientPool().GetFromPool()
defer client.Close()
req, err = parseGetFileKeysRequest(data)
if err != nil {
notifyFileKeysGeneralError(client, device, id, err)
return err
}
keys, err := cache.RetrieveFileEncryptionParams(client, db.GetFileKeys(), req.FileIDs)
if err != nil {
notifyFileKeysGeneralError(client, device, id, err)
return err
}
err = client.SafeQueueMessage(device.Key(id), common.Message{
Handler: "filekeys",
Data: keys,
})
if err != nil {
return err
}
logger.Debug().Msgf("GetFileKeys sent %v %s", device, id)
return nil
}
func parseGetFileKeysRequest(data []byte) (*common.FileKeysRequest, error) {
var status common.FileKeysRequest
err := json.Unmarshal(data, &status)
if err != nil {
return nil, errors.WithStack(err)
}
err = validator.ValidateStruct(status)
if err != nil {
return &status, errors.WithStack(err)
}
return &status, nil
}
func notifyFileKeysGeneralError(client redis.Client, device common.Device, id string, err error) {
e := client.SafePublishMessage(device.Key(id), common.Message{
Handler: "filekeys",
Data: []common.FileKeyResponse{
{
FileID: "0",
Error: err.Error(),
},
},
})
if e != nil {
logger.Error().Err(errors.WithStack(e)).Send()
}
}

View File

@@ -0,0 +1,124 @@
package handlers_test
import (
"testing"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/services/attendant/handlers"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
)
func TestGetFileKey(t *testing.T) {
testVIN := "JH4KA7680RC01"
trexKey := "1:JH4KA7680RC01"
hmiKey := "2:JH4KA7680RC01"
fileCache1 := "fileid:b7d94be8c94062cf"
fileCache2 := "fileid:83165a80c940e8b3"
testPayload := `{"file_ids": ["b7d94be8c94062cf","83165a80c940e8b3"]}`
testDBQuery := []common.FileKey{
{
FileID: "b7d94be8c94062cf",
Auth: []byte("AuthValue"),
Key: []byte("KeyValue"),
Nonce: []byte("NonceValue"),
},
{
FileID: "83165a80c940e8b3",
Auth: []byte("AuthValue2"),
Key: []byte("KeyValue2"),
Nonce: []byte("NonceValue2"),
},
}
mockRedis := tester.NewRedisMock()
services.SetRedisClientPool(tester.NewMockClientPool(mockRedis))
mockFileKeys := &mocks.MockFileKeys{}
mockDB := &services.DB{}
mockDB.SetFileKeys(mockFileKeys)
tests := []AttendentRouteTestCase{
{
Name: "[TREX] From DB",
RedisTestCase: tester.RedisTestCase{
Device: common.TRex,
DeviceKey: testVIN,
PayloadData: testPayload,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
fileCache1: {
Value: `{"file_id":"b7d94be8c94062cf","key":"S2V5VmFsdWU=","auth":"QXV0aFZhbHVl","nonce":"Tm9uY2VWYWx1ZQ=="}`,
Expires: 86400,
},
fileCache2: {
Value: `{"file_id":"83165a80c940e8b3","key":"S2V5VmFsdWUy","auth":"QXV0aFZhbHVlMg==","nonce":"Tm9uY2VWYWx1ZTI="}`,
Expires: 86400,
},
},
ExpectedMessages: map[string]string{
trexKey: `{"handler":"filekeys","data":[{"file_id":"b7d94be8c94062cf","key":"S2V5VmFsdWU=","auth":"QXV0aFZhbHVl","nonce":"Tm9uY2VWYWx1ZQ=="},{"file_id":"83165a80c940e8b3","key":"S2V5VmFsdWUy","auth":"QXV0aFZhbHVlMg==","nonce":"Tm9uY2VWYWx1ZTI="}]}`,
},
},
MockFileKeysSelect: testDBQuery,
MockFileKeysError: nil,
},
{
Name: "[HMI] From DB",
RedisTestCase: tester.RedisTestCase{
Device: common.HMI,
DeviceKey: testVIN,
PayloadData: testPayload,
ExpectedCaches: map[string]tester.ExpiringCacheResult{
fileCache1: {
Value: `{"file_id":"b7d94be8c94062cf","key":"S2V5VmFsdWU=","auth":"QXV0aFZhbHVl","nonce":"Tm9uY2VWYWx1ZQ=="}`,
Expires: 86400,
},
fileCache2: {
Value: `{"file_id":"83165a80c940e8b3","key":"S2V5VmFsdWUy","auth":"QXV0aFZhbHVlMg==","nonce":"Tm9uY2VWYWx1ZTI="}`,
Expires: 86400,
},
},
ExpectedMessages: map[string]string{
hmiKey: `{"handler":"filekeys","data":[{"file_id":"b7d94be8c94062cf","key":"S2V5VmFsdWU=","auth":"QXV0aFZhbHVl","nonce":"Tm9uY2VWYWx1ZQ=="},{"file_id":"83165a80c940e8b3","key":"S2V5VmFsdWUy","auth":"QXV0aFZhbHVlMg==","nonce":"Tm9uY2VWYWx1ZTI="}]}`,
},
},
MockFileKeysSelect: testDBQuery,
MockFileKeysError: nil,
},
}
schemaTester := testhelper.NewSchemaTestHelper(t, schemaToTRex)
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
mockRedis.Reset()
test.SetupRedis(mockRedis)
mockFileKeys.GetMultiResponse = test.MockFileKeysSelect
mockFileKeys.Error = test.MockFileKeysError
err := handlers.GetFileKeys(mockDB, test.Device, test.DeviceKey, []byte(test.PayloadData))
test.CheckHandlerError(t, test.Name, err)
test.Validate(t, test.Name, mockRedis)
for _, m := range test.ExpectedMessages {
schemaTester.ValidateSchemaObject(test.Name, []byte(m))
}
})
}
}
func BenchmarkGetFileKey(b *testing.B) {
db := services.GetDB()
vin := "1F15K3R45N1234567"
id := []byte(`{"file_ids": ["b7d94be8c94062cf","83165a80c940e8b3"]}`)
for n := 0; n < b.N; n++ {
err := handlers.GetFileKeys(db, common.TRex, vin, id)
if err != nil {
b.Error(err)
}
}
}

View File

@@ -0,0 +1,113 @@
package handlers_test
import (
"encoding/json"
"time"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/go-pg/pg/v10/orm"
)
var mockRedis *redis.Connection
var mockDB *services.DB
var testDateTime time.Time = time.Date(2022, 1, 2, 3, 4, 5, 6, time.UTC)
func setupRedisMock() {
redis.MockRedisConnection()
mockRedis = &redis.Connection{}
}
func setupDBMock() {
db := services.DB{}
db.SetCarUpdates(&mocks.MockCarUpdates{
SelectCarUpdateResponse: &common.CarUpdate{
ID: 1,
VIN: "FISKER123",
UpdateManifestID: 2,
UpdateManifest: &common.UpdateManifest{
ID: 2,
Name: "TEST_PACKAGE",
Version: "1.0.0",
ReleaseNotes: "http://releasenotes.com",
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
BodyType: "truck",
},
},
})
db.SetECU(&mocks.MockEcuDtc{})
db.SetCars(&mocks.MockCars{
SelectResponse: &common.Car{},
})
carVersionLogMock := mocks.MockCarVersionsLog{MockLogVersionChange: func(log *common.CarVersionLogs) (orm.Result, error) {
return nil, nil
}}
db.SetCarVersionsLog(&carVersionLogMock)
mockDB = &db
}
type mockRedisCache struct {
redis.Connection
}
func (c *mockRedisCache) GetSet(id string, data interface{}) error {
driverIDs := []string{"valid-cognito-id-1", "valid-cognito-id-2"}
dataBytes, err := json.Marshal(driverIDs)
if err != nil {
return err
}
err = json.Unmarshal(dataBytes, data)
if err != nil {
return err
}
return nil
}
func NewRedisMock() *tester.MockRedis {
redis.MockRedisConnection()
return &tester.MockRedis{}
}
type AttendentRouteTestCase struct {
Name string
SelectCarUpdate *common.CarUpdate
SelectCarUpdates []common.CarUpdate
CarUpdateError error
SelectCarToDrivers []common.CarToDriver
CarToDriversError error
MockLoadManifest *common.UpdateManifest
MockFileKeysSelect []common.FileKey
MockFileKeysError error
MockEccKeysSelect []common.ECCKeys
tester.RedisTestCase
}
func (at *AttendentRouteTestCase) SetupDB(mockCars *mocks.MockCars, mockCarUpdates *mocks.MockCarUpdates, test *AttendentRouteTestCase) {
if test == nil {
return
}
if mockCars != nil {
mockCars.SelectCarsForDrivers = test.SelectCarToDrivers
mockCars.Error = test.CarToDriversError
}
if mockCarUpdates != nil {
mockCarUpdates.SelectCarUpdateResponse = test.SelectCarUpdate
mockCarUpdates.SelectCarUpdatesResponse = test.SelectCarUpdates
mockCarUpdates.LoadManifest = test.MockLoadManifest
mockCarUpdates.Error = test.CarUpdateError
}
}

View File

@@ -0,0 +1,160 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/grpc/sms"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/manifestsender"
"github.com/fiskerinc/cloud-services/pkg/tmobile"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
)
var ENABLE_ORDERUPDATE_SENDS = envtool.GetEnvBool("ENABLE_ORDERUPDATE_SENDS", false)
// Take the feature codes from VehicleOrder message to generate the VOD and CDSs
// Create a new common.UpdateManifest with the VOD and CDS and save it as a common.ConfigUpdateType
// Create a new common.CarUpdate for the VIN and the manifest in step 2 and save it to the database
// Copy the car update id into the UpdateManifest
// Send the UpdateManifest to both trex and hmi
func OrderUpdated(db *services.DB, smsClient sms.SMSServiceClient, vin string, data []byte) error {
logger.Debug().Msgf("Order updated %s", vin)
var order common.VehicleOrder
err := json.Unmarshal(data, &order)
if err != nil {
return err
}
err = setCarSettings(db, vin, order)
if err != nil {
return err
}
err = changeRatePlan(db, smsClient, vin, order.VehicleSpecification.DestinationCountry)
if err != nil {
logger.Error().Msgf("Failed to change rate plan for %s, %s", vin, err)
}
if !ENABLE_ORDERUPDATE_SENDS {
return nil
}
cs := services.GetVehicleConfig()
r := services.RedisClientPool().GetFromPool()
defer r.Close()
trex := manifestsender.NewTBOXManifestSender(r, cs, services.GetDB(), nil, nil)
defer trex.Close()
// I don't think this has ever been called on production, or dev !
input := manifestsender.ProcessConfigUpdateStruct{
VIN: vin,
Name: "SAP order change update",
Username: "unidentified attendant sap user",
SendToCar: true,
DontCreateDatabaseEntry: false,
Forced: false,
}
_, err = trex.ProcessConfigUpdate(input, services.GetDB().GetCarConfigData())
return err
}
func setCarSettings(db *services.DB, vin string, order common.VehicleOrder) (err error) {
// If the sequence number is missing, go will fill it in as 0, which is the case where the car has no sequence number
if order.VehicleSpecification.SequenceNumber != "" {
_, err = db.GetCars().SetSetting(&common.CarSetting{
VIN: vin,
Name: common.SEQUENCE_NUMBER,
Value: fmt.Sprint(order.VehicleSpecification.SequenceNumber),
Type: "string",
})
if err != nil {
return err
}
}
_, err = db.GetCars().SetSetting(&common.CarSetting{
VIN: vin,
Name: common.BODY_COLOR,
Value: common.FeatureCodeToBodyColor(order.VehicleSpecification.VehicleFeatures),
Type: "string",
})
if err != nil {
return err
}
// Incase SAP hasn't sent this value yet
if order.VehicleSpecification.DestinationCountry != "" {
_, err = db.GetCars().SetSetting(&common.CarSetting{
VIN: vin,
Name: common.DELIVERY_DESTINATION,
Value: order.VehicleSpecification.DestinationCountry,
Type: "string",
})
if err != nil {
return err
}
}
return
}
func changeRatePlan(db *services.DB, smsClient sms.SMSServiceClient, vin, destinationCountry string) error {
car, err := services.GetDB().GetCars().SelectByVIN(vin)
if err != nil {
return err
}
if len(car.ICCID) == 0 {
return fmt.Errorf("no iccid found for vehicle %s", vin)
}
iccid := strings.TrimSuffix(strings.ToLower(car.ICCID), "f")
customAtributeReq := sms.CustomAtributesRequest{
ICCID: iccid,
AccountCustom1: destinationCountry,
}
_, err = smsClient.HandleCustomAttributes(context.Background(), &customAtributeReq)
if err != nil {
return err
}
ratePlan, err := db.GetRatePlan().Select(destinationCountry)
if err != nil {
return err
}
changeRatePlanRequest := sms.ChangeRatePlanRequest{
ICCID: iccid,
ProductId: ratePlan.ProductID,
AccountId: tmobile.FISKER_TMOBILE_ACCOUNT_ID,
}
_, err = smsClient.HandleChangeRatePlan(context.Background(), &changeRatePlanRequest)
if err != nil {
return err
}
go verifyRatePlan(smsClient, iccid, destinationCountry)
return nil
}
func verifyRatePlan(smsClient sms.SMSServiceClient, iccid, destinationCountry string) error {
deviceDetailsRequest := sms.DeviceDetailsRequest{
ICCID: iccid,
}
details, err := smsClient.HandleDeviceDetails(context.Background(), &deviceDetailsRequest)
if err != nil {
logger.Error().Msgf("failed to check device details for iccid %s", iccid)
}
logger.Info().Msgf("device details for iccid %s: %+v", iccid, details)
return nil
}

View File

@@ -0,0 +1,224 @@
package handlers_test
import (
"encoding/json"
"fmt"
"testing"
"time"
"github.com/pkg/errors"
"github.com/fiskerinc/cloud-services/services/attendant/handlers"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/common"
q "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/grpc/sms"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig"
"github.com/stretchr/testify/assert"
)
var (
someErr = errors.New("some error")
vinMock = "FISKER123"
vodMock = "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"
confMock = "000000000000000000000000000000000000000"
validOrder = common.VehicleOrder{
SpecID: 800010200,
OrderNumber: 8000102,
MessageIdentifier: "VEHICLEORDERSUBMISSION",
VehicleSpecification: common.VehicleSpecification{
OrderIndicator: "S",
FleetOrderIndicator: "N",
ProductionPhaseIndicator: "01",
VehicleIndicator: "000",
ManufacturingPlant: "G",
ExpectedReferenceDate: common.ExpectedReferenceDate{
Time: time.Date(2022, 5, 26, 0, 0, 0, 0, time.UTC),
},
ModelType: "FM29",
ModelYearIndicator: 2023,
VehicleModel: "F29",
VinPrefix: "VCF1ZBU2_PG",
VehicleFeatures: []common.FeatureCodes{
{
FamilyCode: "2801",
FeatureCode: "280102",
},
{
FamilyCode: "2804",
FeatureCode: "280401",
},
{
FamilyCode: "2805",
FeatureCode: "280501",
},
},
},
}
validUpdateManifest = common.UpdateManifest{
ID: 1,
CarUpdateID: 1,
Version: fmt.Sprint(validOrder.OrderNumber),
Description: fmt.Sprintf("configuration %s %s", vinMock, validOrder.MessageIdentifier),
ManifestType: common.ConfigUpdateType,
VOD: "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111",
}
validRespECUs = []*common.UpdateManifestECU{
{
UpdateManifestID: 1,
ECU: "ICC",
Configuration: confMock,
},
}
smsMock = sms.NewSMSMockSuccess()
mockCars = mocks.MockCars{}
)
func TestOrderUpdated(t *testing.T) {
handlers.ENABLE_ORDERUPDATE_SENDS = true
validUpdateManifestResp := validUpdateManifest
validUpdateManifestResp.ID = 1
validUpdateManifestResp.ECUs = validRespECUs
scrubbedValidUpdateManifest := validUpdateManifestResp.ToUpdateConfigManifest()
scrubbedValidUpdateManifest.Type = "standard"
successPayload, _ := json.Marshal(validOrder)
redisMock := tester.NewRedisMock()
services.SetRedisClientPool(tester.NewMockClientPool(redisMock))
mockSap := vconfig.SAPServiceMock{GetConfigurationMock: func(vin string) (common.SAPResponse, error) {
return common.SAPResponse{
ModelYear: 2023,
ModelType: "Ocean",
VersionDuringModelYear: "1",
Features: []common.SAPFeature{
{
FamilyCode: "FamilyCode1",
FeatureCode: "FeatureCode1",
},
{
FamilyCode: "FamilyCode2",
FeatureCode: "FeatureCode2",
},
{
FamilyCode: "VOD",
FeatureCode: vodMock,
},
}}, nil
}}
services.SetSapService(mockSap)
mockCars.SetLoadResp(&common.Car{
VIN: vinMock,
ICCID: "1234567890",
SoldStatus: common.CarSoldStatusRetailed,
})
tests := map[string]struct {
updateManifest q.UpdateManifestsInterface
cars q.CarsInterface
carsUpdate q.CarUpdatesInterface
ratePlan q.RatePlanInterface
vConfig vconfig.ConfigServiceInterface
payload []byte
expRedisMsgs map[string]interface{}
expErr error
}{
"success": {
updateManifest: &mocks.MockUpdateManifests{SelectResponse: []common.UpdateManifest{{ID: 1234}}},
cars: &mockCars,
carsUpdate: &mocks.MockCarUpdates{},
ratePlan: &mocks.MockRatePlan{},
vConfig: vconfig.ConfigMock{GetVODCDSCodingDataMock: SuccessGetCDSMock},
payload: successPayload,
expRedisMsgs: map[string]interface{}{
"2:" + vinMock: common.Message{
Handler: "car_update",
Data: common.CarUpdate{
ID: 1,
VIN: vinMock,
UpdateManifestID: 1,
UpdateManifest: &validUpdateManifestResp,
},
},
"1:" + vinMock: common.Message{
Handler: "config_update",
Data: scrubbedValidUpdateManifest,
},
},
expErr: nil,
},
"json_parse_err": {
payload: []byte(`12`),
expErr: errors.New("json: cannot unmarshal number into Go value of type common.VehicleOrder"),
},
"failed_cars_db": {
updateManifest: &mocks.MockUpdateManifests{SelectResponse: []common.UpdateManifest{{ID: 1234}}},
cars: &mocks.MockCars{
DBMockHelper: mocks.DBMockHelper{
Error: someErr,
},
},
carsUpdate: &mocks.MockCarUpdates{},
ratePlan: &mocks.MockRatePlan{},
vConfig: vconfig.ConfigMock{GetVODCDSCodingDataMock: SuccessGetCDSMock},
payload: successPayload,
expErr: someErr,
},
"failed_cds": {
updateManifest: &mocks.MockUpdateManifests{SelectResponse: []common.UpdateManifest{{ID: 1234}}},
carsUpdate: &mocks.MockCarUpdates{},
cars: &mockCars,
ratePlan: &mocks.MockRatePlan{},
vConfig: vconfig.ConfigMock{GetVODCDSCodingDataMock: FailedGetCDSMock},
payload: successPayload,
expErr: someErr,
},
"failed_db": {
updateManifest: &mocks.MockUpdateManifests{
DBMockHelper: mocks.DBMockHelper{
Error: someErr,
},
},
cars: &mockCars,
carsUpdate: &mocks.MockCarUpdates{},
ratePlan: &mocks.MockRatePlan{},
vConfig: vconfig.ConfigMock{GetVODCDSCodingDataMock: SuccessGetCDSMock},
payload: successPayload,
expErr: someErr,
},
}
for tname, tt := range tests {
t.Run(tname, func(t *testing.T) {
redisMock.Reset()
db := services.GetDB()
db.SetManifests(tt.updateManifest)
db.SetCars(tt.cars)
db.SetCarUpdates(tt.carsUpdate)
db.SetRatePlan(tt.ratePlan)
services.SetVehicleConfig(tt.vConfig)
err := handlers.OrderUpdated(db, &smsMock, vinMock, tt.payload)
if err != nil && tt.expErr != nil {
assert.Equal(t, tt.expErr.Error(), err.Error())
return
}
assert.Equal(t, tt.expErr, err)
// assert.Equal(t, tt.expRedisMsgs, redisMock.PublishedMessages)
})
}
}
func SuccessGetCDSMock(request common.VODCDSRequest) (map[string]string, error) {
ecus := map[string]string{
"VOD": vodMock,
"ICC": confMock,
}
return ecus, nil
}
func FailedGetCDSMock(request common.VODCDSRequest) (map[string]string, error) {
return nil, someErr
}

View File

@@ -0,0 +1,24 @@
package handlers
import (
"github.com/fiskerinc/cloud-services/services/attendant/controllers"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
)
var PrivateSeed int64 // Used only for testing. Do not change otherwise
func SendManifest(d *services.DB, ka *services.KeepAwake, device common.Device, id string, data []byte) error {
logger.Debug().Msgf("SendManifest %v %s", device, id)
clientPool := services.RedisClientPool()
configService := services.GetVehicleConfig()
generator := controllers.NewManifestSender(clientPool, d, configService, device, ka, PrivateSeed)
defer generator.Release()
return generator.Process(id, data)
}

View File

@@ -0,0 +1,25 @@
package handlers
import (
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
)
// If the update is not finished installing on the car, we send out an sms to wake the car up.
// If this message is failed to deliver, we need to cancel the car update and set its status as failed
func CarSendManifestSMSWakeUpCallback(d *services.DB, ka *services.KeepAwake, device common.Device, id string, data []byte) (err error) {
logger.Info().Msgf("SMS delivered but not sending TBox manifest %v %s", device, id)
return nil
/*
clientPool := services.RedisClientPool()
sap := services.GetSapService()
configService := services.GetVehicleConfig()
generator := controllers.NewManifestSender(clientPool, d, sap, configService, device, ka, PrivateSeed)
defer generator.Release()
return generator.ContinueTBOXSend(id, data) */
}

View File

@@ -0,0 +1,962 @@
package handlers_test
import (
"fmt"
"strings"
"testing"
"time"
"github.com/fiskerinc/cloud-services/services/attendant/handlers"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"os"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/dbbasemodel"
"github.com/fiskerinc/cloud-services/pkg/common/manifestfingerprintparams"
dbm "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/grpc/sms"
"github.com/fiskerinc/cloud-services/pkg/kafka"
kafkaMock "github.com/fiskerinc/cloud-services/pkg/kafka/mock"
"github.com/fiskerinc/cloud-services/pkg/redis"
rm "github.com/fiskerinc/cloud-services/pkg/redis/tester"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/testrunner"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig"
"github.com/jinzhu/copier"
)
func TestSendManifest(t *testing.T) {
os.Setenv("APP_SERVICE_NAME", "ATTENDANT")
handlers.PrivateSeed = 123456789
ka := services.NewKeepAwakeService()
ka.SetService(&services.MockKeepAwakeImplementation{})
now := time.Now()
testVIN := "JH4KA7680RC01"
tboxKey := common.TRex.Key(testVIN)
hmiKey := common.HMI.Key(testVIN)
smsKey := "manifest_tbox_send_cache:100"
var bhex common.BinaryHex = []byte("test")
redis.MockRedisConnection()
falsePtrValue := elptr.ElPtr(false)
truePtrValue := elptr.ElPtr(true)
ecuaRollback := []*common.UpdateManifestECU{
{
ID: 100,
UpdateManifestID: 200,
ECU: "ECUA",
Version: "VERSIONOLD",
Mode: "A",
DBModelBase: th.Timestamp,
Files: []*common.UpdateManifestFile{
{
FileID: "FILEIDOLD",
UpdateManifestECUID: 1001,
Filename: "FILENAMEOLD",
URL: "URLOLD",
FileType: common.Software,
WriteRegionID: 700,
WriteRegion: common.MemoryRegion{
Offset: 701,
Length: 702,
},
FileSize: 240,
DBModelBase: th.Timestamp,
},
},
},
}
ecus := []*common.UpdateManifestECU{
{
ECU: "ICC",
Version: "SWVERSION",
HWVersions: []string{"HWVERSION"},
Mode: "D",
SelfDownload: true,
InstallPriority: 13,
Files: []*common.UpdateManifestFile{
{
FileID: "AAAAAAA",
URL: "http://download.com/file1.bin",
FileSize: 1000,
Checksum: "AAAAAAA",
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
FileType: common.Calibration,
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
DBModelBase: th.Timestamp,
},
{
FileID: "SHOULD_NOT_BE_IN_UPDATE",
URL: "http://download.com/SHOULD_NOT_BE_IN_UPDATE.bin",
FileSize: 1000,
Checksum: "AAAAAAA",
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
FileType: common.Calibration,
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
Parsed: falsePtrValue,
DBModelBase: th.Timestamp,
},
{
FileID: "MUST_BE_IN_UPDATE",
URL: "http://download.com/MUST_BE_IN_UPDATE.bin",
FileSize: 1000,
Checksum: "AAAAAAA",
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
FileType: common.Calibration,
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
Parsed: truePtrValue,
DBModelBase: th.Timestamp,
},
},
},
{
ECU: "ECUD",
Version: "SWVERSION",
HWVersions: []string{"HWVERSION"},
Mode: "D",
InstallPriority: 4,
Files: []*common.UpdateManifestFile{
{
FileID: "SHOULD_NOT_BE_IN_UPDATE",
URL: "http://download.com/SHOULD_NOT_BE_IN_UPDATE.bin",
FileSize: 1000,
Checksum: "AAAAAAA",
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
FileType: common.Calibration,
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
Parsed: falsePtrValue,
DBModelBase: th.Timestamp,
},
{
FileID: "MUST_BE_IN_UPDATE",
URL: "http://download.com/MUST_BE_IN_UPDATE.bin",
FileSize: 1000,
Checksum: "AAAAAAA",
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
FileType: common.Calibration,
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
Parsed: truePtrValue,
DBModelBase: th.Timestamp,
},
{
FileID: "BBBBBBB",
URL: "http://download.com/file2.bin",
FileSize: 2000,
Checksum: "BBBBBBB",
FileType: common.Calibration,
WriteRegionID: 300,
WriteRegion: common.MemoryRegion{
Offset: 301,
Length: 302,
},
EraseRegionID: 400,
EraseRegion: &common.MemoryRegion{
Offset: 401,
Length: 402,
},
DBModelBase: th.Timestamp,
},
},
DBModelBase: th.Timestamp,
},
{
ID: 100,
UpdateManifestID: 200,
ECU: "PDU",
Version: "PDU-VERS",
HWVersions: []string{"PDU-VERS"},
Mode: "PDU",
ConfigurationMask: "AAAAAAAA",
InstallPriority: 7,
Files: []*common.UpdateManifestFile{
{
FileID: "NOT_BE_IN_UPDATE",
URL: "http://download.com/NOT_BE_IN_UPDATE.bin",
FileSize: 1000,
Checksum: "AAAAAAA",
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
FileType: common.Calibration,
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
Parsed: falsePtrValue,
DBModelBase: th.Timestamp,
},
{
FileID: "MUST_BE_IN_UPDATE",
URL: "http://download.com/MUST_BE_IN_UPDATE.bin",
FileSize: 1000,
Checksum: "AAAAAAA",
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
FileType: common.Calibration,
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
Parsed: truePtrValue,
DBModelBase: th.Timestamp,
},
{
FileID: "AAAAAAAA",
URL: "http://download.com/filea.bin",
FileSize: 2000,
Checksum: "AAAAAAAA",
WriteRegionID: 500,
FileType: common.Calibration,
WriteRegion: common.MemoryRegion{
Offset: 501,
Length: 502,
},
DBModelBase: th.Timestamp,
},
},
DBModelBase: th.Timestamp,
},
{
ID: 100,
UpdateManifestID: 200,
ECU: "ECUA",
Version: "A-VERS",
HWVersions: []string{"A-VERS"},
Mode: "A",
ConfigurationMask: "AAAAAAAA",
InstallPriority: 1,
Files: []*common.UpdateManifestFile{
{
FileID: "FILEID",
UpdateManifestECUID: 100,
Filename: "FILENAME",
URL: "URL",
FileType: common.Calibration,
WriteRegionID: 600,
WriteRegion: common.MemoryRegion{
Offset: 601,
Length: 602,
},
FileSize: 240,
DBModelBase: th.Timestamp,
},
},
DBModelBase: th.Timestamp,
},
}
fingerprint := getTodaysFingerprint()
standardManifest := common.UpdateManifest{
ID: 100,
CarUpdateID: 297,
Name: "TEST",
Version: "MANIFEST_VERSION",
SUMS: "2023.10.01.00",
Description: "TESTDESC",
ReleaseNotes: "http://fiskerinc.com/release_notes",
Type: common.ManifestTypeForced,
Active: truePtrValue,
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
BodyType: "truck",
RollbackEnabled: true,
ECUs: ecus,
UpdateDuration: 30,
}
rollbackDisabledManifest := common.UpdateManifest{
ID: 100,
CarUpdateID: 297,
Name: "TEST",
Version: "MANIFEST_VERSION",
SUMS: "2023.10.01.00.E",
Description: "TESTDESC",
ReleaseNotes: "http://fiskerinc.com/release_notes",
Type: "standard",
Active: truePtrValue,
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
BodyType: "truck",
ECUs: ecus,
UpdateDuration: 30,
}
standardManifestNoICC := common.UpdateManifest{
ID: 100,
CarUpdateID: 297,
Name: "TEST",
Version: "MANIFEST_VERSION",
SUMS: "2023.10.01.00.E",
Description: "TESTDESC",
ReleaseNotes: "http://fiskerinc.com/release_notes",
Type: "standard",
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
RollbackEnabled: true,
BodyType: "truck",
ECUs: []*common.UpdateManifestECU{
{
ECU: "ECUD",
HWVersions: []string{"HWVERSION"},
Version: "SWVERSION",
Mode: "D",
InstallPriority: 7,
Files: []*common.UpdateManifestFile{
{
FileID: "BBBBBBB",
URL: "http://download.com/file2.bin",
FileSize: 2000,
Checksum: "BBBBBBB",
FileType: common.Calibration,
WriteRegionID: 700,
WriteRegion: common.MemoryRegion{
Offset: 701,
Length: 702,
},
DBModelBase: th.Timestamp,
},
},
DBModelBase: th.Timestamp,
},
{
ID: 100,
UpdateManifestID: 200,
ECU: "PDU",
Version: "PDU-VERS",
HWVersions: []string{"PDU-VERS"},
Mode: "PDU",
ConfigurationMask: "AAAAAAAA",
InstallPriority: 10,
Files: []*common.UpdateManifestFile{
{
FileID: "AAAAAAAA",
URL: "http://download.com/filea.bin",
FileSize: 2000,
FileType: common.Calibration,
Checksum: "AAAAAAAA",
WriteRegionID: 800,
WriteRegion: common.MemoryRegion{
Offset: 801,
Length: 802,
},
EraseRegionID: 900,
EraseRegion: &common.MemoryRegion{
Offset: 901,
Length: 902,
},
DBModelBase: th.Timestamp,
},
},
DBModelBase: th.Timestamp,
},
{
ID: 100,
UpdateManifestID: 200,
ECU: "ECUA",
Version: "A-VERS",
HWVersions: []string{"A-VERS"},
Mode: "A",
ConfigurationMask: "AAAAAAAA",
InstallPriority: 4,
Files: []*common.UpdateManifestFile{
{
FileID: "FILEID",
UpdateManifestECUID: 100,
Filename: "FILENAME",
URL: "URL",
FileType: common.Calibration,
WriteRegionID: 600,
WriteRegion: common.MemoryRegion{
Offset: 601,
Length: 602,
},
FileSize: 240,
DBModelBase: th.Timestamp,
},
},
DBModelBase: th.Timestamp,
},
},
UpdateDuration: 30,
}
mockCarUpdate := &common.CarUpdate{
ID: 297,
VIN: testVIN,
UpdateManifestID: 100,
}
mockCarECUs := []common.CarECU{
{ECU: "ECUD", HWVersion: "HWVERSION"},
{ECU: "ECUA", HWVersion: "A-VERS", Version: "TEST122"},
{ECU: "OBC", HWVersion: "PDU-VERS", Version: "TEST121"},
{ECU: "BCM", HWVersion: ";#A;#"},
{ECU: "ICC", HWVersion: "HWVERSION", Version: "TEST123"},
}
mockRedis := rm.MockRedis{}
mockCUDB := dbm.MockCarUpdates{}
mockManDB := dbm.MockUpdateManifests{}
mockKeys := dbm.MockEccKeys{
MockListResponse: []common.ECCKeys{
{
ECU: "PDU",
PrivKey1: &bhex,
PrivKey2: &bhex,
PrivKey3: &bhex,
PubKey1: &bhex,
PubKey2: &bhex,
PubKey3: &bhex,
Env: "current",
DBModelBase: dbbasemodel.DBModelBase{
CreatedAt: &now,
UpdatedAt: &now,
},
},
{
ECU: "ECUA",
PrivKey1: &bhex,
PrivKey2: &bhex,
PrivKey3: &bhex,
PubKey1: &bhex,
PubKey2: &bhex,
PubKey3: &bhex,
Env: "current",
DBModelBase: dbbasemodel.DBModelBase{
CreatedAt: &now,
UpdatedAt: &now,
},
},
{
ECU: "ECUD",
PrivKey1: &bhex,
PrivKey2: &bhex,
PrivKey3: &bhex,
PubKey1: &bhex,
PubKey2: &bhex,
PubKey3: &bhex,
Env: "current",
DBModelBase: dbbasemodel.DBModelBase{
CreatedAt: &now,
UpdatedAt: &now,
},
},
},
}
services.GetDB().SetCarUpdates(&mockCUDB)
services.GetDB().SetCars(&dbm.MockCars{SelectCarECUs: mockCarECUs, SelectResponse: &common.Car{ICCID: "8000000000000000000"}})
services.GetDB().SetManifests(&mockManDB)
services.GetDB().SetECCKeys(&mockKeys)
services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis))
mockSap := vconfig.SAPServiceMock{GetConfigurationMock: func(vin string) (common.SAPResponse, error) {
return common.SAPResponse{
ModelYear: 2023,
ModelType: "Ocean",
VersionDuringModelYear: "1",
Features: []common.SAPFeature{
{
FamilyCode: "FamilyCode1",
FeatureCode: "FeatureCode1",
},
{
FamilyCode: "FamilyCode2",
FeatureCode: "FeatureCode2",
},
},
}, nil
}}
mockConf := vconfig.ConfigMock{GetVODCDSCodingDataMock: func(request common.VODCDSRequest) (map[string]string, error) {
return map[string]string{
"EPS": "000147303031313632011D",
"ESP": "000F47303031313632000001012301027600000101000002B0",
"ECUA": "config",
"VOD": "",
"ADAS": "000147303031313632011D",
}, nil
}}
mockSMS := &sms.SMSMock{}
mockSMS.SetHandleSMSQueueResponse(&sms.SMSQueueResponse{
SmsMsgID: "100",
SentSuccessful: true,
}, nil)
services.SetSapService(mockSap)
services.SetVehicleConfig(mockConf)
services.SetSmsClient(mockSMS)
mockKafkaProducer := kafkaMock.GetKafkaMock(nil)
services.SetKafkaProducer(mockKafkaProducer)
clonedStandardManifest := common.UpdateManifest{}
clonedStandardManifestNoICC := common.UpdateManifest{}
err := copier.CopyWithOption(&clonedStandardManifest, &standardManifest, copier.Option{DeepCopy: true})
if err != nil {
t.Fatal(err)
}
err = copier.CopyWithOption(&clonedStandardManifestNoICC, &standardManifestNoICC, copier.Option{DeepCopy: true})
if err != nil {
t.Fatal(err)
}
tests := []testrunner.TestCase{
{
Name: "Send manifest w Self Download",
DBTestCase: &dbm.DBTestCase{
MockLoadResponse: mockCarUpdate,
SetupMockResponse: func() {
mockCUDB.LoadManifest = &clonedStandardManifest
mockManDB.ECUUpdatesMock = func(man *common.UpdateManifestECU, vin string) ([]*common.UpdateManifestECU, error) {
if man.ECU == "ECUA" {
return ecuaRollback, nil
}
return nil, nil
}
},
},
RedisTestCase: &rm.RedisTestCase{
Device: common.Service,
DeviceKey: kafka.OTAUpdateService,
PayloadData: `{"car_update_id":297}`,
ExpectedMessages: map[string]string{
hmiKey: `{"handler":"update_manifest","data":{"name":"TEST","version":"MANIFEST_VERSION","description":"TESTDESC","release_notes":"http://fiskerinc.com/release_notes","ecu_updates":[{"name":"ECUA","version":"A-VERS","current_version":"TEST122","hw_version":"A-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"FILEID","url":"URL","file_size":240,"type":"calibration","write_region":{"offset":601,"length":602}}]},{"name":"ECUD","version":"SWVERSION","hw_version":"HWVERSION","files":[{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"BBBBBBB","url":"http://download.com/file2.bin","file_size":2000,"checksum":"BBBBBBB","type":"calibration","write_region":{"offset":301,"length":302},"erase_region":{"offset":401,"length":402}}]},{"name":"OBC","version":"PDU-VERS","current_version":"TEST121","hw_version":"PDU-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"AAAAAAAA","url":"http://download.com/filea.bin","file_size":2000,"checksum":"AAAAAAAA","type":"calibration","write_region":{"offset":501,"length":502}}]},{"name":"ICC","version":"SWVERSION","current_version":"TEST123","hw_version":"HWVERSION","self_download":true,"files":[{"file_id":"AAAAAAA","url":"http://download.com/file1.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}}]}],"fingerprint":"` + fingerprint + `","car_update_id":297,"rollback":true,"type":"forced","vod":"01011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110100202310010011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100001e","update_duration":30}}`,
},
ExpectedCaches: map[string]rm.ExpiringCacheResult{
// carupdateKey: {
// Value: `{"current_size":0,"ecu":"","errorcode":0,"file_size":0,"file_total":0,"id":297,"info":"ICC","installed":0,"status":"sent","total_files":0,"total_size":0}`,
// Expires: expectedExpire,
// },
smsKey: {
Value: `{"VIN":"JH4KA7680RC01","ManifestID":100,"Destination":"ICC"}`,
},
},
},
},
{
Name: "Send manifest w Self Download rollback disabled",
DBTestCase: &dbm.DBTestCase{
MockLoadResponse: mockCarUpdate,
SetupMockResponse: func() {
mockCUDB.LoadManifest = &rollbackDisabledManifest
mockManDB.ECUUpdatesMock = func(man *common.UpdateManifestECU, vin string) ([]*common.UpdateManifestECU, error) {
if man.ECU == "ECUA" {
return ecuaRollback, nil
}
return nil, nil
}
},
},
RedisTestCase: &rm.RedisTestCase{
Device: common.Service,
DeviceKey: kafka.OTAUpdateService,
PayloadData: `{"car_update_id":297}`,
ExpectedMessages: map[string]string{
hmiKey: `{"handler":"update_manifest","data":{"name":"TEST","version":"MANIFEST_VERSION","description":"TESTDESC","release_notes":"http://fiskerinc.com/release_notes","ecu_updates":[{"name":"ECUA","version":"A-VERS","current_version":"TEST122","hw_version":"A-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"FILEID","url":"URL","file_size":240,"type":"calibration","write_region":{"offset":601,"length":602}}]},{"name":"ECUD","version":"SWVERSION","hw_version":"HWVERSION","files":[{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"BBBBBBB","url":"http://download.com/file2.bin","file_size":2000,"checksum":"BBBBBBB","type":"calibration","write_region":{"offset":301,"length":302},"erase_region":{"offset":401,"length":402}}]},{"name":"OBC","version":"PDU-VERS","current_version":"TEST121","hw_version":"PDU-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"AAAAAAAA","url":"http://download.com/filea.bin","file_size":2000,"checksum":"AAAAAAAA","type":"calibration","write_region":{"offset":501,"length":502}}]},{"name":"ICC","version":"SWVERSION","current_version":"TEST123","hw_version":"HWVERSION","self_download":true,"files":[{"file_id":"AAAAAAA","url":"http://download.com/file1.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"MUST_BE_IN_UPDATE","url":"http://download.com/MUST_BE_IN_UPDATE.bin","file_size":1000,"checksum":"AAAAAAA","type":"calibration","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}}]}],"fingerprint":"` + fingerprint + `","car_update_id":297,"rollback":false,"type":"standard","vod":"0101111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111010020231001000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000ba","update_duration":30}}`,
},
ExpectedCaches: map[string]rm.ExpiringCacheResult{
smsKey: {
Value: `{"VIN":"JH4KA7680RC01","ManifestID":100,"Destination":"ICC"}`,
},
},
},
},
{
Name: "Bad message",
RedisTestCase: &rm.RedisTestCase{
Device: common.HMI,
DeviceKey: testVIN,
PayloadData: `{"car_update_id":`,
ExpectedError: "unexpected end of JSON input",
},
},
{
Name: "Send manifest w No Self Download",
DBTestCase: &dbm.DBTestCase{
MockLoadResponse: mockCarUpdate,
SetupMockResponse: func() {
mockCUDB.LoadManifest = &clonedStandardManifestNoICC
},
},
RedisTestCase: &rm.RedisTestCase{
Device: common.Service,
DeviceKey: kafka.OTAUpdateService,
PayloadData: `{"car_update_id":297}`,
ExpectedMessages: map[string]string{
hmiKey: `{"handler":"update_manifest","data":{"name":"TEST","version":"MANIFEST_VERSION","description":"TESTDESC","release_notes":"http://fiskerinc.com/release_notes","ecu_updates":[{"name":"ECUA","version":"A-VERS","current_version":"TEST122","hw_version":"A-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"FILEID","url":"URL","file_size":240,"type":"calibration","write_region":{"offset":601,"length":602}}]},{"name":"ECUD","version":"SWVERSION","hw_version":"HWVERSION","files":[{"file_id":"BBBBBBB","url":"http://download.com/file2.bin","file_size":2000,"checksum":"BBBBBBB","type":"calibration","write_region":{"offset":701,"length":702}}]},{"name":"OBC","version":"PDU-VERS","current_version":"TEST121","hw_version":"PDU-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"AAAAAAAA","url":"http://download.com/filea.bin","file_size":2000,"checksum":"AAAAAAAA","type":"calibration","write_region":{"offset":801,"length":802},"erase_region":{"offset":901,"length":902}}]}],"fingerprint":"` + fingerprint + `","car_update_id":297,"rollback":true,"type":"standard","vod":"0101111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111010020231001000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000ba","update_duration":30}}`,
tboxKey: `{"handler":"update_manifest","data":{"ecu_updates":[{"name":"ECUA","version":"A-VERS","current_version":"TEST122","hw_version":"A-VERS","configuration_mask":"AAAAAAAA","configuration":"config","files":[{"file_id":"FILEID","url":"URL","file_size":240,"type":"calibration","write_region":{"offset":601,"length":602}}],"rollback":[{"version":"VERSIONOLD","files":[{"file_id":"FILEIDOLD","url":"URLOLD","file_size":240,"type":"software","write_region":{"offset":701,"length":702}}]}],"ecc_keys":{"level_1":"74657374","level_2":"74657374","level_3":"74657374"}},{"name":"ECUD","version":"SWVERSION","hw_version":"HWVERSION","files":[{"file_id":"BBBBBBB","url":"http://download.com/file2.bin","file_size":2000,"checksum":"BBBBBBB","type":"calibration","write_region":{"offset":701,"length":702}}],"ecc_keys":{"level_1":"74657374","level_2":"74657374","level_3":"74657374"}},{"name":"OBC","version":"PDU-VERS","current_version":"TEST121","hw_version":"PDU-VERS","configuration_mask":"AAAAAAAA","files":[{"file_id":"AAAAAAAA","url":"http://download.com/filea.bin","file_size":2000,"checksum":"AAAAAAAA","type":"calibration","write_region":{"offset":801,"length":802},"erase_region":{"offset":901,"length":902}}],"ecc_keys":{"level_1":"74657374","level_2":"74657374","level_3":"74657374"}}],"fingerprint":"` + fingerprint + `","car_update_id":297,"rollback":true,"type":"standard","vod":"0101111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111010020231001000111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110000ba","update_duration":30}}`,
},
ExpectedCaches: map[string]rm.ExpiringCacheResult{
"manifest_tbox_send_cache:UQIDEPFQUH": {
Value: `{"VIN":"JH4KA7680RC01","ManifestID":0,"Destination":"ICC/TBOX"}`,
},
},
},
},
}
schemaTesterHMI := th.NewSchemaTestHelper(t, schemaToHMI)
schemaTesterTRex := th.NewSchemaTestHelper(t, schemaToTRex)
for _, test := range tests {
mockRedis.Reset()
for _, ecu := range ecus {
ecu.Rollback = nil
}
if test.DBTestCase != nil {
test.DBTestCase.SetupDB(&mockCUDB)
}
if test.RedisTestCase != nil {
test.RedisTestCase.SetupRedis(&mockRedis)
err := handlers.SendManifest(services.GetDB(), ka, test.RedisTestCase.Device, test.RedisTestCase.DeviceKey, []byte(test.RedisTestCase.PayloadData))
test.RedisTestCase.CheckHandlerError(t, test.Name, err)
test.RedisTestCase.Validate(t, test.Name, &mockRedis)
}
if test.DBTestCase != nil {
test.DBTestCase.Validate(t, test.Name, &mockCUDB)
}
for key, m := range test.RedisTestCase.ExpectedMessages {
name := fmt.Sprintf("%s %s", test.Name, key)
if strings.Contains(key, "2:") {
schemaTesterHMI.ValidateSchemaObject(name, []byte(m))
continue
}
schemaTesterTRex.ValidateSchemaObject(name, []byte(m))
}
}
}
func TestSendManifestMultiFile(t *testing.T) {
os.Setenv("APP_SERVICE_NAME", "ATTENDANT")
testVIN := "JH4KA7680RC01"
now := time.Now()
hmiKey := common.HMI.Key(testVIN)
falsePtrValue := elptr.ElPtr(false)
truePtrValue := elptr.ElPtr(true)
var bhex common.BinaryHex = []byte("test")
redis.MockRedisConnection()
ka := services.NewKeepAwakeService()
ka.SetService(&services.MockKeepAwakeImplementation{})
ecus := []*common.UpdateManifestECU{
{
ECU: "BCM",
Version: "DB22121A",
HWVersions: []string{";#A;#"},
Mode: "D",
SelfDownload: true,
Files: []*common.UpdateManifestFile{
{
FileID: "ShouldNotSee",
Filename: "notparsed.s19",
URL: "fakeurl.com",
Checksum: "shouldnotsee",
Parsed: falsePtrValue,
},
{
FileID: "3ee5fd6bc9402d67",
Filename: "MAGNA_BCM_FBL_driver.s19_0.bin",
URL: "https://upload-dev.fiskerdps.com/4466d78a-50e1-4f1d-a4b8-5f78076758ed/MAGNA_BCM_FBL_driver.s19_0.bin",
FileSize: 1000,
Checksum: "shouldsee",
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
FileType: common.Bootloader,
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
DBModelBase: th.Timestamp,
Parsed: truePtrValue,
},
{
FileID: "7878ede9c9407779",
Filename: "FM29_Application_FR40_220111.s19_0.bin",
URL: "ttps://upload-dev.fiskerdps.com/b5c3c8c8-86cd-422e-aa11-a248697edfa3/FM29_Application_FR40_220111.s19_0.bin",
FileSize: 1000,
Checksum: "SHOULD NOT SEE",
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
FileType: common.Software,
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
Parsed: truePtrValue,
Signature: "ShouldNOtSee",
DBModelBase: th.Timestamp,
},
{
FileID: "7bb5083dc9407ae9",
Filename: "FM29_Application_FR40_220111.s19_1.bin",
URL: "https://upload-dev.fiskerdps.com/2cdeb566-cb00-42cd-93a8-69769b19269a/FM29_Application_FR40_220111.s19_1.bin",
FileSize: 1000,
Checksum: "shantsee",
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
FileType: common.Software,
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
Parsed: truePtrValue,
DBModelBase: th.Timestamp,
Signature: "SHOULD NOT SEE",
},
{
FileID: "7ebb4fadc9409b91",
Filename: "FM29_Application_FR40_220111.s19_2.bin",
URL: "https://upload-dev.fiskerdps.com/ec9f8a84-61fb-4a9b-9735-ac6f91b07a83/FM29_Application_FR40_220111.s19_2.bin",
FileType: common.Software,
Parsed: truePtrValue,
Signature: "SomeSignatureYouShouldSee",
},
},
},
}
fingerprint := getTodaysFingerprint()
standardManifest := common.UpdateManifest{
ID: 100,
CarUpdateID: 297,
Name: "TEST",
Version: "MANIFEST_VERSION",
SUMS: "2023.10.01.00",
Description: "TESTDESC",
ReleaseNotes: "http://fiskerinc.com/release_notes",
Type: "standard",
Active: truePtrValue,
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
BodyType: "truck",
RollbackEnabled: false,
ECUs: ecus,
UpdateDuration: 30,
}
mockCarUpdate := &common.CarUpdate{
ID: 297,
VIN: testVIN,
UpdateManifestID: 100,
}
mockCarECUs := []common.CarECU{
{ECU: "ECUD", HWVersion: "HWVERSION"},
{ECU: "ECUA", HWVersion: "A-VERS"},
{ECU: "OBC", HWVersion: "PDU-VERS"},
{ECU: "BCM", HWVersion: ";#A;#"},
{ECU: "ICC", HWVersion: "HWVERSION"},
}
mockRedis := rm.MockRedis{}
mockCUDB := dbm.MockCarUpdates{}
mockManDB := dbm.MockUpdateManifests{}
mockKeys := dbm.MockEccKeys{
MockListResponse: []common.ECCKeys{
{
ECU: "BCM",
PrivKey1: &bhex,
PrivKey2: &bhex,
PrivKey3: &bhex,
PubKey1: &bhex,
PubKey2: &bhex,
PubKey3: &bhex,
Env: "current",
DBModelBase: dbbasemodel.DBModelBase{
CreatedAt: &now,
UpdatedAt: &now,
},
},
},
}
services.GetDB().SetCarUpdates(&mockCUDB)
services.GetDB().SetCars(&dbm.MockCars{SelectCarECUs: mockCarECUs, SelectResponse: &common.Car{ICCID: ""}})
services.GetDB().SetManifests(&mockManDB)
services.GetDB().SetECCKeys(&mockKeys)
services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis))
mockSap := vconfig.SAPServiceMock{GetConfigurationMock: func(vin string) (common.SAPResponse, error) {
return common.SAPResponse{
ModelYear: 2023,
ModelType: "Ocean",
VersionDuringModelYear: "1",
Features: []common.SAPFeature{
{
FamilyCode: "FamilyCode1",
FeatureCode: "FeatureCode1",
},
{
FamilyCode: "FamilyCode2",
FeatureCode: "FeatureCode2",
},
},
}, nil
}}
mockConf := vconfig.ConfigMock{GetVODCDSCodingDataMock: func(request common.VODCDSRequest) (map[string]string, error) {
return map[string]string{
"ECUA": "config",
"VOD": "",
}, nil
}}
mockSMS := &sms.SMSMock{}
mockSMS.SetHandleSMSQueueResponse(&sms.SMSQueueResponse{
SmsMsgID: "100",
SentSuccessful: true,
}, nil)
services.SetSmsClient(mockSMS)
services.SetSapService(mockSap)
services.SetVehicleConfig(mockConf)
smsKey := "manifest_tbox_send_cache:100"
tests := []testrunner.TestCase{
{
Name: "MultiFile right order",
DBTestCase: &dbm.DBTestCase{
MockLoadResponse: mockCarUpdate,
SetupMockResponse: func() {
mockCUDB.LoadManifest = &standardManifest
mockManDB.ECUUpdatesMock = func(man *common.UpdateManifestECU, vin string) ([]*common.UpdateManifestECU, error) {
if man.ECU == "ECUA" {
return nil, nil
}
return nil, nil
}
},
},
RedisTestCase: &rm.RedisTestCase{
Device: common.Service,
DeviceKey: kafka.OTAUpdateService,
PayloadData: `{"car_update_id":297}`,
ExpectedMessages: map[string]string{
hmiKey: `{"handler":"update_manifest","data":{"name":"TEST","version":"MANIFEST_VERSION","description":"TESTDESC","release_notes":"http://fiskerinc.com/release_notes","ecu_updates":[{"name":"BCM","version":"DB22121A","hw_version":";#A;#","self_download":true,"files":[{"file_id":"3ee5fd6bc9402d67","url":"https://upload-dev.fiskerdps.com/4466d78a-50e1-4f1d-a4b8-5f78076758ed/MAGNA_BCM_FBL_driver.s19_0.bin","file_size":1000,"checksum":"shouldsee","type":"bootloader","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"7878ede9c9407779","url":"ttps://upload-dev.fiskerdps.com/b5c3c8c8-86cd-422e-aa11-a248697edfa3/FM29_Application_FR40_220111.s19_0.bin","file_size":1000,"type":"software","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"7bb5083dc9407ae9","url":"https://upload-dev.fiskerdps.com/2cdeb566-cb00-42cd-93a8-69769b19269a/FM29_Application_FR40_220111.s19_1.bin","file_size":1000,"type":"software","write_region":{"offset":101,"length":102},"erase_region":{"offset":201,"length":202}},{"file_id":"7ebb4fadc9409b91","url":"https://upload-dev.fiskerdps.com/ec9f8a84-61fb-4a9b-9735-ac6f91b07a83/FM29_Application_FR40_220111.s19_2.bin","type":"software","write_region":{"offset":0,"length":0},"signature":"SomeSignatureYouShouldSee"}]}],"fingerprint":"` + fingerprint + `","car_update_id":297,"rollback":false,"type":"standard","vod":"01011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111110100202310010011111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111100001e","update_duration":30}}`,
},
ExpectedCaches: map[string]rm.ExpiringCacheResult{smsKey: {
Value: `{"VIN":"JH4KA7680RC01","ManifestID":100,"Destination":"ICC"}`,
}},
},
},
}
for _, test := range tests {
mockRedis.Reset()
for _, ecu := range ecus {
ecu.Rollback = nil
}
if test.DBTestCase != nil {
test.DBTestCase.SetupDB(&mockCUDB)
}
if test.RedisTestCase != nil {
test.RedisTestCase.SetupRedis(&mockRedis)
err := handlers.SendManifest(services.GetDB(), ka, test.RedisTestCase.Device, test.RedisTestCase.DeviceKey, []byte(test.RedisTestCase.PayloadData))
test.RedisTestCase.CheckHandlerError(t, test.Name, err)
test.RedisTestCase.Validate(t, test.Name, &mockRedis)
}
if test.DBTestCase != nil {
test.DBTestCase.Validate(t, test.Name, &mockCUDB)
}
}
}
func TestSendManifestIntegration(t *testing.T) {
os.Setenv("APP_SERVICE_NAME", "ATTENDANT")
ka := services.NewKeepAwakeService()
ka.SetService(&services.MockKeepAwakeImplementation{})
err := handlers.SendManifest(services.GetDB(), ka, common.Service, "", []byte(`{"car_update_id":297}`))
if err != nil {
t.Error(err)
}
}
func getTodaysFingerprint() string {
// generate today's fingerprint
fpparams := manifestfingerprintparams.GetFPParams()
var fm = common.UpdateManifest{}
fm.GenerateFingerprint(fpparams.CurTime(), fpparams.ManifestSerial())
return fm.Fingerprint
}

View File

@@ -0,0 +1,55 @@
package handlers
import (
"encoding/json"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/pkg/errors"
)
// UpdateData extracts update ID from byte slice
type UpdateData struct {
ID int `json:"id"`
}
// ApproveUpdate updates DB update field and
//
// sends redis message to vehicle to initialize download
func ApproveUpdate(db *services.DB, id string, data []byte) error {
logger.Debug().Msgf("ApproveUpdate %s", id)
client := services.RedisClientPool().GetFromPool()
defer client.Close()
// TODO: NEEDS VALIDATION THAT INCOMING ID CAN APPROVE
// CAN COME FROM MOBILE OR HMI - NEEDS TO HANDLE BOTH CASES
var u UpdateData
err := json.Unmarshal(data, &u)
if err != nil {
return errors.WithStack(err)
}
update, err := db.ModifyUpdateStatus(u.ID, "approved")
if err != nil {
return err
}
logger.Debug().Msgf("Sending redis queue- %s, key- %s, hander- %s, data- %v", "attendant", update.VIN, "update_download", u)
err = client.SafeQueueMessage(
common.TRex.Key(update.VIN),
common.Message{
Handler: "update_download",
Data: u,
},
)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,28 @@
package handlers_test
import (
"testing"
"github.com/fiskerinc/cloud-services/services/attendant/handlers"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestApproveUpdate(t *testing.T) {
mockRedis := &tester.MockRedis{}
services.SetRedisClientPool(tester.NewMockClientPool(mockRedis))
data := []byte(`{"id": 1}`)
err := handlers.ApproveUpdate(mockDB, "FISKER123", data)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "ApproveUpdate", "no error", err)
}
err = handlers.ApproveUpdate(mockDB, "FISKER123", nil)
if err == nil {
t.Errorf(testhelper.TestErrorTemplate, "ApproveUpdate", "error", err)
}
}

View File

@@ -0,0 +1,70 @@
package handlers
import (
"encoding/json"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
s "github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/pkg/errors"
)
// VehicleData extracts VIN from byte slice
type VehicleData struct {
VIN string `json:"vin" validate:"required,vin"`
}
// GetUpdates queries DB for updates based off VIN and
//
// sends redis message back to requester
func GetUpdates(db *services.DB, id string, data []byte) error {
clientPool := services.RedisClientPool()
var v VehicleData
err := json.Unmarshal(data, &v)
if err != nil {
return errors.WithStack(err)
}
err = validator.ValidateStruct(&v)
if err != nil {
return errors.WithStack(err)
}
ok, err := cache.VerifyCarToDriver(clientPool, db.GetCars(), v.VIN, id)
if err != nil {
return err
} else if !ok {
return cache.ErrInvalidCarToDriverAssociation(v.VIN, id)
}
cu := common.CarUpdate{
VIN: v.VIN,
Status: s.InstallApprovalAwait,
}
updates, err := db.GetCarUpdates().Select(&cu, nil)
if err != nil {
return err
}
result := make([]common.ApprovalUpdate, len(updates))
for i := range updates {
result[i] = common.NewApprovalUpdates(&updates[i])
}
client := clientPool.GetFromPool()
defer client.Close()
logger.Debug().Msgf("Sending redis queue- %s, key- %s, hander- %s, data- %v", "attendant", id, "updates", result)
return client.SafeQueueMessage(
common.Mobile.Key(id),
common.Message{
Handler: "updates",
Data: result,
},
)
}

View File

@@ -0,0 +1,148 @@
package handlers_test
import (
"fmt"
"testing"
"time"
"github.com/fiskerinc/cloud-services/services/attendant/handlers"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/common"
s "github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/common/dbbasemodel"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/pkg/errors"
)
func TestUpdatesGet(t *testing.T) {
mockRedis := &tester.MockRedis{}
services.SetRedisClientPool(tester.NewMockClientPool(mockRedis))
mockCars := &mocks.MockCars{}
mockCarUpdates := &mocks.MockCarUpdates{}
mockDB = &services.DB{}
mockDB.SetCars(mockCars)
mockDB.SetCarUpdates(mockCarUpdates)
cognitoID := "valid-cognito-id-1"
mobileKey := common.Mobile.Key(cognitoID)
vin := "JM1BG2241R0797923"
data := fmt.Sprintf(`{"vin":"%s"}`, vin)
now := time.Now()
selectList := []common.CarUpdate{
{
ID: 1234,
VIN: vin,
UpdateManifestID: 4321,
Status: s.InstallApprovalAwait,
UpdateManifest: &common.UpdateManifest{
Name: "TEST",
Description: "TEST",
ReleaseNotes: "http://releasenotes.com",
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
BodyType: "truck",
ECUs: []*common.UpdateManifestECU{
{
ECU: "ADAS",
Version: "VERSION",
Mode: "A",
},
},
DBModelBase: dbbasemodel.DBModelBase{
CreatedAt: &now,
UpdatedAt: &now,
},
},
},
}
carToDrivers := []common.CarToDriver{
{
ID: 2000,
VIN: vin,
DriverID: cognitoID,
},
}
tests := []AttendentRouteTestCase{
{
Name: "No data",
RedisTestCase: tester.RedisTestCase{
Device: common.Mobile,
DeviceKey: cognitoID,
PayloadData: "",
ExpectedError: "unexpected end of JSON input",
},
},
{
Name: "Bad request",
RedisTestCase: tester.RedisTestCase{
Device: common.Mobile,
DeviceKey: cognitoID,
PayloadData: "{}",
ExpectedError: "Key: 'VehicleData.VIN' Error:Field validation for 'VIN' failed on the 'required' tag",
},
},
{
Name: "Bad association",
RedisTestCase: tester.RedisTestCase{
Device: common.Mobile,
DeviceKey: cognitoID,
PayloadData: data,
ExpectedError: "no relationship found between vin JM1BG2241R0797923 and driver valid-cognito-id-1",
},
},
{
Name: "Cache error",
RedisTestCase: tester.RedisTestCase{
Device: common.Mobile,
DeviceKey: cognitoID,
PayloadData: data,
ExpectedError: "cache error",
},
CarToDriversError: errors.New("cache error"),
},
{
Name: "Good request",
RedisTestCase: tester.RedisTestCase{
Device: common.Mobile,
DeviceKey: cognitoID,
PayloadData: data,
ExpectedMessages: map[string]string{
mobileKey: `{"handler":"updates","data":[{"id":1234,"vin":"JM1BG2241R0797923","name":"TEST","description":"TEST","release_notes":"http://releasenotes.com"}]}`,
},
},
SelectCarToDrivers: carToDrivers,
SelectCarUpdates: selectList,
},
{
Name: "DB error",
RedisTestCase: tester.RedisTestCase{
Device: common.Mobile,
DeviceKey: cognitoID,
PayloadData: data,
ExpectedError: "database error",
},
SelectCarToDrivers: carToDrivers,
SelectCarUpdates: nil,
CarUpdateError: errors.New("database error"),
},
}
for i := range tests {
mockRedis.Reset()
test := &tests[i]
test.SetupRedis(mockRedis)
test.SetupDB(mockCars, mockCarUpdates, test)
err := handlers.GetUpdates(mockDB, test.DeviceKey, []byte(test.PayloadData))
test.CheckHandlerError(t, test.Name, err)
test.Validate(t, test.Name, mockRedis)
}
}

View File

@@ -0,0 +1,45 @@
package handlers
import (
"encoding/json"
"time"
"github.com/fiskerinc/cloud-services/services/attendant/controllers"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/pkg/errors"
)
func UploadDtc(id string, data []byte) error {
// ID is a vin in this case
logger.Debug().Msgf("Upload DTC for %s", id)
var DTCEntry controllers.DTCEntry
DTCEntry.VIN = id
err := json.Unmarshal(data, &DTCEntry)
if err != nil {
return err
}
if err = validator.GetValidator().Struct(&DTCEntry); err != nil {
return errors.WithMessage(err, "failed structure validation")
}
err = DTCEntry.ParseSnapshot()
if err != nil {
logger.Warn().Msg(err.Error())
return err
}
client, err := services.GetMongoClient()
if err != nil {
logger.Warn().Str("id", id).Err(err).Send()
return err
}
dtcs := mongo.NewCollection(client.Collection("dtcs"))
DTCEntry.CreatedAt = time.Now()
_, err = dtcs.InsertOne(DTCEntry)
return err
}

View File

@@ -0,0 +1,27 @@
package main
import (
"github.com/fiskerinc/cloud-services/services/attendant/controllers"
"github.com/fiskerinc/cloud-services/services/attendant/server"
"github.com/fiskerinc/cloud-services/pkg/kafka"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils/app"
)
func init() {
app.Setup("attendant", cleanup)
}
func main() {
defer cleanup()
go controllers.HealthCheck()
go server.StartConsumer(kafka.AttendantServiceGRPCKafka, kafka.AttendantService)
select {}
}
func cleanup() {
logger.Close()
}

View File

@@ -0,0 +1,7 @@
package server
import (
"github.com/pkg/errors"
)
var ErrInvalidDevice = errors.New("invalid device associated to message")

View File

@@ -0,0 +1,221 @@
package server
import (
"github.com/fiskerinc/cloud-services/services/attendant/controllers"
"github.com/fiskerinc/cloud-services/services/attendant/handlers"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/fiskerinc/cloud-services/pkg/grpc/sms"
"github.com/fiskerinc/cloud-services/pkg/kafka"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/tmobile"
"google.golang.org/protobuf/proto"
)
// StartConsumer runs consumer and puts events into a channel for router
func StartConsumer(topic, oldTopic string) {
defer func() {
if err := recover(); err != nil {
logger.Error().Msgf("PanicConsumer %v", err)
}
}()
events := make(chan *kafka.Message)
eventsJSON := make(chan common.EventRawJSON)
go routeEvents(events)
go routeOldEvents(eventsJSON)
logger.Info().Msgf("consumer intialized for topic: %v", topic)
consumer, oldConsumer, err := services.GetKafkaConsumer()
if err != nil {
panic(err)
}
go func() {
err = oldConsumer.ConsumeToChannelJson([]string{oldTopic}, eventsJSON)
loggerdataresp.BadDataError(err, loggerdataresp.EofErrorCheck)
}()
err = consumer.ConsumeToChannel([]string{topic}, events)
loggerdataresp.BadDataError(err, loggerdataresp.EofErrorCheck)
}
func routeEvents(events chan *kafka.Message) {
db := services.GetDB()
defer db.Close()
sms := services.GetSMSClient()
ka := services.NewKeepAwakeService()
for {
event := <-events
var err error
payload := &kafka_grpc.GRPC_AttendantPayload{}
err = proto.Unmarshal(event.Value, payload)
if err != nil {
logger.Warn().Err(err).Send()
}
device, key := common.ParseDeviceKey(string(event.Key))
logger.Debug().Str("id", key).Msgf("source: %s, type: %s, handler: %s", key, device, payload.GetHandler())
switch device {
case common.TRex:
if payload.GetHandler() == "dtcs" {
d := &common.ConsumerPayload{
Handler: payload.GetHandler(),
Data: controllers.GRPCToDTCEntry(payload),
}
routeService(db, ka, sms, key, d)
break
}
d, _ := common.AttendantRouteTRexPayload(payload)
routeTRex(db, ka, key, d)
case common.HMI:
d, _ := common.AttendantRouteHMIPayload(payload)
routeHMI(db, ka, key, d)
case common.Mobile:
d, _ := common.AttendantRouteMobilePayload(payload)
routeMobile(db, key, d)
case common.Service:
if payload.GetHandler() == "sms_delivery_status_manifest" {
d := &common.ConsumerPayload{
Handler: payload.GetHandler(),
Data: tmobile.GRPCToTMessage(payload),
}
routeService(db, ka, sms, key, d)
break
}
d, _ := common.AttendantRouteServicePayload(payload)
routeService(db, ka, sms, key, d)
default:
{
logger.Error().Str("id", key).Msgf("Unknown device: %s, type: %s, handler: %s", device, key, payload.GetHandler())
loggerdataresp.BadDataError(ErrInvalidDevice)
}
}
loggerdataresp.BadDataError(err)
}
}
func routeOldEvents(events chan common.EventRawJSON) {
db := services.GetDB()
defer db.Close()
sms := services.GetSMSClient()
ka := services.NewKeepAwakeService()
for {
event := <-events
var p common.Payload
err := p.Unmarshal(event.Payload)
device, key := common.ParseDeviceKey(event.Key)
payload := &common.ConsumerPayload{
Handler: p.Handler,
Data: p.Data,
}
logger.Debug().Str("id", key).Msgf("source: %s, type: %s, handler: %s", key, device, payload.GetHandler())
switch device {
case common.TRex:
routeTRex(db, ka, key, payload)
case common.HMI:
routeHMI(db, ka, key, payload)
case common.Mobile:
routeMobile(db, key, payload)
case common.Service:
routeService(db, ka, sms, key, payload)
default:
{
logger.Error().Str("id", key).Msgf("Unknown device: %s, type: %s, handler: %s", device, key, p.Handler)
loggerdataresp.BadDataError(ErrInvalidDevice)
}
}
loggerdataresp.BadDataError(err)
}
}
func routeTRex(db *services.DB, ka *services.KeepAwake, id string, p common.ConsumerPayloadInterface) {
// route TRex messages. Also update cloud/modules_go/kafka/topics.go
var err error
switch p.GetHandler() {
case "car_state":
err = handlers.UpdateCarState(db, id, p.GetData())
loggerdataresp.BadDataError(err)
case "car_update_status", "car_update_download", "car_update_install":
err = handlers.CarUpdateProgressStatus(db, ka, common.TRex, id, p.GetData())
loggerdataresp.BadDataError(err)
case "get_filekeys":
err = handlers.GetFileKeys(db, common.TRex, id, p.GetData())
case "dtcs":
err = handlers.UploadDtc(id, p.GetData())
case "ecc_keys":
err = handlers.GetAllEccKeys(db, id, p.GetData())
default:
err = kafka.ErrUnhandledMessage(common.TRex, id, p.GetHandler(), string(p.GetData()))
}
loggerdataresp.BadDataError(err)
}
func routeHMI(db *services.DB, ka *services.KeepAwake, id string, p common.ConsumerPayloadInterface) {
// route HMI messages. Also update cloud/modules_go/kafka/topics.go
var err error
switch p.GetHandler() {
case "update_approve":
err = handlers.ApproveUpdate(db, id, p.GetData())
case "car_update_status", "car_update_download", "car_update_install":
go func() {
err = handlers.CarUpdateProgressStatus(db, ka, common.HMI, id, p.GetData())
loggerdataresp.BadDataError(err)
}()
err = handlers.CarUpdateProgressStatus(db, ka, common.HMI, id, p.GetData())
loggerdataresp.BadDataError(err)
case "get_filekeys":
err = handlers.GetFileKeys(db, common.HMI, id, p.GetData())
default:
err = kafka.ErrUnhandledMessage(common.HMI, id, p.GetHandler(), string(p.GetData()))
}
loggerdataresp.BadDataError(err)
}
func routeMobile(db *services.DB, id string, p common.ConsumerPayloadInterface) {
// route mobile messages. Also update cloud/modules_go/kafka/topics.go
var err error
switch p.GetHandler() {
case "update_approve":
err = handlers.ApproveUpdate(db, id, p.GetData())
case "updates_get":
err = handlers.GetUpdates(db, id, p.GetData())
default:
err = kafka.ErrUnhandledMessage(common.Mobile, id, p.GetHandler(), string(p.GetData()))
}
loggerdataresp.BadDataError(err)
}
func routeService(db *services.DB, ka *services.KeepAwake, sms sms.SMSServiceClient, id string, p common.ConsumerPayloadInterface) {
// route cloud service messages. Also update cloud/modules_go/kafka/topics.go
var err error
// We should switch out these strings for constants in the kafka library
switch p.GetHandler() {
case "send_manifest":
err = handlers.SendManifest(db, ka, common.Service, id, p.GetData())
case "order_updated":
err = handlers.OrderUpdated(db, sms, id, p.GetData())
case "sms_delivery_status_manifest":
err = handlers.CarSendManifestSMSWakeUpCallback(db, ka, common.Service, id, p.GetData())
default:
err = kafka.ErrUnhandledMessage(common.Service, id, p.GetHandler(), string(p.GetData()))
}
loggerdataresp.BadDataError(err)
}

View File

@@ -0,0 +1,29 @@
package services
import (
"sync"
"github.com/fiskerinc/cloud-services/pkg/logger"
vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig"
)
var (
configOnce sync.Once
configInstance vconfig.ConfigServiceInterface
)
func GetVehicleConfig() vconfig.ConfigServiceInterface {
configOnce.Do(func() {
if configInstance != nil {
return
}
logger.Info().Msg("init vehicle config instance")
configInstance = vconfig.NewConfigService()
})
return configInstance
}
func SetVehicleConfig(c vconfig.ConfigServiceInterface) {
configInstance = c
}

View File

@@ -0,0 +1,278 @@
package services
import (
"sync"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db"
q "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/logger"
)
var (
dbOnce sync.Once
dbInstance *DB
)
type DB struct {
client *db.DBClient
cars q.CarsInterface
carVersionsLog q.CarVersionsLogInterface
carupdates q.CarUpdatesInterface
filekeys q.FileKeysInterface
manifests q.UpdateManifestsInterface
ecu q.ECUInterface
eccKeys q.EccKeysInterface
ratePlan q.RatePlanInterface
updateManifestSUMSVersions q.SUMSVersionsInterface
carConfigData q.CarConfigDataInterface
onceEcu sync.Once
onceClient sync.Once
onceCars sync.Once
onceCarVersionsLog sync.Once
onceCarUpdates sync.Once
onceFileKeys sync.Once
onceManifests sync.Once
onceEccKeys sync.Once
onceRatePlan sync.Once
onceUpdateManifestSUMSVersions sync.Once
onceCarConfigData sync.Once
}
func GetDB() *DB {
dbOnce.Do(func() {
if dbInstance != nil {
return
}
logger.Info().Msg("init DB instance")
dbInstance = &DB{}
})
return dbInstance
}
func SetDB(db *DB) {
if dbInstance != nil {
dbInstance.Close()
}
dbInstance = db
}
func (d *DB) GetDBClient() *db.DBClient {
d.onceClient.Do(func() {
if d.client != nil {
return
}
logger.Info().Msg("init DBClient instance")
client := &db.DBClient{}
client.RegisterManyToManyRel([]interface{}{
(*common.CarToDriver)(nil),
})
err := client.InitSchema([]interface{}{
(*common.UpdateManifest)(nil),
(*common.Car)(nil),
(*common.CarToDriver)(nil),
(*common.CarUpdateStatus)(nil),
(*common.CarUpdate)(nil),
(*common.FileKey)(nil),
(*common.RatePlanTMobile)(nil),
})
if err != nil {
logger.Error().Err(err).Send()
}
// Uncomment below to show generated SQL queries
// client.GetConn().AddQueryHook(db.SQLLogger{})
d.client = client
})
return d.client
}
func (d *DB) SetDBClient(client *db.DBClient) {
if d.client != nil {
d.client.Close()
}
d.client = client
}
func (d *DB) Close() {
if d.client == nil {
return
}
d.client.Close()
}
func (d *DB) GetCarUpdates() q.CarUpdatesInterface {
d.onceCarUpdates.Do(func() {
if d.carupdates != nil {
return
}
instance := &q.CarUpdates{}
instance.SetClient(d.GetDBClient())
d.carupdates = instance
})
return d.carupdates
}
func (d *DB) SetCarUpdates(carupdates q.CarUpdatesInterface) {
d.carupdates = carupdates
}
func (d *DB) GetCars() q.CarsInterface {
d.onceCars.Do(func() {
if d.cars != nil {
return
}
instance := &q.Cars{}
instance.SetClient(d.GetDBClient())
d.cars = instance
})
return d.cars
}
func (d *DB) SetCars(cars q.CarsInterface) {
d.cars = cars
}
func (d *DB) GetCarVersionsLog() q.CarVersionsLogInterface {
d.onceCarVersionsLog.Do(func() {
if d.carVersionsLog != nil {
return
}
instance := &q.CarVersionsLog{}
instance.SetClient(d.GetDBClient())
d.carVersionsLog = instance
})
return d.carVersionsLog
}
func (d *DB) SetCarVersionsLog(log q.CarVersionsLogInterface) {
d.carVersionsLog = log
}
func (d *DB) GetFileKeys() q.FileKeysInterface {
d.onceFileKeys.Do(func() {
if d.filekeys != nil {
return
}
instance := &q.FileKeys{}
instance.SetClient(d.GetDBClient())
d.filekeys = instance
})
return d.filekeys
}
func (d *DB) SetFileKeys(filekeys q.FileKeysInterface) {
d.filekeys = filekeys
}
func (d *DB) GetUpdateManifests() q.UpdateManifestsInterface {
d.onceManifests.Do(func() {
if d.manifests != nil {
return
}
instance := q.NewUpdateManifest(nil)
instance.SetClient(d.GetDBClient())
d.manifests = instance
})
return d.manifests
}
func (d *DB) GetECU() q.ECUInterface {
d.onceEcu.Do(func() {
if d.ecu != nil {
return
}
instance := &q.ECU{}
instance.SetClient(d.GetDBClient())
d.ecu = instance
})
return d.ecu
}
func (d *DB) SetECU(instance q.ECUInterface) {
d.ecu = instance
}
func (d *DB) SetManifests(manifests q.UpdateManifestsInterface) {
d.manifests = manifests
}
func (d *DB) ModifyUpdateStatus(id int, status string) (*common.CarUpdate, error) {
cu := d.GetCarUpdates()
updates, err := cu.SelectByID(int64(id))
if err != nil {
return updates, err
}
updates.Status = status
_, err = cu.UpdateStatus(updates)
return updates, err
}
func (d *DB) GetECCKeys() q.EccKeysInterface {
d.onceEccKeys.Do(func() {
if d.eccKeys != nil {
return
}
eccKeys := &q.EccKeys{}
eccKeys.SetClient(d.GetDBClient())
d.eccKeys = eccKeys
})
return d.eccKeys
}
func (d *DB) SetECCKeys(eccKeys q.EccKeysInterface) {
d.eccKeys = eccKeys
}
func (d *DB) GetRatePlan() q.RatePlanInterface {
d.onceRatePlan.Do(func() {
if d.ratePlan != nil {
return
}
instance := &q.RatePlanTmobile{}
instance.SetClient(d.GetDBClient())
d.ratePlan = instance
})
return d.ratePlan
}
func (d *DB) SetRatePlan(ratePlan q.RatePlanInterface) {
d.ratePlan = ratePlan
}
func (d *DB) GetUpdateManifestSUMSVersions() q.SUMSVersionsInterface {
d.onceUpdateManifestSUMSVersions.Do(func() {
if d.updateManifestSUMSVersions != nil {
return
}
instance := &q.SUMSVersions{}
instance.SetClient(d.GetDBClient())
d.updateManifestSUMSVersions = instance
})
return d.updateManifestSUMSVersions
}
func (d *DB) SetUpdateManifestVersions(umv q.SUMSVersionsInterface) {
d.updateManifestSUMSVersions = umv
}
func (d *DB) GetCarConfigData() q.CarConfigDataInterface {
d.onceCarConfigData.Do(func() {
if d.carConfigData != nil {
return
}
logger.Debug().Msg("Init CarConfigData instance")
carConfigData := &q.CarConfigData{}
carConfigData.SetClient(d.GetDBClient())
d.carConfigData = carConfigData
})
return d.carConfigData
}
func (d *DB) SetCarConfigData(carConfigData q.CarConfigDataInterface) {
d.carConfigData = carConfigData
}

View File

@@ -0,0 +1,20 @@
package services
import (
"sync"
"github.com/fiskerinc/cloud-services/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/dbc/models"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
)
var model models.DBCVersionInterface
var collectionOnce sync.Once
// GetDBCCollection returns singleton instance of collection of DBCs
func GetDBC() *descriptor.Database {
collectionOnce.Do(func() {
model = dbc.NewFM29_FRSD390_DBC()
})
return model.GetDatabase()
}

View File

@@ -0,0 +1,24 @@
package services
import (
"sync"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
)
var (
MAX_KEY_CACHE int = envtool.GetEnvInt("MAX_KEY_CACHE", 10000)
carDtcsCache cache.CarDTCsCacheInterface
onceDTCCache sync.Once
)
func GetCarDtcCache() cache.CarDTCsCacheInterface {
onceDTCCache.Do(func() {
if carDtcsCache == nil {
carDtcsCache = cache.NewCarDTCsCache(MAX_KEY_CACHE)
}
})
return carDtcsCache
}

View File

@@ -0,0 +1,28 @@
package services
import (
"sync"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
)
var (
MAX_ECU_KEY_CACHE int = envtool.GetEnvInt("MAX_ECU_KEY_CACHE", 10000)
ring cache.RingMapInterface
onceECUCache sync.Once
)
func GetCarEcuCache() cache.RingMapInterface {
onceECUCache.Do(func() {
if ring == nil {
ring = cache.NewRingMap(MAX_ECU_KEY_CACHE)
}
})
return ring
}
func SetCarEcuCache(value cache.RingMapInterface) {
ring = value
}

View File

@@ -0,0 +1,88 @@
package services
import (
"net/http"
"net/url"
"slices"
"sync"
"github.com/fiskerinc/cloud-services/pkg/common"
s "github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/foa"
"github.com/fiskerinc/cloud-services/pkg/httpclient"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
)
var UPDATE_MANIFEST_IDS_TO_NOTIFY_FOA = []int64{816, 817, 818, 819, 820}
var (
foaService FoaServiceInterface
foaOnce sync.Once
)
func GetFoaService() FoaServiceInterface {
foaOnce.Do(func() {
if foaService != nil {
return
}
foaService = NewFoaService()
})
return foaService
}
func SetFoaService(foa FoaServiceInterface) {
foaService = foa
}
func NewFoaService() FoaServiceInterface {
return &FoaService{
foaURL: envtool.GetEnv("FOA_URL", "REPLACE_ME"),
foaAPIToken: envtool.GetEnv("FOA_API_KEY", "REPLACE_ME"),
}
}
type FoaServiceInterface interface {
OtaUpdateStatus(vin string, carUpdate *common.CarUpdate, status *common.CarUpdateProgress) (*http.Response, error)
}
type FoaService struct {
foaURL string
foaAPIToken string
}
func (f *FoaService) OtaUpdateStatus(vin string, carUpdate *common.CarUpdate, status *common.CarUpdateProgress) (*http.Response, error) {
if !slices.Contains(UPDATE_MANIFEST_IDS_TO_NOTIFY_FOA, carUpdate.UpdateManifestID) {
// Nothing to send if the manifest is not one of the specified IDs
return nil, nil
}
var body interface{} = nil
switch status.Status {
case s.ManifestSucceeded:
body = foa.BuildOtaUpdateStatusSuccessRequest(vin, carUpdate.UpdateManifestID)
case s.ManifestError:
body = foa.BuildOtaUpdateStatusFailedRequest(vin, carUpdate.UpdateManifestID, status.Info)
case s.ManifestCanceled:
body = foa.BuildOtaUpdateStatusCanceledRequest(vin, carUpdate.UpdateManifestID, status.Info)
}
if body == nil {
return nil, nil
}
logger.Info().Msgf("Notifying FOA for %s of update %d status %s", vin, carUpdate.UpdateManifestID, status.Status)
urlString, err := url.JoinPath(f.foaURL, "ota/update_status")
if err != nil {
return nil, err
}
postHeader := http.Header{}
postHeader.Add("Authorization", "Bearer "+f.foaAPIToken)
postHeader.Add("Content-Type", "application/json")
return httpclient.Post(urlString, body, postHeader)
}

View File

@@ -0,0 +1,62 @@
package services
import (
"context"
"sync"
"github.com/fiskerinc/cloud-services/pkg/kafka"
"github.com/fiskerinc/cloud-services/pkg/logger"
)
const serviceName = "attendant"
const oldServiceName = "old-attendant"
var consumer, oldConsumer kafka.ConsumerInterface
var consumerOnce sync.Once
// GetKafkaConsumer returns singleton instance of kafka consumer
func GetKafkaConsumer() (kafka.ConsumerInterface, kafka.ConsumerInterface, error) {
var err error
consumerOnce.Do(func() {
consumer, err = kafka.NewConsumer(serviceName)
if err != nil {
logger.Error().Err(err).Send()
}
oldConsumer, err = kafka.NewConsumer(oldServiceName)
if err != nil {
logger.Error().Err(err).Send()
}
})
if err != nil {
return nil, nil, err
}
return consumer, oldConsumer, nil
}
var producer kafka.ProducerInterface
var producerOnce sync.Once
func GetKafkaProducer() (kafka.ProducerInterface, error) {
var err error
producerOnce.Do(func() {
if producer == nil {
var producerTemp kafka.ProducerInterface
producerTemp, err = kafka.NewProducer(context.Background())
if err != nil {
logger.Err(err).Send()
}
producer = producerTemp
}
})
if err != nil {
return nil, err
}
return producer, err
}
func SetKafkaProducer(k kafka.ProducerInterface) {
producer = k
}

View File

@@ -0,0 +1,215 @@
package services
import (
"fmt"
"reflect"
"sync"
"time"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/pkg/errors"
)
// This can probably be moved into a better spot, but we are on a time limit
var ADD_TIME = time.Minute
type KeepAwake struct {
tick *time.Ticker
KAI KeepAwakeInterface
}
type KeepAwakeInterface interface {
CheckKeepAwakeMessages()
// addToKeepAwakeMessages(vin string) (err error)
RemoveKeepAwakeMessage(vin string) (err error)
// getKeepAwakeMessages() (vins []string, err error)
SendFirstKeepAwakeMessage(vin string) (err error)
// sendKeepAwakeMessage(vin string) (err error)
}
type KeepAwakeImplementation struct {
Cars map[string]time.Time // Map of car vins to the time it was last ran
oneTime sync.Once
mapLock sync.Mutex
}
func NewKeepAwakeService() (ka *KeepAwake) {
ka = &KeepAwake{}
ka.tick = time.NewTicker(time.Minute)
ka.KAI = &KeepAwakeImplementation{}
// Start our timer
go func() {
for {
select {
case <-ka.tick.C:
ka.KAI.CheckKeepAwakeMessages()
}
}
}()
return
}
func (ka *KeepAwake) SetService(inf KeepAwakeInterface) {
ka.KAI = inf
}
func (ka *KeepAwake) RemoveKeepAwakeMessage(vin string) (err error) {
return ka.KAI.RemoveKeepAwakeMessage(vin)
}
func (ka *KeepAwake) SendFirstKeepAwakeMessage(vin string) (err error) {
return ka.KAI.SendFirstKeepAwakeMessage(vin)
}
// Need to move this out of here, as this manifest sender is created every time a manifest is to be sent
// On a timeout, check if the keep awake messages need to be sent
func (k *KeepAwakeImplementation) CheckKeepAwakeMessages() {
k.oneTime.Do(func() {
k.Cars = make(map[string]time.Time)
})
k.mapLock.Lock()
vins, _ := k.getKeepAwakeMessages()
for _, vin := range vins {
k.sendKeepAwakeMessage(vin)
k.addToKeepAwakeMessages(vin)
}
k.mapLock.Unlock()
// Call this function again in some amount of time
}
func (k *KeepAwakeImplementation) addToKeepAwakeMessages(vin string) (err error) {
k.Cars[vin] = time.Now().Add(ADD_TIME)
return
}
func (k *KeepAwakeImplementation) RemoveKeepAwakeMessage(vin string) (err error) {
k.oneTime.Do(func() {
k.Cars = make(map[string]time.Time)
})
// Remove from Redis
err = k.removeRedisCarEntry(vin)
if err != nil {
logger.Err(err).Msg("Unable to remove keeep awake message from redis")
}
logger.Info().Msgf("Ended keep awake messaging for %s", vin)
k.mapLock.Lock()
defer k.mapLock.Unlock()
_, ok := k.Cars[vin]
if !ok {
logger.Debug().Msgf("Attempted to end keep awake for %s, but not in list", vin)
}
delete(k.Cars, vin)
return err
}
// This is a list of vins that need sendKeepAwakeMessage
func (k *KeepAwakeImplementation) getKeepAwakeMessages() (vins []string, err error) {
for vin, t := range k.Cars {
// Parse out the time and check if its before the time it is now
if t.Before(time.Now()) {
// This needs processing
vins = append(vins, vin)
}
}
return
}
func (k *KeepAwakeImplementation) SendFirstKeepAwakeMessage(vin string) (err error) {
k.oneTime.Do(func() {
k.Cars = make(map[string]time.Time)
})
// Additionally add to the list of redis
err = k.addRedisCarEntry(vin)
logger.Err(err).Msg("")
logger.Info().Msgf("Started keep awake messaging for %s", vin)
k.mapLock.Lock()
defer k.mapLock.Unlock()
err = k.sendKeepAwakeMessage(vin)
k.addToKeepAwakeMessages(vin)
return err
}
// Send a special message to keep the tbox awake to download the update
// We want to continue to call this until the download is complete or if we fail
func (k *KeepAwakeImplementation) sendKeepAwakeMessage(vin string) (err error) {
client := RedisClientPool().GetFromPool()
defer client.Close()
// check if in redis
found, err := k.checkRedisForCarEntry(vin, client)
if err != nil {
return
}
// If we did not find the redis entry, then another service has removed it from the keep alive set
// Don't call k.Delete as it creates a lock on the map, and then we will be deadlocked
if !found {
delete(k.Cars, vin)
return
}
logger.Info().Msgf("sending keep awake message for %s", vin)
type Action struct {
Action string `json:"action"`
Timeout int32 `json:"timeout"`
}
logger.Debug().Msgf("Sending redis queue- %s, key- %s, hander- %s, data- %v", "attendant", vin, "can_network", Action{Action: "on"})
err = client.SafeQueueMessage(common.TRex.Key(vin), common.Message{
Handler: "can_network",
Data: Action{Action: "on", Timeout: 120},
})
return errors.WithStack(err)
}
func (k *KeepAwakeImplementation) checkRedisForCarEntry(vin string, client redis.Client) (found bool, err error) {
line, err := client.Execute("SISMEMBER", "can_keep_awake", vin)
if err != nil {
return false, errors.WithStack(err)
}
check, ok := line.(int64)
if !ok {
err = fmt.Errorf("received wrong type from redis for the can_keep_awake for %s, %v, %s", vin, line, reflect.TypeOf(line))
}
return check > 0, err
}
func (k *KeepAwakeImplementation) removeRedisCarEntry(vin string) (err error) {
client := RedisClientPool().GetFromPool()
defer client.Close()
_, err = client.Execute("SREM", "can_keep_awake", vin)
return
}
func (k *KeepAwakeImplementation) addRedisCarEntry(vin string) (err error) {
client := RedisClientPool().GetFromPool()
defer client.Close()
_, err = client.Execute("SADD", "can_keep_awake", vin)
return
}
type MockKeepAwakeImplementation struct{}
func (k *MockKeepAwakeImplementation) CheckKeepAwakeMessages() {}
func (k *MockKeepAwakeImplementation) RemoveKeepAwakeMessage(vin string) (err error) {
return err
}
func (k *MockKeepAwakeImplementation) SendFirstKeepAwakeMessage(vin string) (err error) {
return
}

View File

@@ -0,0 +1,68 @@
package services
import (
"testing"
"time"
)
func TestKeepAwake(t *testing.T){
t.Skip()
ADD_TIME = 0
ka := KeepAwake{}
impl := &KeepAwakeImplementation{}
ka.SetService(impl)
testVin := "JH4KA7680RC01"
// Check that trying to delete a non-existing set is okay
err := impl.RemoveKeepAwakeMessage(testVin)
if err != nil{
t.Error(err)
}
client := RedisClientPool().GetFromPool()
found, err := impl.checkRedisForCarEntry(testVin, client)
if err != nil {
t.Error(err)
}
if found{
t.Error("did find expected vin when not supposed to")
}
client.Close()
// Check we can successfully add
err = impl.SendFirstKeepAwakeMessage(testVin)
if err != nil{
t.Error(err)
}
client = RedisClientPool().GetFromPool()
found, err = impl.checkRedisForCarEntry(testVin, client)
if err != nil {
t.Error(err)
}
if !found{
t.Error("did not find expected vin")
}
client.Close()
time.Sleep(time.Second * 2)
vins, err := impl.getKeepAwakeMessages()
if err != nil{
t.Error(err)
}
if len(vins) != 1{
t.Error("len of vins wrong ", len(vins))
}
err = impl.RemoveKeepAwakeMessage(testVin)
if err != nil{
t.Error(err)
}
vins, err = impl.getKeepAwakeMessages()
if err != nil{
t.Error(err)
}
if len(vins) != 0{
t.Error("len of vins wrong ", len(vins))
}
}

View File

@@ -0,0 +1,32 @@
package services
import (
"sync"
"github.com/fiskerinc/cloud-services/pkg/mongo"
)
var (
clientOnce sync.Once
client mongo.Client
)
// GetMongoClient returns singleton instance of mongo client
func GetMongoClient() (mongo.Client, error) {
var err error
clientOnce.Do(func() {
client, err = initMongoClient()
})
return client, err
}
func initMongoClient() (mongo.Client, error) {
var err error
if client == nil {
client, err = mongo.NewClient(mongo.StandardDB)
}
return client, err
}

View File

@@ -0,0 +1,28 @@
package services
import (
"sync"
"github.com/fiskerinc/cloud-services/pkg/redis"
)
var (
clientPoolOnce sync.Once
clientPool redis.ClientPoolInterface
)
func RedisClientPool() redis.ClientPoolInterface {
clientPoolOnce.Do(func() {
if clientPool != nil {
return
}
clientPool = redis.NewClientPool()
})
return clientPool
}
func SetRedisClientPool(cp redis.ClientPoolInterface) {
clientPool = cp
}

View File

@@ -0,0 +1,28 @@
package services
import (
"sync"
vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig"
)
var (
sapService vconfig.SAPServiceInterface
sapOnce sync.Once
)
func GetSapService() vconfig.SAPServiceInterface {
sapOnce.Do(func() {
if sapService != nil {
return
}
sapService = vconfig.NewSAPService()
})
return sapService
}
// SetSapService is supposed t be used for testing.
func SetSapService(sap vconfig.SAPServiceInterface) {
sapService = sap
}

View File

@@ -0,0 +1,43 @@
package services
import (
"fmt"
"sync"
"github.com/fiskerinc/cloud-services/pkg/grpc/sms"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
var smsClient sms.SMSServiceClient
var smsClientOnce sync.Once
func newSmsClient() {
logger.Info().Msg("Init SMS client")
target := fmt.Sprintf("%s:%s",
envtool.GetEnv("SMS_HOST", "sms"),
envtool.GetEnv("SMS_PORT", "8077"))
c, err := grpc.Dial(target, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
logger.Error().Err(err).Send()
}
smsClient = sms.NewSMSServiceClient(c)
}
func GetSMSClient() sms.SMSServiceClient {
smsClientOnce.Do(func() {
if smsClient != nil {
return
}
newSmsClient()
})
return smsClient
}
func SetSmsClient(c sms.SMSServiceClient) {
smsClient = c
}