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,46 @@
package handlers
import (
"context"
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
redis "github.com/fiskerinc/cloud-services/pkg/redisv2"
"github.com/fiskerinc/cloud-services/pkg/validator"
)
// HandleGetCarsHMIKey godoc
// @Summary Returns HMI session ID from Redis
// @Description If you don't know what this is, don't call it. It's that simple
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin query string true "VIN"
// @Success 200 {object} string
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 404 {object} common.JSONError "Not Found"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /cars/hmi_key [get]
func HandleGetCarsHMIKey(w http.ResponseWriter, r *http.Request) {
queryParams := r.URL.Query()
vin := queryParams.Get("vin")
ok := validator.ValidateVINSimple(vin)
if !ok {
loggerdataresp.BadDataErrorResp(w, ErrInvalidVIN, http.StatusBadRequest)
return
}
rd := services.GetRedisV2Client()
res := rd.Client.Get(context.Background(), redis.HMISessionKey(vin))
sessionOrSalt, err := res.Result()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed To Get SessionOrSalt"))
return
}
w.Write([]byte(sessionOrSalt))
}

View File

@@ -0,0 +1,92 @@
package handlers
import (
"net/http"
"net/url"
"otaupdate/services"
"time"
"github.com/pkg/errors"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleAPICallsGet godoc
// @Summary Search API calls
// @Description Get API calls filtered by method, user, path and date.
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param search query string false "Text search"
// @Param from query string false "Date before requests which client is looking for"
// @Param to query string false "Date after requests which client is looking for"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Param order query string false "Sort on column with asc or desc"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.APICall}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /apicalls [get]
func HandleAPICallsGet(w http.ResponseWriter, r *http.Request) {
filter, err := parseAPICallsFilter(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := orm.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
if options.Order == "" {
options.Order = "created_at DESC"
}
csDB := services.GetDB().GetAPICalls()
cs, total, err := csDB.Search(filter, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: cs,
Total: total,
})
}
func parseAPICallsFilter(r *http.Request) (common.APICallsSearch, error) {
qs := r.URL.Query()
from, err := parseTimeFilter(qs, "from")
if err != nil {
return common.APICallsSearch{}, err
}
to, err := parseTimeFilter(qs, "to")
if err != nil {
return common.APICallsSearch{}, err
}
return common.APICallsSearch{
Search: qs.Get("search"),
From: from,
To: to,
}, nil
}
func parseTimeFilter(qs url.Values, pname string) (*time.Time, error) {
stime := qs.Get(pname)
if stime == "" {
return nil, nil
}
t, err := time.Parse(time.RFC3339, stime)
if err != nil {
return nil, errors.WithStack(err)
}
return &t, nil
}

View File

@@ -0,0 +1,108 @@
package handlers_test
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/stretchr/testify/assert"
)
func TestHandleAPICallsGet(t *testing.T) {
db := services.GetDB()
timeMock := time.Date(2022, 3, 11, 3, 16, 12, 0, time.UTC)
callsList := []common.APICall{
{
ClientID: "jkm@fisker.com",
AccessType: "jwt_token",
Method: "GET",
Endpoint: "/some/path",
CreatedAt: &timeMock,
},
{
ClientID: "3dec092e-d869-46e3-be85-258aed85b2fc",
AccessType: "api_token",
Method: "GET",
Endpoint: "/some/path",
CreatedAt: &timeMock,
},
}
tests := map[string]struct {
urlQ string
callsDB queries.APICallsInterface
expStatus int
expBody string
}{
"correct": {
urlQ: "?from=" + timeMock.Format(time.RFC3339) + "&to=" + timeMock.Format(time.RFC3339) + "&search=text",
callsDB: &mocks.MockAPICalls{
SearchMock: func(filter common.APICallsSearch, paging *queries.PageQueryOptions) ([]common.APICall, int, error) {
assert.Equal(t, common.APICallsSearch{
Search: "text",
From: &timeMock,
To: &timeMock,
}, filter)
return callsList, 2, nil
},
},
expStatus: http.StatusOK,
expBody: `{"data":[{"client_id":"jkm@fisker.com","access_type":"jwt_token","method":"GET","endpoint":"/some/path","created_at":"2022-03-11T03:16:12Z"},{"client_id":"3dec092e-d869-46e3-be85-258aed85b2fc","access_type":"api_token","method":"GET","endpoint":"/some/path","created_at":"2022-03-11T03:16:12Z"}],"total":2}`,
},
"correct_no_params": {
callsDB: &mocks.MockAPICalls{
SearchMock: func(filter common.APICallsSearch, paging *queries.PageQueryOptions) ([]common.APICall, int, error) {
return callsList, 2, nil
},
},
expStatus: http.StatusOK,
expBody: `{"data":[{"client_id":"jkm@fisker.com","access_type":"jwt_token","method":"GET","endpoint":"/some/path","created_at":"2022-03-11T03:16:12Z"},{"client_id":"3dec092e-d869-46e3-be85-258aed85b2fc","access_type":"api_token","method":"GET","endpoint":"/some/path","created_at":"2022-03-11T03:16:12Z"}],"total":2}`,
},
"bad_from": {
urlQ: "?from=kk&to=" + timeMock.Format(time.RFC3339) + "&search=text",
expStatus: http.StatusBadRequest,
expBody: `{"message":"parsing time \"kk\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"kk\" as \"2006\"","error":"Bad Request"}`,
},
"bad_to": {
urlQ: "?from=" + timeMock.Format(time.RFC3339) + "&to=kk&search=text",
expStatus: http.StatusBadRequest,
expBody: `{"message":"parsing time \"kk\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"kk\" as \"2006\"","error":"Bad Request"}`,
},
"bad_limit": {
urlQ: "?limit=-2",
expStatus: http.StatusBadRequest,
expBody: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
"bad_db": {
callsDB: &mocks.MockAPICalls{
SearchMock: func(filter common.APICallsSearch, paging *queries.PageQueryOptions) ([]common.APICall, int, error) {
return nil, 0, someErr
},
},
expStatus: http.StatusServiceUnavailable,
expBody: `{"message":"some err","error":"Service Unavailable"}`,
},
}
for tname, tt := range tests {
t.Run(tname, func(t *testing.T) {
r := th.MakeTestRequest(http.MethodPost, "http://example.com/apicalls"+tt.urlQ, nil)
w := httptest.NewRecorder()
db.SetAPICalls(tt.callsDB)
handlers.HandleAPICallsGet(w, r)
assert.Equal(t, tt.expStatus, w.Code)
assert.Equal(t, tt.expBody, w.Body.String())
})
}
}

View File

@@ -0,0 +1,37 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/go-pg/pg/v10/orm"
)
// HandleAPITokenAdd godoc
// @Summary Add API token
// @Description Create API token. Requires API token permission
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body common.APIToken true "API token data"
// @Success 200 {object} common.APIToken
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /apitoken [post]
func HandleAPITokenAdd(w http.ResponseWriter, r *http.Request) {
apiTokenAdd.Handle(w, r)
}
var apiTokenAdd = controllers.NewCreate(&apiTokenCreateHelper{})
type apiTokenCreateHelper struct {
APITokensHelper
}
func (h *apiTokenCreateHelper) QueryInsert(model interface{}) (orm.Result, error) {
return services.GetDB().GetAPITokens().Insert(*model.(*common.APIToken))
}

View File

