| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package org.apache.unomi.itests; |
| |
| import org.apache.http.client.methods.CloseableHttpResponse; |
| import org.apache.http.client.methods.HttpPost; |
| import org.apache.http.entity.ContentType; |
| import org.apache.http.entity.StringEntity; |
| import org.apache.http.util.EntityUtils; |
| import org.apache.unomi.api.Event; |
| import org.apache.unomi.api.Metadata; |
| import org.apache.unomi.api.Scope; |
| import org.apache.unomi.api.conditions.Condition; |
| import org.apache.unomi.api.services.ScopeService; |
| import org.apache.unomi.itests.tools.httpclient.HttpClientThatWaitsForUnomi; |
| import org.apache.unomi.schema.api.JsonSchemaWrapper; |
| import org.apache.unomi.schema.api.SchemaService; |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.ops4j.pax.exam.junit.PaxExam; |
| import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; |
| import org.ops4j.pax.exam.spi.reactors.PerSuite; |
| import org.ops4j.pax.exam.util.Filter; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import javax.inject.Inject; |
| import java.io.IOException; |
| import java.util.*; |
| |
| import static org.junit.Assert.*; |
| |
| /** |
| * Class to tests the JSON schema features |
| */ |
| |
| @RunWith(PaxExam.class) |
| @ExamReactorStrategy(PerSuite.class) |
| public class JSONSchemaIT extends BaseIT { |
| private final static Logger LOGGER = LoggerFactory.getLogger(JSONSchemaIT.class); |
| private final static String EVENT_COLLECTOR_URL = "/cxs/eventcollector"; |
| private final static String JSONSCHEMA_URL = "/cxs/jsonSchema"; |
| private static final int DEFAULT_TRYING_TIMEOUT = 2000; |
| private static final int DEFAULT_TRYING_TRIES = 30; |
| public static final String DUMMY_SCOPE = "dummy_scope"; |
| |
| @Before |
| public void setUp() throws InterruptedException { |
| keepTrying("Couldn't find json schema endpoint", () -> get(JSONSCHEMA_URL, List.class), Objects::nonNull, DEFAULT_TRYING_TIMEOUT, |
| DEFAULT_TRYING_TRIES); |
| |
| TestUtils.createScope(DUMMY_SCOPE, "Dummy scope", scopeService); |
| keepTrying("Scope "+ DUMMY_SCOPE +" not found in the required time", () -> scopeService.getScope(DUMMY_SCOPE), |
| Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| } |
| |
| @After |
| public void tearDown() throws InterruptedException { |
| removeItems(JsonSchemaWrapper.class, Event.class, Scope.class); |
| // ensure all schemas have been cleaned from schemaService. |
| keepTrying("Should not find json schemas anymore", |
| () -> schemaService.getInstalledJsonSchemaIds(), |
| (list) -> (!list.contains("https://vendor.test.com/schemas/json/events/dummy/1-0-0") && |
| !list.contains("https://vendor.test.com/schemas/json/events/dummy/properties/1-0-0")), |
| DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| } |
| |
| @Test |
| public void testValidation_SaveDeleteSchemas() throws InterruptedException, IOException { |
| // check that event is not valid at first |
| assertFalse(schemaService |
| .isEventValid(resourceAsString("schemas/event-dummy-valid.json"), "dummy")); |
| |
| // Push schemas |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy.json")); |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties.json")); |
| keepTrying("Event should be valid", () -> schemaService |
| .isEventValid(resourceAsString("schemas/event-dummy-valid.json"), "dummy"), |
| isValid -> isValid, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| |
| // Test multiple invalid event: |
| // unevaluated property at root: |
| assertFalse(schemaService.isEventValid(resourceAsString("schemas/event-dummy-invalid-1.json"), |
| "dummy")); |
| // unevaluated property in properties: |
| assertFalse(schemaService.isEventValid(resourceAsString("schemas/event-dummy-invalid-2.json"), |
| "dummy")); |
| // bad type number but should be string: |
| assertFalse(schemaService.isEventValid(resourceAsString("schemas/event-dummy-invalid-3.json"), |
| "dummy")); |
| |
| // Event with unexisting scope: |
| assertFalse(schemaService.isEventValid(resourceAsString("schemas/event-dummy-invalid-4.json"), |
| "dummy")); |
| |
| // remove one of the schema: |
| assertTrue(schemaService.deleteSchema("https://vendor.test.com/schemas/json/events/dummy/properties/1-0-0")); |
| keepTrying("Event should be invalid since of the schema have been deleted", () -> schemaService |
| .isEventValid(resourceAsString("schemas/event-dummy-valid.json"), "dummy"), |
| isValid -> !isValid, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| } |
| |
| @Test |
| public void testValidation_UpdateSchema() throws InterruptedException, IOException { |
| // check that event is not valid at first |
| assertFalse(schemaService |
| .isEventValid(resourceAsString("schemas/event-dummy-valid.json"), "dummy")); |
| |
| // Push schemas |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy.json")); |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties.json")); |
| keepTrying("Event should be valid", () -> schemaService |
| .isEventValid(resourceAsString("schemas/event-dummy-valid.json"), "dummy"), |
| isValid -> isValid, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| |
| // Test the invalid event, that use the new prop "invalidPropName" in properties: |
| assertFalse(schemaService.isEventValid(resourceAsString("schemas/event-dummy-invalid-2.json"), |
| "dummy")); |
| |
| // update the schema to allow "invalidPropName": |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties-updated.json")); |
| keepTrying("Event should be valid since of the schema have been updated", () -> schemaService |
| .isEventValid(resourceAsString("schemas/event-dummy-invalid-2.json"), |
| "dummy"), isValid -> isValid, DEFAULT_TRYING_TIMEOUT, |
| DEFAULT_TRYING_TRIES); |
| } |
| |
| @Test |
| public void testExtension_SaveDelete() throws InterruptedException, IOException { |
| // Push base schemas |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy.json")); |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties.json")); |
| keepTrying("Event should be valid", () -> schemaService |
| .isEventValid(resourceAsString("schemas/event-dummy-valid.json"), "dummy"), |
| isValid -> isValid, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| |
| // check that extended event is not valid at first |
| assertFalse(schemaService.isEventValid(resourceAsString("schemas/event-dummy-extended.json"), |
| "dummy")); |
| |
| // register both extensions (for root event and the properties level) |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy-extension.json")); |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties-extension.json")); |
| keepTrying("Extended event should be valid since of the extensions have been deployed", () -> schemaService |
| .isEventValid(resourceAsString("schemas/event-dummy-extended.json"), "dummy"), |
| isValid -> isValid, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| |
| // delete one of the extension |
| schemaService.deleteSchema("https://vendor.test.com/schemas/json/events/dummy/properties/extension/1-0-0"); |
| keepTrying("Extended event should be invalid again, one necessary extension have been removed", () -> schemaService |
| .isEventValid(resourceAsString("schemas/event-dummy-extended.json"), "dummy"), |
| isValid -> !isValid, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| } |
| |
| @Test |
| public void testExtension_Update() throws InterruptedException, IOException { |
| // Push base schemas |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy.json")); |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties.json")); |
| keepTrying("Event should be valid", () -> schemaService |
| .isEventValid(resourceAsString("schemas/event-dummy-valid.json"), "dummy"), |
| isValid -> isValid, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| |
| // check that extended event is not valid at first |
| assertFalse(schemaService.isEventValid(resourceAsString("schemas/event-dummy-extended.json"), |
| "dummy")); |
| |
| // register both extensions (for root event and the properties level) |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy-extension.json")); |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties-extension.json")); |
| keepTrying("Extended event should be valid since of the extensions have been deployed", () -> schemaService |
| .isEventValid(resourceAsString("schemas/event-dummy-extended.json"), "dummy"), |
| isValid -> isValid, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| |
| // check that extended event 2 is not valid due to usage of unevaluatedProperty not bring by schemas or extensions |
| assertFalse(schemaService.isEventValid(resourceAsString("schemas/event-dummy-extended-2.json"), |
| "dummy")); |
| |
| // Update extensions to allow the extended event 2 |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties-extension-2.json")); |
| keepTrying("Extended event 2 should be valid since of the extensions have been updated", () -> schemaService |
| .isEventValid(resourceAsString("schemas/event-dummy-extended-2.json"), |
| "dummy"), isValid -> isValid, DEFAULT_TRYING_TIMEOUT, |
| DEFAULT_TRYING_TRIES); |
| } |
| |
| @Test |
| public void testEndPoint_GetInstalledJsonSchemas() { |
| List<String> jsonSchemas = get(JSONSCHEMA_URL, List.class); |
| assertFalse("JSON schema list should not be empty, it should contain predefined Unomi schemas", jsonSchemas.isEmpty()); |
| } |
| |
| @Test |
| public void testEndPoint_GetJsonSchemasById() throws Exception { |
| // Push base schemas |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy.json")); |
| schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties.json")); |
| |
| final String schemaId = "https://vendor.test.com/schemas/json/events/dummy/1-0-0"; |
| final HttpPost request = new HttpPost(getFullUrl(JSONSCHEMA_URL + "/query")); |
| |
| request.setEntity(new StringEntity(schemaId)); |
| |
| keepTrying("Should return a schema when calling the endpoint", () -> { |
| try (CloseableHttpResponse response = executeHttpRequest(request)) { |
| return EntityUtils.toString(response.getEntity()); |
| } catch (IOException e) { |
| LOGGER.error("Failed to get the json schema with the id: {}", schemaId); |
| } |
| return ""; |
| }, entity -> entity.contains("DummyEvent"), DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| } |
| |
| @Test |
| public void testEndPoint_SaveDelete() throws Exception { |
| assertNull(schemaService.getSchema("https://vendor.test.com/schemas/json/events/dummy/1-0-0")); |
| |
| // Post schema using REST call |
| try (CloseableHttpResponse response = post(JSONSCHEMA_URL, "schemas/schema-dummy.json", ContentType.TEXT_PLAIN)) { |
| assertEquals("Invalid response code", 200, response.getStatusLine().getStatusCode()); |
| } |
| |
| // See schema is available |
| keepTrying("Schema should have been created", |
| () -> schemaService.getSchema("https://vendor.test.com/schemas/json/events/dummy/1-0-0"), Objects::nonNull, |
| DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| |
| // Delete Schema using REST call |
| final HttpPost request = new HttpPost(getFullUrl(JSONSCHEMA_URL + "/delete")); |
| request.setEntity(new StringEntity("https://vendor.test.com/schemas/json/events/dummy/1-0-0")); |
| CloseableHttpResponse response = executeHttpRequest(request); |
| assertEquals("Invalid response code", 200, response.getStatusLine().getStatusCode()); |
| |
| waitForNullValue("Schema should have been deleted", |
| () -> schemaService.getSchema("https://vendor.test.com/schemas/json/events/dummy/1-0-0"), DEFAULT_TRYING_TIMEOUT, |
| DEFAULT_TRYING_TRIES); |
| } |
| |
| @Test |
| public void testFlattenedProperties() throws Exception { |
| assertNull(schemaService.getSchema("https://vendor.test.com/schemas/json/events/flattened/1-0-0")); |
| assertNull(schemaService.getSchema("https://vendor.test.com/schemas/json/events/flattened/properties/1-0-0")); |
| assertNull(schemaService.getSchema("https://vendor.test.com/schemas/json/events/flattened/properties/interests/1-0-0")); |
| |
| // Test that at first the flattened event is not valid. |
| assertFalse(schemaService.isEventValid(resourceAsString("schemas/event-flattened-valid.json"), "flattened")); |
| |
| // save schemas and check the event pass the validation |
| schemaService.saveSchema(resourceAsString("schemas/schema-flattened.json")); |
| schemaService.saveSchema(resourceAsString("schemas/schema-flattened-flattenedProperties.json")); |
| schemaService.saveSchema(resourceAsString("schemas/schema-flattened-flattenedProperties-interests.json")); |
| schemaService.saveSchema(resourceAsString("schemas/schema-flattened-properties.json")); |
| keepTrying("Event should be valid now", |
| () -> schemaService.isEventValid(resourceAsString("schemas/event-flattened-valid.json"), "flattened"), |
| isValid -> isValid, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| |
| // insure event is correctly indexed when send to /cxs/eventCollector |
| Event event = sendEventAndWaitItsIndexed("schemas/event-flattened-valid.json"); |
| Map<String, Integer> interests = (Map<String, Integer>) event.getFlattenedProperties().get("interests"); |
| assertEquals(15, interests.get("cars").intValue()); |
| assertEquals(59, interests.get("football").intValue()); |
| assertEquals(2, interests.size()); |
| |
| // check that range query is not working on flattened props: |
| Condition condition = new Condition(definitionsService.getConditionType("eventPropertyCondition")); |
| condition.setParameter("propertyName","flattenedProperties.interests.cars"); |
| condition.setParameter("comparisonOperator","greaterThan"); |
| condition.setParameter("propertyValueInteger", 2); |
| assertNull(persistenceService.query(condition, null, Event.class, 0, -1)); |
| |
| // check that term query is working on flattened props: |
| condition = new Condition(definitionsService.getConditionType("eventPropertyCondition")); |
| condition.setParameter("propertyName","flattenedProperties.interests.cars"); |
| condition.setParameter("comparisonOperator","equals"); |
| condition.setParameter("propertyValueInteger", 15); |
| List<Event> events = persistenceService.query(condition, null, Event.class, 0, -1).getList(); |
| assertEquals(1, events.size()); |
| assertEquals(events.get(0).getItemId(), event.getItemId()); |
| |
| // Bonus: Check that other invalid flattened events are correctly rejected by schema service: |
| assertFalse(schemaService.isEventValid(resourceAsString("schemas/event-flattened-invalid-1.json"), "flattened")); |
| assertFalse(schemaService.isEventValid(resourceAsString("schemas/event-flattened-invalid-2.json"), "flattened")); |
| assertFalse(schemaService.isEventValid(resourceAsString("schemas/event-flattened-invalid-3.json"), "flattened")); |
| } |
| |
| @Test |
| public void testSaveFail_PredefinedJSONSchema() throws IOException { |
| try (CloseableHttpResponse response = post(JSONSCHEMA_URL, "schemas/schema-predefined.json", ContentType.TEXT_PLAIN)) { |
| assertEquals("Unable to save schema", 400, response.getStatusLine().getStatusCode()); |
| } |
| } |
| |
| @Test |
| public void testSaveFail_NewInvalidJSONSchema() throws IOException { |
| try (CloseableHttpResponse response = post(JSONSCHEMA_URL, "schemas/schema-invalid.json", ContentType.TEXT_PLAIN)) { |
| assertEquals("Unable to save schema", 400, response.getStatusLine().getStatusCode()); |
| } |
| } |
| |
| @Test |
| public void testSaveFail_SchemaWithInvalidName() throws IOException { |
| try (CloseableHttpResponse response = post(JSONSCHEMA_URL, "schemas/schema-invalid-name.json", ContentType.TEXT_PLAIN)) { |
| assertEquals("Unable to save schema", 400, response.getStatusLine().getStatusCode()); |
| } |
| } |
| |
| private Event sendEventAndWaitItsIndexed(String eventResourcePath) throws Exception { |
| // build event collector request |
| String eventMarker = UUID.randomUUID().toString(); |
| HashMap<String, String> eventReplacements = new HashMap<>(); |
| eventReplacements.put("EVENT_MARKER", eventMarker); |
| String event = getValidatedBundleJSON(eventResourcePath, eventReplacements); |
| HashMap<String, String> eventRequestReplacements = new HashMap<>(); |
| eventRequestReplacements.put("EVENTS", event); |
| String eventRequest = getValidatedBundleJSON("schemas/event-request.json", eventRequestReplacements); |
| |
| // send event |
| eventCollectorPost(eventRequest); |
| |
| // wait for the event to be indexed |
| Condition condition = new Condition(definitionsService.getConditionType("eventPropertyCondition")); |
| condition.setParameter("propertyName","properties.marker.keyword"); |
| condition.setParameter("comparisonOperator","equals"); |
| condition.setParameter("propertyValue", eventMarker); |
| List<Event> events = keepTrying("The event should have been persisted", |
| () -> persistenceService.query(condition, null, Event.class), results -> results.size() == 1, |
| DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES); |
| return events.get(0); |
| } |
| |
| private void eventCollectorPost(String eventCollectorRequest) throws Exception { |
| HttpPost request = new HttpPost(getFullUrl(EVENT_COLLECTOR_URL)); |
| request.addHeader("Content-Type", "application/json"); |
| request.setEntity(new StringEntity(eventCollectorRequest, ContentType.create("application/json"))); |
| CloseableHttpResponse response; |
| try { |
| response = HttpClientThatWaitsForUnomi.doRequest(request, 200); |
| } catch (Exception e) { |
| fail("Something went wrong with the request to Unomi that is unexpected: " + e.getMessage()); |
| return; |
| } |
| assertEquals("Invalid response code", 200, response.getStatusLine().getStatusCode()); |
| } |
| } |