Add depot, attendant, jetfire, optimus, ota services with kustomize overlays
This commit is contained in:
46
services/ota_update_go/handlers/HandleGetCarsHMIKey.go
Normal file
46
services/ota_update_go/handlers/HandleGetCarsHMIKey.go
Normal 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))
|
||||
}
|
||||
92
services/ota_update_go/handlers/apicalls_get.go
Normal file
92
services/ota_update_go/handlers/apicalls_get.go
Normal 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
|
||||
}
|
||||
108
services/ota_update_go/handlers/apicalls_get_test.go
Normal file
108
services/ota_update_go/handlers/apicalls_get_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
37
services/ota_update_go/handlers/apitoken_add.go
Normal file
37
services/ota_update_go/handlers/apitoken_add.go
Normal 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))
|
||||
}
|
||||
56
services/ota_update_go/handlers/apitoken_add_test.go
Normal file
56
services/ota_update_go/handlers/apitoken_add_test.go
Normal 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)
|
||||
}
|
||||
53
services/ota_update_go/handlers/apitoken_delete.go
Normal file
53
services/ota_update_go/handlers/apitoken_delete.go
Normal 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"`
|
||||
}
|
||||
64
services/ota_update_go/handlers/apitoken_delete_test.go
Normal file
64
services/ota_update_go/handlers/apitoken_delete_test.go
Normal 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)
|
||||
}
|
||||
38
services/ota_update_go/handlers/apitoken_update.go
Normal file
38
services/ota_update_go/handlers/apitoken_update.go
Normal 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))
|
||||
}
|
||||
59
services/ota_update_go/handlers/apitoken_update_test.go
Normal file
59
services/ota_update_go/handlers/apitoken_update_test.go
Normal 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)
|
||||
}
|
||||
81
services/ota_update_go/handlers/apitokens_get.go
Normal file
81
services/ota_update_go/handlers/apitokens_get.go
Normal 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
|
||||
}
|
||||
125
services/ota_update_go/handlers/apitokens_get_test.go
Normal file
125
services/ota_update_go/handlers/apitokens_get_test.go
Normal 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)
|
||||
}
|
||||
44
services/ota_update_go/handlers/can_signal_list_get.go
Normal file
44
services/ota_update_go/handlers/can_signal_list_get.go
Normal 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})
|
||||
}
|
||||
48
services/ota_update_go/handlers/can_signal_list_get_test.go
Normal file
48
services/ota_update_go/handlers/can_signal_list_get_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
200
services/ota_update_go/handlers/can_signal_vin_get.go
Normal file
200
services/ota_update_go/handlers/can_signal_vin_get.go
Normal 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
|
||||
}
|
||||
73
services/ota_update_go/handlers/can_signal_vin_get_test.go
Normal file
73
services/ota_update_go/handlers/can_signal_vin_get_test.go
Normal file
File diff suppressed because one or more lines are too long
83
services/ota_update_go/handlers/car_configuration_update.go
Normal file
83
services/ota_update_go/handlers/car_configuration_update.go
Normal 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",
|
||||
})
|
||||
}
|
||||
77
services/ota_update_go/handlers/car_software_information.go
Normal file
77
services/ota_update_go/handlers/car_software_information.go
Normal 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
|
||||
}
|
||||
@@ -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"}
|
||||
153
services/ota_update_go/handlers/cars_allowed_access.go
Normal file
153
services/ota_update_go/handlers/cars_allowed_access.go
Normal 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"`
|
||||
}
|
||||
71
services/ota_update_go/handlers/cars_by_manifest.go
Normal file
71
services/ota_update_go/handlers/cars_by_manifest.go
Normal 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,
|
||||
})
|
||||
}
|
||||
116
services/ota_update_go/handlers/cars_by_manifest_get_test.go
Normal file
116
services/ota_update_go/handlers/cars_by_manifest_get_test.go
Normal 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)
|
||||
}
|
||||
104
services/ota_update_go/handlers/cars_change_access.go
Normal file
104
services/ota_update_go/handlers/cars_change_access.go
Normal 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"`
|
||||
}
|
||||
102
services/ota_update_go/handlers/carupdate_add.go
Normal file
102
services/ota_update_go/handlers/carupdate_add.go
Normal 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
|
||||
}
|
||||
317
services/ota_update_go/handlers/carupdate_add_test.go
Normal file
317
services/ota_update_go/handlers/carupdate_add_test.go
Normal 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
|
||||
}
|
||||
142
services/ota_update_go/handlers/carupdate_cancel.go
Normal file
142
services/ota_update_go/handlers/carupdate_cancel.go
Normal 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(¤t)
|
||||
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(¤t)
|
||||
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
|
||||
}
|
||||
138
services/ota_update_go/handlers/carupdate_cancel_test.go
Normal file
138
services/ota_update_go/handlers/carupdate_cancel_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
services/ota_update_go/handlers/carupdate_delete.go
Normal file
41
services/ota_update_go/handlers/carupdate_delete.go
Normal 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",
|
||||
})
|
||||
}
|
||||
38
services/ota_update_go/handlers/carupdate_delete_test.go
Normal file
38
services/ota_update_go/handlers/carupdate_delete_test.go
Normal 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)
|
||||
}
|
||||
135
services/ota_update_go/handlers/carupdate_deploy.go
Normal file
135
services/ota_update_go/handlers/carupdate_deploy.go
Normal 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(¤t)
|
||||
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
|
||||
}
|
||||
}
|
||||
156
services/ota_update_go/handlers/carupdate_deploy_test.go
Normal file
156
services/ota_update_go/handlers/carupdate_deploy_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
services/ota_update_go/handlers/carupdate_vehicle_cancel.go
Normal file
133
services/ota_update_go/handlers/carupdate_vehicle_cancel.go
Normal 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
|
||||
}
|
||||
151
services/ota_update_go/handlers/carupdate_vehicle_cancel_test.go
Normal file
151
services/ota_update_go/handlers/carupdate_vehicle_cancel_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
80
services/ota_update_go/handlers/carupdates_get.go
Normal file
80
services/ota_update_go/handlers/carupdates_get.go
Normal 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
|
||||
}
|
||||
143
services/ota_update_go/handlers/carupdates_get_test.go
Normal file
143
services/ota_update_go/handlers/carupdates_get_test.go
Normal 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)
|
||||
}
|
||||
76
services/ota_update_go/handlers/carupdates_log.go
Normal file
76
services/ota_update_go/handlers/carupdates_log.go
Normal 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
|
||||
}
|
||||
56
services/ota_update_go/handlers/carupdates_log_test.go
Normal file
56
services/ota_update_go/handlers/carupdates_log_test.go
Normal 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)
|
||||
}
|
||||
109
services/ota_update_go/handlers/carupdates_statuses.go
Normal file
109
services/ota_update_go/handlers/carupdates_statuses.go
Normal 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"`
|
||||
}
|
||||
40
services/ota_update_go/handlers/carupdates_statuses_test.go
Normal file
40
services/ota_update_go/handlers/carupdates_statuses_test.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
59
services/ota_update_go/handlers/dbc_signals_get.go
Normal file
59
services/ota_update_go/handlers/dbc_signals_get.go
Normal 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})
|
||||
}
|
||||
115
services/ota_update_go/handlers/dbc_signals_get_test.go
Normal file
115
services/ota_update_go/handlers/dbc_signals_get_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
21
services/ota_update_go/handlers/docs.go
Normal file
21
services/ota_update_go/handlers/docs.go
Normal 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, ",")
|
||||
}
|
||||
100
services/ota_update_go/handlers/docs_test.go
Normal file
100
services/ota_update_go/handlers/docs_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
194
services/ota_update_go/handlers/dtc_ecu_get.go
Normal file
194
services/ota_update_go/handlers/dtc_ecu_get.go
Normal 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
|
||||
}
|
||||
83
services/ota_update_go/handlers/dtc_ecu_get_test.go
Normal file
83
services/ota_update_go/handlers/dtc_ecu_get_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
*/
|
||||
}
|
||||
155
services/ota_update_go/handlers/ecu_stats_get.go
Normal file
155
services/ota_update_go/handlers/ecu_stats_get.go
Normal 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
|
||||
}
|
||||
71
services/ota_update_go/handlers/ecu_stats_get_test.go
Normal file
71
services/ota_update_go/handlers/ecu_stats_get_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
165
services/ota_update_go/handlers/ecu_stats_vin_get.go
Normal file
165
services/ota_update_go/handlers/ecu_stats_vin_get.go
Normal 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
|
||||
}
|
||||
82
services/ota_update_go/handlers/ecu_stats_vin_get_test.go
Normal file
82
services/ota_update_go/handlers/ecu_stats_vin_get_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
12
services/ota_update_go/handlers/errors.go
Normal file
12
services/ota_update_go/handlers/errors.go
Normal 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")
|
||||
46
services/ota_update_go/handlers/experiment.go
Normal file
46
services/ota_update_go/handlers/experiment.go
Normal 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)
|
||||
}
|
||||
472
services/ota_update_go/handlers/external_driver_handlers.go
Normal file
472
services/ota_update_go/handlers/external_driver_handlers.go
Normal 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")
|
||||
141
services/ota_update_go/handlers/flashpack_version_add.go
Normal file
141
services/ota_update_go/handlers/flashpack_version_add.go
Normal 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",
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
66
services/ota_update_go/handlers/flashpack_version_delete.go
Normal file
66
services/ota_update_go/handlers/flashpack_version_delete.go
Normal 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",
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
80
services/ota_update_go/handlers/fleet_add.go
Normal file
80
services/ota_update_go/handlers/fleet_add.go
Normal 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
|
||||
}
|
||||
45
services/ota_update_go/handlers/fleet_add_test.go
Normal file
45
services/ota_update_go/handlers/fleet_add_test.go
Normal 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")
|
||||
}
|
||||
68
services/ota_update_go/handlers/fleet_delete.go
Normal file
68
services/ota_update_go/handlers/fleet_delete.go
Normal 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"`
|
||||
}
|
||||
33
services/ota_update_go/handlers/fleet_delete_test.go
Normal file
33
services/ota_update_go/handlers/fleet_delete_test.go
Normal 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")
|
||||
}
|
||||
128
services/ota_update_go/handlers/fleet_filter_add.go
Normal file
128
services/ota_update_go/handlers/fleet_filter_add.go
Normal 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
|
||||
}
|
||||
68
services/ota_update_go/handlers/fleet_filter_add_test.go
Normal file
68
services/ota_update_go/handlers/fleet_filter_add_test.go
Normal 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")
|
||||
}
|
||||
74
services/ota_update_go/handlers/fleet_filter_delete.go
Normal file
74
services/ota_update_go/handlers/fleet_filter_delete.go
Normal 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"`
|
||||
}
|
||||
33
services/ota_update_go/handlers/fleet_filter_delete_test.go
Normal file
33
services/ota_update_go/handlers/fleet_filter_delete_test.go
Normal 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")
|
||||
}
|
||||
80
services/ota_update_go/handlers/fleet_filter_get_list.go
Normal file
80
services/ota_update_go/handlers/fleet_filter_get_list.go
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
111
services/ota_update_go/handlers/fleet_filter_update.go
Normal file
111
services/ota_update_go/handlers/fleet_filter_update.go
Normal 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"`
|
||||
}
|
||||
56
services/ota_update_go/handlers/fleet_filter_update_test.go
Normal file
56
services/ota_update_go/handlers/fleet_filter_update_test.go
Normal 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")
|
||||
}
|
||||
63
services/ota_update_go/handlers/fleet_get.go
Normal file
63
services/ota_update_go/handlers/fleet_get.go
Normal 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"`
|
||||
}
|
||||
73
services/ota_update_go/handlers/fleet_get_list.go
Normal file
73
services/ota_update_go/handlers/fleet_get_list.go
Normal 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))
|
||||
}
|
||||
59
services/ota_update_go/handlers/fleet_get_list_test.go
Normal file
59
services/ota_update_go/handlers/fleet_get_list_test.go
Normal 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")
|
||||
}
|
||||
50
services/ota_update_go/handlers/fleet_get_test.go
Normal file
50
services/ota_update_go/handlers/fleet_get_test.go
Normal 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")
|
||||
}
|
||||
149
services/ota_update_go/handlers/fleet_update.go
Normal file
149
services/ota_update_go/handlers/fleet_update.go
Normal 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
|
||||
}
|
||||
144
services/ota_update_go/handlers/fleet_update_test.go
Normal file
144
services/ota_update_go/handlers/fleet_update_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
134
services/ota_update_go/handlers/fleet_vehicle_get_list.go
Normal file
134
services/ota_update_go/handlers/fleet_vehicle_get_list.go
Normal 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"`
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
141
services/ota_update_go/handlers/fleet_vehicles_add.go
Normal file
141
services/ota_update_go/handlers/fleet_vehicles_add.go
Normal 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"`
|
||||
}
|
||||
153
services/ota_update_go/handlers/fleet_vehicles_add_test.go
Normal file
153
services/ota_update_go/handlers/fleet_vehicles_add_test.go
Normal 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")
|
||||
}
|
||||
85
services/ota_update_go/handlers/fleet_vehicles_delete.go
Normal file
85
services/ota_update_go/handlers/fleet_vehicles_delete.go
Normal 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"`
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
100
services/ota_update_go/handlers/fleetupdate_add.go
Normal file
100
services/ota_update_go/handlers/fleetupdate_add.go
Normal 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
|
||||
}
|
||||
188
services/ota_update_go/handlers/fleetupdate_add_test.go
Normal file
188
services/ota_update_go/handlers/fleetupdate_add_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
58
services/ota_update_go/handlers/get_car_configuration.go
Normal file
58
services/ota_update_go/handlers/get_car_configuration.go
Normal 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)
|
||||
}
|
||||
65
services/ota_update_go/handlers/get_car_version.go
Normal file
65
services/ota_update_go/handlers/get_car_version.go
Normal 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()
|
||||
}
|
||||
56
services/ota_update_go/handlers/get_car_version_logs.go
Normal file
56
services/ota_update_go/handlers/get_car_version_logs.go
Normal 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,
|
||||
})
|
||||
}
|
||||
94
services/ota_update_go/handlers/get_car_version_logs_test.go
Normal file
94
services/ota_update_go/handlers/get_car_version_logs_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
50
services/ota_update_go/handlers/get_car_version_test.go
Normal file
50
services/ota_update_go/handlers/get_car_version_test.go
Normal 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")
|
||||
}
|
||||
72
services/ota_update_go/handlers/guest_token_test.go
Normal file
72
services/ota_update_go/handlers/guest_token_test.go
Normal 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
|
||||
}
|
||||
48
services/ota_update_go/handlers/issue_get.go
Normal file
48
services/ota_update_go/handlers/issue_get.go
Normal 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,
|
||||
})
|
||||
}
|
||||
41
services/ota_update_go/handlers/issue_get_test.go
Normal file
41
services/ota_update_go/handlers/issue_get_test.go
Normal 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")
|
||||
}
|
||||
44
services/ota_update_go/handlers/issues_delete.go
Normal file
44
services/ota_update_go/handlers/issues_delete.go
Normal 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",
|
||||
})
|
||||
}
|
||||
40
services/ota_update_go/handlers/issues_delete_test.go
Normal file
40
services/ota_update_go/handlers/issues_delete_test.go
Normal 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
Reference in New Issue
Block a user