@@ -0,0 +1,56 @@
package handlers_test
import (
"fmt"
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestAPITokenAdd(t *testing.T) {
mock := mo.MockAPITokens{}
services.GetDB().SetAPITokens(&mock)
validData := common.APIToken{
Token: "TESTTOKEN",
Roles: "TESTROLES1,TESTROLES2",
Description: "FOR UNIT TESTS",
}
tests := []mo.DBHttpTest{
{
Name: "No data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/apitoken", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Token required. Roles required. Description required","error":"Bad Request"}`,
},
{
Name: "Invalid data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/apitoken", common.APIToken{
Token: "TESTTOKEN",
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Roles required. Description required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/apitoken", validData),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"token":"TESTTOKEN","roles":"TESTROLES1,TESTROLES2","description":"FOR UNIT TESTS","expires_at":null}`,
},
{
Name: "DB error",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/apitoken", validData),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
DBTestCase: mo.DBTestCase{
MockError: fmt.Errorf("something went wrong"),
},
},
}
mo.RunDBTests(t, tests, handlers.HandleAPITokenAdd, &mock)
}

View File

@@ -0,0 +1,53 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/go-pg/pg/v10/orm"
"github.com/gorilla/schema"
)
// APITokenDelete godoc
// @Summary Delete API token
// @Description Delete API token. Requires delete permissions
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param token query string true "API token"
// @Success 200 {object} common.JSONMessage
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /apitoken [delete]
func HandleAPITokenDelete(w http.ResponseWriter, r *http.Request) {
apiTokenDelete.Handle(w, r)
}
var apiTokenDelete = controllers.NewDelete(&apiTokenDeleteHelper{})
type apiTokenDeleteHelper struct {
APITokensHelper
}
func (h *apiTokenDeleteHelper) ParseDeleteQueryParams(r *http.Request) interface{} {
req := common.APIToken{}
decoder := schema.NewDecoder()
decoder.SetAliasTag("json")
decoder.Decode(&req, r.URL.Query())
return &req
}
func (h *apiTokenDeleteHelper) QueryDelete(model interface{}) (orm.Result, error) {
result := model.(*common.APIToken)
return services.GetDB().GetAPITokens().Delete(result.Token)
}
type APITokenDeleteRequest struct {
Token string `json:"token"`
}

View File

@@ -0,0 +1,64 @@
package handlers_test
import (
"fmt"
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestAPITokenDelete(t *testing.T) {
results := mocks.MockORMResults{
ReturnedRows: 1,
AffectedRows: 1,
}
mock := mocks.MockAPITokens{
DBMockHelper: mocks.DBMockHelper{
ORMResponse: &results,
},
}
services.GetDB().SetAPITokens(&mock)
tests := []mocks.DBHttpTest{
{
Name: "No id",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/apitoken", common.APIToken{}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
{
Name: "Invalid data",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/apitoken?roles=TESTTOKEN", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
{
Name: "Good id",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/apitoken?token=TESTTOKEN", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Deleted"}`,
DBTestCase: mocks.DBTestCase{
ExpectedFilter: &common.APIToken{
Token: "TESTTOKEN",
},
},
},
{
Name: "DB error",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/apitoken?token=TESTTOKEN", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
DBTestCase: mocks.DBTestCase{
MockError: fmt.Errorf("something went wrong"),
},
},
}
mocks.RunDBTests(t, tests, handlers.HandleAPITokenDelete, &mock)
}

View File

@@ -0,0 +1,38 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/go-pg/pg/v10/orm"
)
// APITokenUpdate godoc
// @Summary Update API token
// @Description Update API token. Requires API token permission
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body common.APIToken true "API token data"
// @Success 200 {object} common.SubscriptionConfiguration
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /apitoken [put]
func HandleAPITokenUpdate(w http.ResponseWriter, r *http.Request) {
apiTokenUpdate.Handle(w, r)
}
var apiTokenUpdate = controllers.NewUpdate(&apiTokenUpdateHelper{})
type apiTokenUpdateHelper struct {
APITokensHelper
}
func (h *apiTokenUpdateHelper) QueryUpdate(model interface{}) (orm.Result, error) {
return services.GetDB().GetAPITokens().Update(model.(*common.APIToken))
}

View File

@@ -0,0 +1,59 @@
package handlers_test
import (
"fmt"
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestAPITokenUpdate(t *testing.T) {
mock := mocks.MockAPITokens{}
services.GetDB().SetAPITokens(&mock)
validData := common.APIToken{
Token: "TESTTOKEN",
Roles: "TESTROLES1,TESTROLES2",
Description: "FOR UNIT TESTS",
}
tests := []mocks.DBHttpTest{
{
Name: "No data",
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/apitoken", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Token required. Roles required. Description required","error":"Bad Request"}`,
},
{
Name: "Missing PK",
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/apitoken", common.APIToken{
Roles: "TESTROLES1,TESTROLES2",
Description: "FOR UNIT TESTS",
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Token required","error":"Bad Request"}`,
},
{
Name: "Good data",
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/apitoken", validData),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"token":"TESTTOKEN","roles":"TESTROLES1,TESTROLES2","description":"FOR UNIT TESTS","expires_at":null}`,
},
{
Name: "Error",
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/apitoken", validData),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
DBTestCase: mocks.DBTestCase{
MockError: fmt.Errorf("something went wrong"),
},
},
}
mocks.RunDBTests(t, tests, handlers.HandleAPITokenUpdate, &mock)
}

View File

@@ -0,0 +1,81 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/gorilla/schema"
)
// HandleAPITokensGetList godoc
// @Summary List API tokens
// @Description List API tokens. Requires API token permission
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param token query string false "API token"
// @Param role query string false "Role"
// @Param description query string false "Description"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.APIToken}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /apitokens [get]
func HandleAPITokensGetList(w http.ResponseWriter, r *http.Request) {
apiTokensGetList.Handle(w, r)
}
var apiTokensGetList = controllers.NewGetList(&apiTokensGetListHelper{})
type apiTokensGetListHelper struct {
APITokensHelper
}
func (h *apiTokensGetListHelper) ParseGetListQueryParams(r *http.Request) interface{} {
schema := schema.NewDecoder()
filter := common.APIToken{}
schema.SetAliasTag("json")
schema.Decode(&filter, r.URL.Query())
return &filter
}
func (h *apiTokensGetListHelper) QueryCount(filter interface{}) (int, error) {
return services.GetDB().GetAPITokens().Count(filter.(*common.APIToken))
}
func (h *apiTokensGetListHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) {
return services.GetDB().GetAPITokens().Select(filter.(*common.APIToken), options)
}
type APITokensHelper struct {
controllers.HelperBase
}
func (h *APITokensHelper) NewModel() interface{} {
return &common.APIToken{}
}
func (h *APITokensHelper) HasPK(filter interface{}) bool {
result := filter.(*common.APIToken)
return result.Token != ""
}
func (h *APITokensHelper) ValidatePK(model interface{}) error {
result := model.(*common.APIToken)
err := validator.ValidateField(result.Token, "required")
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}

View File

@@ -0,0 +1,125 @@
package handlers_test
import (
"fmt"
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestAPITokensGetList(t *testing.T) {
mock := mo.MockAPITokens{}
services.GetDB().SetAPITokens(&mock)
listData := []common.APIToken{
{
Token: "TESTTOKEN",
Roles: "TESTROLES1,TESTROLES2",
Description: "FOR UNIT TESTS",
},
}
expectedResp := `{"data":[{"token":"TESTTOKEN","roles":"TESTROLES1,TESTROLES2","description":"FOR UNIT TESTS","expires_at":null}],"total":1}`
expectedRespNoTotal := `{"data":[{"token":"TESTTOKEN","roles":"TESTROLES1,TESTROLES2","description":"FOR UNIT TESTS","expires_at":null}]}`
defaultOrder := "created_at DESC"
tests := []mo.DBHttpTest{
{
Name: "No parameters",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedResp,
DBTestCase: mo.DBTestCase{
ExpectedFilter: &common.APIToken{},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "Token parameter",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens?token=TESTTOKEN", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedRespNoTotal,
DBTestCase: mo.DBTestCase{
ExpectedFilter: &common.APIToken{
Token: "TESTTOKEN",
},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "ECU parameter",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens?roles=TEST", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedResp,
DBTestCase: mo.DBTestCase{
ExpectedFilter: &common.APIToken{
Roles: "TEST",
},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "Paging parameters",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens?offset=10&limit=5", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedRespNoTotal,
DBTestCase: mo.DBTestCase{
ExpectedFilter: &common.APIToken{},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: 5,
Offset: 10,
},
MockListResponse: listData,
},
},
{
Name: "Wrong limit, -100",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens?limit=-100", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
{
Name: "Wrong limit, 1000",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens?limit=1000", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`,
},
{
Name: "Error",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/apitokens", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
DBTestCase: mo.DBTestCase{
ExpectedFilter: &common.APIToken{},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: 100,
Offset: 0,
},
MockError: fmt.Errorf("something went wrong"),
},
},
}
mo.RunDBTests(t, tests, handlers.HandleAPITokensGetList, &mock)
}

View File

@@ -0,0 +1,44 @@
package handlers
import (
"context"
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils"
ch "github.com/ClickHouse/clickhouse-go/v2"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCanSignalListGet godoc
// @Summary Lists of Can Signals used in Feature Table
// @Description Returns a list of can signals Requires API token permission.
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.CANSignalNameList} "list of cars"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /can_signals_list [get]
func HandleCanSignalListGet(w http.ResponseWriter, r *http.Request) {
var canlist []common.CANSignalNameList
conn, err := services.GetClickhouseConn()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
logger.Error().Err(err).Msg("cannot get clickhouse client")
return
}
chCtx := ch.Context(context.Background())
err = conn.Select(chCtx, &canlist, "select Signal_Name from ml_var_list_table where Is_Feature=True")
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{Data: canlist})
}

View File

@@ -0,0 +1,48 @@
package handlers_test
import (
"context"
"net/http"
"net/http/httptest"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/stretchr/testify/assert"
)
func TestHandleCanSignalListGetList(t *testing.T) {
tests := map[string]struct {
conn clickhouse.ConnInterface
expStatus int
expBody string
}{
"correct": {
conn: &clickhouse.MockConn{ExpectedResult: []common.CANSignalNameList{{"A"}, {"B"}},},
expStatus: http.StatusOK,
expBody: `{"data":[{"signal_name":"A"},{"signal_name":"B"}]}`,
},
"failed_query": {
conn: &clickhouse.MockConn{ExpectedResult: someErr},
expStatus: http.StatusServiceUnavailable,
expBody: `{"message":"json: cannot unmarshal object into Go value of type []common.CANSignalNameList","error":"Service Unavailable"}`,
},
}
for tname, tt := range tests {
t.Run(tname, func(t *testing.T) {
services.SetClickhouseConn(tt.conn)
w := httptest.NewRecorder()
ctx := context.Background()
r := httptest.NewRequest(http.MethodGet, "http://example.com/can_signals_list", nil).
WithContext(ctx)
handlers.HandleCanSignalListGet(w, r)
assert.Equal(t, tt.expStatus, w.Code)
assert.Equal(t, tt.expBody, w.Body.String())
})
}
}

View File

@@ -0,0 +1,200 @@
package handlers
import (
"context"
"fmt"
"net/http"
"otaupdate/services"
"reflect"
"strconv"
"strings"
"time"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/validator"
ch "github.com/ClickHouse/clickhouse-go/v2"
"github.com/gorilla/schema"
"github.com/pkg/errors"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
const (
limit = 100000
)
// HandleCanSignalVINGet godoc
// @Summary Export CAN signals for a specific VIN
// @Description Exports CAN signals for a specific VIN based on specified time range and CAN signals. Requires API token permission.
// @Accept json
// @Produce octet-stream
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param select_all query boolean false "Select All CAN Signals"
// @Param can_signals query []string false "CAN Signals"
// @Param timestamp_start query float64 true "Start time must be included"
// @Param timestamp_end query float64 true "End time must be included"
// @Param vin query string true "VIN must be included in the query"
// @Success 200 {file} CSV file with the specified CAN signals data
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /can_signals_export [get]
func HandleCanSignalVINGet(w http.ResponseWriter, r *http.Request) {
filter, err := parseCANSignalFilter(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
filter.Limit = limit
conn, err := services.GetClickhouseConn()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
logger.Error().Err(err).Msg("cannot get clickhouse client")
return
}
if filter.SelectAll {
allCanSignals, err := getListOfAllCanSignals(conn)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
filter.CanSignals = []string{allCanSignals}
}
w.Header().Set("Content-Type", "text/csv")
res := 0
for res == filter.Limit || res == 0 {
res, err = getCanSignalVin(conn, filter, w)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
if res == 0 {
return
}
filter.Offset += res
}
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
}
func parseCANSignalFilter(r *http.Request) (common.CANSignalQuery, error) {
sch := schema.NewDecoder()
filter := common.CANSignalQuery{}
sch.SetAliasTag("json")
//parse, err := r.URL.Parse(r.URL.String())
err := sch.Decode(&filter, r.URL.Query())
if err != nil {
return common.CANSignalQuery{}, errors.WithStack(err)
}
err = validator.GetValidator().Struct(filter)
if err != nil {
return common.CANSignalQuery{}, errors.WithStack(err)
}
if len(filter.CanSignals) == 0 && !filter.SelectAll {
return common.CANSignalQuery{}, errors.New("either Select All of a list of CAN Signals required")
}
return filter, nil
}
func getCanSignalVin(conn clickhouse.ConnInterface, filter common.CANSignalQuery, w http.ResponseWriter) (int, error) {
chCtx := ch.Context(context.Background())
query := fmt.Sprintf(`SELECT * from (SELECT VIN, Timestamp, %s FROM feature_table WHERE VIN = '%s' AND Timestamp BETWEEN %f AND %f LIMIT %d, %d) ORDER BY Timestamp DESC`,
strings.Join(filter.CanSignals, ", "),
filter.VIN,
filter.TimestampStart,
filter.TimestampEnd,
filter.Offset,
filter.Limit)
rows, err := conn.Query(chCtx, query)
if err != nil {
return 0, errors.WithStack(err)
}
if filter.Offset == 0 {
headerLine := "VIN,Timestamp"
for _, signal := range filter.CanSignals {
headerLine += "," + signal
}
headerLine += "\n"
_, err = w.Write([]byte(headerLine))
if err != nil {
return 0, errors.WithStack(err)
}
}
columnTypes := rows.ColumnTypes()
row := make([]interface{}, len(columnTypes))
for i, cType := range columnTypes {
kind := cType.ScanType().Kind()
scanName := cType.ScanType().Name()
if kind == reflect.String || strings.Contains(scanName, "string") {
row[i] = new(string)
} else if kind == reflect.Float64 || kind == reflect.Float32 || strings.Contains(scanName, "float") || strings.Contains(scanName, "decimal") {
row[i] = new(float64)
} else if kind == reflect.Int64 || kind == reflect.Int || strings.Contains(scanName, "int") {
row[i] = new(int64)
} else if strings.Contains(scanName, "Time") {
row[i] = new(time.Time)
} else {
row[i] = new(string)
}
}
var canSignals []string
if len(filter.CanSignals) == 1 {
canSignals = strings.Split(filter.CanSignals[0], ",")
}
rowCounter := 0
defer rows.Close()
for rows.Next() {
rowCounter++
err := rows.Scan(row...)
if err != nil {
return 0, errors.WithStack(err)
}
vin := *row[0].(*string)
timestamp := *row[1].(*time.Time)
dataline := vin + "," + timestamp.Format("2006-01-02 15:04:05.000000")
for i := 2; i < len(canSignals)+2; i++ {
val := "0"
if v, ok := row[i].(*float64); ok {
val = strconv.FormatFloat(*v, 'f', -1, 64)
}
dataline += "," + val
}
w.Write([]byte(dataline + "\n"))
}
w.(http.Flusher).Flush()
return rowCounter, nil
}
func getListOfAllCanSignals(conn clickhouse.ConnInterface) (string, error) {
var allCanSignals []string
var canlist []common.CANSignalNameList
chCtx := ch.Context(context.Background())
err := conn.Select(chCtx, &canlist, "select Signal_Name from ml_var_list_table where Is_Feature=True")
if err != nil {
return "", err
}
for _, signalName := range canlist {
allCanSignals = append(allCanSignals, signalName.Signal_Name)
}
return strings.Join(allCanSignals, ","), nil
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,83 @@
package handlers
import (
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/actionlogger"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/manifestsender"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/julienschmidt/httprouter"
"github.com/pkg/errors"
)
var errInvalidVIN = errors.New("invalid VIN entered")
// CarConfigurationUpdate godoc
// @Summary Send VOD and CDS to car
// @Description Get all sap codes for a car, transform them to VOD and CDS, then send it to the car
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin path string true "VIN to send configuration update"
// @Param forced query bool false "Force configuration update"
// @Success 200 {object} common.JSONMessage
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /car_config/{vin} [post]
func CarConfigurationUpdate(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
vin := params.ByName("vin")
queryParams := r.URL.Query()
forced, _ := strconv.ParseBool(queryParams.Get("forced"))
rds := services.RedisClientPool().GetFromPool()
defer rds.Close()
cs := services.GetVehicleConfig()
db := services.GetDB()
sms := services.GetSMSClient()
alDB := services.GetDB().GetActionLog()
actionLog := actionlogger.ActionLog{
VIN: vin,
Action: actionlogger.CarConfigurationUpdate,
UserIdentifier: httphandlers.GetClientID(r),
CallLocation: "github.com/fiskerinc/cloud-services/services/ota_update_go/handlers/car_configuration_update.go",
}
go func() {
err := alDB.Insert(actionLog)
if err != nil {
logger.Err(err).Msg("failed to insert action log inside CarConfigurationUpdate")
}
}()
username := httphandlers.GetClientID(r)
manifestSender := manifestsender.NewTBOXManifestSender(rds, cs, db, sms, nil)
input := manifestsender.ProcessConfigUpdateStruct{
VIN: vin,
Username: username,
SendToCar: true,
DontCreateDatabaseEntry: false,
Forced: forced,
}
// Process and send as this is a new manifest that needs a car update
_, err := manifestSender.ProcessConfigUpdate(input, services.GetDB().GetCarConfigData())
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "Sent",
})
}

View File

@@ -0,0 +1,77 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
)
// HandleCarSoftwareInformation godoc
// @Summary Get overall software version from a car and its ecu version information
// @Description Get overall software version from a car and its ecu version information
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin query string true "VIN"
// @Success 200 {object} CarSoftwareInformationResponse
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /car/software_information [get]
func HandleCarSoftwareInformation(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
vin := qs.Get("vin")
information, err := carSoftwareInformation(vin)
if loggerdataresp.BadDataError(err) {
return
}
utils.RespJSON(w, http.StatusOK, information)
}
func carSoftwareInformation(vin string) (info CarSoftwareInformationResponse, err error) {
info.VIN = vin
var ecus []common.CarECU
ecus, err = services.GetDB().GetCars().GetCarECUs(common.CarECUFilter{VIN: vin, Unique: true}, nil)
if err != nil {
return
}
info.ECUVersionInformation = convertCommonCarECUToECUVersionInformationArray(ecus)
var carVersion common.CarPKCOSVersion
carVersion, err = services.GetDB().GetCars().GetSoftwareVersion(vin)
info.OSVersion = carVersion.OSVersion
info.SUMsVersion = carVersion.SumsVersion
return
}
type CarSoftwareInformationResponse struct {
VIN string `json:"vin"`
SUMsVersion string `json:"sum_version"`
OSVersion string `json:"os_version"`
ECUVersionInformation []ECUVersionInformation `json:"ecu_version_information"`
}
type ECUVersionInformation struct {
ECU string `json:"ecu"`
SoftwareVersion string `json:"software_version"`
HardwareVersion string `json:"hardware_version"`
}
func convertCommonCarECUToECUVersionInformationArray(input []common.CarECU) (output []ECUVersionInformation) {
output = make([]ECUVersionInformation, len(input))
for index, ecuInfo := range input {
output[index] = convertCommonCarECUToECUVersionInformation(ecuInfo)
}
// Probably want to sort the ECU's alphabetically
return output
}
func convertCommonCarECUToECUVersionInformation(input common.CarECU) (output ECUVersionInformation) {
output.ECU = input.ECU
output.SoftwareVersion = input.Version
output.HardwareVersion = input.HWVersion
return output
}

View File

@@ -0,0 +1,79 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
)
// HandleCarSoftwareInformationV2 godoc
// @Summary Get overall software version from a car and its ecu version information
// @Description Get overall software version from a car and its ecu version information
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin query string true "VIN"
// @Success 200 {object} CarSoftwareInformationResponseV2
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /car/software_information/v2 [get]
func HandleCarSoftwareInformationV2(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
vin := qs.Get("vin")
information, err := carSoftwareInformationV2(vin)
if loggerdataresp.BadDataError(err) {
return
}
utils.RespJSON(w, http.StatusOK, information)
}
func carSoftwareInformationV2(vin string) (info CarSoftwareInformationResponseV2, err error) {
info.VIN = vin
var ecus []common.CarECU
ecus, err = services.GetDB().GetCars().GetCarECUs(common.CarECUFilter{VIN: vin, Unique: true, ECUs: OVLoopECUList}, nil)
if err != nil {
return
}
info.ECUVersionInformation = convertCommonCarECUToECUVersionInformationArrayV2(ecus)
var carVersion common.CarPKCOSVersion
carVersion, err = services.GetDB().GetCars().GetSoftwareVersion(vin)
info.OSVersion = carVersion.OSVersion
info.SUMsVersion = carVersion.SumsVersion
return
}
type CarSoftwareInformationResponseV2 struct {
VIN string `json:"vin"`
SUMsVersion string `json:"sum_version"`
OSVersion string `json:"os_version"`
ECUVersionInformation []ECUVersionInformationV2 `json:"ecu_version_information"`
}
type ECUVersionInformationV2 struct {
ECU string `json:"ecu"`
SupplierSWVersion string `json:"supplier_sw_version,omitempty" validate:"max=1024"`
BootLoaderVersion string `json:"boot_loader_version,omitempty" validate:"max=1024"`
}
func convertCommonCarECUToECUVersionInformationArrayV2(input []common.CarECU) (output []ECUVersionInformationV2) {
output = make([]ECUVersionInformationV2, len(input))
for index, ecuInfo := range input {
output[index] = convertCommonCarECUToECUVersionInformationV2(ecuInfo)
}
// Probably want to sort the ECU's alphabetically
return output
}
func convertCommonCarECUToECUVersionInformationV2(input common.CarECU) (output ECUVersionInformationV2) {
output.ECU = input.ECU
output.SupplierSWVersion = input.SupplierSWVersion
output.BootLoaderVersion = input.BootLoaderVersion
return output
}
var OVLoopECUList = []string{"ACU","ADAS","AMP","BCM","BMS","CIM","CMRR_FL","CMRR_FR","CMRR_RL","CMRR_RR","DSMC","ECC","EPS1","EPS2","ESP","FCM","GW","iBooster","ICC","MCU","MCU_F","MCU_R","MRR","OBC","OHC","PKC","PLGM","PSM","PVIU","PWC","PWC_R","TRM","VCU","VSP"}

View File

@@ -0,0 +1,153 @@
package handlers
import (
"context"
"encoding/json"
"net/http"
"slices"
"strings"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/tmobile"
)
// FIX BUG: current system allows for vins to be tmobile activated,
// but because we don't have them, we don't get them in response list
// HandleCarsAllowedAccess godoc
// @Summary Fetch the list of VINs that can connect to fisker cloud
// @Description Note: when a VIN is added or removed, changes will take time to propagate to the network filters
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Success 200 {object} HandleCarsAllowedAccessResponse
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /cars/allowed_access [get]
func HandleCarsAllowedAccess(w http.ResponseWriter, r *http.Request) {
var err error
res := HandleCarsAllowedAccessResponse{}
res.AllowedVINs, err = fetchVINList()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
res.AllowAll = len(res.AllowedVINs) == 0
err = json.NewEncoder(w).Encode(res)
loggerdataresp.BadDataErrorResp(w, err, http.StatusInternalServerError)
}
type HandleCarsAllowedAccessResponse struct {
AllowAll bool `json:"allow_all"`
AllowedVINs []string `json:"allowed_vins"`
}
func fetchVINList() (vins []string, err error) {
vins, err = services.GetDB().GetCars().GetWhiteListCars()
return
}
// HandleCarsAllowedAccess godoc
// @Summary Check if a single VIN has cloud and t-mobile access
// @Description
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param VINList body HandleAccessCheckInput true "List of VINs to check"
// @Success 200 {object} HandleAccessCheckResponse
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /cars/allowed_access [post]
func HandleCarAllowedAccess(w http.ResponseWriter, r *http.Request) {
vinput := HandleAccessCheckInput{}
err := json.NewDecoder(r.Body).Decode(&vinput)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
results, err := CarAllowedAccess(vinput.VINs)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
res := HandleAccessCheckResponse{
VINStatuses: results,
}
err = json.NewEncoder(w).Encode(res)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
}
func CarAllowedAccess(vins []string) (results []AllowedStruct, err error) {
var allowedVINs []string
allowedVINs, err = fetchVINList()
if err != nil {
return
}
slices.Sort(allowedVINs)
carDB := services.GetDB().GetCars()
tmobileClient, err := tmobile.NewTMobileMiniClient()
if err != nil {
return
}
results = make([]AllowedStruct, 0, len(vins))
for _, v := range vins {
temp := AllowedStruct{}
temp.VIN = v
temp.CloudAccess = slices.Contains(allowedVINs, v)
// Slow but I dont give a damn
var car *common.Car
car, err = carDB.SelectByVIN(v)
if err != nil {
logger.Err(err).Msg("failed to select car by vin")
} else if car.ICCID != "" && car.ICCID != "N/A" {
car.ICCID = strings.TrimSuffix(car.ICCID, "F")
var details *tmobile.DeviceDetailsResponse
input := tmobile.DeviceDetailsRequest{
ICCID: car.ICCID,
}
details, err = tmobileClient.DeviceDetails(context.Background(), &input)
if err != nil {
break
}
temp.TMobileStatus = string(details.Status)
} else {
temp.TMobileStatus = "INVALID ICCID"
}
results = append(results, temp)
}
return
}
type HandleAccessCheckInput struct {
VINs []string `json:"vins"`
}
type HandleAccessCheckResponse struct {
VINStatuses []AllowedStruct `json:"vin_statuses"`
}
type AllowedStruct struct {
VIN string `json:"vin"`
CloudAccess bool `json:"cloud_access"`
TMobileStatus string `json:"tmobile_access"`
}

View File

@@ -0,0 +1,71 @@
package handlers
import (
"net/http"
"strconv"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/julienschmidt/httprouter"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleGetCarsByManifest godoc
// @Summary Get cars by manifest
// @Description Returns list of cars selected by manifest id
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Param order query string false "Sort on column with asc or desc"
// @Param manifest_id path int true "Manifest ID"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.Car} "list of cars"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /manifests/{manifest_id}/vehicles [get]
func HandleGetCarsByManifest(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
idS := params.ByName("manifest_id")
id, err := strconv.Atoi(idS)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := queries.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
manDB := services.GetDB().GetUpdateManifests()
man := common.UpdateManifest{ID: int64(id)}
err = manDB.Load(&man)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
carsDB := services.GetDB().GetCars()
cars, err := carsDB.CarsByManifest(man, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
total, err := carsDB.CountCarsByManifest(man)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
if cars == nil {
cars = []common.Car{}
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: cars,
Total: total,
})
}

View File

@@ -0,0 +1,116 @@
package handlers_test
import (
"context"
"fmt"
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
m "github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/julienschmidt/httprouter"
)
func TestHandleGetCarsByManifest(t *testing.T) {
mock := mo.MockCars{}
mockMan := mo.MockUpdateManifests{
LoadResponse: &m.UpdateManifest{},
}
services.GetDB().SetCars(&mock)
services.GetDB().SetUpdateManifests(&mockMan)
expectedCar := `{"vin":"1G1FP87S3GN100062","country":"US","year":2022,"model":"Ocean","trim":"Base","powertrain":"MD23","restraint":"None","body_type":"truck"}`
expectedResp := fmt.Sprintf(`{"data":[%s],"total":1}`, expectedCar)
expectedEmptyResp := `{"data":[]}`
listData := []m.Car{
{
VIN: "1G1FP87S3GN100062",
Model: "Ocean",
Year: 2022,
Trim: "Base",
Country: "US",
Powertrain: "MD23",
Restraint: "None",
BodyType: "truck",
},
}
p := httprouter.Params{
{"manifest_id", "8"},
}
ctx := context.WithValue(context.Background(), httprouter.ParamsKey, p)
invalidP := httprouter.Params{
{"manifest_id", "o"},
}
invalidCtx := context.WithValue(context.Background(), httprouter.ParamsKey, invalidP)
tests := []mo.DBHttpTest{
{
Name: "Success",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests/8/vehicles", nil).
WithContext(ctx),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedResp,
DBTestCase: mo.DBTestCase{
ExpectedPage: &orm.PageQueryOptions{
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "Empty",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests/8/vehicles", nil).
WithContext(ctx),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedEmptyResp,
DBTestCase: mo.DBTestCase{
ExpectedPage: &orm.PageQueryOptions{
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: nil,
},
},
{
Name: "Invalid param",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests/8/vehicles", nil).
WithContext(invalidCtx),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"strconv.Atoi: parsing \"o\": invalid syntax","error":"Bad Request"}`,
},
{
Name: "Error",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests/8/vehicles", nil).
WithContext(ctx),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
DBTestCase: mo.DBTestCase{
ExpectedPage: &orm.PageQueryOptions{
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockError: fmt.Errorf("something went wrong"),
},
},
{
Name: "Wrong limit, -100",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests/8/vehicles?limit=-100", nil).
WithContext(ctx),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
{
Name: "Wrong limit, 1000",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/manifests/8/vehicles?limit=1000", nil).
WithContext(ctx),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`,
},
}
mo.RunDBTests(t, tests, handlers.HandleGetCarsByManifest, &mock)
}

View File

@@ -0,0 +1,104 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/tmobile"
"github.com/fiskerinc/cloud-services/pkg/vindecoder"
"github.com/pkg/errors"
)
// HandleCarChangeAccess godoc
// @Summary change access for a vin to connect to fisker cloud
// @Description Note: when a VIN is added or removed, changes will take time to propagate to the network filters
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body CarChangeAccessInput true "car change access data"
// @Success 200 {object} HandleCarsAllowedAccessResponse
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /cars/change_access [post]
func HandleCarChangeAccess(w http.ResponseWriter, r *http.Request) {
ccai := CarChangeAccessInput{}
err := json.NewDecoder(r.Body).Decode(&ccai)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
failedVINs := []string{}
for _, vin := range ccai.VINs {
ok := vindecoder.ValidateVINSimple(vin)
if !ok {
failedVINs = append(failedVINs, vin)
}
}
if len(failedVINs) > 0 {
errors.New(fmt.Sprintf("VINS Invalid, no changes made: %+v", failedVINs))
loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest)
return
}
err = CarChangeAccess(ccai)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
}
func CarChangeAccess(ccai CarChangeAccessInput) (err error) {
// Add/Remove from the database
if ccai.AllowCloudAccess != nil {
if *ccai.AllowCloudAccess {
err = services.GetDB().GetCars().WhitelistCars(ccai.VINs, ccai.Source)
if err != nil {
return err
}
} else {
err = services.GetDB().GetCars().BlacklistCars(ccai.VINs)
if err != nil {
return err
}
}
}
if ccai.AllowTMobileAccess != nil {
var miniClient *tmobile.TMobileMiniClient
miniClient, err = tmobile.NewTMobileMiniClient()
if err != nil {
return errors.WithMessage(err, "Failed to Create TMOible Client")
}
var iccids []string
iccids, err = services.GetDB().GetCars().GetICCIDs(ccai.VINs)
if err != nil {
return errors.WithMessage(err, "Failed to get ICCIDs")
}
tmobileInput := tmobile.ChangeDeviceActivation{
ICCIDs: iccids,
Enabled: *ccai.AllowTMobileAccess,
}
// Add/Remove from TMobile
// TODO when T-Mobile gets back about access
err = miniClient.ChangeDeviceStatus(context.Background(), tmobileInput)
if err != nil {
return
}
}
return
}
type CarChangeAccessInput struct {
VINs []string `json:"vins"` // List of VINs that will be changed
AllowCloudAccess *bool `json:"allow_cloud_access,omitempty"` // Cars can connect through gateway
AllowTMobileAccess *bool `json:"allow_tmobile_access,omitempty"` // Cars get tmobile access
Source string `json:"source,omitempty"`
}

View File

@@ -0,0 +1,102 @@
package handlers
import (
"fmt"
"io"
"net/http"
"github.com/fiskerinc/cloud-services/pkg/common/actionlogger"
"github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/logger"
uhelpers "github.com/fiskerinc/cloud-services/pkg/usecase_helpers"
"github.com/fiskerinc/cloud-services/pkg/utils"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCarUpdatesAdd godoc
// @Summary Add car updates
// @Description Create car updates assigning manifest package to cars, and send notifications
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body usecase_helpers.JSONCarUpdatesRequest true "Update manifest or package id and, car ids"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.CarUpdate} "Created car updates result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdate [post]
func HandleCarUpdatesAdd(w http.ResponseWriter, r *http.Request) {
var req uhelpers.JSONCarUpdatesRequest
var ups []common.CarUpdate
err := httphandlers.ParseRequest(r, &req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
username := httphandlers.GetClientID(r)
manifest, err := getManifest(req.UpdateManifestID)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusNotFound, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
d := services.GetDB().GetCarUpdates()
k := services.GetKafkaProducer()
alDB := services.GetDB().GetActionLog()
go func() {
actionLog := actionlogger.ActionLog{
VIN: "",
Action: actionlogger.CarUpdate,
UserIdentifier: httphandlers.GetClientID(r),
CallLocation: "github.com/fiskerinc/cloud-services/services/ota_update_go/handlers/car_update_add.go",
Description: fmt.Sprintf("UpdateManifest: %d", req.UpdateManifestID),
}
for _, vin := range req.VINs {
actionLog.VIN = vin
err = alDB.Insert(actionLog)
if err != nil {
logger.Err(err).Msg("failed to insert action log inside HandleCarUpdatesAdd")
}
}
}()
notifier := uhelpers.NewUpdateNotifier(d, k)
ups, err = notifier.Send(req.VINs, manifest, username)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
for _, vin := range req.VINs {
// Notify car user of in progress update through FOA API
fs := services.GetFoaService()
foaResp, err := fs.OtaUpdateStatus(vin, &common.CarUpdate{UpdateManifestID: req.UpdateManifestID}, &common.CarUpdateProgress{Status: carupdatestatus.Pending})
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 pending state %s for %s failed with http status %d and message %s", req.UpdateManifestID, carupdatestatus.Pending, vin, foaResp.StatusCode, bodyString)
err = nil
}
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: ups,
})
}
func getManifest(manifestID int64) (common.UpdateManifest, error) {
um := services.GetDB().GetUpdateManifests()
manifest := common.UpdateManifest{ID: manifestID}
err := um.Load(&manifest)
return manifest, err
}

View File

@@ -0,0 +1,317 @@
package handlers_test
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
dbm "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
htc "github.com/fiskerinc/cloud-services/pkg/httpclient/tester"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/kafka"
km "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"
uhelpers "github.com/fiskerinc/cloud-services/pkg/usecase_helpers"
"github.com/fiskerinc/cloud-services/pkg/utils/whereami"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/go-pg/pg/v10"
"google.golang.org/protobuf/proto"
)
func TestCarUpdateAdd(t *testing.T) {
whereami.SetService(whereami.OTA)
redis.MockRedisConnection()
mockDB := dbm.MockCarUpdates{}
mockKafka := km.KafkaMock{}
mockRedis := rm.MockRedis{}
vin := "1G1FP87S3GN100062"
mockFoa := FoaServiceMock{}
services.SetFoaService(&mockFoa)
services.GetDB().SetCarUpdates(&mockDB)
services.SetKafkaProducer(&mockKafka)
services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis))
otaUpdateKey := common.Service.Key(vin)
attendentTopic := kafka.AttendantServiceGRPCKafka
updateMsg := &kafka_grpc.GRPC_AttendantPayload_UpdateManifest{
UpdateManifest: &kafka_grpc.UpdateManifest{
CarUpdateId: 1,
},
}
kafkaMSG := kafka_grpc.GRPC_AttendantPayload{
Handler: "send_manifest",
Data: updateMsg,
}
binaryPayload, _ := proto.Marshal(&kafkaMSG)
validMessage := fmt.Sprintf(`"%s"`, base64.StdEncoding.EncodeToString(binaryPayload))
standardManifest := common.UpdateManifest{
ID: 100,
Name: "TEST",
Version: "10000",
Type: "standard",
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
BodyType: "truck",
ECUs: []*common.UpdateManifestECU{
{
ECU: "ICC",
Version: "SWVERSION",
HWVersions: []string{"HWVERSION"},
Mode: "ICC",
SelfDownload: true,
Files: []*common.UpdateManifestFile{
{
FileID: "AAAAAAA",
URL: "http://download.com/file1.bin",
FileSize: 1000,
Checksum: "AAAAAAA",
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
},
},
},
{
ECU: "ECU",
Version: "SWVERSION",
HWVersions: []string{"HWVERSION"},
Mode: "D",
Files: []*common.UpdateManifestFile{
{
FileID: "BBBBBBB",
URL: "http://download.com/file2.bin",
FileSize: 2000,
Checksum: "BBBBBBB",
WriteRegionID: 300,
WriteRegion: common.MemoryRegion{
Offset: 301,
Length: 302,
},
EraseRegionID: 400,
EraseRegion: &common.MemoryRegion{
Offset: 401,
Length: 402,
},
},
},
},
},
}
forcedManifest := standardManifest
forcedManifest.Type = "forced"
tests := []testrunner.TestCase{
{
Name: "Bad car ids",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", uhelpers.JSONCarUpdatesRequest{
UpdateManifestID: 1,
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"VINs required","error":"Bad Request"}`,
},
},
{
Name: "No data",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"UpdateManifestID required. VINs required","error":"Bad Request"}`,
},
},
{
Name: "Bad package ids",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", uhelpers.JSONCarUpdatesRequest{
VINs: []string{"NONEXISTENT", "NONEXISTENT2"},
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"UpdateManifestID required. VINs[0] 'NONEXISTENT' invalid. VINs[1] 'NONEXISTENT2' invalid","error":"Bad Request"}`,
},
},
{
Name: "Good data standard manifest id",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", uhelpers.JSONCarUpdatesRequest{
UpdateManifestID: 1,
VINs: []string{"1G1FP87S3GN100062"},
}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"id":1,"vin":"1G1FP87S3GN100062","manifest_id":1,"status":"pending","username":"testusername","UpdateSource":"OTA"}]}`,
},
DBTestCase: &dbm.DBTestCase{
SetupMockResponse: func() {
services.GetDB().SetUpdateManifests(&dbm.MockUpdateManifests{
LoadResponse: &standardManifest,
})
},
},
RedisTestCase: &rm.RedisTestCase{},
KafkaTestCase: &km.KafkaTestCase{
ExpectedProduceMessages: map[string]map[string]interface{}{
attendentTopic: {
otaUpdateKey: validMessage,
},
},
},
},
{
Name: "Case where manifest id does not exist",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", uhelpers.JSONCarUpdatesRequest{
UpdateManifestID: 1,
VINs: []string{"1G1FP87S3GN100062"},
}),
ExpectedStatus: http.StatusNotFound,
ExpectedResponse: `{"message":"pg: no rows in result set","error":"Not Found"}`,
Setup: func() {
services.GetDB().SetUpdateManifests(&dbm.MockUpdateManifests{
LoadResponse: nil,
DBMockHelper: dbm.DBMockHelper{
Error: pg.ErrNoRows,
},
})
},
},
},
{
Name: "Good data forced manifest id",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", uhelpers.JSONCarUpdatesRequest{
UpdateManifestID: 1,
VINs: []string{"1G1FP87S3GN100062"},
}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"id":1,"vin":"1G1FP87S3GN100062","manifest_id":1,"status":"pending","username":"testusername","UpdateSource":"OTA"}]}`,
},
DBTestCase: &dbm.DBTestCase{
SetupMockResponse: func() {
services.GetDB().SetUpdateManifests(&dbm.MockUpdateManifests{
LoadResponse: &forcedManifest,
})
},
},
RedisTestCase: &rm.RedisTestCase{},
KafkaTestCase: &km.KafkaTestCase{
ExpectedProduceMessages: map[string]map[string]interface{}{
attendentTopic: {
otaUpdateKey: validMessage,
},
},
},
},
{
Name: "Error",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdate", uhelpers.JSONCarUpdatesRequest{
UpdateManifestID: 1,
VINs: []string{"1G1FP87S3GN100062"},
}),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
},
DBTestCase: &dbm.DBTestCase{
MockError: fmt.Errorf("something went wrong"),
},
},
}
for _, test := range tests {
mockRedis.Reset()
mockKafka.Reset()
if test.DBTestCase != nil {
test.DBTestCase.SetupDB(&mockDB)
}
if test.RedisTestCase != nil {
test.RedisTestCase.SetupRedis(&mockRedis)
}
if test.KafkaTestCase != nil {
test.KafkaTestCase.Setup(&mockKafka)
}
if test.HttpTestCase != nil {
if test.HttpTestCase.Setup != nil {
test.HttpTestCase.Setup()
}
ctx := context.WithValue(test.HttpTestCase.Request.Context(), httphandlers.ClientIDContextKey, "testusername")
test.HttpTestCase.Request = test.HttpTestCase.Request.WithContext(ctx)
w := test.HttpTestCase.Test(handlers.HandleCarUpdatesAdd)
test.HttpTestCase.ValidateHttp(t, test.Name, w)
}
if test.DBTestCase != nil {
test.DBTestCase.Validate(t, test.Name, &mockDB)
}
if test.RedisTestCase != nil {
test.RedisTestCase.Validate(t, test.Name, &mockRedis)
}
if test.KafkaTestCase != nil {
test.KafkaTestCase.Validate(t, test.Name, &mockKafka)
}
}
}
func TestJSONCarUpdatesRequestValidation(t *testing.T) {
request := uhelpers.JSONCarUpdatesRequest{
UpdateManifestID: 1,
VINs: []string{},
}
err := validator.ValidateStruct(request)
if err == nil {
t.Errorf(th.TestErrorTemplate, "Validate VINs count", nil, err)
} else {
_, msg := validator.GetValidationErrorMsg(err)
expected := "VINs less than 1"
if msg != expected {
t.Errorf(th.TestErrorTemplate, "Validate VINs count", expected, msg)
}
}
request.VINs = []string{"1G1FP87S3GN100062"}
err = validator.ValidateStruct(request)
if err != nil {
t.Errorf(th.TestErrorTemplate, "Validate Good VIN", nil, err)
}
request.VINs = []string{"1G1FP87S3GN10006I"}
err = validator.ValidateStruct(request)
if err == nil {
t.Errorf(th.TestErrorTemplate, "Validate Bad VIN", nil, err)
} else {
_, msg := validator.GetValidationErrorMsg(err)
expected := "VINs[0] '1G1FP87S3GN10006I' invalid"
if msg != expected {
t.Errorf(th.TestErrorTemplate, "Validate Bad VIN", expected, msg)
}
}
}
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,142 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/actionlogger"
"github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/common/handlers"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
re "github.com/gomodule/redigo/redis"
"github.com/julienschmidt/httprouter"
)
// HandleCarUpdateCancel godoc
// @Summary Cancel car update
// @Description Cancels car update and send notifications
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param id path string true "Car update id to cancel"
// @Success 200 {object} common.JSONMessage "Request result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdate/{id}/cancel [post]
func HandleCarUpdateCancel(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
req := common.CarUpdateRequest{
CarUpdateID: id,
}
err = validator.ValidateStruct(req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
carupdates := services.GetDB().GetCarUpdates()
cu, err := carupdates.SelectByID(req.CarUpdateID)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
alDB := services.GetDB().GetActionLog()
go func() {
actionLog := actionlogger.ActionLog{
VIN: cu.VIN,
Action: actionlogger.CarUpdate,
UserIdentifier: httphandlers.GetClientID(r),
CallLocation: "github.com/fiskerinc/cloud-services/services/ota_update_go/handlers/carupdate_cancel.go",
Description: fmt.Sprintf("car update id: %d", req.CarUpdateID),
}
err = alDB.Insert(actionLog)
if err != nil {
logger.Err(err).Msg("failed to insert action log inside HandleCarUpdateCancel")
}
}()
// If this update was from aftersales, then we just mark as canceled
if cu.UpdateSource == common.UPDATE_SOURCE_AFTERSALES {
err = cancelUpdateAftersales(req, cu, carupdates)
} else {
err = cancelUpdateOTA(req, cu, carupdates)
}
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "OK",
})
}
func cancelUpdateAftersales(req common.CarUpdateRequest, cu *common.CarUpdate, carupdates queries.CarUpdatesInterface) (err error) {
current := common.CarUpdate{
ID: req.CarUpdateID,
Status: carupdatestatus.ManifestCanceled,
}
_, err = carupdates.UpdateStatus(&current)
if err != nil {
return
}
return
}
func cancelUpdateOTA(req common.CarUpdateRequest, cu *common.CarUpdate, carupdates queries.CarUpdatesInterface) (err error) {
current := common.CarUpdate{
ID: req.CarUpdateID,
Status: carupdatestatus.ManifestCancelPending,
}
_, err = carupdates.UpdateStatus(&current)
if err != nil {
return
}
client := services.RedisClientPool().GetFromPool()
defer client.Close()
key := redis.CarUpdateStatusHashKey(req.CarUpdateID)
msg, err := json.Marshal(common.Message{
Handler: handlers.UpdateManifestCancel,
Data: req,
})
batch := redis.NewRedisBatchCommands()
batch.Add(re.Args{}.Add("HSET").Add(key).AddFlat(common.CarUpdateProgress{
CarUpdateID: req.CarUpdateID,
Status: carupdatestatus.ManifestCancelPending,
})...)
batch.Add("EXPIRE", key, 3600)
batch.Add("RPUSH", redis.QueueKey(common.TRex.Key(cu.VIN)), msg)
batch.Add("EXPIRE", redis.QueueKey(common.TRex.Key(cu.VIN)), 3600)
batch.Add("RPUSH", redis.QueueKey(common.HMI.Key(cu.VIN)), msg)
batch.Add("EXPIRE", redis.QueueKey(common.HMI.Key(cu.VIN)), 3600)
_, err = client.ExecuteBatch(batch)
if err != nil {
return
}
return
}

View File

@@ -0,0 +1,138 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/httpclient/tester"
"github.com/fiskerinc/cloud-services/pkg/redis"
r "github.com/fiskerinc/cloud-services/pkg/redis"
rm "github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/testrunner"
"github.com/go-pg/pg/v10"
)
func TestCarUpdateCancel(t *testing.T) {
r.MockRedisConnection()
mockRedis := rm.MockRedis{
GetSetResults: "[]",
}
services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis))
mock := mo.MockCarUpdates{}
services.GetDB().SetCarUpdates(&mock)
vin := "1G1FP87S3GN100062"
mockCarUpdate := common.CarUpdate{
ID: 1000,
VIN: vin,
UpdateManifestID: 10,
}
trexKey := common.TRex.Key(vin)
hmiKey := common.HMI.Key(vin)
tests := []testrunner.TestCase{
{
Name: "invalid update id",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodDelete, "http://example.com/carupdate/1000/cancel", nil),
ExpectedStatus: http.StatusNotFound,
ExpectedResponse: `{"message":"pg: no rows in result set","error":"Not Found"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: pg.ErrNoRows,
},
RedisTestCase: &rm.RedisTestCase{},
},
{
Name: "non-numeric update id",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodDelete, "http://example.com/carupdate/xxxx/cancel", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"strconv.ParseInt: parsing \"xxxx\": invalid syntax","error":"Bad Request"}`,
},
DBTestCase: &mo.DBTestCase{},
RedisTestCase: &rm.RedisTestCase{},
},
{
Name: "database error",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodDelete, "http://example.com/carupdate/1000/cancel", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: someErr,
},
},
{
Name: "redis error",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodDelete, "http://example.com/carupdate/1000/cancel", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`,
},
DBTestCase: &mo.DBTestCase{
MockLoadResponse: &mockCarUpdate,
},
RedisTestCase: &rm.RedisTestCase{
MockRedisError: someErr,
},
},
{
Name: "good request",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodDelete, "http://example.com/carupdate/1000/cancel", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"OK"}`,
},
DBTestCase: &mo.DBTestCase{
MockLoadResponse: &mockCarUpdate,
},
RedisTestCase: &rm.RedisTestCase{
ExpectedMessages: map[string]string{
trexKey: `{"handler":"update_manifest_cancel","data":{"car_update_id":1000}}`,
hmiKey: `{"handler":"update_manifest_cancel","data":{"car_update_id":1000}}`,
},
ExpectedCaches: map[string]rm.ExpiringCacheResult{
redis.CarUpdateStatusHashKey(int64(1000)): {
Value: `{"current_size":0,"ecu":"","errorcode":0,"file_size":0,"file_total":0,"id":1000,"installed":0,"status":"manifest_cancel_pending","total_files":0,"total_size":0}`,
Expires: 3600,
},
},
},
},
}
schemaTesterTRex := testhelper.NewSchemaTestHelper(t, schemaToTRex)
for _, test := range tests {
mockRedis.Reset()
if test.RedisTestCase != nil {
test.RedisTestCase.SetupRedis(&mockRedis)
}
if test.DBTestCase != nil {
test.DBTestCase.SetupDB(&mock)
}
if test.HttpTestCase != nil {
w := test.HttpTestCase.TestWithParamPath(handlers.HandleCarUpdateCancel, "/carupdate/:id/cancel")
test.HttpTestCase.ValidateHttp(t, test.Name, w)
}
if test.DBTestCase != nil {
test.DBTestCase.Validate(t, test.Name, &mock)
}
if test.RedisTestCase != nil {
test.RedisTestCase.Validate(t, test.Name, &mockRedis)
for _, mes := range test.RedisTestCase.ExpectedMessages {
schemaTesterTRex.ValidateSchemaObject(test.Name, []byte(mes))
}
}
}
}

View File

@@ -0,0 +1,41 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/utils/urlhelper"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCarUpdateDelete godoc
// @Summary Delete car update
// @Description Delete car update. Requires delete permissions
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param id query int true "Car update id"
// @Success 200 {object} common.JSONMessage
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdate [delete]
func HandleCarUpdateDelete(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
carupdate := common.CarUpdate{
ID: urlhelper.GetQueryInt64(qs, "id"),
}
_, err := services.GetDB().GetCarUpdates().Delete(&carupdate)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "Deleted",
})
}

View File

@@ -0,0 +1,38 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestCarUpdateDelete(t *testing.T) {
services.GetDB().SetCarUpdates(&mocks.MockCarUpdates{})
tests := []th.BasicHttpTest{
{
Name: "No id",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"id required","error":"Bad Request"}`,
},
{
Name: "Zero id",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?id=0", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"id required","error":"Bad Request"}`,
},
{
Name: "Good id",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?id=1", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Deleted"}`,
},
}
th.RunBasicHttpTests(t, tests, handlers.HandleCarUpdateDelete)
}

View File

@@ -0,0 +1,135 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/actionlogger"
"github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/common/handlers"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
re "github.com/gomodule/redigo/redis"
"github.com/julienschmidt/httprouter"
)
// HandleCarUpdateDeploy godoc
// @Summary Deploy car update
// @Description Deploys car update and send notifications
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param id path string true "Car update id to deploy"
// @Success 200 {object} common.JSONMessage "Request result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdate/{id}/deploy [post]
func HandleCarUpdateDeploy(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
req := common.CarUpdateRequest{
CarUpdateID: id,
}
err = validator.ValidateStruct(req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
carupdates := services.GetDB().GetCarUpdates()
cu, err := carupdates.SelectByID(req.CarUpdateID)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
alDB := services.GetDB().GetActionLog()
go func() {
actionLog := actionlogger.ActionLog{
VIN: cu.VIN,
Action: actionlogger.CarUpdate,
UserIdentifier: httphandlers.GetClientID(r),
CallLocation: "github.com/fiskerinc/cloud-services/services/ota_update_go/handlers/carupdate_deploy.go",
Description: fmt.Sprintf("car update id: %d", req.CarUpdateID),
}
err = alDB.Insert(actionLog)
if err != nil {
logger.Err(err).Msg("failed to insert action log inside HandleCarUpdateDeploy")
}
}()
if !isRedeployAvailable(cu.Status) {
utils.RespJSON(w, http.StatusUnprocessableEntity, common.JSONMessage{
Message: fmt.Sprintf("Unable to redeploy, CarUpdate is currently %s", cu.Status),
})
return
}
current := common.CarUpdate{
ID: req.CarUpdateID,
Status: carupdatestatus.Pending,
}
_, err = carupdates.UpdateStatus(&current)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
client := services.RedisClientPool().GetFromPool()
defer client.Close()
key := redis.CarUpdateStatusHashKey(req.CarUpdateID)
msg, err := json.Marshal(common.Message{
Handler: handlers.UpdateManifestInstall,
Data: req,
})
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
batch := redis.NewRedisBatchCommands()
batch.Add(re.Args{}.Add("HSET").Add(key).AddFlat(common.CarUpdateProgress{
CarUpdateID: req.CarUpdateID,
Status: carupdatestatus.Pending,
})...)
batch.Add("EXPIRE", key, 3600)
batch.Add("RPUSH", redis.QueueKey(common.TRex.Key(cu.VIN)), msg)
batch.Add("EXPIRE", redis.QueueKey(common.TRex.Key(cu.VIN)), 3600)
batch.Add("RPUSH", redis.QueueKey(common.HMI.Key(cu.VIN)), msg)
batch.Add("EXPIRE", redis.QueueKey(common.HMI.Key(cu.VIN)), 3600)
_, err = client.ExecuteBatch(batch)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "OK",
})
}
func isRedeployAvailable(status string) bool {
switch status {
case carupdatestatus.ManifestSucceeded, carupdatestatus.ManifestCanceled,
carupdatestatus.ManifestError, carupdatestatus.ManifestCancelPending,
carupdatestatus.RollbackSucceeded, carupdatestatus.ManifestRejected,
carupdatestatus.RollbackFailed, carupdatestatus.CleanupSucceeded:
return true
default:
return false
}
}

View File

@@ -0,0 +1,156 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/httpclient/tester"
"github.com/fiskerinc/cloud-services/pkg/redis"
rm "github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/testrunner"
"github.com/go-pg/pg/v10"
)
func TestCarUpdateDeploy(t *testing.T) {
redis.MockRedisConnection()
mockRedis := rm.MockRedis{
GetSetResults: "[]",
}
services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis))
mock := mo.MockCarUpdates{}
services.GetDB().SetCarUpdates(&mock)
vin := "1G1FP87S3GN100062"
mockCarUpdate := common.CarUpdate{
ID: 1000,
VIN: vin,
UpdateManifestID: 10,
Status: carupdatestatus.ManifestSucceeded,
}
trexKey := common.TRex.Key(vin)
hmiKey := common.HMI.Key(vin)
tests := []testrunner.TestCase{
{
Name: "invalid update id",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, "http://example.com/carupdate/1000/deploy", nil),
ExpectedStatus: http.StatusNotFound,
ExpectedResponse: `{"message":"pg: no rows in result set","error":"Not Found"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: pg.ErrNoRows,
},
RedisTestCase: &rm.RedisTestCase{},
},
{
Name: "non-numeric update id",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, "http://example.com/carupdate/xxxx/deploy", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"strconv.ParseInt: parsing \"xxxx\": invalid syntax","error":"Bad Request"}`,
},
DBTestCase: &mo.DBTestCase{},
RedisTestCase: &rm.RedisTestCase{},
},
{
Name: "database error",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, "http://example.com/carupdate/1000/deploy", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: someErr,
},
},
{
Name: "redis error",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, "http://example.com/carupdate/1000/deploy", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`,
},
DBTestCase: &mo.DBTestCase{
MockLoadResponse: &mockCarUpdate,
},
RedisTestCase: &rm.RedisTestCase{
MockRedisError: someErr,
},
},
{
Name: "invalid status",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, "http://example.com/carupdate/1000/deploy", nil),
ExpectedStatus: http.StatusUnprocessableEntity,
ExpectedResponse: `{"message":"Unable to redeploy, CarUpdate is currently pending"}`,
},
DBTestCase: &mo.DBTestCase{
MockLoadResponse: &common.CarUpdate{
ID: 1000,
VIN: vin,
UpdateManifestID: 10,
Status: carupdatestatus.Pending,
},
},
},
{
Name: "good request",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, "http://example.com/carupdate/1000/deploy", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"OK"}`,
},
DBTestCase: &mo.DBTestCase{
MockLoadResponse: &mockCarUpdate,
},
RedisTestCase: &rm.RedisTestCase{
ExpectedMessages: map[string]string{
trexKey: `{"handler":"update_manifest_install","data":{"car_update_id":1000}}`,
hmiKey: `{"handler":"update_manifest_install","data":{"car_update_id":1000}}`,
},
ExpectedCaches: map[string]rm.ExpiringCacheResult{
redis.CarUpdateStatusHashKey(int64(1000)): {
Value: `{"current_size":0,"ecu":"","errorcode":0,"file_size":0,"file_total":0,"id":1000,"installed":0,"status":"pending","total_files":0,"total_size":0}`,
Expires: 3600,
},
},
},
},
}
schemaTesterTRex := testhelper.NewSchemaTestHelper(t, schemaToTRex)
for _, test := range tests {
mockRedis.Reset()
if test.RedisTestCase != nil {
test.RedisTestCase.SetupRedis(&mockRedis)
}
if test.DBTestCase != nil {
test.DBTestCase.SetupDB(&mock)
}
if test.HttpTestCase != nil {
w := test.HttpTestCase.TestWithParamPath(handlers.HandleCarUpdateDeploy, "/carupdate/:id/deploy")
test.HttpTestCase.ValidateHttp(t, test.Name, w)
}
if test.DBTestCase != nil {
test.DBTestCase.Validate(t, test.Name, &mock)
}
if test.RedisTestCase != nil {
test.RedisTestCase.Validate(t, test.Name, &mockRedis)
for _, mes := range test.RedisTestCase.ExpectedMessages {
schemaTesterTRex.ValidateSchemaObject(test.Name, []byte(mes))
}
}
}
}

View File

@@ -0,0 +1,133 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/actionlogger"
"github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/common/handlers"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
re "github.com/gomodule/redigo/redis"
"github.com/julienschmidt/httprouter"
)
// HandleCarUpdateVehicleCancel godoc
// @Summary Cancel car update on vehicle
// @Description Cancel a rogue car update on a vehicle that's not found on cloud.
// A car update may not be found in cloud following physical vehicle maintenance.
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param id path string true "Car update id to cancel"
// @Param vin query string true "VIN of vehicle to send to"
// @Success 200 {object} common.JSONMessage "Request result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdate/{id}/vehicle-cancel [post]
func HandleCarUpdateVehicleCancel(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
vin := r.URL.Query().Get("vin")
ok := validator.ValidateVINSimple(vin)
if !ok {
loggerdataresp.BadDataErrorResp(w, ErrInvalidVIN, http.StatusBadRequest)
return
}
req := common.CarUpdateRequest{
CarUpdateID: id,
}
err = validator.ValidateStruct(req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
alDB := services.GetDB().GetActionLog()
go func() {
actionLog := actionlogger.ActionLog{
VIN: vin,
Action: actionlogger.CarUpdate,
UserIdentifier: httphandlers.GetClientID(r),
CallLocation: "github.com/fiskerinc/cloud-services/services/ota_update_go/handlers/carupdate_vehicle_cancel.go",
Description: fmt.Sprintf("car update id: %d", req.CarUpdateID),
}
err = alDB.Insert(actionLog)
if err != nil {
logger.Err(err).Msg("failed to insert action log inside HandleCarUpdateVehicleCancel")
}
}()
cu := &common.CarUpdate{
ID: id,
VIN: vin,
}
carupdates := services.GetDB().GetCarUpdates()
_, err = carupdates.SelectByID(req.CarUpdateID)
if err == nil {
err = fmt.Errorf("car update was found in cloud, use /carupdate/%d/cancel", req.CarUpdateID)
} else {
err = nil
}
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
err = sendCancelUpdate(req, cu)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "OK",
})
}
func sendCancelUpdate(req common.CarUpdateRequest, cu *common.CarUpdate) (err error) {
client := services.RedisClientPool().GetFromPool()
defer client.Close()
key := redis.CarUpdateStatusHashKey(req.CarUpdateID)
msg, err := json.Marshal(common.Message{
Handler: handlers.UpdateManifestCancel,
Data: req,
})
batch := redis.NewRedisBatchCommands()
batch.Add(re.Args{}.Add("HSET").Add(key).AddFlat(common.CarUpdateProgress{
CarUpdateID: req.CarUpdateID,
Status: carupdatestatus.ManifestCancelPending,
})...)
batch.Add("EXPIRE", key, 3600)
batch.Add("RPUSH", redis.QueueKey(common.TRex.Key(cu.VIN)), msg)
batch.Add("EXPIRE", redis.QueueKey(common.TRex.Key(cu.VIN)), 3600)
batch.Add("RPUSH", redis.QueueKey(common.HMI.Key(cu.VIN)), msg)
batch.Add("EXPIRE", redis.QueueKey(common.HMI.Key(cu.VIN)), 3600)
_, err = client.ExecuteBatch(batch)
if err != nil {
return
}
return
}

View File

@@ -0,0 +1,151 @@
package handlers_test
import (
"fmt"
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/httpclient/tester"
"github.com/fiskerinc/cloud-services/pkg/redis"
r "github.com/fiskerinc/cloud-services/pkg/redis"
rm "github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/testrunner"
"github.com/go-pg/pg/v10"
)
func TestCarUpdateVehicleCancel(t *testing.T) {
r.MockRedisConnection()
mockRedis := rm.MockRedis{
GetSetResults: "[]",
}
services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis))
mock := mo.MockCarUpdates{}
services.GetDB().SetCarUpdates(&mock)
vin := "1G1FP87S3GN100062"
mockCarUpdate := common.CarUpdate{
ID: 1000,
VIN: vin,
UpdateManifestID: 10,
}
trexKey := common.TRex.Key(vin)
hmiKey := common.HMI.Key(vin)
tests := []testrunner.TestCase{
{
Name: "car update known to cloud",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, fmt.Sprintf("http://example.com/carupdate/1000/vehicle-cancel?vin=%s", vin), nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"car update was found in cloud, use /carupdate/1000/cancel","error":"Service Unavailable"}`,
},
DBTestCase: &mo.DBTestCase{
MockLoadResponse: &mockCarUpdate,
},
RedisTestCase: &rm.RedisTestCase{},
},
{
Name: "non-numeric update id",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, fmt.Sprintf("http://example.com/carupdate/xxxx/vehicle-cancel?vin=%s", vin), nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"strconv.ParseInt: parsing \"xxxx\": invalid syntax","error":"Bad Request"}`,
},
DBTestCase: &mo.DBTestCase{},
RedisTestCase: &rm.RedisTestCase{},
},
{
Name: "database error",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, fmt.Sprintf("http://example.com/carupdate/1000/vehicle-cancel?vin=%s", vin), nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"OK"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: someErr,
},
RedisTestCase: &rm.RedisTestCase{
ExpectedMessages: map[string]string{
trexKey: `{"handler":"update_manifest_cancel","data":{"car_update_id":1000}}`,
hmiKey: `{"handler":"update_manifest_cancel","data":{"car_update_id":1000}}`,
},
ExpectedCaches: map[string]rm.ExpiringCacheResult{
redis.CarUpdateStatusHashKey(int64(1000)): {
Value: `{"current_size":0,"ecu":"","errorcode":0,"file_size":0,"file_total":0,"id":1000,"installed":0,"status":"manifest_cancel_pending","total_files":0,"total_size":0}`,
Expires: 3600,
},
},
},
},
{
Name: "redis error",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, fmt.Sprintf("http://example.com/carupdate/1000/vehicle-cancel?vin=%s", vin), nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: pg.ErrNoRows,
},
RedisTestCase: &rm.RedisTestCase{
MockRedisError: someErr,
},
},
{
Name: "car update unknown to cloud",
HttpTestCase: &tester.HttpTestCase{
Request: testhelper.MakeTestRequest(http.MethodPost, fmt.Sprintf("http://example.com/carupdate/1000/vehicle-cancel?vin=%s", vin), nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"OK"}`,
},
DBTestCase: &mo.DBTestCase{
MockError: pg.ErrNoRows,
},
RedisTestCase: &rm.RedisTestCase{
ExpectedMessages: map[string]string{
trexKey: `{"handler":"update_manifest_cancel","data":{"car_update_id":1000}}`,
hmiKey: `{"handler":"update_manifest_cancel","data":{"car_update_id":1000}}`,
},
ExpectedCaches: map[string]rm.ExpiringCacheResult{
redis.CarUpdateStatusHashKey(int64(1000)): {
Value: `{"current_size":0,"ecu":"","errorcode":0,"file_size":0,"file_total":0,"id":1000,"installed":0,"status":"manifest_cancel_pending","total_files":0,"total_size":0}`,
Expires: 3600,
},
},
},
},
}
schemaTesterTRex := testhelper.NewSchemaTestHelper(t, schemaToTRex)
for _, test := range tests {
mockRedis.Reset()
if test.RedisTestCase != nil {
test.RedisTestCase.SetupRedis(&mockRedis)
}
if test.DBTestCase != nil {
test.DBTestCase.SetupDB(&mock)
}
if test.HttpTestCase != nil {
w := test.HttpTestCase.TestWithParamPath(handlers.HandleCarUpdateVehicleCancel, "/carupdate/:id/vehicle-cancel")
test.HttpTestCase.ValidateHttp(t, test.Name, w)
}
if test.DBTestCase != nil {
test.DBTestCase.Validate(t, test.Name, &mock)
}
if test.RedisTestCase != nil {
test.RedisTestCase.Validate(t, test.Name, &mockRedis)
for _, mes := range test.RedisTestCase.ExpectedMessages {
schemaTesterTRex.ValidateSchemaObject(test.Name, []byte(mes))
}
}
}
}

