diff --git a/build.sbt b/build.sbt index 50a5921293..28bd1dacf9 100644 --- a/build.sbt +++ b/build.sbt @@ -171,7 +171,7 @@ lazy val webapi: Project = Project(id = "webapi", base = file("webapi")) Docker / dockerRepository := Some("daschswiss"), Docker / packageName := "knora-api", dockerUpdateLatest := true, - dockerBaseImage := "eclipse-temurin:11-jre-focal", + dockerBaseImage := "eclipse-temurin:17-jre-focal", Docker / maintainer := "support@dasch.swiss", Docker / dockerExposedPorts ++= Seq(3333), Docker / defaultLinuxInstallLocation := "/opt/docker", diff --git a/test_data/ontologies/freetest-onto.ttl b/test_data/ontologies/freetest-onto.ttl index a178b8b481..5d3a8d9f4f 100644 --- a/test_data/ontologies/freetest-onto.ttl +++ b/test_data/ontologies/freetest-onto.ttl @@ -148,6 +148,18 @@ rdfs:comment """A comment for FTRC."""@de . +:FreeTestSubClassOfFoafPerson + rdf:type owl:Class ; + rdfs:subClassOf foaf:Person, + knora-base:Resource, + [ rdf:type owl:Restriction ; + owl:onProperty :hasFoafName ; + owl:minCardinality "0"^^xsd:nonNegativeInteger ; + salsah-gui:guiOrder "1"^^xsd:nonNegativeInteger ] ; + rdfs:label "FTRCFoafPerson en"@en ; + rdfs:comment """A comment for FTRCFoafPerson."""@de . + + :Author rdf:type owl:Class ; rdfs:subClassOf knora-base:Resource, @@ -299,3 +311,14 @@ knora-base:objectClassConstraint knora-base:TextValue ; salsah-gui:guiElement salsah-gui:Richtext . + +:hasFoafName + rdf:type owl:ObjectProperty ; + rdfs:subPropertyOf knora-base:hasValue, + foaf:name ; + rdfs:label "FoafName"@en ; + knora-base:subjectClassConstraint :FreeTestSubClassOfFoafPerson ; + knora-base:objectClassConstraint knora-base:TextValue ; + salsah-gui:guiElement salsah-gui:SimpleText ; + salsah-gui:guiAttribute "size=80", + "maxlength=255" . diff --git a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala index 6aea291f4f..020051517e 100644 --- a/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala +++ b/webapi/src/main/scala/org/knora/webapi/responders/v2/ontology/OntologyHelpers.scala @@ -956,11 +956,15 @@ object OntologyHelpers { directCardinalities = internalClassDef.directCardinalities ++ linkValuePropCardinalitiesToAdd ) - // Get the cardinalities that the class can inherit. + // Get the cardinalities that the class can inherit. If the ontology of the base class can't be found, it's assumed to be an external ontology (p.ex. foaf). val cardinalitiesAvailableToInherit: Map[SmartIri, KnoraCardinalityInfo] = - classDefWithAddedLinkValueProps.subClassOf.flatMap { baseClassIri => - cacheData.ontologies(baseClassIri.getOntologyFromEntity).classes(baseClassIri).allCardinalities + classDefWithAddedLinkValueProps.subClassOf.flatMap { baseClassIri: SmartIri => + val ontology = cacheData.ontologies.getOrElse(baseClassIri.getOntologyFromEntity, None) + ontology match { + case ontology: ReadOntologyV2 => ontology.classes(baseClassIri).allCardinalities + case _ => None + } }.toMap // Check that the cardinalities directly defined on the class are compatible with any inheritable diff --git a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala index 8c3b49b9b5..1eee6f0290 100644 --- a/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala +++ b/webapi/src/test/scala/org/knora/webapi/e2e/v2/ResourcesRouteV2E2ESpec.scala @@ -61,7 +61,11 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { override lazy val rdfDataObjects: List[RdfDataObject] = List( RdfDataObject(path = "test_data/all_data/incunabula-data.ttl", name = "http://www.knora.org/data/0803/incunabula"), RdfDataObject(path = "test_data/demo_data/images-demo-data.ttl", name = "http://www.knora.org/data/00FF/images"), - RdfDataObject(path = "test_data/all_data/anything-data.ttl", name = "http://www.knora.org/data/0001/anything") + RdfDataObject(path = "test_data/all_data/anything-data.ttl", name = "http://www.knora.org/data/0001/anything"), + RdfDataObject( + path = "test_data/ontologies/freetest-onto.ttl", + name = "http://www.knora.org/ontology/0001/freetest" + ) ) private val instanceChecker: InstanceChecker = InstanceChecker.getJsonLDChecker @@ -181,7 +185,11 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { val responseAsString = responseToString(response) assert(response.status == StatusCodes.OK, responseAsString) val expectedAnswerJSONLD = - readOrWriteTextFile(responseAsString, Paths.get("..", "test_data/resourcesR2RV2/AThing.jsonld"), writeTestDataFiles) + readOrWriteTextFile( + responseAsString, + Paths.get("..", "test_data/resourcesR2RV2/AThing.jsonld"), + writeTestDataFiles + ) compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = responseAsString) clientTestDataCollector.addFile( @@ -476,7 +484,11 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { val responseAsString = responseToString(response) assert(response.status == StatusCodes.OK, responseAsString) val expectedAnswerJSONLD = - readOrWriteTextFile(responseAsString, Paths.get("..", "test_data/resourcesR2RV2/Testding.jsonld"), writeTestDataFiles) + readOrWriteTextFile( + responseAsString, + Paths.get("..", "test_data/resourcesR2RV2/Testding.jsonld"), + writeTestDataFiles + ) compareJSONLDForResourcesResponse(expectedJSONLD = expectedAnswerJSONLD, receivedJSONLD = responseAsString) clientTestDataCollector.addFile( @@ -1008,6 +1020,74 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { assert(text == "this is text with standoff") } + "create a resource and a property with references to an external ontology (FOAF)" in { + val createResourceWithRefToFoaf: String = + """{ + | "@type" : "freetest:FreeTestSubClassOfFoafPerson", + | "freetest:hasFoafName" : { + | "@type" : "knora-api:TextValue", + | "knora-api:valueAsString" : "this is a foaf name" + | }, + | "knora-api:attachedToProject" : { + | "@id" : "http://rdfh.ch/projects/0001" + | }, + | "rdfs:label" : "Test foaf Person", + | "@context" : { + | "rdf" : "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + | "knora-api" : "http://api.knora.org/ontology/knora-api/v2#", + | "rdfs" : "http://www.w3.org/2000/01/rdf-schema#", + | "xsd" : "http://www.w3.org/2001/XMLSchema#", + | "freetest" : "http://0.0.0.0:3333/ontology/0001/freetest/v2#" + | } + |}""".stripMargin + + val request = Post( + s"$baseApiUrl/v2/resources", + HttpEntity(RdfMediaTypes.`application/ld+json`, createResourceWithRefToFoaf) + ) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val response: HttpResponse = singleAwaitingRequest(request) + assert(response.status == StatusCodes.OK, response.toString) + val responseJsonDoc: JsonLDDocument = responseToJsonLDDocument(response) + val resourceIri: IRI = + responseJsonDoc.body.requireStringWithValidation(JsonLDKeywords.ID, stringFormatter.validateAndEscapeIri) + assert(resourceIri.toSmartIri.isKnoraDataIri) + + // Request the newly created resource in the complex schema, and check that it matches the ontology. + val resourceComplexGetRequest = Get( + s"$baseApiUrl/v2/resources/${URLEncoder.encode(resourceIri, "UTF-8")}" + ) ~> addCredentials(BasicHttpCredentials(anythingUserEmail, password)) + val resourceComplexGetResponse: HttpResponse = singleAwaitingRequest(resourceComplexGetRequest) + val resourceComplexGetResponseAsString = responseToString(resourceComplexGetResponse) + + instanceChecker.check( + instanceResponse = resourceComplexGetResponseAsString, + expectedClassIri = "http://0.0.0.0:3333/ontology/0001/freetest/v2#FreeTestSubClassOfFoafPerson".toSmartIri, + knoraRouteGet = doGetRequest + ) + + // Request the newly created resource in the simple schema, and check that it matches the ontology. + val resourceSimpleGetRequest = Get(s"$baseApiUrl/v2/resources/${URLEncoder.encode(resourceIri, "UTF-8")}") + .addHeader(new SchemaHeader(RouteUtilV2.SIMPLE_SCHEMA_NAME)) ~> addCredentials( + BasicHttpCredentials(anythingUserEmail, password) + ) + val resourceSimpleGetResponse: HttpResponse = singleAwaitingRequest(resourceSimpleGetRequest) + val resourceSimpleGetResponseAsString = responseToString(resourceSimpleGetResponse) + + instanceChecker.check( + instanceResponse = resourceSimpleGetResponseAsString, + expectedClassIri = + "http://0.0.0.0:3333/ontology/0001/freetest/simple/v2#FreeTestSubClassOfFoafPerson".toSmartIri, + knoraRouteGet = doGetRequest + ) + + // Check that the value is correct in the simple schema. + val resourceSimpleAsJsonLD: JsonLDDocument = JsonLDUtil.parseJsonLD(resourceSimpleGetResponseAsString) + println(resourceSimpleAsJsonLD.body) + val foafName: String = + resourceSimpleAsJsonLD.body.requireString("http://0.0.0.0:3333/ontology/0001/freetest/simple/v2#hasFoafName") + assert(foafName == "this is a foaf name") + } + "create a resource whose label contains a Unicode escape and quotation marks" in { val jsonLDEntity: String = FileUtil.readTextFile(Paths.get("..", "test_data/resourcesR2RV2/ThingWithUnicodeEscape.jsonld")) @@ -1566,7 +1646,8 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { } "create a resource containing escaped text" in { - val jsonLDEntity = FileUtil.readTextFile(Paths.get("..", "test_data/resourcesR2RV2/CreateResourceWithEscape.jsonld")) + val jsonLDEntity = + FileUtil.readTextFile(Paths.get("..", "test_data/resourcesR2RV2/CreateResourceWithEscape.jsonld")) val request = Post( s"$baseApiUrl/v2/resources", HttpEntity(RdfMediaTypes.`application/ld+json`, jsonLDEntity) @@ -2205,7 +2286,11 @@ class ResourcesRouteV2E2ESpec extends E2ESpec(ResourcesRouteV2E2ESpec.config) { val responseJson: JsValue = JsonParser(responseStr) val expectedJson: JsValue = JsonParser( - readOrWriteTextFile(responseStr, Paths.get("..", "test_data/resourcesR2RV2/IIIFManifest.jsonld"), writeTestDataFiles) + readOrWriteTextFile( + responseStr, + Paths.get("..", "test_data/resourcesR2RV2/IIIFManifest.jsonld"), + writeTestDataFiles + ) ) assert(responseJson == expectedJson) diff --git a/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala b/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala index a78f8d0967..f7786239a6 100644 --- a/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala +++ b/webapi/src/test/scala/org/knora/webapi/responders/v2/OntologyResponderV2Spec.scala @@ -6254,6 +6254,285 @@ class OntologyResponderV2Spec extends CoreSpec() with ImplicitSender { } } + "create a class anything:FoafPerson as a subclass of foaf:Person" in { + // create the class anything:FoafPerson + val classIri: SmartIri = AnythingOntologyIri.makeEntityIri("FoafPerson") + + val classInfoContent: ClassInfoContentV2 = ClassInfoContentV2( + classIri = classIri, + predicates = Map( + OntologyConstants.Rdf.Type.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdf.Type.toSmartIri, + objects = Seq(SmartIriLiteralV2(OntologyConstants.Owl.Class.toSmartIri)) + ), + OntologyConstants.Rdfs.Label.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdfs.Label.toSmartIri, + objects = Seq(StringLiteralV2("FOAF person", Some("en"))) + ), + OntologyConstants.Rdfs.Comment.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdfs.Comment.toSmartIri, + objects = Seq(StringLiteralV2("FOAF person with reference to foaf:Person", Some("en"))) + ) + ), + subClassOf = Set( + "http://api.knora.org/ontology/knora-api/v2#Resource".toSmartIri, + "http://xmlns.com/foaf/0.1/Person".toSmartIri + ), + directCardinalities = + Map(ExampleSharedOntologyIri.makeEntityIri("hasName") -> KnoraCardinalityInfo(Cardinality.MayHaveOne)), + ontologySchema = ApiV2Complex + ) + + responderManager ! CreateClassRequestV2( + classInfoContent = classInfoContent, + lastModificationDate = anythingLastModDate, + apiRequestID = UUID.randomUUID, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingAdminUser + ) + + // check if class was created correctly + expectMsgPF(timeout) { case msg: ReadOntologyV2 => + val externalOntology: ReadOntologyV2 = msg.toOntologySchema(ApiV2Complex) + assert(externalOntology.classes.size == 1) + val readClassInfo: ReadClassInfoV2 = externalOntology.classes(classIri) + readClassInfo.entityInfoContent should ===(classInfoContent) + + val metadata: OntologyMetadataV2 = externalOntology.ontologyMetadata + val newAnythingLastModDate: Instant = metadata.lastModificationDate.getOrElse( + throw AssertionException(s"${metadata.ontologyIri} has no last modification date") + ) + assert(newAnythingLastModDate.isAfter(anythingLastModDate)) + anythingLastModDate = newAnythingLastModDate + } + + } + + "create a property anything:hasFoafName as a subproperty of foaf:name" in { + // get the class IRI for anything:FoafPerson + val classIri: SmartIri = AnythingOntologyIri.makeEntityIri("FoafPerson") + + // create the property anything:hasFoafName + responderManager ! OntologyMetadataGetByProjectRequestV2( + projectIris = Set(anythingProjectIri), + requestingUser = anythingAdminUser + ) + + val metadataResponse: ReadOntologyMetadataV2 = expectMsgType[ReadOntologyMetadataV2](timeout) + assert(metadataResponse.ontologies.size == 3) + anythingLastModDate = metadataResponse + .toOntologySchema(ApiV2Complex) + .ontologies + .find(_.ontologyIri == AnythingOntologyIri) + .get + .lastModificationDate + .get + + val propertyIri: SmartIri = AnythingOntologyIri.makeEntityIri("hasFoafName") + + val propertyInfoContent: PropertyInfoContentV2 = PropertyInfoContentV2( + propertyIri = propertyIri, + predicates = Map( + OntologyConstants.Rdf.Type.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdf.Type.toSmartIri, + objects = Seq(SmartIriLiteralV2(OntologyConstants.Owl.ObjectProperty.toSmartIri)) + ), + OntologyConstants.KnoraApiV2Complex.SubjectType.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.KnoraApiV2Complex.SubjectType.toSmartIri, + objects = Seq(SmartIriLiteralV2(classIri)) + ), + OntologyConstants.KnoraApiV2Complex.ObjectType.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.KnoraApiV2Complex.ObjectType.toSmartIri, + objects = Seq(SmartIriLiteralV2(OntologyConstants.KnoraApiV2Complex.TextValue.toSmartIri)) + ), + OntologyConstants.Rdfs.Label.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdfs.Label.toSmartIri, + objects = Seq( + StringLiteralV2("has foaf name", Some("en")), + StringLiteralV2("hat foaf Namen", Some("de")) + ) + ), + OntologyConstants.Rdfs.Comment.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdfs.Comment.toSmartIri, + objects = Seq( + StringLiteralV2("The foaf name of something", Some("en")), + StringLiteralV2("Der foaf Name eines Dinges", Some("de")) + ) + ) + ), + subPropertyOf = + Set(OntologyConstants.KnoraApiV2Complex.HasValue.toSmartIri, "http://xmlns.com/foaf/0.1/name".toSmartIri), + ontologySchema = ApiV2Complex + ) + + responderManager ! CreatePropertyRequestV2( + propertyInfoContent = propertyInfoContent, + lastModificationDate = anythingLastModDate, + apiRequestID = UUID.randomUUID, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingAdminUser + ) + + // check if property was created correctly + expectMsgPF(timeout) { case msg: ReadOntologyV2 => + val externalOntology: ReadOntologyV2 = msg.toOntologySchema(ApiV2Complex) + val property: ReadPropertyInfoV2 = externalOntology.properties(propertyIri) + property.entityInfoContent should ===(propertyInfoContent) + val metadata: OntologyMetadataV2 = externalOntology.ontologyMetadata + val newAnythingLastModDate: Instant = metadata.lastModificationDate.getOrElse( + throw AssertionException(s"${metadata.ontologyIri} has no last modification date") + ) + assert(newAnythingLastModDate.isAfter(anythingLastModDate)) + anythingLastModDate = newAnythingLastModDate + + } + } + + "add property anything:hasFoafName to the class anything:FoafPerson" in { + // get the class IRI for anything:FoafPerson + val classIri: SmartIri = AnythingOntologyIri.makeEntityIri("FoafPerson") + + val propertyIri: SmartIri = AnythingOntologyIri.makeEntityIri("hasFoafName") + + // add a cardinality for the property anything:hasFoafName to the class anything:FoafPerson + val classWithNewCardinalityInfoContent = ClassInfoContentV2( + classIri = classIri, + predicates = Map( + OntologyConstants.Rdf.Type.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdf.Type.toSmartIri, + objects = Seq(SmartIriLiteralV2(OntologyConstants.Owl.Class.toSmartIri)) + ) + ), + directCardinalities = Map( + propertyIri -> KnoraCardinalityInfo( + cardinality = Cardinality.MayHaveOne, + guiOrder = Some(0) + ) + ), + ontologySchema = ApiV2Complex + ) + + responderManager ! AddCardinalitiesToClassRequestV2( + classInfoContent = classWithNewCardinalityInfoContent, + lastModificationDate = anythingLastModDate, + apiRequestID = UUID.randomUUID, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingAdminUser + ) + + // check if cardinality was added correctly + val expectedDirectCardinalities: Map[SmartIri, KnoraCardinalityInfo] = Map( + propertyIri -> KnoraCardinalityInfo( + cardinality = Cardinality.MayHaveOne, + guiOrder = Some(0) + ), + ExampleSharedOntologyIri.makeEntityIri("hasName") -> KnoraCardinalityInfo( + cardinality = Cardinality.MayHaveOne + ) + ) + + val expectedProperties: Set[SmartIri] = Set( + OntologyConstants.KnoraApiV2Complex.HasStandoffLinkTo.toSmartIri, + OntologyConstants.KnoraApiV2Complex.HasStandoffLinkToValue.toSmartIri, + ExampleSharedOntologyIri.makeEntityIri("hasName"), + propertyIri + ) + + expectMsgPF(timeout) { case msg: ReadOntologyV2 => + val externalOntology: ReadOntologyV2 = msg.toOntologySchema(ApiV2Complex) + assert(externalOntology.classes.size == 1) + val readClassInfo: ReadClassInfoV2 = externalOntology.classes(classIri) + readClassInfo.entityInfoContent.directCardinalities should ===(expectedDirectCardinalities) + readClassInfo.allResourcePropertyCardinalities.keySet should ===(expectedProperties) + + val metadata: OntologyMetadataV2 = externalOntology.ontologyMetadata + val newAnythingLastModDate: Instant = metadata.lastModificationDate.getOrElse( + throw AssertionException(s"${metadata.ontologyIri} has no last modification date") + ) + assert(newAnythingLastModDate.isAfter(anythingLastModDate)) + anythingLastModDate = newAnythingLastModDate + } + } + + "remove all properties from class anything:FoafPerson" in { + // get the class IRI for anything:FoafPerson + val classIri: SmartIri = AnythingOntologyIri.makeEntityIri("FoafPerson") + + val propertyIri: SmartIri = AnythingOntologyIri.makeEntityIri("hasFoafName") + + // check if cardinalities on class anything:FoafPerson can be removed + + val classInfoContentWithCardinalityToDeleteAllow: ClassInfoContentV2 = ClassInfoContentV2( + classIri = classIri, + predicates = Map( + OntologyConstants.Rdf.Type.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdf.Type.toSmartIri, + objects = Seq(SmartIriLiteralV2(OntologyConstants.Owl.Class.toSmartIri)) + ) + ), + directCardinalities = Map( + propertyIri -> KnoraCardinalityInfo( + cardinality = Cardinality.MayHaveOne, + guiOrder = Some(0) + ) + ), + ontologySchema = ApiV2Complex + ) + + responderManager ! CanDeleteCardinalitiesFromClassRequestV2( + classInfoContent = classInfoContentWithCardinalityToDeleteAllow, + lastModificationDate = anythingLastModDate, + apiRequestID = UUID.randomUUID, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingAdminUser + ) + + expectMsgPF(timeout) { case msg: CanDoResponseV2 => + assert(msg.canDo) + } + + // remove cardinalities on the class anything:FoafPerson + val classChangeInfoContent: ClassInfoContentV2 = ClassInfoContentV2( + classIri = classIri, + predicates = Map( + OntologyConstants.Rdf.Type.toSmartIri -> PredicateInfoV2( + predicateIri = OntologyConstants.Rdf.Type.toSmartIri, + objects = Seq(SmartIriLiteralV2(OntologyConstants.Owl.Class.toSmartIri)) + ) + ), + ontologySchema = ApiV2Complex + ) + + responderManager ! ChangeCardinalitiesRequestV2( + classInfoContent = classChangeInfoContent, + lastModificationDate = anythingLastModDate, + apiRequestID = UUID.randomUUID, + featureFactoryConfig = defaultFeatureFactoryConfig, + requestingUser = anythingAdminUser + ) + + // check if cardinalities were removed correctly + val expectedPropertiesAfterDeletion: Set[SmartIri] = Set( + OntologyConstants.KnoraApiV2Complex.HasStandoffLinkTo.toSmartIri, + OntologyConstants.KnoraApiV2Complex.HasStandoffLinkToValue.toSmartIri + ) + + expectMsgPF(timeout) { case msg: ReadOntologyV2 => + val externalOntology: ReadOntologyV2 = msg.toOntologySchema(ApiV2Complex) + assert(externalOntology.classes.size == 1) + val readClassInfo: ReadClassInfoV2 = externalOntology.classes(classIri) + readClassInfo.entityInfoContent.directCardinalities should ===(classChangeInfoContent.directCardinalities) + readClassInfo.allResourcePropertyCardinalities.keySet should ===(expectedPropertiesAfterDeletion) + + val metadata: OntologyMetadataV2 = externalOntology.ontologyMetadata + val newAnythingLastModDate: Instant = metadata.lastModificationDate.getOrElse( + throw AssertionException(s"${metadata.ontologyIri} has no last modification date") + ) + assert(newAnythingLastModDate.isAfter(anythingLastModDate)) + anythingLastModDate = newAnythingLastModDate + } + } + "change the GUI order of a cardinality in a base class, and update its subclass in the ontology cache" in { val classIri = AnythingOntologyIri.makeEntityIri("Thing")