From 347540f7f588382b0baa45010e10bdcb8f6ad4bd Mon Sep 17 00:00:00 2001 From: Ali Ince Date: Tue, 3 Apr 2018 16:32:37 +0100 Subject: [PATCH] Address review comments and report errors during type unpacking --- .../org/neo4j/bolt/messaging/KnownType.java | 60 +++--- .../transport/pipeline/MessageDecoder.java | 2 +- .../bolt/v1/messaging/BoltIOException.java | 2 +- .../neo4j/bolt/v1/messaging/Neo4jPackV1.java | 21 +- .../neo4j/bolt/v1/packstream/PackStream.java | 11 + .../neo4j/bolt/v2/messaging/Neo4jPackV2.java | 110 +++++++--- .../UnsupportedKnownTypesV2IT.java | 198 ++++++++++++++++++ 7 files changed, 336 insertions(+), 68 deletions(-) create mode 100644 community/bolt/src/test/java/org/neo4j/bolt/v2/transport/integration/UnsupportedKnownTypesV2IT.java diff --git a/community/bolt/src/main/java/org/neo4j/bolt/messaging/KnownType.java b/community/bolt/src/main/java/org/neo4j/bolt/messaging/KnownType.java index 7e6caf8153ef7..2a0f866e51443 100644 --- a/community/bolt/src/main/java/org/neo4j/bolt/messaging/KnownType.java +++ b/community/bolt/src/main/java/org/neo4j/bolt/messaging/KnownType.java @@ -22,30 +22,30 @@ import java.util.HashMap; import java.util.Map; +import org.neo4j.bolt.v1.messaging.Neo4jPackV1; +import org.neo4j.bolt.v2.messaging.Neo4jPackV2; + +import static java.util.Collections.unmodifiableMap; + public enum KnownType { - NODE( 'N', "Node" ), - RELATIONSHIP( 'R', "Relationship" ), - UNBOUND_RELATIONSHIP( 'r', "Relationship" ), - PATH( 'P', "Path" ), - POINT_2D( 'X', "Point" ), - POINT_3D( 'Y', "Point" ), - DATE( 'D', "LocalDate" ), - TIME( 'T', "OffsetTime" ), - LOCAL_TIME( 't', "LocalTime" ), - LOCAL_DATE_TIME( 'd', "LocalDateTime" ), - DATE_TIME_WITH_ZONE_OFFSET( 'F', "OffsetDateTime" ), - DATE_TIME_WITH_ZONE_NAME( 'f', "ZonedDateTime" ), - DURATION( 'E', "Duration" ); + NODE( Neo4jPackV1.NODE, "Node" ), + RELATIONSHIP( Neo4jPackV1.RELATIONSHIP, "Relationship" ), + UNBOUND_RELATIONSHIP( Neo4jPackV1.UNBOUND_RELATIONSHIP, "Relationship" ), + PATH( Neo4jPackV1.PATH, "Path" ), + POINT_2D( Neo4jPackV2.POINT_2D, "Point" ), + POINT_3D( Neo4jPackV2.POINT_3D, "Point" ), + DATE( Neo4jPackV2.DATE, "LocalDate" ), + TIME( Neo4jPackV2.TIME, "OffsetTime" ), + LOCAL_TIME( Neo4jPackV2.LOCAL_TIME, "LocalTime" ), + LOCAL_DATE_TIME( Neo4jPackV2.LOCAL_DATE_TIME, "LocalDateTime" ), + DATE_TIME_WITH_ZONE_OFFSET( Neo4jPackV2.DATE_TIME_WITH_ZONE_OFFSET, "OffsetDateTime" ), + DATE_TIME_WITH_ZONE_NAME( Neo4jPackV2.DATE_TIME_WITH_ZONE_NAME, "ZonedDateTime" ), + DURATION( Neo4jPackV2.DURATION, "Duration" ); private final byte signature; private final String description; - KnownType( char signature, String description ) - { - this( (byte)signature, description ); - } - KnownType( byte signature, String description ) { this.signature = signature; @@ -62,22 +62,26 @@ public String description() return description; } - private static Map byteToKnownTypeMap = new HashMap<>(); - static - { - for ( KnownType type : KnownType.values() ) - { - byteToKnownTypeMap.put( type.signature, type ); - } - } + private static Map knownTypesBySignature = knownTypesBySignature(); public static KnownType valueOf( byte signature ) { - return byteToKnownTypeMap.get( signature ); + return knownTypesBySignature.get( signature ); } public static KnownType valueOf( char signature ) { - return KnownType.valueOf( (byte)signature ); + return knownTypesBySignature.get( (byte)signature ); + } + + private static Map knownTypesBySignature() + { + KnownType[] types = KnownType.values(); + Map result = new HashMap<>( types.length * 2 ); + for ( KnownType type : types ) + { + result.put( type.signature, type ); + } + return unmodifiableMap( result ); } } diff --git a/community/bolt/src/main/java/org/neo4j/bolt/transport/pipeline/MessageDecoder.java b/community/bolt/src/main/java/org/neo4j/bolt/transport/pipeline/MessageDecoder.java index ebfc07feb82f8..98699f3a814ca 100644 --- a/community/bolt/src/main/java/org/neo4j/bolt/transport/pipeline/MessageDecoder.java +++ b/community/bolt/src/main/java/org/neo4j/bolt/transport/pipeline/MessageDecoder.java @@ -56,7 +56,7 @@ protected void channelRead0( ChannelHandlerContext channelHandlerContext, ByteBu } catch ( BoltIOException ex ) { - if ( ex.causesFailure() ) + if ( ex.causesFailureMessage() ) { messageHandler.onExternalError( Neo4jError.from( ex ) ); } diff --git a/community/bolt/src/main/java/org/neo4j/bolt/v1/messaging/BoltIOException.java b/community/bolt/src/main/java/org/neo4j/bolt/v1/messaging/BoltIOException.java index f386703b78ff6..372dafaf5b233 100644 --- a/community/bolt/src/main/java/org/neo4j/bolt/v1/messaging/BoltIOException.java +++ b/community/bolt/src/main/java/org/neo4j/bolt/v1/messaging/BoltIOException.java @@ -44,7 +44,7 @@ public Status status() return status; } - public boolean causesFailure() + public boolean causesFailureMessage() { return status != Status.Request.InvalidFormat; } diff --git a/community/bolt/src/main/java/org/neo4j/bolt/v1/messaging/Neo4jPackV1.java b/community/bolt/src/main/java/org/neo4j/bolt/v1/messaging/Neo4jPackV1.java index efdb0b13fded6..b33691604f02d 100644 --- a/community/bolt/src/main/java/org/neo4j/bolt/v1/messaging/Neo4jPackV1.java +++ b/community/bolt/src/main/java/org/neo4j/bolt/v1/messaging/Neo4jPackV1.java @@ -61,9 +61,16 @@ public class Neo4jPackV1 implements Neo4jPack public static final long VERSION = 1; public static final byte NODE = 'N'; + public static final int NODE_SIZE = 3; + public static final byte RELATIONSHIP = 'R'; + public static final int RELATIONSHIP_SIZE = 5; + public static final byte UNBOUND_RELATIONSHIP = 'r'; + public static final int UNBOUND_RELATIONSHIP_SIZE = 3; + public static final byte PATH = 'P'; + public static final int PATH_SIZE = 3; @Override public Neo4jPack.Packer newPacker( PackOutput output ) @@ -118,7 +125,7 @@ public void writeNodeReference( long nodeId ) @Override public void writeNode( long nodeId, TextArray labels, MapValue properties ) throws IOException { - packStructHeader( 3, NODE ); + packStructHeader( NODE_SIZE, NODE ); pack( nodeId ); packListHeader( labels.length() ); for ( int i = 0; i < labels.length(); i++ ) @@ -138,7 +145,7 @@ public void writeRelationshipReference( long relationshipId ) public void writeRelationship( long relationshipId, long startNodeId, long endNodeId, TextValue type, MapValue properties ) throws IOException { - packStructHeader( 5, RELATIONSHIP ); + packStructHeader( RELATIONSHIP_SIZE, RELATIONSHIP ); pack( relationshipId ); pack( startNodeId ); pack( endNodeId ); @@ -194,7 +201,7 @@ public void writePath( NodeValue[] nodes, RelationshipValue[] relationships ) th // the offset // into the // node list (zero indexed) and so on. - packStructHeader( 3, PATH ); + packStructHeader( PATH_SIZE, PATH ); writeNodesForPath( nodes ); writeRelationshipsForPath( relationships ); @@ -282,7 +289,7 @@ private void writeRelationshipsForPath( RelationshipValue[] relationships ) thro //Note that we are not doing relationship.writeTo(this) here since the serialization protocol //requires these to be _unbound relationships_, thus relationships without any start node nor // end node. - packStructHeader( 3, UNBOUND_RELATIONSHIP ); + packStructHeader( UNBOUND_RELATIONSHIP_SIZE, UNBOUND_RELATIONSHIP ); pack( edge.id() ); edge.type().writeTo( this ); edge.properties().writeTo( this ); @@ -463,9 +470,9 @@ public AnyValue unpack() throws IOException } case STRUCT: { - unpackStructHeader(); + long size = unpackStructHeader(); char signature = unpackStructSignature(); - return unpackStruct( signature ); + return unpackStruct( signature, size ); } case END_OF_STREAM: { @@ -514,7 +521,7 @@ else if ( size == UNKNOWN_SIZE ) } } - protected AnyValue unpackStruct( char signature ) throws IOException + protected AnyValue unpackStruct( char signature, long size ) throws IOException { KnownType knownType = KnownType.valueOf( signature ); if ( knownType == null ) diff --git a/community/bolt/src/main/java/org/neo4j/bolt/v1/packstream/PackStream.java b/community/bolt/src/main/java/org/neo4j/bolt/v1/packstream/PackStream.java index 030c1788f465a..401cdcbc2db38 100644 --- a/community/bolt/src/main/java/org/neo4j/bolt/v1/packstream/PackStream.java +++ b/community/bolt/src/main/java/org/neo4j/bolt/v1/packstream/PackStream.java @@ -23,6 +23,7 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import org.neo4j.bolt.messaging.KnownType; import org.neo4j.bolt.v1.packstream.utf8.UTF8Encoder; /** @@ -764,6 +765,16 @@ public PackType peekNextType() throws IOException final byte markerByte = in.peekByte(); return type( markerByte ); } + + public static void ensureCorrectStructSize( KnownType knownType, int expected, long actual ) throws IOException + { + if ( expected != actual ) + { + throw new PackStreamException( + String.format( "Invalid message received, serialized %s structures should have %d fields, " + "received %s structure has %d fields.", + knownType.description(), expected, knownType.description(), actual ) ); + } + } } public static class PackStreamException extends IOException diff --git a/community/bolt/src/main/java/org/neo4j/bolt/v2/messaging/Neo4jPackV2.java b/community/bolt/src/main/java/org/neo4j/bolt/v2/messaging/Neo4jPackV2.java index 2a76b46178ebb..5e5fa4cb1a14d 100644 --- a/community/bolt/src/main/java/org/neo4j/bolt/v2/messaging/Neo4jPackV2.java +++ b/community/bolt/src/main/java/org/neo4j/bolt/v2/messaging/Neo4jPackV2.java @@ -30,10 +30,14 @@ import java.time.ZonedDateTime; import java.util.Arrays; +import org.neo4j.bolt.messaging.KnownType; +import org.neo4j.bolt.v1.messaging.BoltIOException; import org.neo4j.bolt.v1.messaging.Neo4jPack; import org.neo4j.bolt.v1.messaging.Neo4jPackV1; import org.neo4j.bolt.v1.packstream.PackInput; import org.neo4j.bolt.v1.packstream.PackOutput; +import org.neo4j.bolt.v1.packstream.PackStream; +import org.neo4j.kernel.api.exceptions.Status; import org.neo4j.values.AnyValue; import org.neo4j.values.storable.CoordinateReferenceSystem; import org.neo4j.values.storable.DateTimeValue; @@ -59,15 +63,31 @@ public class Neo4jPackV2 extends Neo4jPackV1 public static final long VERSION = 2; public static final byte POINT_2D = 'X'; + public static final int POINT_2D_SIZE = 3; + public static final byte POINT_3D = 'Y'; + public static final int POINT_3D_SIZE = 4; public static final byte DATE = 'D'; + public static final int DATE_SIZE = 1; + public static final byte TIME = 'T'; + public static final int TIME_SIZE = 2; + public static final byte LOCAL_TIME = 't'; + public static final int LOCAL_TIME_SIZE = 1; + public static final byte LOCAL_DATE_TIME = 'd'; + public static final int LOCAL_DATE_TIME_SIZE = 2; + public static final byte DATE_TIME_WITH_ZONE_OFFSET = 'F'; + public static final int DATE_TIME_WITH_ZONE_OFFSET_SIZE = 3; + public static final byte DATE_TIME_WITH_ZONE_NAME = 'f'; + public static final int DATE_TIME_WITH_ZONE_NAME_SIZE = 3; + public static final byte DURATION = 'E'; + public static final int DURATION_SIZE = 4; @Override public Neo4jPack.Packer newPacker( PackOutput output ) @@ -99,14 +119,14 @@ public void writePoint( CoordinateReferenceSystem crs, double[] coordinate ) thr { if ( coordinate.length == 2 ) { - packStructHeader( 3, POINT_2D ); + packStructHeader( POINT_2D_SIZE, POINT_2D ); pack( crs.getCode() ); pack( coordinate[0] ); pack( coordinate[1] ); } else if ( coordinate.length == 3 ) { - packStructHeader( 4, POINT_3D ); + packStructHeader( POINT_3D_SIZE, POINT_3D ); pack( crs.getCode() ); pack( coordinate[0] ); pack( coordinate[1] ); @@ -122,7 +142,7 @@ else if ( coordinate.length == 3 ) @Override public void writeDuration( long months, long days, long seconds, int nanos ) throws IOException { - packStructHeader( 4, DURATION ); + packStructHeader( DURATION_SIZE, DURATION ); pack( months ); pack( days ); pack( seconds ); @@ -134,7 +154,7 @@ public void writeDate( LocalDate localDate ) throws IOException { long epochDay = localDate.toEpochDay(); - packStructHeader( 1, DATE ); + packStructHeader( DATE_SIZE, DATE ); pack( epochDay ); } @@ -143,7 +163,7 @@ public void writeLocalTime( LocalTime localTime ) throws IOException { long nanoOfDay = localTime.toNanoOfDay(); - packStructHeader( 1, LOCAL_TIME ); + packStructHeader( LOCAL_TIME_SIZE, LOCAL_TIME ); pack( nanoOfDay ); } @@ -153,7 +173,7 @@ public void writeTime( OffsetTime offsetTime ) throws IOException long nanosOfDayLocal = offsetTime.toLocalTime().toNanoOfDay(); int offsetSeconds = offsetTime.getOffset().getTotalSeconds(); - packStructHeader( 2, TIME ); + packStructHeader( TIME_SIZE, TIME ); pack( nanosOfDayLocal ); pack( offsetSeconds ); } @@ -164,7 +184,7 @@ public void writeLocalDateTime( LocalDateTime localDateTime ) throws IOException long epochSecond = localDateTime.toEpochSecond( UTC ); int nano = localDateTime.getNano(); - packStructHeader( 2, LOCAL_DATE_TIME ); + packStructHeader( LOCAL_DATE_TIME_SIZE, LOCAL_DATE_TIME ); pack( epochSecond ); pack( nano ); } @@ -180,7 +200,7 @@ public void writeDateTime( ZonedDateTime zonedDateTime ) throws IOException { int offsetSeconds = ((ZoneOffset) zone).getTotalSeconds(); - packStructHeader( 3, DATE_TIME_WITH_ZONE_OFFSET ); + packStructHeader( DATE_TIME_WITH_ZONE_OFFSET_SIZE, DATE_TIME_WITH_ZONE_OFFSET ); pack( epochSecondLocal ); pack( nano ); pack( offsetSeconds ); @@ -189,7 +209,7 @@ public void writeDateTime( ZonedDateTime zonedDateTime ) throws IOException { String zoneId = zone.getId(); - packStructHeader( 3, DATE_TIME_WITH_ZONE_NAME ); + packStructHeader( DATE_TIME_WITH_ZONE_NAME_SIZE, DATE_TIME_WITH_ZONE_NAME ); pack( epochSecondLocal ); pack( nano ); pack( zoneId ); @@ -205,35 +225,63 @@ private static class UnpackerV2 extends Neo4jPackV1.UnpackerV1 } @Override - protected AnyValue unpackStruct( char signature ) throws IOException + protected AnyValue unpackStruct( char signature, long size ) throws IOException { - switch ( signature ) + try + { + switch ( signature ) + { + case POINT_2D: + ensureCorrectStructSize( KnownType.POINT_2D, POINT_2D_SIZE, size ); + return unpackPoint2D(); + case POINT_3D: + ensureCorrectStructSize( KnownType.POINT_3D, POINT_3D_SIZE, size ); + return unpackPoint3D(); + case DURATION: + ensureCorrectStructSize( KnownType.DURATION, DURATION_SIZE, size ); + return unpackDuration(); + case DATE: + ensureCorrectStructSize( KnownType.DATE, DATE_SIZE, size ); + return unpackDate(); + case LOCAL_TIME: + ensureCorrectStructSize( KnownType.LOCAL_TIME, LOCAL_TIME_SIZE, size ); + return unpackLocalTime(); + case TIME: + ensureCorrectStructSize( KnownType.TIME, TIME_SIZE, size ); + return unpackTime(); + case LOCAL_DATE_TIME: + ensureCorrectStructSize( KnownType.LOCAL_DATE_TIME, LOCAL_DATE_TIME_SIZE, size ); + return unpackLocalDateTime(); + case DATE_TIME_WITH_ZONE_OFFSET: + ensureCorrectStructSize( KnownType.DATE_TIME_WITH_ZONE_OFFSET, DATE_TIME_WITH_ZONE_OFFSET_SIZE, size ); + return unpackDateTimeWithZoneOffset(); + case DATE_TIME_WITH_ZONE_NAME: + ensureCorrectStructSize( KnownType.DATE_TIME_WITH_ZONE_NAME, DATE_TIME_WITH_ZONE_NAME_SIZE, size ); + return unpackDateTimeWithZoneName(); + default: + return super.unpackStruct( signature, size ); + } + } + catch ( PackStream.PackStreamException | BoltIOException ex ) { - case POINT_2D: - return unpackPoint2D(); - case POINT_3D: - return unpackPoint3D(); - case DURATION: - return unpackDuration(); - case DATE: - return unpackDate(); - case LOCAL_TIME: - return unpackLocalTime(); - case TIME: - return unpackTime(); - case LOCAL_DATE_TIME: - return unpackLocalDateTime(); - case DATE_TIME_WITH_ZONE_OFFSET: - return unpackDateTimeWithZoneOffset(); - case DATE_TIME_WITH_ZONE_NAME: - return unpackDateTimeWithZoneName(); - default: - return super.unpackStruct( signature ); + throw ex; + } + catch ( Throwable ex ) + { + KnownType type = KnownType.valueOf( signature ); + if ( type != null ) + { + throw new BoltIOException( Status.Statement.TypeError, + String.format( "Unable to construct %s value: `%s`", type.description(), ex.getMessage() ), ex ); + } + + throw ex; } } private PointValue unpackPoint2D() throws IOException { + int crsCode = unpackInteger(); CoordinateReferenceSystem crs = CoordinateReferenceSystem.get( crsCode ); double[] coordinates = {unpackDouble(), unpackDouble()}; diff --git a/community/bolt/src/test/java/org/neo4j/bolt/v2/transport/integration/UnsupportedKnownTypesV2IT.java b/community/bolt/src/test/java/org/neo4j/bolt/v2/transport/integration/UnsupportedKnownTypesV2IT.java new file mode 100644 index 0000000000000..5da588223037a --- /dev/null +++ b/community/bolt/src/test/java/org/neo4j/bolt/v2/transport/integration/UnsupportedKnownTypesV2IT.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2002-2018 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.bolt.v2.transport.integration; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import org.neo4j.bolt.messaging.KnownType; +import org.neo4j.bolt.v1.messaging.BoltRequestMessage; +import org.neo4j.bolt.v1.messaging.Neo4jPack; +import org.neo4j.bolt.v1.packstream.PackedOutputArray; +import org.neo4j.bolt.v1.transport.integration.Neo4jWithSocket; +import org.neo4j.bolt.v1.transport.integration.TransportTestUtil; +import org.neo4j.bolt.v1.transport.socket.client.SecureSocketConnection; +import org.neo4j.bolt.v1.transport.socket.client.SecureWebSocketConnection; +import org.neo4j.bolt.v1.transport.socket.client.SocketConnection; +import org.neo4j.bolt.v1.transport.socket.client.TransportConnection; +import org.neo4j.bolt.v1.transport.socket.client.WebSocketConnection; +import org.neo4j.bolt.v2.messaging.Neo4jPackV2; +import org.neo4j.function.ThrowingConsumer; +import org.neo4j.helpers.HostnamePort; +import org.neo4j.kernel.api.exceptions.Status; +import org.neo4j.values.storable.CoordinateReferenceSystem; +import org.neo4j.values.storable.Values; + +import static java.util.Arrays.asList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.neo4j.bolt.v1.messaging.message.InitMessage.init; +import static org.neo4j.bolt.v1.messaging.util.MessageMatchers.msgFailure; +import static org.neo4j.bolt.v1.messaging.util.MessageMatchers.msgSuccess; +import static org.neo4j.bolt.v1.transport.integration.TransportTestUtil.eventuallyDisconnects; +import static org.neo4j.graphdb.factory.GraphDatabaseSettings.auth_enabled; + +@RunWith( Parameterized.class ) +public class UnsupportedKnownTypesV2IT +{ + private static final String USER_AGENT = "TestClient/2.0"; + + @Rule + public Neo4jWithSocket server = new Neo4jWithSocket( getClass(), settings -> settings.put( auth_enabled.name(), "false" ) ); + + @Parameterized.Parameter + public Class connectionClass; + + private HostnamePort address; + private TransportConnection connection; + private TransportTestUtil util; + + @Parameterized.Parameters( name = "{0}" ) + public static List> transports() + { + return asList( SocketConnection.class, WebSocketConnection.class, SecureSocketConnection.class, SecureWebSocketConnection.class ); + } + + @Before + public void setup() throws Exception + { + address = server.lookupDefaultConnector(); + connection = connectionClass.newInstance(); + util = new TransportTestUtil( new Neo4jPackV2() ); + } + + @After + public void cleanup() throws Exception + { + if ( connection != null ) + { + connection.disconnect(); + } + } + + @Test + public void shouldFailWhenPoint2DIsSentWithInvalidCrsId() throws Exception + { + testFailureWithUnpackableValue( packer -> + { + packer.packStructHeader( 3, KnownType.POINT_2D.signature() ); + packer.pack( Values.of( 5 ) ); + packer.pack( Values.of( 3.15 ) ); + packer.pack( Values.of( 4.012 ) ); + }, "Unable to construct Point value: `Unknown coordinate reference system code: 5`" ); + } + + @Test + public void shouldFailWhenPoint3DIsSentWithInvalidCrsId() throws Exception + { + testFailureWithUnpackableValue( packer -> + { + packer.packStructHeader( 4, KnownType.POINT_3D.signature() ); + packer.pack( Values.of( 1200 ) ); + packer.pack( Values.of( 3.15 ) ); + packer.pack( Values.of( 4.012 ) ); + packer.pack( Values.of( 5.905 ) ); + }, "Unable to construct Point value: `Unknown coordinate reference system code: 1200`" ); + } + + @Test + public void shouldFailWhenPoint2DDimensionsDoNotMatch() throws Exception + { + testDisconnectWithUnpackableValue( packer -> + { + packer.packStructHeader( 3, KnownType.POINT_3D.signature() ); + packer.pack( Values.of( CoordinateReferenceSystem.Cartesian_3D.getCode() ) ); + packer.pack( Values.of( 3.15 ) ); + packer.pack( Values.of( 4.012 ) ); + }, "Unable to construct Point value: `Cannot create point, CRS cartesian-3d expects 3 dimensions, but got coordinates [3.15, 4.012]`" ); + } + + @Test + public void shouldFailWhenPoint3DDimensionsDoNotMatch() throws Exception + { + testFailureWithUnpackableValue( packer -> + { + packer.packStructHeader( 4, KnownType.POINT_3D.signature() ); + packer.pack( Values.of( CoordinateReferenceSystem.Cartesian.getCode() ) ); + packer.pack( Values.of( 3.15 ) ); + packer.pack( Values.of( 4.012 ) ); + packer.pack( Values.of( 5.905 ) ); + }, "Unable to construct Point value: `Cannot create point, CRS cartesian expects 2 dimensions, but got coordinates [3.15, 4.012, 5.905]`" ); + } + + @Test + public void shouldFailWhenZonedDateTimeZoneIdIsNotKnown() throws Exception + { + testFailureWithUnpackableValue( packer -> + { + packer.packStructHeader( 3, KnownType.DATE_TIME_WITH_ZONE_NAME.signature() ); + packer.pack( Values.of( 0 ) ); + packer.pack( Values.of( 0 ) ); + packer.pack( Values.of( "Europe/Marmaris" ) ); + }, "Unable to construct ZonedDateTime value: `Unknown time-zone ID: Europe/Marmaris`" ); + } + + private void testFailureWithUnpackableValue( ThrowingConsumer valuePacker, String expectedMessage ) throws Exception + { + connection.connect( address ).send( util.defaultAcceptedVersions() ); + assertThat( connection, util.eventuallyReceivesSelectedProtocolVersion() ); + connection.send( util.chunk( init( USER_AGENT, Collections.emptyMap() ) ) ); + assertThat( connection, util.eventuallyReceives( msgSuccess() ) ); + + connection.send( util.chunk( 64, createRunWith( valuePacker ) ) ); + + assertThat( connection, + util.eventuallyReceives( msgFailure( Status.Statement.TypeError, expectedMessage ) ) ); + assertThat( connection, eventuallyDisconnects() ); + } + + private void testDisconnectWithUnpackableValue( ThrowingConsumer valuePacker, String expectedMessage ) throws Exception + { + connection.connect( address ).send( util.defaultAcceptedVersions() ); + assertThat( connection, util.eventuallyReceivesSelectedProtocolVersion() ); + connection.send( util.chunk( init( USER_AGENT, Collections.emptyMap() ) ) ); + assertThat( connection, util.eventuallyReceives( msgSuccess() ) ); + + connection.send( util.chunk( 64, createRunWith( valuePacker ) ) ); + + assertThat( connection, eventuallyDisconnects() ); + } + + private byte[] createRunWith( ThrowingConsumer valuePacker ) throws IOException + { + PackedOutputArray out = new PackedOutputArray(); + Neo4jPack.Packer packer = new Neo4jPackV2().newPacker( out ); + + packer.packStructHeader( 2, BoltRequestMessage.RUN.signature() ); + packer.pack( "RETURN $x" ); + packer.packMapHeader( 1 ); + packer.pack( "x" ); + valuePacker.accept( packer ); + + return out.bytes(); + } +}