View File

@@ -0,0 +1,80 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/utils/urlhelper"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCarUpdatesGet godoc
// @Summary Search car updates
// @Description Get car updates filtered by id, car id, and update package id
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param id query int false "CarUpdate id"
// @Param vin query string false "Car VIN"
// @Param manifest_id query int false "Update manifest id"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Param order query string false "Sort on column with asc or desc"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.CarUpdate}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdates [get]
func HandleCarUpdatesGet(w http.ResponseWriter, r *http.Request) {
var total int
cu := services.GetDB().GetCarUpdates()
filter, err := parseCarsUpdateFilter(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := orm.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
if options.Order == "" {
options.Order = "id DESC"
}
ups, err := cu.Select(filter, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
if options.Offset == 0 && filter.ID == 0 {
total, err = cu.Count(filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: ups,
Total: total,
})
}
func parseCarsUpdateFilter(r *http.Request) (*common.CarUpdate, error) {
qs := r.URL.Query()
filter := common.CarUpdate{
ID: urlhelper.GetQueryInt64(qs, "id"),
VIN: qs.Get("vin"),
UpdateManifestID: urlhelper.GetQueryInt64(qs, "manifest_id"),
}
err := validator.ValidateNonRequired(filter)
return &filter, err
}

View File

@@ -0,0 +1,143 @@
package handlers_test
import (
"fmt"
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
m "github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestCarUpdatesGet(t *testing.T) {
mock := mo.MockCarUpdates{}
services.GetDB().SetCarUpdates(&mock)
expectedCarUpdate := `{"id":1,"vin":"1G1FP87S3GN100062","manifest_id":1,"updatemanifest":{"name":"TEST","version":"1.1","description":"description","release_notes":"http://releasenotes.com","rollback":false,"type":"forced","country":"US","powertrain":"MD23","restraint":"None","model":"Ocean","trim":"Sport","year":2022,"body_type":"truck"},"UpdateSource":"OTA"}`
expectedResp := fmt.Sprintf(`{"data":[%s],"total":1}`, expectedCarUpdate)
expectedRespNoTotal := fmt.Sprintf(`{"data":[%s]}`, expectedCarUpdate)
defaultOrder := "id DESC"
listData := []m.CarUpdate{
{
ID: 1,
VIN: "1G1FP87S3GN100062",
UpdateManifestID: 1,
UpdateManifest: &m.UpdateManifest{
Name: "TEST",
Description: "description",
Version: "1.1",
ReleaseNotes: "http://releasenotes.com",
RollbackEnabled: false,
Type: "forced",
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
BodyType: "truck",
},
UpdateSource: "OTA",
},
}
tests := []mo.DBHttpTest{
{
Name: "No parameters",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedResp,
DBTestCase: mo.DBTestCase{
ExpectedFilter: m.CarUpdate{},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "Id parameter",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?id=1", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedRespNoTotal,
DBTestCase: mo.DBTestCase{
ExpectedFilter: m.CarUpdate{
ID: 1,
},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "VIN and UpdateManifestID parameters",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?vin=1G1FP87S3GN100062&manifest_id=1", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedResp,
DBTestCase: mo.DBTestCase{
ExpectedFilter: m.CarUpdate{
VIN: "1G1FP87S3GN100062",
UpdateManifestID: 1,
},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockListResponse: listData,
},
},
{
Name: "Paging parameters",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?offset=10&limit=5", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: expectedRespNoTotal,
DBTestCase: mo.DBTestCase{
ExpectedFilter: m.CarUpdate{},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: 5,
Offset: 10,
},
MockListResponse: listData,
},
},
{
Name: "Error",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
DBTestCase: mo.DBTestCase{
ExpectedFilter: m.CarUpdate{},
ExpectedPage: &orm.PageQueryOptions{
Order: defaultOrder,
Limit: orm.PageQueryOptionsLimitMaximum,
Offset: 0,
},
MockError: fmt.Errorf("something went wrong"),
},
},
{
Name: "Wrong limit, -100",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?limit=-100", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
{
Name: "Wrong limit, 1000",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdates?limit=1000", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`,
},
}
mo.RunDBTests(t, tests, handlers.HandleCarUpdatesGet, &mock)
}

View File

@@ -0,0 +1,76 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/utils"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCarUpdatesLog godoc
// @Summary Gets log of car update statuses
// @Description Returns array of car update statuses
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param carupdateid query int true "car update id"
// @Success 200 {object} CarUpdateStatuses "Car update statuses"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdateslog [get]
func HandleCarUpdatesLog(w http.ResponseWriter, r *http.Request) {
var total int
carupdateID, err := validateCarUpdatesLog(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := orm.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
if options.Order == "" {
options.Order = "id DESC"
}
cu := services.GetDB().GetCarUpdates()
statuses, err := cu.GetUpdateStatuses(carupdateID, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
if options.Offset == 0 {
total, err = cu.CountUpdateStatuses(carupdateID)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: statuses,
Total: total,
})
}
func validateCarUpdatesLog(r *http.Request) (int64, error) {
qs := r.URL.Query()
qsID := qs.Get("carupdateid")
if qsID == "" {
return 0, fmt.Errorf("car update id required")
}
id, err := strconv.ParseInt(qsID, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid id %s", qsID)
}
return id, nil
}

View File

@@ -0,0 +1,56 @@
package handlers_test
import (
"net/http"
"testing"
"time"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/dbbasemodel"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleCarUpdatesLog(t *testing.T) {
date := time.Date(2021, time.November, 10, 23, 0, 0, 0, time.UTC)
mock := mo.MockCarUpdates{
SelectCarUpdateStatusesResponse: []common.CarUpdateStatus{
{
ID: 1000,
CarUpdateID: 100,
Status: "pending",
DBModelBase: dbbasemodel.DBModelBase{
CreatedAt: &date,
UpdatedAt: &date,
},
},
},
}
services.GetDB().SetCarUpdates(&mock)
//year int, month Month, day, hour, min, sec, nsec int, loc *Location
tests := []th.BasicHttpTest{
{
Name: "Missing query",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdateslog", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"car update id required","error":"Bad Request"}`,
},
{
Name: "Bad car update ids",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdateslog?carupdateid=XXXXXXXXX", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"invalid id XXXXXXXXX","error":"Bad Request"}`,
},
{
Name: "Good request",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdateslog?carupdateid=100", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"id":1000,"carupdate_id":100,"status":"pending","error_code":0,"created":"2021-11-10T23:00:00Z","updated":"2021-11-10T23:00:00Z"}],"total":1}`,
},
}
th.RunBasicHttpTests(t, tests, handlers.HandleCarUpdatesLog)
}

View File

@@ -0,0 +1,109 @@
package handlers
import (
"errors"
"fmt"
"net/http"
"otaupdate/services"
"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/utils"
"github.com/fiskerinc/cloud-services/pkg/utils/querystring"
"github.com/go-pg/pg/v10"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCarUpdatesStatuses godoc
// @Summary Gets statuses for car update by car update ids
// @Description Returns array of car update statuses
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param carupdateids query string true "Comma delimited list of car update ids"
// @Success 200 {object} CarUpdateStatuses "Car update statuses"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /carupdatesstatuses [get]
func HandleCarUpdatesStatuses(w http.ResponseWriter, r *http.Request) {
data, err := validateCarUpdatesStatuses(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
keys := make([]string, len(data))
// These should be of type common.CarUpdateProgress
statuses := make([]interface{}, len(data))
for i, carupdateID := range data {
keys[i] = redis.CarUpdateStatusHashKey(carupdateID)
statuses[i] = &common.CarUpdateProgress{}
}
conn := services.RedisClientPool().GetFromPool()
defer conn.Close()
err = conn.GetObjectsMulti(keys, statuses)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, CarUpdateStatuses{
Statuses: statuses,
})
}
// I am not a fan of doing this, going from Redis and then checking the database
func filterCarUpdatedStatuses(statuses []interface{}) (filtered []interface{}) {
filtered = make([]interface{}, 0)
for x := range statuses {
status, ok := statuses[x].(common.CarUpdateProgress)
if !ok {
continue
}
// LOAD does not search on CarUpdateID
manifest := &common.CarUpdate{ID: status.CarUpdateID, UpdateManifest: &common.UpdateManifest{ManifestType: common.MagnaManifestUpdateType}}
um := services.GetDB().GetCarUpdates()
err := um.Load(manifest)
if err != nil {
if errors.Is(err, pg.ErrNoRows) {
continue
}
logger.Warn().Err(err).Send()
}
if manifest.ID > 0 {
filtered = append(filtered, statuses[x])
}
}
return filtered
}
func validateCarUpdatesStatuses(r *http.Request) ([]int64, error) {
qs := r.URL.Query()
qsIDs := qs.Get("carupdateids")
if qsIDs == "" {
return nil, fmt.Errorf("car update ids required")
}
if len(qsIDs) > 6000 {
return nil, fmt.Errorf("carupdateids too long")
}
carupdateIDs, err := querystring.SplitIntArray(qsIDs)
if err != nil {
return nil, err
}
return carupdateIDs, nil
}
type CarUpdateStatuses struct {
Statuses []interface{} `json:"statuses"`
}

View File

@@ -0,0 +1,40 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleCarUpdatesStatuses(t *testing.T) {
redis.MockRedisConnection()
services.SetRedisClientPool(tester.NewMockClientPool())
tests := []th.BasicHttpTest{
{
Name: "Missing query",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/carupdatesstatuses", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"car update ids required","error":"Bad Request"}`,
},
{
Name: "Bad car update ids",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdatesstatuses?carupdateids=XXXXXXXXX", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"invalid id XXXXXXXXX","error":"Bad Request"}`,
},
{
Name: "Good request",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/carupdatesstatuses?carupdateids=100,101,102", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"statuses":[{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":0,"ecu":"","msg":"","err":0},{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":0,"ecu":"","msg":"","err":0},{"file_current":0,"file_total":0,"package_current":0,"package_total":0,"installed":0,"total_files":0,"car_update_id":0,"ecu":"","msg":"","err":0}]}`,
},
}
th.RunBasicHttpTests(t, tests, handlers.HandleCarUpdatesStatuses)
}

View File

@@ -0,0 +1,50 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleCustomerOtaEmails godoc
// @Summary Sends customer emails by list of vins
// @Description Sends OTA notification emails to all emails associated with vins in request body
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body common.CustomerOtaEmailsRequest true "Customer OTA Emails Request"
// @Success 200 {object} map[string]bool "Customer Ota Emails"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /customer_ota_emails [put]
func HandleCustomerOtaEmails(w http.ResponseWriter, r *http.Request) {
var request common.CustomerOtaEmailsRequest
err := httphandlers.ParseRequest(r, &request)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
var fromEmail = "fastservice@ovloop.com"
var toEmails = []string{}
var subject = request.EmailSubject
var body = request.EmailBody
driverEmails, err := services.GetDB().GetDriverEmails().SelectByVINs(request.VINs)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
for _, driverEmail := range driverEmails {
toEmails = append(toEmails, driverEmail.Email)
}
err = services.GetSMTP().Send(fromEmail, toEmails, subject, body)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
}

View File

@@ -0,0 +1,77 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/smtpclient"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestCustomerOtaEmails(t *testing.T) {
mock := MockDriverEmails{
SelectResponse: &[]common.DriverEmail{
{
Vin: "VCF1ZBU28PG002114",
DriverId: "446b4d69-8768-4e6b-bcf8-0507ea74c952",
Email: "ivan.delgadillo@indigotech.com",
GivenName: "Ivan",
FamilyName: "Delgadillo",
},
{
Vin: "VCF1ZBU25PG003608",
DriverId: "7d9c8fed-51b3-4df3-9603-5dfe0e8979f2",
Email: "junsub.lee@indigotech.com",
GivenName: "Junsub",
FamilyName: "Lee",
},
{
Vin: "VCF1EBU2XPG011442",
DriverId: "9855e14c-22f6-438d-84ee-47a7be675773",
Email: "csimpson@ovloop.com",
GivenName: "Clea",
FamilyName: "Simpson",
},
},
}
services.GetDB().SetDriverEmails(&mock)
mocksmtp := smtpclient.MockSMTP{}
services.SetSMTP(&mocksmtp)
tests := []mo.DBHttpTest{
{
Name: "Good data",
Request: th.MakeTestRequest(
http.MethodGet,
"/customer_ota_emails",
common.CustomerOtaEmailsRequest{
VINs: []string{"VCF1ZBU28PG002114", "VCF1ZBU25PG003608", "VCF1EBU2XPG011442"},
EmailSubject: "Test Email Subject",
EmailBody: `Dear Ocean Owner:
Test Email Body
Sincerely,
Me`,
}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: ``,
},
}
mo.RunParamHttpTests(t, tests, handlers.HandleCustomerOtaEmails, "/customer_ota_emails", &mock)
}
type MockDriverEmails struct {
SelectResponse *[]common.DriverEmail
Error error
mo.DBMockHelper
}
func (d *MockDriverEmails) SelectByVINs(vins []string) ([]common.DriverEmail, error) {
return *d.SelectResponse, d.Error
}

View File

@@ -0,0 +1,59 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleDBCSignalsGetList godoc
// @Summary List API tokens
// @Description List API tokens. Requires API token permission
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Param dbc path string true "DBC hash"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.SignalDescWithECU}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /can_signals/{dbc} [get]
func HandleDBCSignalsGetList(w http.ResponseWriter, r *http.Request) {
options, err := clickhouse.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
params := httprouter.ParamsFromContext(r.Context())
dbc := params.ByName("dbc")
err = validator.GetValidator().Var(dbc, "required")
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
cl, err := services.GetClickhouseClient()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
logger.Error().Err(err).Msg("cannot get clickhouse client")
return
}
signals, count, err := cl.SelectDBCSignals(dbc, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{Data: signals, Total: count})
}

View File

@@ -0,0 +1,115 @@
package handlers_test
import (
"context"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/julienschmidt/httprouter"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"otaupdate/handlers"
"otaupdate/services"
"testing"
)
func TestHandleDBCSignalsGetList(t *testing.T) {
validQuery := "?limit=5&offset=2"
tests := map[string]struct {
q string
conn clickhouse.ConnInterface
expStatus int
expBody string
}{
"correct": {
q: validQuery,
conn: &clickhouse.MockConn{
ExpectedResult: []common.SignalDescWithECU{
{
ECUName: "ADAS",
SignalDesc: common.SignalDesc{
DBCHash: "hash",
MessageID: 2,
Name: "All_Signals_Sum_Check0",
Start: 0,
Length: 8,
IsBigEndian: true,
IsSigned: true,
IsMultiplexer: true,
IsMultiplexed: false,
MultiplexerValue: 5,
Offset: 0,
Scale: 3,
Min: 0,
Max: 20,
Unit: "sm",
Description: "desc",
ValueDescriptions: []string{"lt", "lg", "lk"},
ReceiverNodes: []string{"GW", "OO"},
DefaultValue: 0,
},
}, {
ECUName: "ICC",
SignalDesc: common.SignalDesc{
DBCHash: "hash",
MessageID: 20,
Name: "All_Signals_Sum_Check1",
Start: 8,
Length: 12,
IsBigEndian: false,
IsSigned: false,
IsMultiplexer: false,
IsMultiplexed: true,
MultiplexerValue: 7,
Offset: 8,
Scale: 31,
Min: 5,
Max: 12,
Unit: "kg",
Description: "desc 1",
ValueDescriptions: nil,
ReceiverNodes: nil,
DefaultValue: 10,
},
},
},
QueryRowtMock: func(ctx context.Context, query string, args ...interface{}) driver.Row {
return clickhouse.RowMock{RowResult: 5}
}},
expStatus: http.StatusOK,
expBody: `{"data":[{"dbc_hash":"hash","message_id":2,"name":"All_Signals_Sum_Check0","start":0,"length":8,"big_endian":true,"signed":true,"multiplexer":true,"multiplexed":false,"multiplexer_value":5,"offset":0,"scale":3,"min":0,"max":20,"unit":"sm","description":"desc","value_descriptions":["lt","lg","lk"],"receiver_nodes":["GW","OO"],"default_value":0,"ECUName":"","ecu_name":"ADAS"},{"dbc_hash":"hash","message_id":20,"name":"All_Signals_Sum_Check1","start":8,"length":12,"big_endian":false,"signed":false,"multiplexer":false,"multiplexed":true,"multiplexer_value":7,"offset":8,"scale":31,"min":5,"max":12,"unit":"kg","description":"desc 1","value_descriptions":null,"receiver_nodes":null,"default_value":10,"ECUName":"","ecu_name":"ICC"}]}`,
},
"failed_query": {
q: validQuery,
conn: &clickhouse.MockConn{ExpectedResult: someErr},
expStatus: http.StatusServiceUnavailable,
expBody: `{"message":"json: cannot unmarshal object into Go value of type []common.SignalDescWithECU","error":"Service Unavailable"}`,
},
"wrong limit": {
q: "?limit=-2",
expStatus: http.StatusBadRequest,
expBody: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
}
for tname, tt := range tests {
t.Run(tname, func(t *testing.T) {
services.SetClickhouseConn(tt.conn)
w := httptest.NewRecorder()
p := httprouter.Params{
{
Key: "dbc",
Value: "hash",
},
}
ctx := context.WithValue(context.Background(), httprouter.ParamsKey, p)
r := httptest.NewRequest(http.MethodGet, "http://example.com/can_signals/dbc"+tt.q, nil).
WithContext(ctx)
handlers.HandleDBCSignalsGetList(w, r)
assert.Equal(t, tt.expStatus, w.Code)
assert.Equal(t, tt.expBody, w.Body.String())
})
}
}

View File

@@ -0,0 +1,55 @@
package handlers
import (
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/utils"
// "github.com/fiskerinc/cloud-services/pkg/validator"
)
// HandleDigitalTwinSignal godoc
// @Summary List signals with updated timestamp
// @Description Returns list of state with last updated timestamp.
// @Accept json
// @Produce json
// @Param Api-Key header string false "<API token>"
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {object} []interface{}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /ditto/carstate [get]
func HandleDigitalTwinSignal(w http.ResponseWriter, r *http.Request) {
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
if limit < 1 || limit > 1000 {
limit = 1000
}
if offset < 1 {
offset = 0
}
// vin := r.URL.Query().Get("vin")
// if vin != "" {
// vins := strings.Split(vin, ",")
// for _, v := range vins {
// ok, err := validator.ValidateVINSimple(v)
// if !ok || err != nil {
// loggerdataresp.BadDataErrorResp(w, ErrInvalidVIN, http.StatusBadRequest)
// return
// }
// }
// }
conn := services.RedisClientPool().GetFromPool()
defer conn.Close()
data := cache.NewDigitalTwinTimestampState(conn).GetDigitalTwinSignals(offset, limit)
utils.RespJSON(w, http.StatusOK, data)
}

View File

@@ -0,0 +1,21 @@
package handlers
import (
"strings"
"otaupdate/docs"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
)
func InitSwaggerDoc() {
schemes := envtool.GetEnv("SWAGGER_SCHEMES", "https")
docs.SwaggerInfo.Title = "Fisker Inc OTA API"
docs.SwaggerInfo.Description = "Fisker Inc OTA portals APIs"
docs.SwaggerInfo.Version = "1.0"
docs.SwaggerInfo.Host = ""
docs.SwaggerInfo.BasePath = httphandlers.ServiceBaseURL
docs.SwaggerInfo.Schemes = strings.Split(schemes, ",")
}

View File

@@ -0,0 +1,100 @@
package handlers_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"otaupdate/handlers"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
)
const headerContentType = "Content-Type"
var swaggerHandler http.HandlerFunc
func init() {
handlers.InitSwaggerDoc()
swaggerHandler = httphandlers.GetSwaggerHandler()
}
func TestHandleSwaggerRedirect(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/api", nil)
req.RequestURI = req.URL.Path
recorder := httptest.NewRecorder()
swaggerHandler(recorder, req)
validateSwaggerRedirect(t, recorder)
}
func TestHandleSwagger(t *testing.T) {
req, _ := http.NewRequest("GET", "http://example.com/api/", nil)
req.RequestURI = req.URL.Path
recorder := httptest.NewRecorder()
swaggerHandler(recorder, req)
validateSwaggerRedirect(t, recorder)
}
func TestHandleSwaggerJSON(t *testing.T) {
contentType := "application/json; charset=utf-8"
req, _ := http.NewRequest("GET", "http://example.com/api/doc.json", nil)
req.RequestURI = req.URL.Path
recorder := httptest.NewRecorder()
swaggerHandler(recorder, req)
headers := recorder.Result().Header
if headers.Get(headerContentType) != contentType {
t.Errorf(testhelper.TestErrorTemplate, headerContentType, contentType, headers.Get(headerContentType))
}
var data map[string]interface{}
err := json.Unmarshal(recorder.Body.Bytes(), &data)
if err != nil {
t.Error(err)
}
}
func TestHandleSwaggerHTML(t *testing.T) {
contentType := "text/html; charset=utf-8"
htmlTitle := "<title>Swagger UI</title>"
req, _ := http.NewRequest("GET", "http://example.com/api/index.html", nil)
req.RequestURI = req.URL.Path
recorder := httptest.NewRecorder()
swaggerHandler(recorder, req)
headers := recorder.Result().Header
if headers.Get(headerContentType) != contentType {
t.Errorf(testhelper.TestErrorTemplate, headerContentType, contentType, headers.Get(headerContentType))
}
if !strings.Contains(recorder.Body.String(), htmlTitle) {
t.Errorf(testhelper.TestErrorTemplate, "HTML", htmlTitle, recorder.Body.String())
}
}
func validateSwaggerRedirect(t *testing.T, recorder *httptest.ResponseRecorder) {
if recorder.Code != http.StatusMovedPermanently {
t.Errorf(testhelper.TestErrorTemplate, "Status code", http.StatusMovedPermanently, recorder.Code)
}
u, err := url.Parse(recorder.Header().Get("location"))
if err != nil {
t.Error(err)
}
if u.Path != "/api/index.html" {
t.Errorf(testhelper.TestErrorTemplate, "Path", "/api/index.html", u.Path)
}
}

View File

@@ -0,0 +1,194 @@
package handlers
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
"github.com/pkg/errors"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleECUDTCGet godoc
// @Summary Get ECU DTCs for a specific vehicle
// @Description Get ECU diagnostic trouble codes (DTCs) for a specific vehicle within a given time range
// @Tags ECU
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin path string true "VIN"
// @Param ecu query string false "ECU"
// @Param trouble_code query string false "Trouble Code"
// @Param start_time query string false "Start time (RFC3339 format)"
// @Param end_time query string false "End time (RFC3339 format)"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Param order query string false "Sort on column with asc or desc"
// @Param decode query bool false "Return decoded dtc information"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.DTC_ECU} "List of DTC ECU data"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 404 {object} common.JSONError "Not found"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /dtcs/{vin} [get]
func HandleECUDTCGet(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
vin := params.ByName("vin")
queryParams := r.URL.Query()
ecu := queryParams.Get("ecu")
troubleCode := queryParams.Get("trouble_code")
startStr := queryParams.Get("start_time")
endStr := queryParams.Get("end_time")
decode, _ := strconv.ParseBool(queryParams.Get("decode"))
filter := bson.M{
"vin": vin}
if ecu != "" {
filter["ecu"] = ecu
}
if troubleCode != "" {
troubleCodeInt, err := strconv.ParseInt(troubleCode, 10, 64)
if err != nil {
http.Error(w, "Invalid trouble_code format, use int64", http.StatusBadRequest)
return
}
filter["dtc"] = troubleCodeInt
}
err := validator.GetValidator().Var(vin, "vin|vinsuffix")
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
var start time.Time
if startStr != "" {
start, err = time.Parse(time.RFC3339, startStr)
if err != nil {
http.Error(w, "Invalid start_time format, use RFC3339 format", http.StatusBadRequest)
return
}
}
var end time.Time
if endStr != "" {
end, err = time.Parse(time.RFC3339, endStr)
if err != nil {
http.Error(w, "Invalid end_time format, use RFC3339 format", http.StatusBadRequest)
return
}
}
if !start.IsZero() && !end.IsZero() {
filter["created_at"] = bson.M{
"$gte": start,
"$lte": end,
}
} else if !start.IsZero() {
filter["created_at"] = bson.M{
"$gte": start,
}
} else if !end.IsZero() {
filter["created_at"] = bson.M{
"$lte": end,
}
}
mongoOpts := options.Find()
query_params, err := orm.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
mongoOpts.SetLimit(int64(query_params.Limit))
if query_params.Order != "" {
mongoOpts.SetSort(bson.D{
{"created_at", -1}}) // Descending order for 'created_at'.
}
if query_params.Offset != 0 {
mongoOpts.SetSkip(int64(query_params.Offset))
}
mongo, err := services.GetMongoClient()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var total int64
if query_params.Offset == 0 {
total, err = mongo.Collection("dtcs").CountDocuments(ctx, filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
}
cursor, err := mongo.Collection("dtcs").Find(ctx, filter, mongoOpts)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
var dtcs []common.DTC_ECU
for cursor.Next(ctx) {
var result common.DTC_ECU
err := cursor.Decode(&result)
if err != nil {
logger.Warn().Msg(err.Error())
continue
}
if decode {
fetchDTCDataFromMongo(&result)
}
dtcs = append(dtcs, result)
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: dtcs,
Total: int(total),
})
}
func fetchDTCDataFromMongo(dtc *common.DTC_ECU) (err error) {
client, err := mongo.GetPDXMongoClient()
if err != nil {
err = errors.WithStack(err)
return
}
// Handle the dtc string, need to drop the first byte as its the status code
troubleCodeHex := fmt.Sprintf("%X", dtc.TroubleCode)
troubleCodeHex = strings.ToUpper(troubleCodeHex)
info, err := client.GetDTCDefinitionByHexString(troubleCodeHex, dtc.ECU)
if err != nil {
return
}
if info == nil {
logger.Warn().Msgf("Failed to find dtc code from ecu: %s troubleCodeHex: %s", dtc.ECU, troubleCodeHex)
}
dtc.Information = info
dtc.StatusByteDecode = dtc.DTCStatusByteMeaning()
return
}

View File

@@ -0,0 +1,83 @@
package handlers_test
import (
"testing"
)
func TestHandlers_HandleECUDTCGet(t *testing.T) {
/*
db := services.GetDB()
dtcs := []common.DTC_ECU{
{
ID: 196,
VIN: "1GNGC26RXXJ407648",
ECU: "AMP",
DTC: []byte{9, 81, 118, 19},
Epoch_usec: 1624485598,
},
{
ID: 197,
VIN: "1GNGC26RXXJ407648",
ECU: "AMP",
TroubleCode: 12,
DTC: []byte{9, 81, 118, 19},
Epoch_usec: 1624485598,
},
}
tests := map[string]struct {
vin string
ecu string
start string
end string
dtcs q.ECUInterface
expStatus int
expBody string
}{
"success": {
vin: "1GNGC26RXXJ407648",
ecu: "AMP",
start: "",
end: "",
dtcs: &mocks.MockEcuDtc{
SelectDTCECUResponse: dtcs,
},
expStatus: http.StatusOK,
expBody: `{"data":[{"id":196,"vin":"1GNGC26RXXJ407648","ecu_name":"AMP","dtc":"CVF2Ew==","trouble_code":0,"status_byte":0,"epoch_usec":1624485598},{"id":197,"vin":"1GNGC26RXXJ407648","ecu_name":"AMP","dtc":"CVF2Ew==","trouble_code":12,"status_byte":0,"epoch_usec":1624485598}]}`,
},
"invalid_vin": {
vin: "INVALID_VIN",
ecu: "AMP",
start: "",
end: "",
dtcs: &mocks.MockEcuDtc{},
expStatus: http.StatusBadRequest,
expBody: `{"message":"vin|vinsuffix vin|vinsuffix ","error":"Bad Request"}`,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
w := httptest.NewRecorder()
db.SetDTCECU(tt.dtcs)
p := httprouter.Params{
{"vin", tt.vin},
{"ecu", tt.ecu},
{"trouble_code", "12"},
{"start_time", tt.start},
{"end_time", tt.end},
}
ctx := context.WithValue(context.Background(), httprouter.ParamsKey, p)
request := httptest.NewRequest(http.MethodGet, "http://example.com/dtcs/"+tt.vin, nil).
WithContext(ctx)
handlers.HandleECUDTCGet(w, request)
assert.Equal(t, tt.expStatus, w.Code)
assert.Equal(t, tt.expBody, w.Body.String())
})
}
*/
}

View File

@@ -0,0 +1,155 @@
package handlers
import (
"context"
"fmt"
"net/http"
"strings"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
ch "github.com/ClickHouse/clickhouse-go/v2"
"github.com/gorilla/schema"
"github.com/pkg/errors"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleECUStatsGetList godoc
// @Summary List API tokens
// @Description List API tokens. Requires API token permission
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param ecus query []string true "ECU names"
// @Param dbcs query []string true "DBC hashes"
// @Param vins query []string true "Array of VINs"
// @Param hours query int true "Past hours that must be included into the request"
// @Param min_zero_pct query float32 true "Minimum zero values percent"
// @Param min_out_of_range_pct query int true "Minimum out of range percent"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.ECUStat}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /ecu_stats [get]
func HandleECUStatsGetList(w http.ResponseWriter, r *http.Request) {
conn, err := services.GetClickhouseConn()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
logger.Error().Err(err).Msg("cannot get clickhouse client")
return
}
filter, err := parseStatsFilter(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
stats, err := getEcusStats(conn, filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{Data: stats})
}
func parseStatsFilter(r *http.Request) (common.StatsFilter, error) {
sch := schema.NewDecoder()
filter := common.StatsFilter{}
sch.SetAliasTag("json")
err := sch.Decode(&filter, r.URL.Query())
if err != nil {
return common.StatsFilter{}, errors.WithStack(err)
}
err = validator.GetValidator().Struct(filter)
if err != nil {
return common.StatsFilter{}, errors.WithStack(err)
}
return filter, nil
}
func getEcusStats(conn clickhouse.ConnInterface, filter common.StatsFilter) ([]common.ECUStat, error) {
var result []common.ECUStat
chCtx := ch.Context(context.Background(), ch.WithParameters(ch.Parameters{
"minOutOfRangePct": fmt.Sprint(filter.MinOutOfRangePct),
"minZeroPct": fmt.Sprint(filter.MinZeroPct),
"hours": fmt.Sprint(filter.Hours),
"vins": "['" + strings.Join(filter.VINs, "','") + "']",
"dbcs": "['" + strings.Join(filter.DBCs, "','") + "']",
"ecus": "['" + strings.Join(filter.ECUs, "','") + "']",
}))
if err := conn.Select(chCtx, &result, `select ecu_name,
sum(case when value_out_range_pct> {minOutOfRangePct:Float32} then 1 else 0 end) as signals_w_incorrect_values,
sum(case when zero_pct> {minZeroPct:Float32} then 1 else 0 end ) as signals_all_zero,
count(*) as number_of_ecu_signals,
sum(tot_cnt) as total_signal_records,
(signals_w_incorrect_values/number_of_ecu_signals) incorrect_val_signal_pct,
signals_all_zero/number_of_ecu_signals as zero_signals_pct
from ( select
case when aa.Name<>'' then aa.Name
when bb.signal_name<>'' then bb.signal_name
else null end as signal_name,
case when aa.ID<>'0' then aa.ID
when bb.message_id<>'0' then bb.message_id
else null end as can_id,
case when aa.ecu_name<>'' then aa.ecu_name
when dbcm.ecu_name<>'' then dbcm.ecu_name
else null end as ecu_name,
zero_count,value_out_range_cnt, tot_cnt,
case when tot_cnt <> 0 then value_out_range_pct else 0 end as value_out_range_pct,
case when tot_cnt <> 0 then zero_pct else 0 end as zero_pct
from
(/* check if signals are within dbc value range and if 0 on joined CAN signals and dbc*/
select Name, ID, signal_name, ecu_name,cycle_time_ns,sender_node,
sum(case when Value=0 then 1 else 0 end) as zero_count,
sum(case when a.Value<b.min or a.Value>b.max then 1 else 0 end) as value_out_range_cnt,
count(*) as tot_cnt,
value_out_range_cnt/tot_cnt as value_out_range_pct,
zero_count/tot_cnt as zero_pct
from vehicle_signal as a
inner join
(/* select dbc_messages and dbc_signals */
select b1.*, b2.message_id, b2.ecu_name, b2.cycle_time_ns, b2.sender_node
from dbc_signals as b1
inner join dbc_messages as b2
on b1.dbc_hash=b2.dbc_hash
and b1.message_id=b2.message_id
where b1.dbc_hash in {dbcs:Array(String)}
) as b
on a.Name=b.signal_name
where
a.Timestamp> (select max(Timestamp) from vehicle_signal where VIN in {vins:Array(String)}) - toIntervalHour({hours:UInt64})
and
a.VIN in {vins:Array(String)}
group by 1,2,3,4,5,6
) as aa
full outer join dbc_signals as bb
on aa.Name=bb.signal_name
and aa.ID=bb.message_id
inner join dbc_messages as dbcm
on bb.message_id=dbcm.message_id
where bb.dbc_hash in {dbcs:Array(String)} and dbcm.dbc_hash in {dbcs:Array(String)}
and ecu_name in {ecus:Array(String)})
group by ecu_name
order by zero_signals_pct desc, incorrect_val_signal_pct desc`); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,71 @@
package handlers_test
import (
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"otaupdate/handlers"
"otaupdate/services"
"testing"
)
func TestHandleECUStatsGetList(t *testing.T) {
validQuery := "?min_out_of_range_pct=10&min_zero_pct=0.009&hours=160&vins=TREXTEST7TUR9NXGC&vins=TREXTEST5T61T1BR2&dbcs=73583d63735b404f5209a71107c3d2174b0ab1ba40bd826b8cb69668598b0395&ecus=ADAS&ecus=ICC&ecus=TREX"
tests := map[string]struct {
q string
conn clickhouse.ConnInterface
expStatus int
expBody string
}{
"correct": {
q: validQuery,
conn: &clickhouse.MockConn{ExpectedResult: []common.ECUStat{
{
ECUName: "ADAS",
IncorrectValues: 1,
AllZero: 1,
ECUSignalsTotal: 231,
SignalsTotal: 2221,
IncorrectPercent: 0.04,
ZeroPercent: 0.003,
},
{
ECUName: "TREX",
IncorrectValues: 0,
AllZero: 0,
ECUSignalsTotal: 77,
SignalsTotal: 9789,
IncorrectPercent: 0.55,
ZeroPercent: 0.36,
},
}},
expStatus: http.StatusOK,
expBody: `{"data":[{"ecu_name":"ADAS","signals_w_incorrect_values":1,"signals_all_zero":1,"number_of_ecu_signals":231,"total_signal_records":2221,"incorrect_val_signal_pct":0.04,"zero_signals_pct":0.003},{"ecu_name":"TREX","signals_w_incorrect_values":0,"signals_all_zero":0,"number_of_ecu_signals":77,"total_signal_records":9789,"incorrect_val_signal_pct":0.55,"zero_signals_pct":0.36}]}`,
},
"failed_filter": {
q: "",
expStatus: http.StatusBadRequest,
expBody: `{"message":"MinOutOfRangePct required. MinZeroPct required. Hours required. VINs required. DBCs required. ECUs required","error":"Bad Request"}`,
},
"failed_query": {
q: validQuery,
conn: &clickhouse.MockConn{ExpectedResult: someErr},
expStatus: http.StatusServiceUnavailable,
expBody: `{"message":"json: cannot unmarshal object into Go value of type []common.ECUStat","error":"Service Unavailable"}`,
},
}
for tname, tt := range tests {
t.Run(tname, func(t *testing.T) {
services.SetClickhouseConn(tt.conn)
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "http://example.com/ecu_stats"+tt.q, nil)
handlers.HandleECUStatsGetList(w, r)
assert.Equal(t, tt.expStatus, w.Code)
assert.Equal(t, tt.expBody, w.Body.String())
})
}
}

View File

@@ -0,0 +1,165 @@
package handlers
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/julienschmidt/httprouter"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
ch "github.com/ClickHouse/clickhouse-go/v2"
"github.com/gorilla/schema"
"github.com/pkg/errors"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleVINECUStatsGetList godoc
// @Summary List API tokens
// @Description List API tokens. Requires API token permission
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param ecus query []string true "ECU names"
// @Param hours query int true "Past hours that must be included into the request"
// @Param min_zero_pct query float32 true "Minimum zero values percent"
// @Param min_out_of_range_pct query int true "Minimum out of range percent"
// @Param dbc path string true "DBC hash"
// @Param vin path string true "VIN"
// @Success 200 {object} common.JSONDBQueryResult{data=[]common.ECUStat}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /ecu_stats/{vin}/{dbc} [get]
func HandleVINECUStatsGetList(w http.ResponseWriter, r *http.Request) {
conn, err := services.GetClickhouseConn()
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
logger.Error().Err(err).Msg("cannot get clickhouse client")
return
}
filter, err := parseVINStatsFilter(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
stats, err := getEcusVINStats(conn, filter)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{Data: stats})
}
func parseVINStatsFilter(r *http.Request) (common.VINStatsFilter, error) {
sch := schema.NewDecoder()
filter := common.VINStatsFilter{}
sch.SetAliasTag("json")
err := sch.Decode(&filter, r.URL.Query())
if err != nil {
return common.VINStatsFilter{}, errors.WithStack(err)
}
params := httprouter.ParamsFromContext(r.Context())
filter.VIN = params.ByName("vin")
filter.DBC = params.ByName("dbc")
err = validator.GetValidator().Struct(filter)
if err != nil {
return common.VINStatsFilter{}, errors.WithStack(err)
}
return filter, nil
}
func getEcusVINStats(conn clickhouse.ConnInterface, filter common.VINStatsFilter) ([]common.ECUStat, error) {
var result []common.ECUStat
chCtx := ch.Context(context.Background(), ch.WithParameters(ch.Parameters{
"minOutOfRangePct": fmt.Sprint(filter.MinOutOfRangePct),
"minZeroPct": fmt.Sprint(filter.MinZeroPct),
"hours": fmt.Sprint(filter.Hours),
"vin": filter.VIN,
"dbc": filter.DBC,
"ecus": "['" + strings.Join(filter.ECUs, "','") + "']",
}))
if err := conn.Select(chCtx, &result, `
select ecu_name,
sum(case when value_out_range_pct>{minOutOfRangePct:Float32} then 1 else 0 end) as signals_w_incorrect_values,
sum(case when zero_pct>{minZeroPct:Float32} then 1 else 0 end ) as signals_all_zero,
count(*) as number_of_ecu_signals,
sum(tot_cnt) as total_signal_records,
(signals_w_incorrect_values/number_of_ecu_signals) incorrect_val_signal_pct,
signals_all_zero/number_of_ecu_signals as zero_signals_pct
from
(/* add missing signals and ecus from dbc as full outer join and generate per a CAN signal stats */
select
case when aa.Name<>'' then aa.Name
when bb.signal_name<>'' then bb.signal_name
else null end as signal_name,
case when aa.ID<>'0' then aa.ID
when bb.message_id<>'0' then bb.message_id
else null end as can_id,
case when aa.ecu_name<>'' then aa.ecu_name
when dbcm.ecu_name<>'' then dbcm.ecu_name
else null end as ecu_name,
zero_count,value_out_range_cnt, tot_cnt,
case when tot_cnt <> 0 then value_out_range_pct else 0 end as value_out_range_pct,
case when tot_cnt <> 0 then zero_pct else 0 end as zero_pct
from
(/* check if signals are within dbc value range and if 0 on joined CAN signals and dbc*/
select Name, ID, signal_name, ecu_name,cycle_time_ns,sender_node,
sum(case when Value=0 then 1 else 0 end) as zero_count,
sum(case when a.Value<b.min or a.Value>b.max then 1 else 0 end) as value_out_range_cnt,
count(*) as tot_cnt,
value_out_range_cnt/tot_cnt as value_out_range_pct,
zero_count/tot_cnt as zero_pct
from vehicle_signal as a
inner join
(/* select dbc_messages and dbc_signals */
select b1.*, b2.message_id, b2.ecu_name, b2.cycle_time_ns, b2.sender_node
from dbc_signals as b1
inner join dbc_messages as b2
on b1.dbc_hash=b2.dbc_hash
and b1.message_id=b2.message_id
where b1.dbc_hash = {dbc:String}
) as b
on a.Name=b.signal_name
where
a.Timestamp> (select max(Timestamp) from vehicle_signal where VIN = {vin:String}) - toIntervalHour({hours:UInt64})
and
a.VIN = {vin:String}
group by 1,2,3,4,5,6
) as aa
full outer join dbc_signals as bb
on aa.Name=bb.signal_name
and aa.ID=bb.message_id
inner join dbc_messages as dbcm
on bb.message_id=dbcm.message_id
where bb.dbc_hash = {dbc:String} and dbcm.dbc_hash = {dbc:String}
and ecu_name in {ecus:Array(String)}
)
group by ecu_name
order by zero_signals_pct desc, incorrect_val_signal_pct desc
`); err != nil {
return result, err
}
return result, nil
}

View File

@@ -0,0 +1,82 @@
package handlers_test
import (
"context"
"github.com/fiskerinc/cloud-services/pkg/clickhouse"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/julienschmidt/httprouter"
"github.com/stretchr/testify/assert"
"net/http"
"net/http/httptest"
"otaupdate/handlers"
"otaupdate/services"
"testing"
)
func TestHandleVINECUStatsGetList(t *testing.T) {
validQuery := "?min_out_of_range_pct=10&min_zero_pct=0.009&hours=160&ecus=ADAS&ecus=ICC&ecus=TREX"
validVin := "TREXTEST7TUR9NXGC"
validDBC := "73583d63735b404f5209a71107c3d2174b0ab1ba40bd826b8cb69668598b0395"
tests := map[string]struct {
q string
conn clickhouse.ConnInterface
expStatus int
expBody string
}{
"correct": {
q: validQuery,
conn: &clickhouse.MockConn{ExpectedResult: []common.ECUStat{
{
ECUName: "ADAS",
IncorrectValues: 1,
AllZero: 1,
ECUSignalsTotal: 231,
SignalsTotal: 2221,
IncorrectPercent: 0.04,
ZeroPercent: 0.003,
},
{
ECUName: "TREX",
IncorrectValues: 0,
AllZero: 0,
ECUSignalsTotal: 77,
SignalsTotal: 9789,
IncorrectPercent: 0.55,
ZeroPercent: 0.36,
},
}},
expStatus: http.StatusOK,
expBody: `{"data":[{"ecu_name":"ADAS","signals_w_incorrect_values":1,"signals_all_zero":1,"number_of_ecu_signals":231,"total_signal_records":2221,"incorrect_val_signal_pct":0.04,"zero_signals_pct":0.003},{"ecu_name":"TREX","signals_w_incorrect_values":0,"signals_all_zero":0,"number_of_ecu_signals":77,"total_signal_records":9789,"incorrect_val_signal_pct":0.55,"zero_signals_pct":0.36}]}`,
},
"failed_filter": {
q: "",
expStatus: http.StatusBadRequest,
expBody: `{"message":"MinOutOfRangePct required. MinZeroPct required. Hours required. ECUs required","error":"Bad Request"}`,
},
"failed_query": {
q: validQuery,
conn: &clickhouse.MockConn{ExpectedResult: someErr},
expStatus: http.StatusServiceUnavailable,
expBody: `{"message":"json: cannot unmarshal object into Go value of type []common.ECUStat","error":"Service Unavailable"}`,
},
}
for tname, tt := range tests {
t.Run(tname, func(t *testing.T) {
services.SetClickhouseConn(tt.conn)
w := httptest.NewRecorder()
p := httprouter.Params{
{Key: "vin", Value: validVin},
{Key: "dbc", Value: validDBC},
}
ctx := context.WithValue(context.Background(), httprouter.ParamsKey, p)
r := httptest.NewRequest(http.MethodGet, "http://example.com/ecu_stats/vin/dbc"+tt.q, nil).
WithContext(ctx)
handlers.HandleVINECUStatsGetList(w, r)
assert.Equal(t, tt.expStatus, w.Code)
assert.Equal(t, tt.expBody, w.Body.String())
})
}
}

View File

@@ -0,0 +1,12 @@
package handlers
import (
"github.com/pkg/errors"
)
var ErrInvalidVIN = errors.New("invalid VIN")
var ErrMissingVIN = errors.New("missing VIN")
var ErrInvalidType = errors.New("invalid object type")
var ErrInvalidURLParams = errors.New("missing URL parameters")

View File

@@ -0,0 +1,46 @@
package handlers
import (
"fmt"
"net/http"
)
// HandleExperiment godoc
// @Summary Testing msg preview
// @Description Blank
// @Accept json
// @Produce json
// @Param id query string true "ID of request"
// @Router /experiment [get]
func HandleExperiment(w http.ResponseWriter, r *http.Request) {
// Get the "id" query parameter
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "missing id parameter", http.StatusBadRequest)
return
}
// Build Open Graph tags dynamically
title := fmt.Sprintf("Page for ID %s", id)
url := fmt.Sprintf("https://dev-gw.cloud.fiskerinc.com/ota_update/expirment?id=%s", id)
image := "https://www.google.com/url?sa=i&url=https%3A%2F%2Fulife.vpul.upenn.edu%2Fcareerservices%2Fblog%2F2010%2F11%2F12%2Fprofessionalism-and-the-pre-health-student-beyond-please-and-thank-you%2Ffunny-cat-green-avacado%2F&psig=AOvVaw3bK13MXk_hL91SyLrmdrMS&ust=1755886127191000&source=images&cd=vfe&opi=89978449&ved=0CBYQjRxqFwoTCODu-du_nI8DFQAAAAAdAAAAABAE"
// Write headers
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>%s</title>
<meta property="og:title" content="%s" />
<meta property="og:type" content="website" />
<meta property="og:url" content="%s" />
<meta property="og:image" content="%s" />
<meta property="og:description" content="This is the Open Graph preview for ID %s" />
</head>
<body>
<h1>Open Graph Page for %s</h1>
<p>Preview metadata has been set in the HTML headers.</p>
</body>
</html>`, title, title, url, image, id, id)
}

View File

@@ -0,0 +1,472 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"runtime/debug"
"strings"
"time"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/redisv2"
"github.com/fiskerinc/cloud-services/pkg/security"
"github.com/fiskerinc/cloud-services/pkg/smtpclient"
"github.com/pkg/errors"
)
type logCollector struct {
messages []string
startTime time.Time
}
func newLogCollector() *logCollector {
return &logCollector{
messages: make([]string, 0),
startTime: time.Now(),
}
}
func (lc *logCollector) add(message string) {
timestamp := time.Now().Format("15:04:05.000")
lc.messages = append(lc.messages, fmt.Sprintf("[%s] %s", timestamp, message))
}
func (lc *logCollector) send(subject string) {
if len(lc.messages) == 0 {
return
}
duration := time.Since(lc.startTime)
body := fmt.Sprintf("Request Duration: %v\n\nLogs:\n%s", duration, strings.Join(lc.messages, "\n"))
smtp := smtpclient.NewSMTP("email-smtp.us-west-2.amazonaws.com", 587)
smtp.Auth("AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY")
to := []string{"marner@ovloop.com", "padamsen@ovloop.com"}
err := smtp.Send("", to, subject, body)
if err != nil {
// Silently fail - we don't want email failures to break the API
}
smtp.Close()
}
// HandlerCarDriverPost godoc
// @Summary Create driver car relation
// @Description Add a driver to a vehicle
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body VehicleDriverAddInput true "User INFO"
// @Router /drivers/add_external [post]
func HandleVehicleExternalDriverAdd(w http.ResponseWriter, r *http.Request) {
logs := newLogCollector()
defer func() {
subject := fmt.Sprintf("[OTA UPDATE] External Driver Add Request - %s", time.Now().Format("2006-01-02 15:04:05"))
logs.send(subject)
}()
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request received\nEndpoint: /drivers/add_external\nMethod: %s\nRemoteAddr: %s\nUserAgent: %s", r.Method, r.RemoteAddr, r.UserAgent()))
// Log request headers for debugging
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request headers\nContent-Type: %s\nContent-Length: %s\nAuthorization: %s\nApi-Key: %s",
r.Header.Get("Content-Type"), r.Header.Get("Content-Length"), r.Header.Get("Authorization"), r.Header.Get("Api-Key")))
vdai := VehicleDriverAddInput{}
err := json.NewDecoder(r.Body).Decode(&vdai)
if err != nil {
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Failed to decode request body\nError: %v\nStack Trace: %s", err, string(debug.Stack())))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Returning bad request response\nStatus Code: %d", http.StatusBadRequest))
return
}
return
}
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request data decoded successfully\nUserID: %s\nSource: %s\nVIN: %s\nFirstName: %s\nLastName: %s\nCallbackURL: %s",
vdai.UserID, vdai.Source, vdai.PairingInfo.VIN, vdai.Person.FirstName, vdai.Person.LastName, vdai.CallbackURL))
// If there is an error, than we did not succesfuly beign pairng
err = VehicleExternalDriverAdd(vdai, logs)
if err != nil {
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: VehicleExternalDriverAdd failed\nError: %v\nStack Trace: %s\nUserID: %s\nVIN: %s",
err, string(debug.Stack()), vdai.UserID, vdai.PairingInfo.VIN))
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Returning internal server error response\nStatus Code: %d\nError Message: %s",
http.StatusInternalServerError, err.Error()))
return
}
logs.add(fmt.Sprintf("HandleVehicleExternalDriverAdd: Request completed successfully\nUserID: %s\nVIN: %s", vdai.UserID, vdai.PairingInfo.VIN))
}
func VehicleExternalDriverAdd(vdai VehicleDriverAddInput, logs *logCollector) (err error) {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Starting driver addition process\nUserID: %s\nSource: %s\nVIN: %s", vdai.UserID, vdai.Source, vdai.PairingInfo.VIN))
// TODO: CHECK CAR IS ON
// TODO: Check that the salt or session matches
// Check that the QR code is valid and from the car
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Validating connection info\nVIN: %s\nSalt: %s\nSessionID: %s",
vdai.PairingInfo.VIN, vdai.PairingInfo.Salt, vdai.PairingInfo.SessionID))
err = ValidateConnectionInfo(vdai.PairingInfo, logs)
if err != nil {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Connection info validation failed\nError: %v\nStack Trace: %s\nVIN: %s\nSalt: %s\nSessionID: %s",
err, string(debug.Stack()), vdai.PairingInfo.VIN, vdai.PairingInfo.Salt, vdai.PairingInfo.SessionID))
return
}
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Connection info validation successful\nVIN: %s", vdai.PairingInfo.VIN))
// Try to Create an account for this user. If they already have an account, that is fine as well
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Adding new driver to database\nUserID: %s\nSource: %s", vdai.UserID, vdai.Source))
userID, err := addNewDriverDatabase(vdai.UserID, vdai.Source, logs)
if err != nil {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Failed to add new driver to database\nError: %v\nStack Trace: %s\nUserID: %s\nSource: %s",
err, string(debug.Stack()), vdai.UserID, vdai.Source))
return
}
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Driver added to database successfully\nUserID: %s\nSource: %s\nFiskerUserID: %s", vdai.UserID, vdai.Source, userID))
// So we now have a user, we can now begin the car pairing
// Create car to driver entry
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Creating car to driver relationship\nVIN: %s\nFiskerUserID: %s", vdai.PairingInfo.VIN, userID))
cars := services.GetDB().GetCars()
relation, err := cars.AddDriver(&common.Car{VIN: vdai.PairingInfo.VIN}, &common.Driver{ID: userID}, "OWNER") // Don't know if there is any other role
if err != nil {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Failed to create car to driver relationship\nError: %v\nStack Trace: %s\nVIN: %s\nFiskerUserID: %s\nRole: %s",
err, string(debug.Stack()), vdai.PairingInfo.VIN, userID, "OWNER"))
return
}
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Car to driver relationship created successfully\nVIN: %s\nDriverID: %s\nDriverRole: %s", relation.VIN, relation.DriverID, relation.DriverRole))
// Send HMI command
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Getting Redis connection from pool\nVIN: %s\nDriverID: %s", relation.VIN, relation.DriverID))
conn := services.RedisClientPool().GetFromPool()
defer conn.Close()
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Preparing HMI message\nVIN: %s\nDriverID: %s\nDriverRole: %s\nFirstName: %s\nLastName: %s",
relation.VIN, relation.DriverID, relation.DriverRole, vdai.Person.FirstName, vdai.Person.LastName))
// TODO: Add settings HERE
err = conn.SafePublishMessage(
common.HMI.Key(relation.VIN),
common.Message{
Handler: "profile_new",
Data: common.JSONHMIProfile{
DriverID: relation.DriverID,
DriverRole: relation.DriverRole,
User: common.UserProfile{
FirstName: vdai.Person.FirstName,
LastName: vdai.Person.LastName,
},
Settings: make([]common.CarSetting, 0),
Subscriptions: make([]common.Subscription, 0),
},
},
)
if err != nil {
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Failed to publish HMI message\nError: %v\nStack Trace: %s\nVIN: %s\nDriverID: %s\nHMIKey: %s",
err, string(debug.Stack()), relation.VIN, relation.DriverID, common.HMI.Key(relation.VIN)))
return
}
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: HMI message published successfully\nVIN: %s\nDriverID: %s\nHMIKey: %s", relation.VIN, relation.DriverID, common.HMI.Key(relation.VIN)))
logs.add(fmt.Sprintf("VehicleExternalDriverAdd: Driver addition process completed successfully\nUserID: %s\nVIN: %s\nFiskerUserID: %s", vdai.UserID, vdai.PairingInfo.VIN, userID))
return
}
func ValidateConnectionInfo(pi PairingInfo, logs *logCollector) (err error) {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Starting validation\nVIN: %s\nSalt: %s\nSessionID: %s", pi.VIN, pi.Salt, pi.SessionID))
salter, err := security.NewSalter(pi.VIN)
if err != nil {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Failed to create salter\nError: %v\nStack Trace: %s\nVIN: %s", err, string(debug.Stack()), pi.VIN))
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Salter created successfully\nVIN: %s", pi.VIN))
clientPool := services.GetRedisV2Client()
switch {
case pi.SessionID != "":
logs.add(fmt.Sprintf("ValidateConnectionInfo: Using session ID validation\nVIN: %s\nSessionID: %s", pi.VIN, pi.SessionID))
err = salter.ValidateSessionID(pi.SessionID)
if err != nil {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Session ID validation failed\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s",
err, string(debug.Stack()), pi.VIN, pi.SessionID))
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Session ID validation successful\nVIN: %s\nSessionID: %s", pi.VIN, pi.SessionID))
err = checkSession(clientPool, pi.VIN, pi.SessionID, logs)
if err != nil {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Session check failed\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s",
err, string(debug.Stack()), pi.VIN, pi.SessionID))
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Session validation completed successfully\nVIN: %s\nSessionID: %s", pi.VIN, pi.SessionID))
case pi.Salt != "":
logs.add(fmt.Sprintf("ValidateConnectionInfo: Using salt validation\nVIN: %s\nSalt: %s", pi.VIN, pi.Salt))
err = checkSession(clientPool, pi.VIN, pi.Salt, logs)
if err != nil {
logs.add(fmt.Sprintf("ValidateConnectionInfo: Salt check failed\nError: %v\nStack Trace: %s\nVIN: %s\nSalt: %s",
err, string(debug.Stack()), pi.VIN, pi.Salt))
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Salt validation completed successfully\nVIN: %s\nSalt: %s", pi.VIN, pi.Salt))
//sessionID = salter.GenerateSessionID(pi.VIN, pi.Salt)
default:
logs.add(fmt.Sprintf("ValidateConnectionInfo: Missing both salt and session ID\nError: %v\nStack Trace: %s\nVIN: %s\nSalt: %s\nSessionID: %s",
ErrMissingSaltAndSessionID, string(debug.Stack()), pi.VIN, pi.Salt, pi.SessionID))
err = ErrMissingSaltAndSessionID
return
}
logs.add(fmt.Sprintf("ValidateConnectionInfo: Connection info validation completed successfully\nVIN: %s", pi.VIN))
return
}
func addNewDriverDatabase(externalID, source string, logs *logCollector) (userID string, err error) {
logs.add(fmt.Sprintf("addNewDriverDatabase: Starting database operation\nExternalID: %s\nSource: %s", externalID, source))
// This complicated query does the following things
// Checks to see if the external user already exists. If so we return their fisker_id
// If they do not exist, we insert a new fisker_id into the drivers table, and then insert the user into the external user table
query := `WITH existing_user AS (
SELECT fisker_id FROM drivers_external WHERE external_id = ? AND source = ?
), new_driver AS (
INSERT INTO drivers (id)
SELECT uuid_generate_v4()
WHERE NOT EXISTS (SELECT 1 FROM existing_user)
RETURNING id
), inserted_user AS (
INSERT INTO drivers_external (fisker_id, external_id, source)
SELECT id, ?, ? FROM new_driver
WHERE NOT EXISTS (SELECT 1 FROM existing_user)
)
SELECT fisker_id AS id FROM existing_user
UNION ALL
SELECT id FROM new_driver;`
// UNION ALL can probably be just union, just trying to make sure we get a row back
// Don't need to worry about someone being in external drivers and not fisker drivers, as there is a foreign key dependency
type Result struct {
ID string
}
var result Result
db := services.GetDB().GetDBClient()
logs.add(fmt.Sprintf("addNewDriverDatabase: Executing database query\nExternalID: %s\nSource: %s", externalID, source))
_, err = db.GetConn().QueryOne(&result, query, externalID, source, externalID, source)
if err != nil {
logs.add(fmt.Sprintf("addNewDriverDatabase: Database query failed\nError: %v\nStack Trace: %s\nExternalID: %s\nSource: %s",
err, string(debug.Stack()), externalID, source))
return
}
logs.add(fmt.Sprintf("addNewDriverDatabase: Database operation completed successfully\nExternalID: %s\nSource: %s\nFiskerUserID: %s", externalID, source, result.ID))
return result.ID, err
}
// TODO: Add validation to struct
type VehicleDriverAddInput struct {
UserID string `json:"user_id"` // However the user wants to be placed in
Source string `json:"source"` // Ideally from the key or token that is used to access this route, security wise
PairingInfo PairingInfo `json:"pairing_info"`
Person UserInfo `json:"user_info"`
CallbackURL string `json:"callback_url"` // Where to send the BLE key when pairing is done
}
type PairingInfo struct {
VIN string `json:"vin"`
Salt string `json:"salt"` // either salt or session is required
SessionID string `json:"session_id"`
}
type UserInfo struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
type VehicleDriverAddResponse struct {
AccessAllowed bool `json:"access_allowed"` // True if the user provided the correct QR code data to connect with the car
Error error `json:"error,omitempty"`
}
// HandleExternalDriverDelete godoc
// @Summary Remove driver from DB
// @Description Remove a drivers profile completely
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body VehicleDriverAddInput true "User INFO"
// @Router /drivers/remove_external [delete]
func HandleExternalDriverDelete(w http.ResponseWriter, r *http.Request) {
logs := newLogCollector()
defer func() {
subject := fmt.Sprintf("[OTA UPDATE] External Driver Delete Request - %s", time.Now().Format("2006-01-02 15:04:05"))
logs.send(subject)
}()
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request received\nEndpoint: /drivers/remove_external\nMethod: %s\nRemoteAddr: %s\nUserAgent: %s", r.Method, r.RemoteAddr, r.UserAgent()))
// Log request headers for debugging
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request headers\nContent-Type: %s\nContent-Length: %s\nAuthorization: %s\nApi-Key: %s",
r.Header.Get("Content-Type"), r.Header.Get("Content-Length"), r.Header.Get("Authorization"), r.Header.Get("Api-Key")))
vrddi := ExternalDriverDeleteInput{}
err := json.NewDecoder(r.Body).Decode(&vrddi)
if err != nil {
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Failed to decode request body\nError: %v\nStack Trace: %s", err, string(debug.Stack())))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Returning bad request response\nStatus Code: %d", http.StatusBadRequest))
return
}
return
}
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request data decoded successfully\nUserID: %s\nSource: %s", vrddi.UserID, vrddi.Source))
err = ExternalDriverDelete(vrddi, logs)
if err != nil {
logs.add(fmt.Sprintf("HandleExternalDriverDelete: ExternalDriverDelete failed\nError: %v\nStack Trace: %s\nUserID: %s\nSource: %s",
err, string(debug.Stack()), vrddi.UserID, vrddi.Source))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusInternalServerError) {
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Returning internal server error response\nStatus Code: %d\nError Message: %s",
http.StatusInternalServerError, err.Error()))
return
}
return
}
logs.add(fmt.Sprintf("HandleExternalDriverDelete: Request completed successfully\nUserID: %s\nSource: %s", vrddi.UserID, vrddi.Source))
}
// Delete an external driver from the database
func ExternalDriverDelete(eddi ExternalDriverDeleteInput, logs *logCollector) (err error) {
logs.add(fmt.Sprintf("ExternalDriverDelete: Starting driver deletion process\nUserID: %s\nSource: %s", eddi.UserID, eddi.Source))
query := `WITH to_delete AS (
SELECT fisker_id FROM drivers_external WHERE external_id = ? AND source = ?
)
DELETE FROM drivers
WHERE id IN (SELECT fisker_id FROM to_delete)`
logs.add(fmt.Sprintf("ExternalDriverDelete: Executing database deletion query\nUserID: %s\nSource: %s", eddi.UserID, eddi.Source))
db := services.GetDB().GetDBClient()
_, err = db.GetConn().Exec(query, eddi.UserID, eddi.Source)
if err != nil {
logs.add(fmt.Sprintf("ExternalDriverDelete: Database deletion failed\nError: %v\nStack Trace: %s\nUserID: %s\nSource: %s",
err, string(debug.Stack()), eddi.UserID, eddi.Source))
return
}
logs.add(fmt.Sprintf("ExternalDriverDelete: Driver deletion completed successfully\nUserID: %s\nSource: %s", eddi.UserID, eddi.Source))
return
}
type ExternalDriverDeleteInput struct {
UserID string `json:"user_id"` // However the user wants to be placed in
Source string `json:"source"` // Ideally from the key or token that is used to access this route, security wise
}
// Go here, and add function to remove a car driver relationship for an external driver
// // HandleVehicleExternalDriverDelete godoc
// // @Summary Remove driver from DB
// // @Description Remove a drivers profile completely
// // @Accept json
// // @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// // @Param Api-Key header string false "<API token>"
// // @Param data body VehicleDriverAddInput true "User INFO"
// // @Router /drivers/remove_external [delete]
// func HandleVehicleExternalDriverVehicleRemove(w http.ResponseWriter, r *http.Request) {
// vrddi := VehicleExternalDriverDeleteInput{}
// err := json.NewDecoder(r.Body).Decode(&vrddi)
// if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
// return
// }
// err = VehicleExternalDriverRemove(vrddi)
// if loggerdataresp.BadDataErrorResp(w, err, http.StatusInternalServerError) {
// return
// }
// }
// func VehicleExternalDriverRemove(vrddi VehicleExternalDriverDeleteInput) (err error) {
// query := ``
// return
// }
// type VehicleExternalDriverDeleteInput struct{
// UserID string `json:"user_id"` // However the user wants to be placed in
// Source string `json:"source"` // Ideally from the key or token that is used to access this route, security wise
// }
func checkSession(redisClient *redisv2.Connection, vin string, sessionID string, logs *logCollector) error {
logs.add(fmt.Sprintf("checkSession: Starting session validation\nVIN: %s\nSessionID: %s", vin, sessionID))
if sessionID == "" {
logs.add(fmt.Sprintf("checkSession: Session ID is empty\nError: %v\nStack Trace: %s\nVIN: %s", ErrMissingSaltAndSessionID, string(debug.Stack()), vin))
return ErrMissingSaltAndSessionID
}
logs.add(fmt.Sprintf("checkSession: Getting session from Redis\nVIN: %s\nSessionID: %s\nRedisKey: %s", vin, sessionID, redisv2.HMISessionKey(vin)))
redisResponse := redisClient.Client.Get(context.Background(), redisv2.HMISessionKey(vin))
session, err := redisResponse.Result()
if err != nil {
logs.add(fmt.Sprintf("checkSession: Failed to get session from Redis\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s\nRedisKey: %s",
err, string(debug.Stack()), vin, sessionID, redisv2.HMISessionKey(vin)))
return err
}
logs.add(fmt.Sprintf("checkSession: Retrieved session from Redis\nVIN: %s\nSessionID: %s\nRedisSession: %s", vin, sessionID, session))
if session != sessionID {
logs.add(fmt.Sprintf("checkSession: Session mismatch detected\nError: %v\nStack Trace: %s\nVIN: %s\nSessionID: %s\nRedisSession: %s",
ErrSessionMismatch, string(debug.Stack()), vin, sessionID, session))
return ErrSessionMismatch
}
logs.add(fmt.Sprintf("checkSession: Session validation completed successfully\nVIN: %s\nSessionID: %s", vin, sessionID))
return nil
}
var ErrSessionMismatch = errors.New("sessions do not match")
var ErrMissingSaltAndSessionID = errors.New("request missing salt and sessionID")

View File

@@ -0,0 +1,141 @@
package handlers
import (
"errors"
"net/http"
"otaupdate/services"
"sort"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"github.com/fiskerinc/cloud-services/pkg/validator"
)
var apiCreateToken string = envtool.GetEnv("MIGRATE_CREATE_TOKEN", "")
// HandleFlashpackVersionAdd godoc
// @Summary Add a flashpack version
// @Description Add a flashpack version
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body common.CarFlashpackVersionAddRequest true "Mappings between ECU versions and a flashpack number"
// @Success 200 {object} common.JSONDBQueryResult "Created flashpack ecu mapping result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /flashpack_version [post]
func HandleFlashpackVersionAdd(w http.ResponseWriter, r *http.Request) {
var req common.CarFlashpackVersionAddRequest
err := httphandlers.ParseRequest(r, &req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
if len(req.ECUVersions) < 1 {
loggerdataresp.BadDataErrorResp(w, errors.New("CarECUName and CarECUVersion required"), http.StatusBadRequest)
return
}
// Include previous flashpack mappings
previousMappings, err := services.GetDB().GetCars().GetCarFlashpackVersionMappingsByModelTrim(req.CarModel, req.CarTrim, nil)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
// the flashpacks are stored in the database as strings, so we have to sort them numerically
// in descending order by year and flashpack
sort.Slice(previousMappings, func(i, j int) bool {
iFp, _ := strconv.ParseFloat(previousMappings[i].Flashpack, 64) // guaranteed numeric
jFp, _ := strconv.ParseFloat(previousMappings[j].Flashpack, 64) // guaranteed numeric
iYear := previousMappings[i].CarYear
jYear := previousMappings[j].CarYear
if iYear == jYear {
return iFp > jFp
} else {
return iYear > jYear
}
})
// Put all the mappings into one array, still in descending order by flashpack number
// only include the ones that are less than or equal to the new flashpack number being added
mappings := []common.CarFlashpackVersion{}
for _, v := range req.ECUVersions {
mappings = append(mappings, common.CarFlashpackVersion{
CarECUName: v.CarECUName,
CarECUVersion: v.CarECUVersion,
Flashpack: req.Flashpack,
CarModel: req.CarModel,
CarTrim: req.CarTrim,
CarYear: req.CarYear,
})
}
for _, m := range previousMappings {
reqFp, _ := strconv.ParseFloat(req.Flashpack, 64) // already validated as numeric
mFp, _ := strconv.ParseFloat(m.Flashpack, 64) // already validated as numeric
if (m.CarYear < req.CarYear) ||
(m.CarYear == req.CarYear && mFp <= reqFp) {
mappings = append(mappings, m)
}
}
// Put the mappings in a map by ecu name
// There can be more than one ECU version for an ECU for a flashpack
var newMappings = make(map[string][]common.CarFlashpackVersion)
for _, m := range mappings {
// Only include the mapping if it is one of the latest
latestVersionMappings, ok := newMappings[m.CarECUName]
// Include multiple versions for the same ecu and flashpack number
if (ok && m.Flashpack == latestVersionMappings[0].Flashpack) || !ok {
newMappings[m.CarECUName] = append(newMappings[m.CarECUName], m)
}
}
// Flatten the map into an array
var newMappingsArray []common.CarFlashpackVersion
for _, m := range newMappings {
newMappingsArray = append(newMappingsArray, m...)
}
// Apply the new flashpack number to all the mappings to be inserted
for i := range newMappingsArray {
newMappingsArray[i].Flashpack = req.Flashpack
newMappingsArray[i].CarYear = req.CarYear
}
err = services.GetDB().GetCars().AddCarFlashpackVersionMappings(newMappingsArray)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
// Also add to other environments, as required
for _, targetURL := range targetURLS {
if !validator.ValidateURL(targetURL) || apiCreateToken == "" {
break // No URL in MANIFEST_MIGRATE_URLS
}
otaService := services.NewOtaService(targetURL, apiCreateToken)
resp, err := otaService.FlashpackVersionAdd(req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
utils.ForwardResponse(w, resp)
}
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "Created",
})
}

View File

@@ -0,0 +1,74 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"testing"
m "github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleFlashpackVersionAdd(t *testing.T) {
// mock := mo.MockCars{}
// services.GetDB().SetCars(&mock)
tests := []mo.DBHttpTest{
{
Name: "Bad data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/flashpack_version", m.CarFlashpackVersionAddRequest{
ECUVersions: []m.ECUVersionRequest{
{
CarECUName: "ADAS",
CarECUVersion: "ADASVersion",
},
{
CarECUName: "RV",
CarECUVersion: "RVVersion",
},
},
Flashpack: "41.14",
CarModel: "Ocean",
CarTrim: "Base",
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"CarYear required","error":"Bad Request"}`,
},
{
Name: "Bad data no ECUs",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/flashpack_version", m.CarFlashpackVersionAddRequest{
ECUVersions: []m.ECUVersionRequest{},
Flashpack: "41.14",
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"CarECUName and CarECUVersion required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/flashpack_version", m.CarFlashpackVersionAddRequest{
ECUVersions: []m.ECUVersionRequest{
{
CarECUName: "ADAS",
CarECUVersion: "ADASVersion5",
},
{
CarECUName: "RV",
CarECUVersion: "RVVersion",
},
},
Flashpack: "11.14",
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2025,
}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Created"}`,
},
}
mo.RunParamHttpTests(t, tests, handlers.HandleFlashpackVersionAdd, "/flashpack_version", nil)
}

