New API to support org/env instead of scope
diff --git a/README.md b/README.md
index 699a2f6..39fb159 100644
--- a/README.md
+++ b/README.md
@@ -24,9 +24,11 @@
1. Each time a Snapshot is received, create an in-memory cache for data scope
2. Each time a changeList is received, if data_scope info changed, then insert/delete info for changed scope from tenantCache
3. Developer info cache will be invalidated periodically and populated when 1st request for that apiKey comes in
-3. Initialize POST /analytics/{scope_uuid} API
-4. Upon receiving POST requests
- 1. Validate and enrich each batch of analytics records
+3. Initialize POST /analytics/{scope_uuid} and POST /analytics API's
+4. Upon receiving requests
+ 1. Validate and enrich each batch of analytics records. If scope_uuid is given, then that is used to validate.
+ If scope_uuid is not provided, then the payload should have organization and environment. The org/env
+ is then used to validate the scope for this cluster.
1. If developerCache does not have info for apiKey then get from DB and insert into cache.
This way the cache will only have info for developers/app with active traffic
2. If valid, then publish records to an internal buffer channel
@@ -53,5 +55,7 @@
### Exposed API
```sh
POST /analytics/{bundle_scope_uuid}
+POST /analytics
+
```
Complete spec is listed in `api.yaml`
diff --git a/api.go b/api.go
index 760407c..b84fdac 100644
--- a/api.go
+++ b/api.go
@@ -37,6 +37,8 @@
analyticsBasePath = config.GetString(configAnalyticsBasePath)
services.API().HandleFunc(analyticsBasePath+"/{bundle_scope_uuid}",
saveAnalyticsRecord).Methods("POST")
+ services.API().HandleFunc(analyticsBasePath,
+ processAnalyticsRecord).Methods("POST")
}
func saveAnalyticsRecord(w http.ResponseWriter, r *http.Request) {
@@ -70,12 +72,63 @@
"UNKNOWN_SCOPE", dbErr.Reason)
}
} else {
- err := processPayload(tenant, scopeuuid, r)
+ body, err := getJsonBody(r)
if err.ErrorCode == "" {
- w.WriteHeader(http.StatusOK)
+ err = validateEnrichPublish(tenant, body)
+ if err.ErrorCode == "" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ }
+ writeError(w, http.StatusBadRequest, err.ErrorCode, err.Reason)
+ }
+}
+
+func processAnalyticsRecord(w http.ResponseWriter, r *http.Request) {
+
+ w.Header().Set("Content-Type", "application/json; charset=UTF-8")
+
+ db := getDB() // When database isnt initialized
+ if db == nil {
+ writeError(w, http.StatusInternalServerError,
+ "INTERNAL_SERVER_ERROR",
+ "Service is not initialized completely")
+ return
+ }
+
+ if !strings.EqualFold(r.Header.Get("Content-Type"), "application/json") {
+ writeError(w, http.StatusBadRequest, "UNSUPPORTED_CONTENT_TYPE",
+ "Only supported content type is application/json")
+ return
+ }
+
+ body, err := getJsonBody(r)
+ if err.ErrorCode == "" {
+ tenant, e := getTenantFromPayload(body)
+ if e.ErrorCode == "" {
+ _, dbErr := validateTenant(&tenant)
+ if dbErr.ErrorCode != "" {
+ switch dbErr.ErrorCode {
+ case "INTERNAL_SEARCH_ERROR":
+ writeError(w, http.StatusInternalServerError,
+ "INTERNAL_SEARCH_ERROR", dbErr.Reason)
+ case "UNKNOWN_SCOPE":
+ writeError(w, http.StatusBadRequest,
+ "UNKNOWN_SCOPE", dbErr.Reason)
+ }
+ return
+ } else {
+ err = validateEnrichPublish(tenant, body)
+ if err.ErrorCode == "" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+ }
} else {
writeError(w, http.StatusBadRequest,
- err.ErrorCode, err.Reason)
+ e.ErrorCode, e.Reason)
+ return
}
}
+ writeError(w, http.StatusBadRequest, err.ErrorCode, err.Reason)
}
diff --git a/api.yaml b/api.yaml
index 1ea4597..0414d5b 100644
--- a/api.yaml
+++ b/api.yaml
@@ -26,6 +26,32 @@
produces:
- application/json
paths:
+ '/analytics':
+ x-swagger-router-controller: analytics
+ parameters:
+ - name: analytics_data
+ in: body
+ description: The analytics data you want to post
+ required: true
+ schema:
+ $ref: "#/definitions/analytics_data"
+ post:
+ responses:
+ "200":
+ description: Success
+ "400":
+ description: Bad Request
+ schema:
+ $ref: "#/definitions/errClientError"
+ "500":
+ description: Server error
+ schema:
+ $ref: "#/definitions/errServerError"
+ default:
+ description: Error
+ schema:
+ $ref: "#/definitions/errResponse"
+
'/analytics/{bundle_scope_uuid}':
x-swagger-router-controller: analytics
parameters:
@@ -34,7 +60,7 @@
required: true
description: bundle UUID that can be mapped to a scope by APID
type: string
- - name: analytics_data
+ - name: records
in: body
description: The analytics data you want to post
required: true
@@ -58,6 +84,53 @@
$ref: "#/definitions/errResponse"
definitions:
+ analytics_data:
+ type: object
+ required:
+ - organization
+ - environment
+ - records
+ properties:
+ organization:
+ type: string
+ environment:
+ type: string
+ records:
+ type: object
+ $ref: "#/definitions/records"
+ example: {
+ "organization":"orgname",
+ "enironment":"envname",
+ "records":[{
+ "response_status_code": 400,
+ "client_received_start_timestamp": 1462850097576,
+ "client_received_end_timestamp": 1462850097580,
+ "client_id":"0GJKn7EQmNkKYcGL7x3gHaawWLs5gUPr"
+ },{
+ "response_status_code": 200,
+ "client_id":"2ngXgr6Rl2PXWiEmbt8zCkWY3Ptjb8ep",
+ "request_verb" : "GET",
+ "api_product":" test_product",
+ "access_token" : "fewGWG343LDV346345YCDS",
+ "apiproxy" : "OAuthProxy",
+ "apiproxy_revision" : "2",
+ "client_ip": "10.16.9.11",
+ "client_sent_end_timestamp": 1462850097894,
+ "client_received_start_timestamp": 1462850097576,
+ "client_received_end_timestamp": 1462850097580,
+ "client_sent_start_timestamp": 1462850097894,
+ "request_path" : "/oauth/oauthv2/auth_code/",
+ "request_uri": "/oauth/oauthv2/auth_code/?response_type=code&redirect_url=http%3A%2F%2Fexample.com&client_id=A1h6yYAVeADnEKji8M37zCSn6olcmQDB",
+ "useragent" : "Chrome",
+ "target" : "target_name",
+ "target_received_end_timestamp": 1462850097800,
+ "target_received_start_timestamp": 1462850097800,
+ "target_response_code" : 200,
+ "target_sent_end_timestamp" : 1462850097802,
+ "target_sent_start_timestamp" : 1462850097802
+ }]
+ }
+
records:
type: object
required:
@@ -72,6 +145,7 @@
"records":[{
"response_status_code": 400,
"client_received_start_timestamp": 1462850097576,
+ "client_received_end_timestamp": 1462850097580,
"client_id":"0GJKn7EQmNkKYcGL7x3gHaawWLs5gUPr"
},{
"response_status_code": 200,
@@ -103,13 +177,18 @@
type: object
required:
- client_received_start_timestamp
+ - client_received_end_timestamp
properties:
client_received_start_timestamp:
type: integer
format: int64
+ client_received_end_timestamp:
+ type: integer
+ format: int64
example: {
"response_status_code":400,
"client_received_start_timestamp":1462850097576,
+ "client_received_end_timestamp": 1462850097580,
"client_id":"0GJKn7EQmNkKYcGL7x3gHaawWLs5gUPr"
}
diff --git a/api_helper.go b/api_helper.go
index 3b6f63d..5e49016 100644
--- a/api_helper.go
+++ b/api_helper.go
@@ -46,11 +46,11 @@
TenantId string
}
-func processPayload(tenant tenant, scopeuuid string, r *http.Request) errResponse {
+func getJsonBody(r *http.Request) (map[string]interface{}, errResponse) {
var gzipEncoded bool
if r.Header.Get("Content-Encoding") != "" {
if !strings.EqualFold(r.Header.Get("Content-Encoding"), "gzip") {
- return errResponse{
+ return nil, errResponse{
ErrorCode: "UNSUPPORTED_CONTENT_ENCODING",
Reason: "Only supported content encoding is gzip"}
} else {
@@ -63,7 +63,7 @@
if gzipEncoded {
reader, err = gzip.NewReader(r.Body) // reader for gzip encoded data
if err != nil {
- return errResponse{
+ return nil, errResponse{
ErrorCode: "BAD_DATA",
Reason: "Gzip Encoded data cannot be read"}
}
@@ -71,30 +71,60 @@
reader = r.Body
}
- errMessage := validateEnrichPublish(tenant, scopeuuid, reader)
- if errMessage.ErrorCode != "" {
- return errMessage
- }
- return errResponse{}
-}
-
-func validateEnrichPublish(tenant tenant, scopeuuid string, reader io.Reader) errResponse {
var raw map[string]interface{}
decoder := json.NewDecoder(reader) // Decode payload to JSON data
decoder.UseNumber()
if err := decoder.Decode(&raw); err != nil {
- return errResponse{ErrorCode: "BAD_DATA",
+ return nil, errResponse{ErrorCode: "BAD_DATA",
Reason: "Not a valid JSON payload"}
}
+ return raw, errResponse{}
+}
+
+/*
+Get tenant from payload based on the 2 required fields - organization and environment
+*/
+func getTenantFromPayload(raw map[string]interface{}) (tenant, errResponse) {
+ elems := []string{"organization", "environment"}
+ for _, elem := range elems {
+ if raw[elem] == nil || raw[elem].(string) == "" {
+ return tenant{}, errResponse{
+ ErrorCode: "MISSING_FIELD",
+ Reason: "Missing Required field: " + elem}
+ }
+ }
+
+ org := raw["organization"].(string)
+ env := raw["environment"].(string)
+ return tenant{Org: org, Env: env}, errResponse{}
+}
+
+func validateEnrichPublish(tenant tenant, raw map[string]interface{}) errResponse {
if records := raw["records"]; records != nil {
+ records, isArray := records.([]interface{})
+ if !isArray {
+ return errResponse{
+ ErrorCode: "BAD_DATA",
+ Reason: "records should be a list of analytics records"}
+ }
+ if len(records) == 0 {
+ return errResponse{
+ ErrorCode: "NO_RECORDS",
+ Reason: "No analytics records in the payload"}
+ }
// Iterate through each record to validate and enrich it
- for _, eachRecord := range records.([]interface{}) {
- recordMap := eachRecord.(map[string]interface{})
+ for _, eachRecord := range records {
+ recordMap, isMap := eachRecord.(map[string]interface{})
+ if !isMap {
+ return errResponse{
+ ErrorCode: "BAD_DATA",
+ Reason: "Each Analytics record in records should be a json object"}
+ }
valid, err := validate(recordMap)
if valid {
- enrich(recordMap, scopeuuid, tenant)
+ enrich(recordMap, tenant)
} else {
// Even if there is one bad record, then reject entire batch
return err
@@ -102,7 +132,7 @@
}
axRecords := axRecords{
Tenant: tenant,
- Records: records.([]interface{})}
+ Records: records}
// publish batch of records to channel (blocking call)
internalBuffer <- axRecords
} else {
@@ -116,7 +146,8 @@
/*
Does basic validation on each analytics message
1. client_received_start_timestamp, client_received_end_timestamp should exist
-2. client_received_end_timestamp should be > client_received_start_timestamp and not 0
+2. client_received_start_timestamp, client_received_end_timestamp should be a number
+3. client_received_end_timestamp should be > client_received_start_timestamp and not 0
*/
func validate(recordMap map[string]interface{}) (bool, errResponse) {
elems := []string{"client_received_start_timestamp", "client_received_end_timestamp"}
@@ -131,12 +162,19 @@
crst, exists1 := recordMap["client_received_start_timestamp"]
cret, exists2 := recordMap["client_received_end_timestamp"]
if exists1 && exists2 {
- if crst.(json.Number) == json.Number("0") || cret.(json.Number) == json.Number("0") {
+ crst, isNumber1 := crst.(json.Number)
+ cret, isNumber2 := cret.(json.Number)
+ if !isNumber1 || !isNumber2 {
+ return false, errResponse{
+ ErrorCode: "BAD_DATA",
+ Reason: "client_received_start_timestamp and " +
+ "client_received_end_timestamp has to be number"}
+ } else if crst == json.Number("0") || cret == json.Number("0") {
return false, errResponse{
ErrorCode: "BAD_DATA",
Reason: "client_received_start_timestamp or " +
- "> client_received_end_timestamp cannot be 0"}
- } else if crst.(json.Number) > cret.(json.Number) {
+ "client_received_end_timestamp cannot be 0"}
+ } else if crst > cret {
return false, errResponse{
ErrorCode: "BAD_DATA",
Reason: "client_received_start_timestamp " +
@@ -148,39 +186,33 @@
/*
Enrich each record by adding org and env fields
-It also finds add developer related information based on the apiKey
+It also finds and adds developer related information based on the apiKey if not already present in the payload
*/
-func enrich(recordMap map[string]interface{}, scopeuuid string, tenant tenant) {
- org, orgExists := recordMap["organization"]
- if !orgExists || org.(string) == "" {
- recordMap["organization"] = tenant.Org
- }
-
- env, envExists := recordMap["environment"]
- if !envExists || env.(string) == "" {
- recordMap["environment"] = tenant.Env
- }
+func enrich(recordMap map[string]interface{}, tenant tenant) {
+ // Always overwrite organization/environment value with the tenant information provided in the payload
+ recordMap["organization"] = tenant.Org
+ recordMap["environment"] = tenant.Env
apiKey, exists := recordMap["client_id"]
// apiKey doesnt exist then ignore adding developer fields
- if exists {
- apiKey := apiKey.(string)
- if apiKey != "" {
+ if exists && apiKey != nil {
+ apiKey, isString := apiKey.(string)
+ if isString {
devInfo := getDeveloperInfo(tenant.TenantId, apiKey)
- _, exists := recordMap["api_product"]
- if !exists {
+ ap, exists := recordMap["api_product"]
+ if !exists || ap == nil {
recordMap["api_product"] = devInfo.ApiProduct
}
- _, exists = recordMap["developer_app"]
- if !exists {
+ da, exists := recordMap["developer_app"]
+ if !exists || da == nil {
recordMap["developer_app"] = devInfo.DeveloperApp
}
- _, exists = recordMap["developer_email"]
- if !exists {
+ de, exists := recordMap["developer_email"]
+ if !exists || de == nil {
recordMap["developer_email"] = devInfo.DeveloperEmail
}
- _, exists = recordMap["developer"]
- if !exists {
+ d, exists := recordMap["developer"]
+ if !exists || d == nil {
recordMap["developer"] = devInfo.Developer
}
}
diff --git a/api_helper_test.go b/api_helper_test.go
index 2f96267..b7f8910 100644
--- a/api_helper_test.go
+++ b/api_helper_test.go
@@ -23,6 +23,38 @@
)
// BeforeSuite setup and AfterSuite cleanup is in apidAnalytics_suite_test.go
+var _ = Describe("test getTenantFromPayload()", func() {
+ Context("invalid record", func() {
+ It("should return invalid record", func() {
+ By("payload with missing required keys")
+
+ var payload = []byte(`{
+ "records":[{
+ "response_status_code": 200,
+ "client_id":"testapikey"
+ }]}`)
+ raw := getRaw(payload)
+ _, e := getTenantFromPayload(raw)
+ Expect(e.ErrorCode).To(Equal("MISSING_FIELD"))
+ })
+ })
+ Context("valid record", func() {
+ It("should return tenant with org and env", func() {
+ var payload = []byte(`{
+ "organization":"testorg",
+ "environment":"testenv",
+ "records":[{
+ "response_status_code": 200,
+ "client_id":"testapikey"
+ }]}`)
+ raw := getRaw(payload)
+ tenant, _ := getTenantFromPayload(raw)
+ Expect(tenant.Org).To(Equal("testorg"))
+ Expect(tenant.Env).To(Equal("testenv"))
+ })
+ })
+})
+
var _ = Describe("test valid() directly", func() {
Context("invalid record", func() {
It("should return invalid record", func() {
@@ -66,7 +98,35 @@
Expect(valid).To(BeFalse())
Expect(e.ErrorCode).To(Equal("BAD_DATA"))
Expect(e.Reason).To(Equal("client_received_start_timestamp or " +
- "> client_received_end_timestamp cannot be 0"))
+ "client_received_end_timestamp cannot be 0"))
+
+ By("payload with clst = null")
+ record = []byte(`{
+ "response_status_code": 200,
+ "client_id":"testapikey",
+ "client_received_start_timestamp": null,
+ "client_received_end_timestamp": 1486406248260
+ }`)
+ raw = getRaw(record)
+ valid, e = validate(raw)
+
+ Expect(valid).To(BeFalse())
+ Expect(e.ErrorCode).To(Equal("MISSING_FIELD"))
+
+ By("payload with clst as a string")
+ record = []byte(`{
+ "response_status_code": 200,
+ "client_id":"testapikey",
+ "client_received_start_timestamp": "",
+ "client_received_end_timestamp": 1486406248260
+ }`)
+ raw = getRaw(record)
+ valid, e = validate(raw)
+
+ Expect(valid).To(BeFalse())
+ Expect(e.ErrorCode).To(Equal("BAD_DATA"))
+ Expect(e.Reason).To(Equal("client_received_start_timestamp and " +
+ "client_received_end_timestamp has to be number"))
})
})
Context("valid record", func() {
@@ -96,7 +156,7 @@
raw := getRaw(record)
tenant := tenant{Org: "testorg", Env: "testenv", TenantId: "tenantid"}
- enrich(raw, "testid", tenant)
+ enrich(raw, tenant)
Expect(raw["organization"]).To(Equal(tenant.Org))
Expect(raw["environment"]).To(Equal(tenant.Env))
@@ -105,6 +165,27 @@
Expect(raw["developer_email"]).To(Equal("testdeveloper@test.com"))
Expect(raw["developer"]).To(Equal("testdeveloper"))
})
+ It("developer related fields should be added only if not already existing or value is null", func() {
+ var record = []byte(`{
+ "response_status_code": 200,
+ "client_id":"testapikey",
+ "client_received_start_timestamp": 1486406248277,
+ "client_received_end_timestamp": 1486406248290,
+ "api_product":"test_prod",
+ "developer_app":null
+ }`)
+
+ raw := getRaw(record)
+ tenant := tenant{Org: "testorg", Env: "testenv", TenantId: "tenantid"}
+ enrich(raw, tenant)
+
+ Expect(raw["organization"]).To(Equal(tenant.Org))
+ Expect(raw["environment"]).To(Equal(tenant.Env))
+ Expect(raw["api_product"]).To(Equal("test_prod"))
+ Expect(raw["developer_app"]).To(Equal("testapp"))
+ Expect(raw["developer_email"]).To(Equal("testdeveloper@test.com"))
+ Expect(raw["developer"]).To(Equal("testdeveloper"))
+ })
})
Context("enrich record where no apikey is set", func() {
@@ -117,7 +198,7 @@
raw := getRaw(record)
tenant := tenant{Org: "testorg", Env: "testenv", TenantId: "tenantid"}
- enrich(raw, "testid", tenant)
+ enrich(raw, tenant)
Expect(raw["organization"]).To(Equal(tenant.Org))
Expect(raw["environment"]).To(Equal(tenant.Env))
diff --git a/api_test.go b/api_test.go
index 1c253c6..96664c8 100644
--- a/api_test.go
+++ b/api_test.go
@@ -30,15 +30,10 @@
var _ = Describe("POST /analytics/{scopeuuid}", func() {
Context("invalid content type header", func() {
It("should return bad request", func() {
- uri, err := url.Parse(testServer.URL)
- uri.Path = fmt.Sprintf(analyticsBasePath+"/%s", "test")
- Expect(err).ShouldNot(HaveOccurred())
-
- req, _ := http.NewRequest("POST", uri.String(), nil)
+ req := getRequestWithScope("test", nil)
req.Header.Set("Content-Type", "application/x-gzip")
res, e := makeRequest(req)
-
Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
Expect(e.ErrorCode).To(Equal("UNSUPPORTED_CONTENT_TYPE"))
})
@@ -46,16 +41,10 @@
Context("invalid content encoding header", func() {
It("should return bad request", func() {
- uri, err := url.Parse(testServer.URL)
- uri.Path = fmt.Sprintf(analyticsBasePath+"/%s", "testid")
- Expect(err).ShouldNot(HaveOccurred())
-
- req, _ := http.NewRequest("POST", uri.String(), nil)
- req.Header.Set("Content-Type", "application/json")
+ req := getRequestWithScope("testid", nil)
req.Header.Set("Content-Encoding", "application/gzip")
res, e := makeRequest(req)
-
Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
Expect(e.ErrorCode).To(Equal("UNSUPPORTED_CONTENT_ENCODING"))
})
@@ -63,15 +52,9 @@
Context("invalid scopeuuid", func() {
It("should return bad request", func() {
- uri, err := url.Parse(testServer.URL)
- uri.Path = fmt.Sprintf(analyticsBasePath+"/%s", "wrongid")
- Expect(err).ShouldNot(HaveOccurred())
-
- req, _ := http.NewRequest("POST", uri.String(), nil)
- req.Header.Set("Content-Type", "application/json")
+ req := getRequestWithScope("wrongid", nil)
res, e := makeRequest(req)
-
Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
Expect(e.ErrorCode).To(Equal("UNKNOWN_SCOPE"))
})
@@ -82,20 +65,13 @@
db := getDB()
setDB(nil)
- uri, err := url.Parse(testServer.URL)
- uri.Path = fmt.Sprintf(analyticsBasePath+"/%s", "testid")
- Expect(err).ShouldNot(HaveOccurred())
-
- req, _ := http.NewRequest("POST", uri.String(), nil)
- req.Header.Set("Content-Type", "application/json")
+ req := getRequestWithScope("testid", nil)
res, e := makeRequest(req)
-
Expect(res.StatusCode).To(Equal(http.StatusInternalServerError))
Expect(e.ErrorCode).To(Equal("INTERNAL_SERVER_ERROR"))
setDB(db)
-
})
})
@@ -103,60 +79,34 @@
It("should return bad request", func() {
By("no payload")
- uri, err := url.Parse(testServer.URL)
- uri.Path = fmt.Sprintf(analyticsBasePath+"/%s", "testid")
- Expect(err).ShouldNot(HaveOccurred())
-
- req, _ := http.NewRequest("POST", uri.String(), nil)
- req.Header.Set("Content-Type", "application/json")
-
+ req := getRequestWithScope("testid", nil)
res, e := makeRequest(req)
-
Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
Expect(e.ErrorCode).To(Equal("BAD_DATA"))
Expect(e.Reason).To(Equal("Not a valid JSON payload"))
By("payload with 0 records")
- uri, err = url.Parse(testServer.URL)
- uri.Path = fmt.Sprintf(analyticsBasePath+"/%s", "testid")
- Expect(err).ShouldNot(HaveOccurred())
-
var payload = []byte(`{}`)
- req, _ = http.NewRequest("POST", uri.String(), bytes.NewReader(payload))
- req.Header.Set("Content-Type", "application/json")
-
+ req = getRequestWithScope("testid", payload)
res, e = makeRequest(req)
-
Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
Expect(e.ErrorCode).To(Equal("NO_RECORDS"))
By("set content encoding to gzip but send json data")
- uri, err = url.Parse(testServer.URL)
- uri.Path = fmt.Sprintf(analyticsBasePath+"/%s", "testid")
- Expect(err).ShouldNot(HaveOccurred())
-
payload = []byte(`{
"records":[{
"response_status_code": 200,
"client_id":"testapikey"
}]
}`)
-
- req, _ = http.NewRequest("POST", uri.String(), bytes.NewReader(payload))
- req.Header.Set("Content-Type", "application/json")
+ req = getRequestWithScope("testid", payload)
req.Header.Set("Content-Encoding", "gzip")
-
res, e = makeRequest(req)
-
Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
Expect(e.ErrorCode).To(Equal("BAD_DATA"))
Expect(e.Reason).To(Equal("Gzip Encoded data cannot be read"))
By("1 bad record")
- uri, err = url.Parse(testServer.URL)
- uri.Path = fmt.Sprintf(analyticsBasePath+"/%s", "testid")
- Expect(err).ShouldNot(HaveOccurred())
-
payload = []byte(`{
"records":[{
"response_status_code": 200,
@@ -168,12 +118,8 @@
"client_id":"testapikey"
}]
}`)
-
- req, _ = http.NewRequest("POST", uri.String(), bytes.NewReader(payload))
- req.Header.Set("Content-Type", "application/json")
-
+ req = getRequestWithScope("testid", payload)
res, e = makeRequest(req)
-
Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
Expect(e.ErrorCode).To(Equal("MISSING_FIELD"))
})
@@ -181,10 +127,6 @@
Context("valid payload", func() {
It("should return successfully", func() {
- uri, err := url.Parse(testServer.URL)
- uri.Path = fmt.Sprintf(analyticsBasePath+"/%s", "testid")
- Expect(err).ShouldNot(HaveOccurred())
-
var payload = []byte(`{
"records":[{
"response_status_code": 200,
@@ -193,17 +135,195 @@
"client_received_end_timestamp": 1486406248290
}]
}`)
-
- req, _ := http.NewRequest("POST", uri.String(), bytes.NewReader(payload))
- req.Header.Set("Content-Type", "application/json")
-
+ req := getRequestWithScope("testid", payload)
res, _ := makeRequest(req)
-
Expect(res.StatusCode).To(Equal(http.StatusOK))
})
})
})
+var _ = Describe("POST /analytics", func() {
+ Context("invalid content type header", func() {
+ It("should return bad request", func() {
+ req := getRequest(nil)
+ req.Header.Set("Content-Type", "application/x-gzip")
+ res, e := makeRequest(req)
+ Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
+ Expect(e.ErrorCode).To(Equal("UNSUPPORTED_CONTENT_TYPE"))
+ })
+ })
+
+ Context("invalid content encoding header", func() {
+ It("should return bad request", func() {
+ req := getRequest(nil)
+ req.Header.Set("Content-Encoding", "application/gzip")
+ res, e := makeRequest(req)
+ Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
+ Expect(e.ErrorCode).To(Equal("UNSUPPORTED_CONTENT_ENCODING"))
+ })
+ })
+
+ Context("Unitialized DB", func() {
+ It("should return internal server error", func() {
+ db := getDB()
+ setDB(nil)
+
+ req := getRequest(nil)
+ res, e := makeRequest(req)
+ Expect(res.StatusCode).To(Equal(http.StatusInternalServerError))
+ Expect(e.ErrorCode).To(Equal("INTERNAL_SERVER_ERROR"))
+
+ setDB(db)
+ })
+ })
+
+ Context("bad payload", func() {
+ It("should return bad request", func() {
+
+ By("no payload")
+ req := getRequest(nil)
+ res, e := makeRequest(req)
+ Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
+ Expect(e.ErrorCode).To(Equal("BAD_DATA"))
+ Expect(e.Reason).To(Equal("Not a valid JSON payload"))
+
+ By("payload with no organization and environment")
+ var payload = []byte(`{}`)
+ req = getRequest(payload)
+ res, e = makeRequest(req)
+ Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
+ Expect(e.ErrorCode).To(Equal("MISSING_FIELD"))
+
+ By("payload with wrong organization and environment")
+ payload = []byte(`{
+ "organization":"wrongorg",
+ "environment":"wrongenv",
+ "records":[{
+ "response_status_code": 200,
+ "client_id":"testapikey"
+ }]
+ }`)
+ req = getRequest(payload)
+ res, e = makeRequest(req)
+ Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
+ Expect(e.ErrorCode).To(Equal("UNKNOWN_SCOPE"))
+
+ By("payload with no records")
+ payload = []byte(`{"organization":"testorg","environment":"testenv"}`)
+ req = getRequest(payload)
+ res, e = makeRequest(req)
+ Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
+ Expect(e.ErrorCode).To(Equal("NO_RECORDS"))
+
+ By("payload with 0 records")
+ payload = []byte(`{
+ "organization":"testorg",
+ "environment":"testenv",
+ "records":[]
+ }`)
+ req = getRequest(payload)
+ res, e = makeRequest(req)
+ Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
+ Expect(e.ErrorCode).To(Equal("NO_RECORDS"))
+
+ By("payload with records not as list")
+ payload = []byte(`{
+ "organization":"testorg",
+ "environment":"testenv",
+ "records":{}
+ }`)
+ req = getRequest(payload)
+ res, e = makeRequest(req)
+ Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
+ Expect(e.ErrorCode).To(Equal("BAD_DATA"))
+
+ By("payload with each record not a json in an array")
+ payload = []byte(`{
+ "organization":"testorg",
+ "environment":"testenv",
+ "records":[""]
+ }`)
+ req = getRequest(payload)
+ res, e = makeRequest(req)
+ Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
+ Expect(e.ErrorCode).To(Equal("BAD_DATA"))
+
+ By("set content encoding to gzip but send json data")
+ payload = []byte(`{
+ "organization":"testorg",
+ "environment":"testenv",
+ "records":[{
+ "response_status_code": 200,
+ "client_id":"testapikey"
+ }]
+ }`)
+ req = getRequest(payload)
+ req.Header.Set("Content-Encoding", "gzip")
+ res, e = makeRequest(req)
+ Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
+ Expect(e.ErrorCode).To(Equal("BAD_DATA"))
+ Expect(e.Reason).To(Equal("Gzip Encoded data cannot be read"))
+
+ By("1 bad record")
+ payload = []byte(`{
+ "organization":"testorg",
+ "environment":"testenv",
+ "records":[{
+ "response_status_code": 200,
+ "client_id":"testapikey",
+ "client_received_start_timestamp": 1486406248277,
+ "client_received_end_timestamp": 1486406248290
+ },{
+ "response_status_code": 200,
+ "client_id":"testapikey"
+ }]
+ }`)
+ req = getRequest(payload)
+ res, e = makeRequest(req)
+ Expect(res.StatusCode).To(Equal(http.StatusBadRequest))
+ Expect(e.ErrorCode).To(Equal("MISSING_FIELD"))
+ })
+ })
+
+ Context("valid payload", func() {
+ It("should return successfully", func() {
+ var payload = []byte(`{
+ "organization":"testorg",
+ "environment":"testenv",
+ "records":[{
+ "response_status_code": 200,
+ "client_id":"testapikey",
+ "client_received_start_timestamp": 1486406248277,
+ "client_received_end_timestamp": 1486406248290
+ }]
+ }`)
+ req := getRequest(payload)
+ res, _ := makeRequest(req)
+ Expect(res.StatusCode).To(Equal(http.StatusOK))
+ })
+ })
+})
+
+func getRequest(payload []byte) *http.Request {
+ uri, err := url.Parse(testServer.URL)
+ uri.Path = analyticsBasePath
+ Expect(err).ShouldNot(HaveOccurred())
+
+ req, _ := http.NewRequest("POST", uri.String(), bytes.NewReader(payload))
+ req.Header.Set("Content-Type", "application/json")
+ return req
+}
+
+func getRequestWithScope(scope string, payload []byte) *http.Request {
+ uri, err := url.Parse(testServer.URL)
+ uri.Path = fmt.Sprintf(analyticsBasePath+"/%s", scope)
+ Expect(err).ShouldNot(HaveOccurred())
+
+ req, _ := http.NewRequest("POST", uri.String(), bytes.NewReader(payload))
+ req.Header.Set("Content-Type", "application/json")
+ return req
+}
+
func makeRequest(req *http.Request) (*http.Response, errResponse) {
res, err := client.Do(req)
defer res.Body.Close()
@@ -212,6 +332,5 @@
var body errResponse
respBody, _ := ioutil.ReadAll(res.Body)
json.Unmarshal(respBody, &body)
-
return res, body
}
diff --git a/common_helper.go b/common_helper.go
index 813acc4..e67855a 100644
--- a/common_helper.go
+++ b/common_helper.go
@@ -172,6 +172,32 @@
TenantId: tenantId}, dbError{}
}
+/*
+Checks if given org/env exists is a valid scope for this apid cluster
+It also stores the scope i.e. tenant_id in the tenant object using pointer.
+tenant_id in combination with apiKey is used to find kms related information
+*/
+func validateTenant(tenant *tenant) (bool, dbError) {
+ db := getDB()
+ error := db.QueryRow("SELECT scope FROM edgex_data_scope"+
+ " where org = ? and env = ?", &tenant.Org, &tenant.Env).Scan(&tenant.TenantId)
+ switch {
+ case error == sql.ErrNoRows:
+ reason := "No tenant found for this org: " + tenant.Org + " and env:" + tenant.Env
+ errorCode := "UNKNOWN_SCOPE"
+ return false, dbError{
+ ErrorCode: errorCode,
+ Reason: reason}
+ case error != nil:
+ reason := error.Error()
+ errorCode := "INTERNAL_SEARCH_ERROR"
+ return false, dbError{
+ ErrorCode: errorCode,
+ Reason: reason}
+ }
+ return true, dbError{}
+}
+
// Returns developer info by querying DB directly
func getDevInfoFromDB(tenantId string, apiKey string) (developerInfo, error) {
var apiProduct, developerApp, developerEmail sql.NullString
diff --git a/common_helper_test.go b/common_helper_test.go
index 901d7e2..972f4b3 100644
--- a/common_helper_test.go
+++ b/common_helper_test.go
@@ -70,21 +70,23 @@
})
var _ = Describe("test getTenantFromDB()", func() {
- Context("get developerInfo for valid scopeuuid", func() {
- It("should return all right data", func() {
+ Context("get tenant for valid scopeuuid", func() {
+ It("should return testorg and testenv", func() {
tenant, dbError := getTenantFromDB("testid")
Expect(dbError.Reason).To(Equal(""))
Expect(tenant.TenantId).To(Equal("tenantid"))
+ Expect(tenant.Org).To(Equal("testorg"))
+ Expect(tenant.Env).To(Equal("testenv"))
+
})
})
- Context("get developerInfo for invalid scopeuuid", func() {
- It("should return error", func() {
+ Context("get tenant for invalid scopeuuid", func() {
+ It("should return error with unknown scope", func() {
tenant, dbError := getTenantFromDB("wrongid")
Expect(tenant.Org).To(Equal(""))
Expect(dbError.ErrorCode).To(Equal("UNKNOWN_SCOPE"))
})
})
-
})
var _ = Describe("test getDeveloperInfo()", func() {
@@ -154,6 +156,27 @@
})
+var _ = Describe("test validateTenant()", func() {
+ Context("validate tenant for org/env that exists in DB", func() {
+ It("should not return an error and tenantId should be populated", func() {
+ tenant := tenant{Org: "testorg", Env: "testenv"}
+ valid, dbError := validateTenant(&tenant)
+ Expect(valid).To(BeTrue())
+ Expect(tenant.TenantId).To(Equal("tenantid"))
+ Expect(dbError.ErrorCode).To(Equal(""))
+ })
+ })
+ Context("validate tenant for org/env that do not exist in DB", func() {
+ It("should return error with unknown_scope", func() {
+ tenant := tenant{Org: "wrongorg", Env: "wrongenv"}
+ valid, dbError := validateTenant(&tenant)
+ Expect(valid).To(BeFalse())
+ Expect(dbError.ErrorCode).To(Equal("UNKNOWN_SCOPE"))
+ })
+ })
+
+})
+
var _ = Describe("test getValuesIgnoringNull()", func() {
Context("Null sql value", func() {
It("should return empty string", func() {