View File

@@ -0,0 +1,66 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"github.com/fiskerinc/cloud-services/pkg/validator"
)
var apiDeleteToken string = envtool.GetEnv("MIGRATE_DELETE_TOKEN", "")
// HandleFlashpackVersionDelete godoc
// @Summary Delete a flashpack version
// @Description Delete a flashpack version
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body common.CarFlashpackVersionRequest true "Flashpack version"
// @Success 200 {object} common.JSONDBQueryResult "Deleted flashpack ecu mapping result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /flashpack_version [delete]
func HandleFlashpackVersionDelete(w http.ResponseWriter, r *http.Request) {
var req common.CarFlashpackVersionRequest
err := httphandlers.ParseRequest(r, &req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
err = services.GetDB().GetCars().DeleteFlashpackVersion(req.CarModel, req.CarTrim, req.CarYear, req.Flashpack)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
// Also delete in other environments, as required
for _, targetURL := range targetURLS {
if !validator.ValidateURL(targetURL) || apiDeleteToken == "" {
break // No URL in MANIFEST_MIGRATE_URLS
}
otaService := services.NewOtaService(targetURL, apiDeleteToken)
resp, err := otaService.FlashpackVersionDelete(req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
utils.ForwardResponse(w, resp)
}
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "Deleted",
})
}

View File

@@ -0,0 +1,43 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
m "github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleFlashpackVersionDelete(t *testing.T) {
mock := mo.MockCars{}
services.GetDB().SetCars(&mock)
tests := []mo.DBHttpTest{
{
Name: "Bad data",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/flashpack_version", m.CarFlashpackVersionRequest{
Flashpack: "41.14",
CarModel: "Ocean",
CarTrim: "Base",
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"CarYear required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/flashpack_version", m.CarFlashpackVersionRequest{
Flashpack: "41.14",
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Deleted"}`,
},
}
mo.RunParamHttpTests(t, tests, handlers.HandleFlashpackVersionDelete, "/flashpack_version", &mock)
}

View File

@@ -0,0 +1,70 @@
package handlers
import (
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/julienschmidt/httprouter"
)
// HandleFlashpackVersionECUMappingsGet godoc
// @Summary Get mappings between a flashpack and ecu versions
// @Description Get mappings between a flashpack and ecu versions
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param model path string true "Model"
// @Param trim path string true "Trim"
// @Param year path int true "Year"
// @Param flashpack path string true "Flashpack"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Success 200 {object} common.JSONDBQueryResult "Get flashpack ecu mappings result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /flashpack_version_ecu_mappings/{model}/{trim}/{year}/{flashpack} [get]
func HandleFlashpackVersionECUMappingsGet(w http.ResponseWriter, r *http.Request) {
var req common.CarFlashpackVersionRequest
var err error
params := httprouter.ParamsFromContext(r.Context())
req.Flashpack = params.ByName("flashpack")
req.CarModel = params.ByName("model")
req.CarTrim = params.ByName("trim")
req.CarYear, err = strconv.Atoi(params.ByName("year"))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := orm.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
if options.Order == "" {
options.Order = "created_at DESC"
}
cars := services.GetDB().GetCars()
flashpackMappings, err := cars.GetCarFlashpackVersionMappingsByModelTrimYearFlashpack(req.CarModel, req.CarTrim, req.CarYear, req.Flashpack, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
total, err := cars.GetCarFlashpackVersionMappingsByModelTrimYearFlashpackCount(req.CarModel, req.CarTrim, req.CarYear, req.Flashpack)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: flashpackMappings,
Total: total,
})
}

View File

@@ -0,0 +1,27 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleFlashpackVersionECUMappingsGet(t *testing.T) {
mock := mo.MockCars{}
services.GetDB().SetCars(&mock)
tests := []mo.DBHttpTest{
{
Name: "Good data",
Request: th.MakeTestRequest(http.MethodGet, "/flashpack_version_ecu_mappings/Ocean/2023/41.14", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"flashpack":"44.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"ADAS","car_ecu_version":"ADASVersion1"},{"flashpack":"41.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"ADAS","car_ecu_version":"ADASVersion"},{"flashpack":"11.0","car_model":"Ocean","car_trim":"Base","car_year":2024,"car_ecu_name":"ADAS","car_ecu_version":"ADASVersion4"},{"flashpack":"41.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"ACUN","car_ecu_version":"ACUNVersion"},{"flashpack":"39.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"BCM","car_ecu_version":"BCMVersion"},{"flashpack":"39.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"ADAS","car_ecu_version":"ADASVersion0"},{"flashpack":"39.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"ACUN","car_ecu_version":"ACUNVersion0"},{"flashpack":"39.14","car_model":"Ocean","car_trim":"Base","car_year":2023,"car_ecu_name":"PDI","car_ecu_version":"PDIVersion"}],"total":8}`,
},
}
mo.RunParamHttpTests(t, tests, handlers.HandleFlashpackVersionECUMappingsGet, "/flashpack_version_ecu_mappings/:model/:year/:flashpack", &mock)
}

View File

@@ -0,0 +1,73 @@
package handlers
import (
"net/http"
"otaupdate/services"
"sort"
"github.com/fiskerinc/cloud-services/pkg/common"
fv "github.com/fiskerinc/cloud-services/pkg/flashpackversion"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFlashpackVersionGetInfo godoc
// @Summary Get flashpack version info for a car
// @Description Get flashpack version info (version number, ECUs to be updated for next version) for a car
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin path string true "VIN"
// @Success 200 {object} common.JSONDBQueryResult "Get flashpack version info result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /flashpack_version_info/{vin} [get]
func HandleFlashpackVersionInfoGet(w http.ResponseWriter, r *http.Request) {
vin := httprouter.ParamsFromContext(r.Context()).ByName("vin")
err := validator.ValidateField(vin, "vin")
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
cars := services.GetDB().GetCars()
car, err := cars.SelectByVIN(vin)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
nextFlashpackVersion, err := cars.GetNextFlashpackVersion(car.Model, car.Trim, car.Flashpack)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
if nextFlashpackVersion != nil {
ecusNeededForNextFlashpack, err := fv.FindCarECUsToUpdateForNextFlashpackNumber(cars, *car, nextFlashpackVersion.Flashpack)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
// Sort by ECU name in alphabetical order
sort.Slice(ecusNeededForNextFlashpack, func(i, j int) bool {
return ecusNeededForNextFlashpack[i].CarECUName < ecusNeededForNextFlashpack[j].CarECUName
})
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: common.CarFlashpackVersionInfoResponse{
Flashpack: car.Flashpack,
NextFlashpack: nextFlashpackVersion.Flashpack,
ECUVersions: ecusNeededForNextFlashpack,
},
})
} else {
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: common.CarFlashpackVersionInfoResponse{
Flashpack: car.Flashpack,
},
})
}
}

View File

@@ -0,0 +1,218 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"github.com/fiskerinc/cloud-services/pkg/common"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleFlashpackVersionInfoGet(t *testing.T) {
mock := setupMockCars()
services.GetDB().SetCars(setupMockCars())
tests := []mo.DBHttpTest{
{
Name: "Get info",
Request: th.MakeTestRequest(http.MethodGet, "/flashpack_version_info/11111111111111111", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":{"flashpack":"39.14","next_flashpack":"41.14","ecu_versions":[{"car_ecu_name":"ACUN","car_ecu_version":"ACUNVersion"}]}}`,
},
}
mo.RunParamHttpTests(t, tests, handlers.HandleFlashpackVersionInfoGet, "/flashpack_version_info/:vin", mock)
}
func setupMockCars() *mo.MockCars {
return &mo.MockCars{
SelectResponse: &common.Car{VIN: "11111111111111111", ICCID: "1111111111111111111F", Flashpack: "39.14", Model: "Ocean", Trim: "Base"},
SelectCarSettings: []common.CarSetting{},
SelectCarFlashpackVersions: []common.CarFlashpackVersion{
// 46.14
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "46.14",
CarECUName: "ADAS",
CarECUVersion: "ADASVersion2",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "46.14",
CarECUName: "ACUN",
CarECUVersion: "ACUNVersionA",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "46.14",
CarECUName: "ACUN",
CarECUVersion: "ACUNVersionB",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "46.14",
CarECUName: "BCM",
CarECUVersion: "BCMVersion",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "46.14",
CarECUName: "PDI",
CarECUVersion: "PDIVersion",
},
// 44.14
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "44.14",
CarECUName: "ADAS",
CarECUVersion: "ADASVersion1",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "44.14",
CarECUName: "ACUN",
CarECUVersion: "ACUNVersionA",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "44.14",
CarECUName: "ACUN",
CarECUVersion: "ACUNVersionB",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "44.14",
CarECUName: "BCM",
CarECUVersion: "BCMVersion",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "44.14",
CarECUName: "PDI",
CarECUVersion: "PDIVersion",
},
// 41.14
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "41.14",
CarECUName: "ADAS",
CarECUVersion: "ADASVersion",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "41.14",
CarECUName: "ACUN",
CarECUVersion: "ACUNVersion",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "41.14",
CarECUName: "BCM",
CarECUVersion: "BCMVersion",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "41.14",
CarECUName: "PDI",
CarECUVersion: "PDIVersion",
},
// 39.14
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "39.14",
CarECUName: "ADAS",
CarECUVersion: "ADASVersion0",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "39.14",
CarECUName: "ACUN",
CarECUVersion: "ACUNVersion0",
},
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "39.14",
CarECUName: "PDI",
CarECUVersion: "PDIVersion",
},
// 37.14
{
CarModel: "Ocean",
CarTrim: "Base",
CarYear: 2023,
Flashpack: "37.14",
CarECUName: "PDI",
CarECUVersion: "PDIVersion",
},
},
SelectCarECUs: []common.CarECU{
{
VIN: "11111111111111111",
ECU: "ADAS",
SupplierSWVersion: "ADASVersion1",
},
{
VIN: "11111111111111111",
ECU: "ACUN",
SupplierSWVersion: "ACUNVersion0",
},
{
VIN: "11111111111111111",
ECU: "BCM",
SupplierSWVersion: "BCMVersion",
},
{
VIN: "11111111111111111",
ECU: "PDI",
SupplierSWVersion: "PDIVersion",
},
},
}
}

View File

@@ -0,0 +1,65 @@
package handlers
import (
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/julienschmidt/httprouter"
)
// HandleFlashpacksGetAll godoc
// @Summary Get all flashpacks
// @Description Get all flashpacks
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param model path string true "Model"
// @Param trim path string true "Trim"
// @Param year path int true "Year"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Success 200 {object} common.JSONDBQueryResult "Get flashpacks result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /flashpack_versions/{model}/{trim}/{year} [get]
func HandleFlashpackVersionsGetAll(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
model := params.ByName("model")
trim := params.ByName("trim")
year, err := strconv.Atoi(params.ByName("year"))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
options, err := orm.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
if options.Order == "" {
options.Order = "flashpack DESC"
}
cars := services.GetDB().GetCars()
flashpacks, err := cars.GetFlashpackVersions(model, trim, year, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
total, err := cars.GetFlashpackVersionsCount(model, trim, year)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: flashpacks,
Total: total,
})
}

View File

@@ -0,0 +1,27 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleFlashpackVersionsGetAll(t *testing.T) {
mock := mo.MockCars{}
services.GetDB().SetCars(&mock)
tests := []mo.DBHttpTest{
{
Name: "Get all",
Request: th.MakeTestRequest(http.MethodGet, "/flashpack_versions/Ocean/Base/2023", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"flashpack":"43.19","car_model":"Ocean","car_trim":"Base","car_year":2023},{"flashpack":"41.14","car_model":"Ocean","car_trim":"Base","car_year":2023}],"total":2}`,
},
}
mo.RunParamHttpTests(t, tests, handlers.HandleFlashpackVersionsGetAll, "/flashpack_versions/:model/:trim/:year", &mock)
}

View File

@@ -0,0 +1,80 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
"github.com/fiskerinc/cloud-services/pkg/validator"
)
// HandleFleetAdd godoc
// @Summary Add a fleet
// @Description Add a fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param config body FleetRequest true "Fleet data"
// @Success 200 {object} FleetRequest
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet [post]
func HandleFleetAdd(w http.ResponseWriter, r *http.Request) {
fleetCreate.Handle(w, r)
}
var fleetCreate = controllers.NewMongoCreate(&fleetCreateHelper{})
type fleetCreateHelper struct {
fleetHelper
}
func (h *fleetCreateHelper) QueryInsert(model interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
fleet, ok := model.(*mongo.Fleet)
if ok {
if fleet.CANBus.DTCEnabled == nil {
fleet.CANBus.DTCEnabled = elptr.ElPtr(false)
}
}
return client.GetFleets().AddFleet(fleet)
}
type FleetRequest struct {
Name string `json:"name"`
LogLevel common.LogLevel `json:"log_level" bson:"log_level"`
CANBus common.CANBus `json:"canbus" bson:"canbus"`
IDPSEnabled bool `json:"idps_enabled" bson:"idps_enabled"`
}
type fleetHelper struct{}
func (h *fleetHelper) NewModel() interface{} {
return &mongo.Fleet{}
}
func (h *fleetHelper) HasPK(filter interface{}) bool {
return filter.(*mongo.Fleet).Name != ""
}
func (h *fleetHelper) ValidatePK(model interface{}) error {
result := model.(*mongo.Fleet)
err := validator.ValidateField(result.Name, "required,fleet")
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,45 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestFleetAdd(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(&mongo.MockCollection{})
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "No data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"required required","error":"Bad Request"}`,
},
{
Name: "Invalid data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet", mongo.Fleet{}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"required required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet", mongo.Fleet{Name: "TEST-FLEET"}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"name":"TEST-FLEET","log_level":"trace","canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"idps_enabled":false,"tags":null,"vehicles":null,"vehicles_count":0}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetAdd, "/fleet")
}

View File

@@ -0,0 +1,68 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFleetDelete godoc
// @Summary Delete fleet
// @Description Delete fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Success 200 {object} common.JSONMessage
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name} [delete]
func HandleFleetDelete(w http.ResponseWriter, r *http.Request) {
fleetDelete.Handle(w, r)
}
var fleetDelete = controllers.NewMongoDelete(&fleetDeleteHelper{})
type fleetDeleteHelper struct {
fleetHelper
}
func (h *fleetDeleteHelper) ParseDeleteURLParams(r *http.Request) interface{} {
var req = &mongo.Fleet{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
return req
}
func (h *fleetDeleteHelper) ValidateFields(model interface{}) error {
p := model.(*mongo.Fleet)
err := validator.ValidateField(p.Name, "required,fleet")
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetDeleteHelper) QueryDelete(model interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
return client.GetFleets().DeleteFleet(model.(*mongo.Fleet))
}
type FleetDeleteRequest struct {
Name string `validate:"required,fleet"`
}

View File

@@ -0,0 +1,33 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestFilterDelete(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewVehiclesCollection(&mongo.MockCollection{})
client.SetVehicles(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/fleet/TESTFLEET", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Deleted"}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetDelete, "/fleet/:name")
}

View File

@@ -0,0 +1,128 @@
package handlers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/pkg/errors"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFleetFilterAdd godoc
// @Summary Add CAN filter for fleet
// @Description Add CAN filter for fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Param config body common.CANFilter true "CAN filter"
// @Success 200 {object} common.SubscriptionConfiguration
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name}/filter [post]
func HandleFleetFilterAdd(w http.ResponseWriter, r *http.Request) {
fleetFilterAdd.Handle(w, r)
}
var fleetFilterAdd = controllers.NewMongoUpdate(&fleetFilterAddHelper{})
type fleetFilterAddHelper struct{}
func (h *fleetFilterAddHelper) ParseUpdateURLParams(r *http.Request) interface{} {
req := &mongo.Fleet{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
return req
}
func (h *fleetFilterAddHelper) ValidateFields(model interface{}) error {
result, ok := model.(*mongo.Fleet)
if !ok {
return nil
}
err := validator.ValidateField(result.Name, "required,fleet")
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetFilterAddHelper) NewModel() interface{} {
return &common.CANFilter{}
}
func (h *fleetFilterAddHelper) ParseRequestBody(r *http.Request, model interface{}) error {
if err := httphandlers.ParseRequest(r, model); err != nil {
return errors.WithMessage(err, "failed to parse request body")
}
p := model.(*common.CANFilter)
if p.EdgeMask == nil && p.Interval == nil {
return &validator.FieldError{
ErrorMsg: "At least one of edge_mask or interval is required",
}
}
if p.EdgeMask != nil && p.Interval != nil {
if (*p.EdgeMask).String() == "" && *p.Interval == 0 ||
(*p.EdgeMask).String() != "" && *p.Interval != 0 {
return &validator.FieldError{
ErrorMsg: "Only one of edge_mask or interval can be specified",
}
}
}
return nil
}
func (h *fleetFilterAddHelper) QueryUpdate(filter interface{}, model interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
if err = client.GetFleets().AddFilterToFleet(filter.(*mongo.Fleet).Name, model.(*common.CANFilter)); err != nil {
return err
}
return ResetFleetVehiclesConfigCache(filter.(*mongo.Fleet).Name)
}
func ResetFleetVehiclesConfigCache(fleetName string) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
vehicles, err := client.GetFleets().GetVehiclesForFleet(fleetName, "", &queries.PageQueryOptions{})
if err != nil {
return err
}
r := services.RedisClientPool().GetFromPool()
defer r.Close()
if err = cache.RemoveCacheConfigForVehicles(r, vehicles); err != nil {
logger.Warn().Msgf("failed to remove cache config for vehicles: %v", err)
}
return nil
}

View File

@@ -0,0 +1,68 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
)
func TestFleetFilterAdd(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(&mongo.MockCollection{})
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Invalid fleet",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/$TEST/filter", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
{
Name: "Invalid vin parameter",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/filter", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"CANID required","error":"Bad Request"}`,
},
{
Name: "Invalid data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/filter", common.CANFilter{}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"CANID required","error":"Bad Request"}`,
},
{
Name: "Invalid data with can id",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/filter", common.CANFilter{CANID: "123"}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"At least one of edge_mask or interval is required","error":"Bad Request"}`,
},
{
Name: "Invalid data with all fields",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/filter",
common.CANFilter{CANID: "123", EdgeMask: elptr.ElPtr(common.BinaryHex("123")), Interval: elptr.ElPtr(1)}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Only one of edge_mask or interval can be specified","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(
http.MethodPost, "http://example.com/fleet/US-TEST/filter",
common.CANFilter{CANID: "123", Interval: elptr.ElPtr(100)}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"can_id":"123","interval":100}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetFilterAdd, "/fleet/:name/filter")
}

View File

@@ -0,0 +1,74 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFleetFilterDelete godoc
// @Summary Delete filter from fleet
// @Description Delete filter from fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Param id path string true "CAN ID"
// @Success 200 {object} common.JSONMessage
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name}/filter/{id} [delete]
func HandleFleetFilterDelete(w http.ResponseWriter, r *http.Request) {
fleetFilterDelete.Handle(w, r)
}
var fleetFilterDelete = controllers.NewMongoDelete(&fleetFilterDeleteHelper{})
type fleetFilterDeleteHelper struct{}
func (h *fleetFilterDeleteHelper) ParseDeleteURLParams(r *http.Request) interface{} {
req := &FleetFilterDeleteParams{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
req.CANID = params.ByName("id")
return req
}
func (h *fleetFilterDeleteHelper) ValidateFields(model interface{}) error {
result := model.(*FleetFilterDeleteParams)
err := validator.ValidateStruct(result)
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetFilterDeleteHelper) QueryDelete(filter interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
f := filter.(*FleetFilterDeleteParams)
if err = client.GetFleets().DeleteFilterFromFleet(f.Name, f.CANID); err != nil {
return err
}
return ResetFleetVehiclesConfigCache(f.Name)
}
type FleetFilterDeleteParams struct {
Name string `validate:"fleet"`
CANID string `validate:"can_id"`
}

View File

@@ -0,0 +1,33 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestFleetFilterDelete(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(&mongo.MockCollection{})
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/fleet/US-TEST/filter/123", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Deleted"}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetFilterDelete, "/fleet/:name/filter/:id")
}

View File

@@ -0,0 +1,80 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFleetFilterGetList godoc
// @Summary Get filters for fleet
// @Description Get filters for fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Success 200 {object} common.JSONDBQueryResult
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name}/filters [get]
func HandleFleetFilterGetList(w http.ResponseWriter, r *http.Request) {
fleetFilterGetList.Handle(w, r)
}
var fleetFilterGetList = controllers.NewMongoGetList(&fleetFilterGetListHelper{})
type fleetFilterGetListHelper struct{}
func (h *fleetFilterGetListHelper) NewModel() interface{} {
return &mongo.Fleet{}
}
func (h *fleetFilterGetListHelper) ParseGetListURLParams(r *http.Request, model interface{}) {
filter := model.(*mongo.Fleet)
params := httprouter.ParamsFromContext(r.Context())
filter.Name = params.ByName("name")
}
func (h *fleetFilterGetListHelper) ValidateStruct(model interface{}) error {
result := model.(*mongo.Fleet)
err := validator.ValidateField(result.Name, "required,fleet")
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetFilterGetListHelper) ParseGetListQueryParams(r *http.Request, model interface{}) {
// does not utilize URL queries so leave this function empty
}
func (h *fleetFilterGetListHelper) QueryCount(filter interface{}) (int64, error) {
client, err := services.GetMongoClient()
if err != nil {
return 0, err
}
return client.GetFleets().GetFiltersForFleetCount(filter.(*mongo.Fleet).Name)
}
func (h *fleetFilterGetListHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) {
client, err := services.GetMongoClient()
if err != nil {
return nil, err
}
return client.GetFleets().GetFiltersForFleet(filter.(*mongo.Fleet).Name, options)
}

View File

@@ -0,0 +1,75 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
)
func TestFleetFilterGetList(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(
&mongo.MockCollection{
AggregateObject: []mongo.Fleet{
{
Name: "US-TEST",
CANBus: common.CANBus{
Filters: []common.CANFilter{
{
CANID: "123",
Interval: elptr.ElPtr(100),
},
},
},
},
},
},
)
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Invalid name parameter",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/$TEST/filters", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/US-TEST/filters", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"can_id":"123","interval":100}]}`,
},
{
Name: "Valid data limit 50",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/US-TEST/filters?limit=50", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"can_id":"123","interval":100}]}`,
},
{
Name: "Wrong limit, -100",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/US-TEST/filters?limit=-100", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
{
Name: "Wrong limit, 1000",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/US-TEST/filters?limit=1000", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetFilterGetList, "/fleet/:name/filters")
}

View File

@@ -0,0 +1,111 @@
package handlers
import (
"net/http"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
"github.com/pkg/errors"
)
// HandleFleetFilterUpdate godoc
// @Summary Update a fleet filter
// @Description Update a fleet filter
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Param id path string true "CAN ID"
// @Param config body common.CANFilter true "Fleet filter data"
// @Success 200 {object} common.SubscriptionConfiguration
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name}/filter/{id} [put]
func HandleFleetFilterUpdate(w http.ResponseWriter, r *http.Request) {
fleetFilterUpdate.Handle(w, r)
}
var fleetFilterUpdate = controllers.NewMongoUpdate(&fleetFilterUpdateHelper{})
type fleetFilterUpdateHelper struct{}
func (h *fleetFilterUpdateHelper) ParseUpdateURLParams(r *http.Request) interface{} {
req := &FleetFilterUpdateParams{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
req.CANID = params.ByName("id")
return req
}
func (h *fleetFilterUpdateHelper) ValidateFields(model interface{}) error {
result, ok := model.(*FleetFilterUpdateParams)
if !ok {
return nil
}
err := validator.ValidateStruct(result)
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetFilterUpdateHelper) NewModel() interface{} {
return &common.CANFilter{}
}
func (h *fleetFilterUpdateHelper) ParseRequestBody(r *http.Request, model interface{}) error {
if err := httphandlers.ParseRequest(r, model); err != nil {
return errors.WithMessage(err, "failed to parse request body")
}
p := model.(*common.CANFilter)
if p.EdgeMask == nil && p.Interval == nil {
return &validator.FieldError{
ErrorMsg: "At least one of edge_mask or interval is required",
}
}
if p.EdgeMask != nil && p.Interval != nil {
if (*p.EdgeMask).String() == "" && *p.Interval == 0 ||
(*p.EdgeMask).String() != "" && *p.Interval != 0 {
return &validator.FieldError{
ErrorMsg: "Only one of edge_mask or interval can be specified",
}
}
}
return nil
}
func (h *fleetFilterUpdateHelper) QueryUpdate(filter interface{}, model interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
f := filter.(*FleetFilterUpdateParams)
if err = client.GetFleets().UpdateFilterForFleet(f.Name, f.CANID, model.(*common.CANFilter)); err != nil {
return err
}
return ResetFleetVehiclesConfigCache(f.Name)
}
type FleetFilterUpdateParams struct {
Name string `validate:"fleet"`
CANID string `validate:"can_id"`
}

View File

@@ -0,0 +1,56 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
)
func TestFleetFilterUpdate(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(&mongo.MockCollection{})
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Invalid data",
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/fleet/US-TEST/filter/123",
common.CANFilter{Interval: elptr.ElPtr(0)}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"CANID required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/fleet/US-TEST/filter/123",
common.CANFilter{CANID: "123", Interval: elptr.ElPtr(100)}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"can_id":"123","interval":100}`,
},
{
Name: "Invalid data with can id",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/filter/123", common.CANFilter{CANID: "123"}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"At least one of edge_mask or interval is required","error":"Bad Request"}`,
},
{
Name: "Invalid data with all fields",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/filter/123",
common.CANFilter{CANID: "123", EdgeMask: elptr.ElPtr(common.BinaryHex("123")), Interval: elptr.ElPtr(1)}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Only one of edge_mask or interval can be specified","error":"Bad Request"}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetFilterUpdate, "/fleet/:name/filter/:id")
}

View File

@@ -0,0 +1,63 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/julienschmidt/httprouter"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
)
// HandleFleetGet godoc
// @Summary Get fleet
// @Description Get fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Success 200 {object} mongo.Fleet
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name} [get]
func HandleFleetGet(w http.ResponseWriter, r *http.Request) {
fleetGet.Handle(w, r)
}
var fleetGet = controllers.NewMongoGetModel(&fleetGetModelHelper{})
type fleetGetModelHelper struct {
fleetHelper
}
func (h *fleetGetModelHelper) ParseGetURLParams(r *http.Request) interface{} {
req := &mongo.Fleet{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
return req
}
func (h *fleetGetModelHelper) Query(filter interface{}) (interface{}, error) {
client, err := services.GetMongoClient()
if err != nil {
return nil, err
}
fleet, ok := filter.(*mongo.Fleet)
if ok {
if fleet.CANBus.DTCEnabled == nil {
fleet.CANBus.DTCEnabled = elptr.ElPtr(false)
}
}
return client.GetFleets().FindFleet(filter.(*mongo.Fleet))
}
type FleetFilterParams struct {
Name string `json:"name" validate:"required,fleet"`
}

View File

@@ -0,0 +1,73 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/mongo"
)
// HandleFleetGetList godoc
// @Summary Get list of fleets
// @Description Get list of fleets
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param tags query string false "Tags associated with fleet"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Success 200 {object} common.JSONDBQueryResult{data=[]mongo.Fleet}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleets [get]
func HandleFleetGetList(w http.ResponseWriter, r *http.Request) {
fleetGetList.Handle(w, r)
}
var fleetGetList = controllers.NewMongoGetList(&fleetGetListHelper{})
var fleetListSort = map[string]string{
"canbus_": "canbus.",
}
type fleetGetListHelper struct {
fleetHelper
}
func (h *fleetGetListHelper) ParseGetListURLParams(r *http.Request, model interface{}) {
// does not utilize URL params so leave this function empty
}
func (h *fleetGetListHelper) ParseGetListQueryParams(r *http.Request, model interface{}) {
filter := model.(*mongo.Fleet)
filter.SetSearchQuery(r.URL.Query().Get("search"))
}
func (h *fleetGetListHelper) ValidateStruct(model interface{}) error { return nil }
func (h *fleetGetListHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) {
client, err := services.GetMongoClient()
if err != nil {
return nil, err
}
if options != nil {
options.Order = mongo.AdaptOrder(options.Order, fleetListSort)
}
return client.GetFleets().SelectFleets(filter.(*mongo.Fleet), options)
}
func (h *fleetGetListHelper) QueryCount(filter interface{}) (int64, error) {
client, err := services.GetMongoClient()
if err != nil {
return 0, err
}
return client.GetFleets().GetFleetCount(filter.(*mongo.Fleet))
}

View File

@@ -0,0 +1,59 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestFleetGetList(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(
&mongo.MockCollection{
FindObject: []mongo.Fleet{
{
Name: "TESTFLEET",
},
},
},
)
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleets", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"name":"TESTFLEET","log_level":"trace","canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"idps_enabled":false,"tags":null,"vehicles":null,"vehicles_count":0}]}`,
},
{
Name: "Valid data with max limit",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleets?limit=100", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"name":"TESTFLEET","log_level":"trace","canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"idps_enabled":false,"tags":null,"vehicles":null,"vehicles_count":0}]}`,
},
{
Name: "Wrong limit, -100",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleets?limit=-100", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
{
Name: "Wrong limit, 1000",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleets?limit=1000", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Limit greater than 100","error":"Bad Request"}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetGetList, "/fleets")
}

View File

@@ -0,0 +1,50 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
)
func TestFleetGet(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(
&mongo.MockCollection{
AggregateObject: []mongo.Fleet{
{
Name: "TESTFLEET",
CANBus: common.CANBus{DTCEnabled: elptr.ElPtr(true)},
},
},
},
)
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/INVALIDFLEET$", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"fleet fleet ","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/TESTFLEET", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"name":"","log_level":"trace","canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":null},"idps_enabled":false,"tags":null,"vehicles":null,"vehicles_count":0}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetGet, "/fleet/:name")
}

View File

@@ -0,0 +1,149 @@
package handlers
import (
"errors"
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/mongo"
e "github.com/fiskerinc/cloud-services/pkg/mongo/error"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFleetUpdate godoc
// @Summary Update fleet
// @Description Update fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Param config body mongo.Fleet true "Fleet data"
// @Success 200 {object} mongo.Fleet
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name} [put]
func HandleFleetUpdate(w http.ResponseWriter, r *http.Request) {
fleetUpdate.Handle(w, r)
}
var fleetUpdate = controllers.NewMongoUpdate(&fleetUpdateHelper{})
type fleetUpdateHelper struct {
fleetHelper
}
func (h *fleetUpdateHelper) ParseUpdateURLParams(r *http.Request) interface{} {
req := &mongo.Fleet{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
return req
}
func (h *fleetUpdateHelper) ValidateFields(model interface{}) error {
p := model.(*mongo.Fleet)
err := validator.ValidateField(p.Name, "required,fleet")
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetUpdateHelper) ParseRequestBody(r *http.Request, model interface{}) error {
err := httphandlers.ParseRequest(r, model)
if err != nil {
return err
}
fleet, ok := model.(*mongo.Fleet)
if ok {
if fleet.CANBus.DTCEnabled == nil {
fleet.CANBus.DTCEnabled = elptr.ElPtr(false)
model = fleet
}
}
return nil
}
func (h *fleetUpdateHelper) QueryUpdate(filter interface{}, model interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
flt := model.(*mongo.Fleet)
if flt.CANBus.DTCEnabled == nil {
flt.CANBus.DTCEnabled = elptr.ElPtr(true)
}
err = client.GetFleets().UpdateFleet(filter.(*mongo.Fleet), flt)
if err != nil {
return err
}
fleetVINs, err := client.GetFleets().GetVehiclesForFleet(flt.Name, "", &queries.PageQueryOptions{})
if err != nil {
return err
}
batch := redis.NewRedisBatchCommands()
for _, fleetVIN := range fleetVINs {
v := &mongo.Vehicle{VIN: fleetVIN, LogLevel: flt.LogLevel, CANBus: flt.CANBus, DebugMask: flt.DebugMask, IDPSEnabled: flt.IDPSEnabled}
err = client.GetVehicles().UpdateVehicle(v)
if err != nil && errors.Is(err, e.ErrInvalidNumberOfDocs) {
logger.At(logger.Warn(), fleetVIN, "mongodb").Err(err).Send()
continue
} else if err != nil {
return err
}
batch.Add("DEL", redis.CarConfigKey(fleetVIN))
if flt.CANBus.DTCEnabled != nil {
data := common.TRexConfigResponse{
LogLevel: flt.LogLevel,
CANBus: flt.CANBus,
}
if cache.ENABLE_DEBUG_MASK {
data.DebugMask = flt.DebugMask
}
data.IDPSEnabled = flt.IDPSEnabled
err = batch.AddPublish(common.TRex.Key(fleetVIN), common.Message{
Handler: "config",
Data: data,
})
if err != nil {
return err
}
}
}
conn := services.RedisClientPool().GetFromPool()
defer conn.Close()
_, err = conn.ExecuteBatch(batch)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,144 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
m "github.com/fiskerinc/cloud-services/pkg/common"
dbtc "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
htc "github.com/fiskerinc/cloud-services/pkg/httpclient/tester"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/redis"
rtc "github.com/fiskerinc/cloud-services/pkg/redis/tester"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
tr "github.com/fiskerinc/cloud-services/pkg/testrunner"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
)
func TestFleetUpdate(t *testing.T) {
mockDB := dbtc.MockCars{}
services.GetDB().SetCars(&mockDB)
vin := "1G1FP87S3GN100062"
trexKey := m.TRex.Key(vin)
cacheKey := redis.CarConfigKey(vin)
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongoFleets := mongo.NewFleetsCollection(&mongo.MockCollection{})
mockMongoVehicles := mongo.NewVehiclesCollection(&mongo.MockCollection{})
client.SetFleets(mockMongoFleets)
client.SetVehicles(mockMongoVehicles)
mockRedis := rtc.MockRedis{}
services.SetRedisClientPool(rtc.NewMockClientPool(&mockRedis))
tests := []tr.TestCase{
{
Name: "No data",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/TESTFLEET", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
},
{
Name: "Invalid data",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/TESTFLEET", mongo.Fleet{Name: "TEST_FLEET"}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
},
{
Name: "Valid data 1",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/fleet/TESTFLEET", mongo.Fleet{Name: "TESTFLEET"}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"name":"TESTFLEET","log_level":"trace","canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"idps_enabled":false,"tags":null,"vehicles":null,"vehicles_count":0}`,
},
RedisTestCase: &rtc.RedisTestCase{
ExpectedMessages: nil,
ExpectedCaches: nil,
},
},
{
Name: "Valid data 2",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPut, "http://example.com/fleet/TESTFLEET",
mongo.Fleet{
VehiclesCount: 1,
Vehicles: []string{vin},
Name: "TESTFLEET",
LogLevel: m.Info,
CANBus: m.CANBus{
Enabled: true,
DTCEnabled: elptr.ElPtr(false),
},
DebugMask: "12",
IDPSEnabled: true,
},
),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"name":"TESTFLEET","log_level":"info","canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":false},"idps_enabled":true,"debug_mask":"12","tags":null,"vehicles":["1G1FP87S3GN100062"],"vehicles_count":1}`,
Setup: func() {
mockMongoFleets = mongo.NewFleetsCollection(&mongo.MockCollection{
AggregateObject: []mongo.Fleet{
{
Name: "US-TEST",
Vehicles: []string{
vin,
},
},
},
})
client.SetFleets(mockMongoFleets)
},
},
RedisTestCase: &rtc.RedisTestCase{
ExpectedMessages: map[string]string{
trexKey: `{"data":{"canbus":{"data_logger_enabled":false,"dtc_enabled":false,"enabled":true},"idps_enabled":true,"log_level":"info"},"handler":"config"}`,
},
ExpectedCaches: map[string]rtc.ExpiringCacheResult{
cacheKey: {Value: "DELETED"},
},
},
},
}
schemaTesterTRex := th.NewSchemaTestHelper(t, schemaToTRex)
for _, test := range tests {
mockRedis.Reset()
if test.DBTestCase != nil {
test.DBTestCase.SetupDB(&mockDB)
}
if test.RedisTestCase != nil {
test.RedisTestCase.SetupRedis(&mockRedis)
}
if test.HttpTestCase != nil {
if test.HttpTestCase.Setup != nil {
test.HttpTestCase.Setup()
}
w := test.HttpTestCase.TestWithParamPath(handlers.HandleFleetUpdate, "/fleet/:name")
test.HttpTestCase.ValidateHttp(t, test.Name, w)
}
if test.DBTestCase != nil {
test.DBTestCase.Validate(t, test.Name, &mockDB)
}
if test.RedisTestCase != nil {
test.RedisTestCase.Validate(t, test.Name, &mockRedis)
for _, mes := range test.RedisTestCase.ExpectedMessages {
schemaTesterTRex.ValidateSchemaObject(test.Name, []byte(mes))
}
}
}
}

View File

@@ -0,0 +1,134 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFleetVehicleGetList godoc
// @Summary Get vehicles for fleet
// @Description Get vehicles for fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Param limit query int false "Max number of records"
// @Param offset query int false "Records offset"
// @Success 200 {object} common.JSONDBQueryResult
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name}/vehicles [get]
func HandleFleetVehicleGetList(w http.ResponseWriter, r *http.Request) {
fleetVehiclesGetList.Handle(w, r)
}
var fleetVehiclesGetList = controllers.NewMongoGetList(&fleetVehiclesGetListHelper{})
type fleetVehiclesGetListHelper struct{}
func (h *fleetVehiclesGetListHelper) NewModel() interface{} {
return &mongo.Fleet{}
}
func (h *fleetVehiclesGetListHelper) ParseGetListURLParams(r *http.Request, model interface{}) {
filter := model.(*mongo.Fleet)
params := httprouter.ParamsFromContext(r.Context())
filter.Name = params.ByName("name")
filter.SetSearchQuery(r.URL.Query().Get("search"))
}
func (h *fleetVehiclesGetListHelper) ParseGetListQueryParams(r *http.Request, model interface{}) {
// does not utilize URL queries so leave this function empty
}
func (h *fleetVehiclesGetListHelper) ValidateStruct(model interface{}) error {
result := model.(*mongo.Fleet)
err := validator.ValidateField(result.Name, "required,fleet")
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetVehiclesGetListHelper) QueryCount(filter interface{}) (int64, error) {
client, err := services.GetMongoClient()
if err != nil {
return 0, err
}
f := filter.(*mongo.Fleet)
return client.GetFleets().GetVehiclesForFleetCount(f.Name, f.SearchQuery())
}
func (h *fleetVehiclesGetListHelper) QuerySelect(filter interface{}, options *queries.PageQueryOptions) (interface{}, error) {
client, err := services.GetMongoClient()
if err != nil {
return nil, err
}
f := filter.(*mongo.Fleet)
vins, err := client.GetFleets().GetVehiclesForFleet(f.Name, f.SearchQuery(), options)
if err != nil {
return nil, err
}
var response []fleetVehicle
// get CarUpdate
carUpdates := services.GetDB().GetCarUpdates()
ups, _ := carUpdates.SelectMostRecentByVINs(vins)
// setup CarState
clientPool := services.RedisClientPool()
// merge Vin, CarUpdate, CarState
parser := cache.NewVehicleState(clientPool)
for _, vin := range vins {
state, err := parser.Get(vin)
if err != nil {
state = common.CarState{}
logger.Warn().Err(err).Send()
}
vehicle := fleetVehicle{
VIN: vin,
CarState: &state,
CarUpdate: &common.CarUpdate{},
}
for _, carUpdate := range ups {
if carUpdate.VIN == vin {
vehicle.CarUpdate = &carUpdate
break
}
}
response = append(response, vehicle)
}
return response, err
}
type fleetVehicle struct {
VIN string `json:"vin" validate:"required,vin"`
CarState *common.CarState `json:"carstate"`
CarUpdate *common.CarUpdate `json:"carupdate,omitempty"`
}

View File

@@ -0,0 +1,71 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestFleetVehicleGetList(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
client.SetFleets(mongo.NewFleetsCollection(
&mongo.MockCollection{
AggregateObject: []mongo.Fleet{
{
Name: "US-TEST",
Vehicles: []string{
"TESTVIN1234567890",
"TESTVIN1234567891",
},
},
},
},
))
redisClient := tester.NewRedisMock()
redisClient.SISMEMBEResults = map[string]map[string]interface{}{
redis.CarSessionsKey(): {
"TESTVIN1234567890": int64(0),
"TESTVIN1234567891": int64(1),
},
redis.HMISessionsKey(): {
"TESTVIN1234567890": int64(0),
"TESTVIN1234567891": int64(1),
},
}
redisClient.HGETALLResults = map[string][]interface{}{
redis.CarStateHashKey("TESTVIN1234567890"): {
[]byte("trex_version"), []byte(`1.2.4`),
},
}
services.SetRedisClientPool(tester.NewMockClientPool(redisClient))
tests := []th.BasicHttpTest{
{
Name: "Invalid vin parameter",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/$INVALIDTEST/vehicles", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/fleet/US-TEST/vehicles", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":[{"vin":"TESTVIN1234567890","carstate":{"online":false,"online_hmi":false,"trex_version":"1.2.4"},"carupdate":{"vin":"","UpdateSource":""}},{"vin":"TESTVIN1234567891","carstate":{"online":true,"online_hmi":true},"carupdate":{"vin":"","UpdateSource":""}}]}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetVehicleGetList, "/fleet/:name/vehicles")
}

View File

@@ -0,0 +1,141 @@
package handlers
import (
"fmt"
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
"github.com/julienschmidt/httprouter"
)
// HandleFleetVehicleAdd godoc
// @Summary Add vehicle to fleet
// @Description Add vehicle to fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Param config body FleetVehicleParams true "Vehicle data"
// @Success 200 {object} common.SubscriptionConfiguration
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name}/vehicles/add [post]
func HandleFleetVehicleAdd(w http.ResponseWriter, r *http.Request) {
fleetVehicleAdd.Handle(w, r)
}
var fleetVehicleAdd = controllers.NewMongoUpdate(&fleetVehicleAddHelper{})
type fleetVehicleAddHelper struct{}
func (h *fleetVehicleAddHelper) ParseUpdateURLParams(r *http.Request) interface{} {
req := &mongo.Fleet{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
return req
}
func (h *fleetVehicleAddHelper) ValidateFields(model interface{}) error {
result, ok := model.(*mongo.Fleet)
if !ok {
return nil
}
err := validator.ValidateField(result.Name, "required,fleet")
if err != nil {
return controllers.ErrorPKRequired
}
return nil
}
func (h *fleetVehicleAddHelper) NewModel() interface{} {
return &FleetVehicleParams{}
}
func (h *fleetVehicleAddHelper) ParseRequestBody(r *http.Request, model interface{}) error {
return httphandlers.ParseRequest(r, model)
}
func (h *fleetVehicleAddHelper) QueryUpdate(filter interface{}, model interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
fvp := model.(*FleetVehicleParams)
if fvp.CANBus == nil {
fvp.CANBus = &common.CANBus{Enabled: true}
}
if fvp.CANBus.DTCEnabled == nil {
fvp.CANBus.DTCEnabled = elptr.ElPtr(true)
}
if len(fvp.VIN) > 0 {
fvp.VINs = append(fvp.VINs, fvp.VIN)
}
// clear out the VIN (single) if it was merged into the new property
// otherwise persist existing behavior
if len(fvp.VIN) > 0 && len(fvp.VINs) > 1 {
fvp.VIN = ""
}
cars, err := utils.ParseVINs(fvp.VINs)
if err != nil {
return err
}
for _, car := range cars {
hasVehicle := doesVehicleExist(car.VIN)
if !hasVehicle {
logger.Warn().Msgf("tried to add a non-existent car %s to fleet %s", car.VIN, filter.(*mongo.Fleet).Name)
err = fmt.Errorf("vin %s was not found in database", car.VIN)
return err
}
}
err = client.GetFleets().AddVehiclesToFleet(filter.(*mongo.Fleet).Name, fvp.VINs)
if err != nil {
return err
}
err = ResetVehiclesConfigCache(fvp.VINs)
if err != nil {
return err
}
return nil
}
func doesVehicleExist(vin string) bool {
count, err := services.GetDB().GetCars().Count(&common.Car{VIN: vin})
if err != nil {
return false
}
return count == 1
}
type FleetVehicleParams struct {
VIN string `json:"vin" validate:"required_without=VINs,omitempty,vin"`
VINs []string `json:"vins,omitempty" validate:"required_without=VIN,max=1000,dive,vin"`
LogLevel common.LogLevel `json:"log_level,omitempty" swaggertype:"string"`
CANBus *common.CANBus `json:"canbus,omitempty"`
IDPSEnabled bool `json:"idps_enabled,omitempty"`
}

View File

@@ -0,0 +1,153 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestFleetVehicleAdd(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
client.SetFleets(mongo.NewFleetsCollection(&mongo.MockCollection{}))
client.SetVehicles(mongo.NewVehiclesCollection(&mongo.MockCollection{}))
mock := mocks.MockCars{SelectCarsResponse: []common.Car{{
VIN: "1F15K3R45N1234567",
}}}
services.GetDB().SetCars(&mock)
tests := []th.BasicHttpTest{
{
Name: "Invalid fleet",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/$TEST/vehicles/add", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
{
Name: "Invalid data",
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleet/US-TEST/vehicles/add", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"VIN required_without VINs. VINs required_without VIN","error":"Bad Request"}`,
},
{
Name: "No VINs",
Request: th.MakeTestRequest(
http.MethodPost,
"http://example.com/fleet/US-TEST/vehicles/add",
handlers.FleetVehicleParams{},
),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"VIN required_without VINs. VINs required_without VIN","error":"Bad Request"}`,
},
{
Name: "Invalid VIN",
Request: th.MakeTestRequest(
http.MethodPost,
"http://example.com/fleet/US-TEST/vehicles/add",
handlers.FleetVehicleParams{
VIN: "TESTVIN",
},
),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"VIN 'TESTVIN' invalid","error":"Bad Request"}`,
},
{
Name: "Valid VIN",
Request: th.MakeTestRequest(
http.MethodPost,
"http://example.com/fleet/US-TEST/vehicles/add",
handlers.FleetVehicleParams{
VIN: "1F15K3R45N1234567",
},
),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"vin":"1F15K3R45N1234567","vins":["1F15K3R45N1234567"],"canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":true}}`,
},
{
Name: "Invalid VINs",
Request: th.MakeTestRequest(
http.MethodPost,
"http://example.com/fleet/US-TEST/vehicles/add",
handlers.FleetVehicleParams{
VINs: []string{"TESTVIN"},
},
),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"VINs[0] 'TESTVIN' invalid","error":"Bad Request"}`,
},
{
Name: "Valid VINs",
Request: th.MakeTestRequest(
http.MethodPost,
"http://example.com/fleet/US-TEST/vehicles/add",
handlers.FleetVehicleParams{
VINs: []string{"1F15K3R45N1234567"},
},
),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"vin":"","vins":["1F15K3R45N1234567"],"canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":true}}`,
},
{
Name: "Multiple Invalid VINs",
Request: th.MakeTestRequest(
http.MethodPost,
"http://example.com/fleet/US-TEST/vehicles/add",
handlers.FleetVehicleParams{
VINs: []string{"TESTVIN", "TESTVIN2"},
},
),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"VINs[0] 'TESTVIN' invalid. VINs[1] 'TESTVIN2' invalid","error":"Bad Request"}`,
},
{
Name: "Multiple Mixed Valid VINs",
Request: th.MakeTestRequest(
http.MethodPost,
"http://example.com/fleet/US-TEST/vehicles/add",
handlers.FleetVehicleParams{
VINs: []string{"1F15K3R45N1234567", "TESTVIN"},
},
),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"VINs[1] 'TESTVIN' invalid","error":"Bad Request"}`,
},
{
Name: "Multiple Valid VINs",
Request: th.MakeTestRequest(
http.MethodPost,
"http://example.com/fleet/US-TEST/vehicles/add",
handlers.FleetVehicleParams{
VINs: []string{"1F15K3R45N1234567", "1F15K3R45N1234567"},
},
),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"vin":"","vins":["1F15K3R45N1234567","1F15K3R45N1234567"],"canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":true}}`,
},
{
Name: "Multiple Valid VINs",
Request: th.MakeTestRequest(
http.MethodPost,
"http://example.com/fleet/US-TEST/vehicles/add",
handlers.FleetVehicleParams{
VIN: "1F15K3R45N1234567",
VINs: []string{"1F15K3R45N1234567"},
},
),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"vin":"","vins":["1F15K3R45N1234567","1F15K3R45N1234567"],"canbus":{"enabled":true,"data_logger_enabled":false,"dtc_enabled":true}}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetVehicleAdd, "/fleet/:name/vehicles/add")
}

View File

@@ -0,0 +1,85 @@
package handlers
import (
"net/http"
"otaupdate/controllers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
)
// HandleFleetVehicleDelete godoc
// @Summary Delete vehicle from fleet
// @Description Delete vehicle from fleet
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param name path string true "Name"
// @Param vins body common.VINs true "VINs"
// @Success 200 {object} common.JSONMessage
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleet/{name}/vehicles/delete [post]
func HandleFleetVehicleDelete(w http.ResponseWriter, r *http.Request) {
fleetVehicleDelete.Handle(w, r)
}
var fleetVehicleDelete = controllers.NewMongoDelete(&fleetVehicleDeleteHelper{})
type fleetVehicleDeleteHelper struct{}
func (h *fleetVehicleDeleteHelper) ParseDeleteURLParams(r *http.Request) interface{} {
req := &FleetVehicleDeleteParams{}
params := httprouter.ParamsFromContext(r.Context())
req.Name = params.ByName("name")
httphandlers.ParseRequest(r, &req) // Populate VINs from body
return req
}
func (h *fleetVehicleDeleteHelper) ValidateFields(model interface{}) error {
result := model.(*FleetVehicleDeleteParams)
err := validator.ValidateField(result.Name, "required,fleet")
if err != nil {
return controllers.ErrorPKRequired
}
for _, vin := range result.VINs {
err := validator.ValidateField(vin, "required,vin")
if err != nil {
return controllers.ErrorPKRequired
}
}
return nil
}
func (h *fleetVehicleDeleteHelper) QueryDelete(filter interface{}) error {
client, err := services.GetMongoClient()
if err != nil {
return err
}
fleet := filter.(*FleetVehicleDeleteParams)
err = client.GetFleets().DeleteVehiclesFromFleet(fleet.Name, fleet.VINs)
if err != nil {
return err
}
return ResetVehiclesConfigCache(fleet.VINs)
}
type FleetVehicleDeleteParams struct {
Name string `validate:"required,fleet"`
VINs []string `json:"vins" validate:"required,max=1000,dive,vin"`
}

View File

@@ -0,0 +1,79 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/mongo"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestFleetVehicleDelete(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
mockMongo := mongo.NewFleetsCollection(&mongo.MockCollection{})
client.SetFleets(mockMongo)
tests := []th.BasicHttpTest{
{
Name: "Invalid fleet",
Request: th.MakeTestRequest(
http.MethodPost,
"http://example.com/fleet/$TEST/vehicles/delete",
handlers.FleetVehicleDeleteParams{
Name: "$TEST", // only needed for test
VINs: []string{"1F15K3R45N1234567"},
},
),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
{
Name: "Invalid vin",
Request: th.MakeTestRequest(
http.MethodPost,
"http://example.com/fleet/US-TEST/vehicles/delete",
handlers.FleetVehicleDeleteParams{
Name: "US-TEST", // only needed for test
VINs: []string{"INVALIDVIN"},
},
),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"primary key required","error":"Bad Request"}`,
},
{
Name: "Valid data",
Request: th.MakeTestRequest(
http.MethodPost,
"http://example.com/fleet/US-TEST/vehicles/delete",
handlers.FleetVehicleDeleteParams{
Name: "US-TEST", // only needed for test
VINs: []string{"1F15K3R45N1234567"},
},
),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Deleted"}`,
},
{
Name: "Multiple VINs",
Request: th.MakeTestRequest(
http.MethodPost,
"http://example.com/fleet/US-TEST/vehicles/delete",
handlers.FleetVehicleDeleteParams{
Name: "US-TEST", // only needed for test
VINs: []string{"1F15K3R45N1234567", "1F15K3R45N1234567"},
},
),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Deleted"}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleFleetVehicleDelete, "/fleet/:name/vehicles/delete")
}

View File

@@ -0,0 +1,100 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
uhelpers "github.com/fiskerinc/cloud-services/pkg/usecase_helpers"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleFleetUpdatesAdd godoc
// @Summary Add car updates by fleet
// @Description Create car updates assigning update package to cars, and send notifications
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param data body usecase_helpers.JSONFleetUpdatesRequest true "Update manifest or package id and, car ids"
// @Success 200 {object} common.JSONDBQueryResult "Created car updates result"
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /fleetupdate [post]
func HandleFleetUpdatesAdd(w http.ResponseWriter, r *http.Request) {
var req uhelpers.JSONFleetUpdatesRequest
err := httphandlers.ParseRequest(r, &req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
manifest, err := getManifest(req.UpdateManifestID)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
username := httphandlers.GetClientID(r)
err = sendManifestToFleets(req.FleetNames, manifest, username)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: req.FleetNames,
})
}
func sendManifestToFleets(fleetNames []string, manifest common.UpdateManifest, username string) error {
var resultNames []string
resC := make(chan string, len(fleetNames))
// to avoid goroutines leak, we add buffer to the errC channel
errC := make(chan error, len(fleetNames))
for _, name := range fleetNames {
go updateFleet(name, manifest, username, errC, resC)
}
for {
select {
case err := <-errC:
return err
case name := <-resC:
resultNames = append(resultNames, name)
if len(resultNames) == len(fleetNames) {
return nil
}
}
}
}
func updateFleet(name string, manifest common.UpdateManifest, username string, errC chan<- error, resC chan<- string) {
client, err := services.GetMongoClient()
if err != nil {
errC <- err
return
}
vins, err := client.GetFleets().GetVehiclesForFleet(name, "", &queries.PageQueryOptions{})
if err != nil {
errC <- err
return
}
d := services.GetDB().GetCarUpdates()
k := services.GetKafkaProducer()
notifier := uhelpers.NewUpdateNotifier(d, k)
_, err = notifier.Send(vins, manifest, username)
if err != nil {
errC <- err
return
}
resC <- name
}

View File

@@ -0,0 +1,188 @@
package handlers_test
import (
"encoding/base64"
"fmt"
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
dbm "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
htc "github.com/fiskerinc/cloud-services/pkg/httpclient/tester"
"github.com/fiskerinc/cloud-services/pkg/kafka"
km "github.com/fiskerinc/cloud-services/pkg/kafka/mock"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"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"
uhelpers "github.com/fiskerinc/cloud-services/pkg/usecase_helpers"
"google.golang.org/protobuf/proto"
)
func TestFleetUpdateAdd(t *testing.T) {
client, err := services.GetMongoClient()
if err != nil {
t.Error(err)
return
}
redis.MockRedisConnection()
vin1 := "1G1FP87S3GN100062"
vin2 := "TESTVIN1234567891"
mockDB := dbm.MockCarUpdates{}
mockKafka := km.KafkaMock{}
mockRedis := rm.MockRedis{}
mockMongo := mongo.NewFleetsCollection(
&mongo.MockCollection{
AggregateObject: []mongo.Fleet{
{
Name: "Grande",
Vehicles: []string{
vin1,
vin2,
},
},
},
},
)
client.SetFleets(mockMongo)
services.GetDB().SetCarUpdates(&mockDB)
services.SetKafkaProducer(&mockKafka)
services.SetRedisClientPool(rm.NewMockClientPool(&mockRedis))
otaUpdateKey1 := common.Service.Key(vin1)
otaUpdateKey2 := common.Service.Key(vin2)
attendentTopic := kafka.AttendantServiceGRPCKafka
updateMsg := &kafka_grpc.GRPC_AttendantPayload_UpdateManifest{
UpdateManifest: &kafka_grpc.UpdateManifest{
CarUpdateId: 1,
},
}
kafkaMSG := kafka_grpc.GRPC_AttendantPayload{
Handler: "send_manifest",
Data: updateMsg,
}
binaryPayload, _ := proto.Marshal(&kafkaMSG)
sEnc := fmt.Sprintf(`"%s"`, base64.StdEncoding.EncodeToString(binaryPayload))
standardManifest := common.UpdateManifest{
ID: 100,
Name: "TEST",
Version: "10000",
Type: "standard",
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
BodyType: "truck",
}
forcedManifest := standardManifest
forcedManifest.Type = "forced"
tests := []testrunner.TestCase{
{
Name: "Bad car ids",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleetupdate", uhelpers.JSONFleetUpdatesRequest{
UpdateManifestID: 1,
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"FleetNames required","error":"Bad Request"}`,
},
},
{
Name: "No data",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleetupdate", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"UpdateManifestID required. FleetNames required","error":"Bad Request"}`,
},
},
{
Name: "Bad package ids",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleetupdate", uhelpers.JSONFleetUpdatesRequest{
FleetNames: []string{"Slick Grande"},
}),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"UpdateManifestID required. FleetNames[0] fleet ","error":"Bad Request"}`,
},
},
{
Name: "Good data standard manifest id",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleetupdate", uhelpers.JSONFleetUpdatesRequest{
UpdateManifestID: 1,
FleetNames: []string{"Slick"},
}),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":["Slick"]}`,
},
DBTestCase: &dbm.DBTestCase{
SetupMockResponse: func() {
services.GetDB().SetUpdateManifests(&dbm.MockUpdateManifests{
LoadResponse: &standardManifest,
})
},
},
RedisTestCase: &rm.RedisTestCase{},
KafkaTestCase: &km.KafkaTestCase{
ExpectedProduceMessages: map[string]map[string]interface{}{
attendentTopic: {
otaUpdateKey1: sEnc,
otaUpdateKey2: sEnc,
},
},
},
},
{
Name: "Error",
HttpTestCase: &htc.HttpTestCase{
Request: th.MakeTestRequest(http.MethodPost, "http://example.com/fleetupdate", uhelpers.JSONFleetUpdatesRequest{
UpdateManifestID: 1,
FleetNames: []string{"Slick"},
}),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"something went wrong","error":"Service Unavailable"}`,
},
DBTestCase: &dbm.DBTestCase{
MockError: fmt.Errorf("something went wrong"),
},
},
}
for _, test := range tests {
mockRedis.Reset()
mockKafka.Reset()
if test.DBTestCase != nil {
test.DBTestCase.SetupDB(&mockDB)
}
if test.RedisTestCase != nil {
test.RedisTestCase.SetupRedis(&mockRedis)
}
if test.KafkaTestCase != nil {
test.KafkaTestCase.Setup(&mockKafka)
}
if test.HttpTestCase != nil {
w := test.HttpTestCase.Test(handlers.HandleFleetUpdatesAdd)
test.HttpTestCase.ValidateHttp(t, test.Name, w)
}
if test.DBTestCase != nil {
test.DBTestCase.Validate(t, test.Name, &mockDB)
}
if test.RedisTestCase != nil {
test.RedisTestCase.Validate(t, test.Name, &mockRedis)
}
if test.KafkaTestCase != nil {
test.KafkaTestCase.Validate(t, test.Name, &mockKafka)
}
}
}

View File

@@ -0,0 +1,58 @@
package handlers
import (
"net/http"
"otaupdate/services"
"strconv"
"github.com/fiskerinc/cloud-services/pkg/httphandlers"
"github.com/fiskerinc/cloud-services/pkg/manifestsender"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/julienschmidt/httprouter"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// GetCarConfiguration godoc
// @Summary Get the vod and cds for car. Does not generate a usable to send to the car
// @Description Get all sap codes for a car, transform them to VOD and CDS, then return it to the user
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin path string true "VIN to get configuration update"
// @Param forced query bool false "Force configuration update"
// @Success 200 {object} common.UpdateConfigManifest
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /car_config/{vin} [get]
func GetCarConfiguration(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
vin := params.ByName("vin")
queryParams := r.URL.Query()
forced, _ := strconv.ParseBool(queryParams.Get("forced"))
rds := services.RedisClientPool().GetFromPool()
defer rds.Close()
cs := services.GetVehicleConfig()
db := services.GetDB()
sms := services.GetSMSClient()
username := httphandlers.GetClientID(r)
manifestSender := manifestsender.NewTBOXManifestSender(rds, cs, db, sms, nil)
input := manifestsender.ProcessConfigUpdateStruct{
VIN: vin,
Username: username,
SendToCar: false,
DontCreateDatabaseEntry: true,
Forced: forced,
}
ucm, err := manifestSender.ProcessConfigUpdate(input, services.GetDB().GetCarConfigData())
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
utils.RespJSON(w, http.StatusOK, ucm)
}

View File

@@ -0,0 +1,65 @@
package handlers
import (
"net/http"
"otaupdate/services"
"time"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleVersionsGet godoc
// @Summary Returns versions for VIN.
// @Description Returns versions for VIN at a point in time
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin path string true "VIN"
// @Param timestamp query string false "at date (2023-01-13)"
// @Success 200 {object} map[string]string
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 404 {object} common.JSONError "Not Found"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /vehicle/{vin}/version [get]
func HandleVersionsGet(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
qs := r.URL.Query()
req := getVersionsRequest{
VIN: params.ByName("vin"),
Timestamp: qs.Get("timestamp"),
}
err := validator.ValidateStruct(req)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
result, err := services.GetDB().GetCarVersionsLog().GetCarVersions(req.VIN, req.GetTimestamp())
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
utils.RespJSON(w, http.StatusOK, result)
}
type getVersionsRequest struct {
VIN string `validate:"vin,required"`
Timestamp string `validate:"yyyymmdddate"`
}
func (g *getVersionsRequest) GetTimestamp() time.Time {
if len(g.Timestamp) > 0 {
date, err := time.Parse("2006-01-02", g.Timestamp)
if err == nil {
return date.Add(time.Second * 86399)
}
logger.Warn().AnErr("getVersionsRequest.GetTimestamp", err).Send()
}
return time.Now()
}

View File

@@ -0,0 +1,56 @@
package handlers
import (
"net/http"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/julienschmidt/httprouter"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleVersionLogsGet godoc
// @Summary Returns version change logs by VIN.
// @Description Returns version change logs by VIN.
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param vin path string true "VIN"
// @Success 200 {object} common.CarVersionLogs
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 404 {object} common.JSONError "Not Found"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /vehicle/{vin}/version/logs [get]
func HandleVersionLogsGet(w http.ResponseWriter, r *http.Request) {
options, err := orm.ParsePageQuery(r)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
params := httprouter.ParamsFromContext(r.Context())
vin := params.ByName("vin")
err = validator.GetValidator().Var(vin, "vin")
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
_, err = services.GetDB().GetCars().SelectByVIN(vin)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
logs, total, err := services.GetDB().GetCarVersionsLog().SelectByVIN(vin, options)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: logs,
Total: total,
})
}

View File

@@ -0,0 +1,94 @@
package handlers_test
import (
"context"
"net/http"
"net/http/httptest"
"otaupdate/handlers"
"otaupdate/services"
"testing"
"time"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/julienschmidt/httprouter"
"github.com/stretchr/testify/assert"
)
func TestHandleVersionLogsGet(t *testing.T) {
createdAt := time.Date(2023, 1, 13, 11, 51, 12, 0, time.UTC)
correctVIN := "00000000000000000"
dbMock := services.GetDB()
dbMock.SetCars(&mocks.MockCars{})
tests := map[string]struct {
vin string
query string
carVersionsDB queries.CarVersionsLogInterface
expStatus int
expBody string
}{
"correct": {
vin: correctVIN,
carVersionsDB: mocks.MockCarVersionsLog{
MockSelectByVIN: func(vin string, options *queries.PageQueryOptions) ([]common.CarVersionLogs, int, error) {
return []common.CarVersionLogs{
{
ID: 1,
VIN: correctVIN,
VersionSource: common.TREXVersionSource,
Version: "2.3.2",
CreatedAt: &createdAt,
},
{
ID: 2,
VIN: correctVIN,
VersionSource: common.DBCVersionSource,
Version: "hash",
CreatedAt: &createdAt,
},
}, 2, nil
},
},
expStatus: http.StatusOK,
expBody: `{"data":[{"id":1,"vin":"00000000000000000","version_source":"TREX","version":"2.3.2","created_at":"2023-01-13T11:51:12Z"},{"id":2,"vin":"00000000000000000","version_source":"DBC","version":"hash","created_at":"2023-01-13T11:51:12Z"}],"total":2}`,
},
"wrong_options": {
query: "limit=-1",
expStatus: http.StatusBadRequest,
expBody: `{"message":"Limit less than 0","error":"Bad Request"}`,
},
"wrong_vin": {
expStatus: http.StatusBadRequest,
expBody: `{"message":"vin '' invalid","error":"Bad Request"}`,
},
"wrong_db": {
vin: correctVIN,
carVersionsDB: mocks.MockCarVersionsLog{
MockSelectByVIN: func(vin string, options *queries.PageQueryOptions) ([]common.CarVersionLogs, int, error) {
return nil, 0, someErr
},
},
expStatus: http.StatusServiceUnavailable,
expBody: `{"message":"some err","error":"Service Unavailable"}`,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
dbMock.SetCarVersionsLog(tt.carVersionsDB)
p := httprouter.Params{
{Key: "vin", Value: tt.vin},
}
ctx := context.WithValue(context.Background(), httprouter.ParamsKey, p)
r := th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/vin/version/logs?"+tt.query, nil).
WithContext(ctx)
w := httptest.NewRecorder()
handlers.HandleVersionLogsGet(w, r)
assert.Equal(t, tt.expStatus, w.Code)
assert.Equal(t, tt.expBody, w.Body.String())
})
}
}

View File

@@ -0,0 +1,50 @@
package handlers_test
import (
"net/http"
"otaupdate/handlers"
"otaupdate/services"
"testing"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestHandleVersionGet(t *testing.T) {
mock := mo.MockCarVersionsLog{
GetCarVersionsResult: map[string]string{
"DBC": "dbc-version",
"TREX": "trex-version",
},
}
services.GetDB().SetCarVersionsLog(&mock)
tests := []th.BasicHttpTest{
{
Name: "Invalid VIN",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/1111/version", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"VIN '1111' invalid","error":"Bad Request"}`,
},
{
Name: "Invalid timestamp",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/11111111111111111/version?timestamp=99-99-99", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"Timestamp yyyymmdddate ","error":"Bad Request"}`,
},
{
Name: "Good request no timestamp",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/11111111111111111/version", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"DBC":"dbc-version","TREX":"trex-version"}`,
},
{
Name: "Good request with timestamp",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/vehicle/11111111111111111/version?timestamp=2023-01-30", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"DBC":"dbc-version","TREX":"trex-version"}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleVersionsGet, "/vehicle/:vin/version")
}

View File

@@ -0,0 +1,72 @@
package handlers_test
import (
"net/http"
"testing"
"github.com/fiskerinc/cloud-services/pkg/httpclient/tester"
"github.com/fiskerinc/cloud-services/pkg/redis"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/pkg/errors"
"otaupdate/handlers"
)
var someErr = errors.New("some err")
func TestHandleDashboardToken(t *testing.T) {
tests := map[string]struct {
mockGetAccessToken func(r redis.Client) (string, error)
mockGetGuestToken func(r redis.Client, accToken string) (string, error)
http tester.HttpTestCase
}{
"success": {
mockGetAccessToken: successGetAccessToken,
mockGetGuestToken: successGetGuestToken,
http: tester.HttpTestCase{
Request: th.MakeTestRequest(http.MethodGet, "", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"token":"valid_token"}`,
},
},
"fail_get_access": {
mockGetAccessToken: failGetAccessToken,
http: tester.HttpTestCase{
Request: th.MakeTestRequest(http.MethodGet, "", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`,
},
},
"fail_get_guest": {
mockGetAccessToken: successGetAccessToken,
mockGetGuestToken: failGetGuestToken,
http: tester.HttpTestCase{
Request: th.MakeTestRequest(http.MethodGet, "", nil),
ExpectedStatus: http.StatusServiceUnavailable,
ExpectedResponse: `{"message":"some err","error":"Service Unavailable"}`,
},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
handlers.SetGetAccessTokenFunc(tt.mockGetAccessToken)
handlers.SetGetGuestTokenFunc(tt.mockGetGuestToken)
w := tt.http.Test(handlers.HandleDashboardToken)
tt.http.ValidateHttp(t, name, w)
})
}
}
func successGetAccessToken(r redis.Client) (string, error) {
return "", nil
}
func failGetAccessToken(r redis.Client) (string, error) {
return "", someErr
}
func successGetGuestToken(r redis.Client, accToken string) (string, error) {
return "valid_token", nil
}
func failGetGuestToken(r redis.Client, accToken string) (string, error) {
return "", someErr
}

View File

@@ -0,0 +1,48 @@
package handlers
import (
"fmt"
"net/http"
"strconv"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/julienschmidt/httprouter"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleIssueGet godoc
// @Summary Search issue by ID
// @Description Returns all Issue related to the issue id
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param id path int true "Issue ID"
// @Success 200 {object} common.JSONDBQueryResult{data=common.Issue}
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /issue/{id} [get]
func HandleIssueGet(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.Atoi(params.ByName("id"))
if err != nil {
err = fmt.Errorf("invalid id")
}
if loggerdataresp.BadDataErrorResp(w, err, http.StatusNotFound) {
return
}
issue, err := services.GetDB().GetIssues().SelectByID(id)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONDBQueryResult{
Data: issue,
})
}

View File

@@ -0,0 +1,41 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
mo "github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestIssueGet(t *testing.T) {
mock := mo.MockIssue{}
services.GetDB().SetIssues(&mock)
tests := []th.BasicHttpTest{
{
Name: "Invalid Issue ID",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/issues/x", nil),
ExpectedStatus: http.StatusNotFound,
ExpectedResponse: `{"message":"invalid id","error":"Not Found"}`,
},
{
Name: "Zero Issue ID",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/issues/0", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"id cannot be less than 0","error":"Bad Request"}`,
},
{
Name: "Good Issue Id",
Request: th.MakeTestRequest(http.MethodGet, "http://example.com/issues/1", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"data":{"id":1,"vin":"","title":"","description":"","timestamp":"0001-01-01T00:00:00Z","images":[{"id":1,"image":"","issue_id":1}]}}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleIssueGet, "/issues/:id")
}

View File

@@ -0,0 +1,44 @@
package handlers
import (
"net/http"
"strconv"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/utils"
"github.com/julienschmidt/httprouter"
"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)
// HandleIssuesDelete godoc
// @Summary Delete an Issue by ID
// @Description Deletes an Issue by its ID
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param id path int true "Issue ID"
// @Success 200 {object} common.JSONMessage
// @Failure 400 {object} common.JSONError "Bad request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /issues/{id} [delete]
func HandleIssuesDelete(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.Atoi(params.ByName("id"))
if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
return
}
_, err = services.GetDB().GetIssues().Delete(id)
if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
return
}
utils.RespJSON(w, http.StatusOK, common.JSONMessage{
Message: "Deleted",
})
}

View File

@@ -0,0 +1,40 @@
package handlers_test
import (
"net/http"
"testing"
"otaupdate/handlers"
"otaupdate/services"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
th "github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestIssuesDelete(t *testing.T) {
db := services.GetDB()
db.SetIssues(&mocks.MockIssue{})
tests := []th.BasicHttpTest{
{
Name: "Invalid ID",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/issues/x", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"strconv.Atoi: parsing \"x\": invalid syntax","error":"Bad Request"}`,
},
{
Name: "Zero id",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/issues/0", nil),
ExpectedStatus: http.StatusBadRequest,
ExpectedResponse: `{"message":"id cannot be less than 0","error":"Bad Request"}`,
},
{
Name: "Good Request",
Request: th.MakeTestRequest(http.MethodDelete, "http://example.com/issues/1", nil),
ExpectedStatus: http.StatusOK,
ExpectedResponse: `{"message":"Deleted"}`,
},
}
th.RunParamHttpTests(t, tests, handlers.HandleIssuesDelete, "/issues/:id")
}

Some files were not shown because too many files have changed in this diff Show More