diff --git a/.github/workflows/testkit.yml b/.github/workflows/testkit.yml index 0f60231b..523f1dfc 100644 --- a/.github/workflows/testkit.yml +++ b/.github/workflows/testkit.yml @@ -68,11 +68,5 @@ jobs: - name: Run integration tests run: | - docker compose up -d --remove-orphans --wait --no-build \ - server1 \ - server2 \ - server3 \ - server4 \ - testkit_backend - - docker compose run --rm testkit ./testkit.sh + docker compose up -d --remove-orphans --wait --no-build testkit_backend neo4j + docker compose up testkit diff --git a/Dockerfile.neo4j-okta b/Dockerfile.neo4j-okta deleted file mode 100644 index 6cc042b6..00000000 --- a/Dockerfile.neo4j-okta +++ /dev/null @@ -1,3 +0,0 @@ -FROM neo4j:5-enterprise - -COPY ./neo4j-with-okta.conf /var/lib/neo4j/conf/neo4j.conf diff --git a/composer.json b/composer.json index f825397f..5c175720 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "psr/http-factory": "^1.0", "psr/http-client": "^1.0", "php-http/message": "^1.0", - "stefanak-michal/bolt": "^7.2.4", + "stefanak-michal/bolt": "^7.1.4", "symfony/polyfill-php80": "^1.2", "psr/simple-cache": ">=2.0", "ext-json": "*", diff --git a/docker-compose.yml b/docker-compose.yml index abf14b8d..8feb9235 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,7 +53,7 @@ services: - .env neo4j: <<: *common - image: neo4j:5.23-enterprise + image: neo4j:5-enterprise hostname: neo4j networks: - neo4j @@ -62,7 +62,7 @@ services: - "11474:7474" environment: <<: *common-env - NEO4J_ACCEPT_LICENSE_AGREEMENT: 'yes' # Also add this + NEO4J_ACCEPT_LICENSE_AGREEMENT: 'yes' NEO4J_server_bolt_advertised__address: neo4j:7687 NEO4J_server_http_advertised__address: neo4j:7474 @@ -119,6 +119,7 @@ services: testkit: image: python:3.13 + command: ./testkit.sh volumes: - .:/opt/project working_dir: /opt/project/testkit-backend @@ -126,11 +127,15 @@ services: - neo4j environment: TEST_NEO4J_HOST: neo4j + TEST_NEO4J_PORT: 7687 # Add this if your testkit uses it TEST_NEO4J_USER: neo4j TEST_NEO4J_PASS: testtest TEST_DRIVER_NAME: php TEST_DRIVER_REPO: /opt/project TEST_BACKEND_HOST: testkit_backend + TEST_STUB_HOST: testkit + depends_on: + - testkit_backend testkit_backend: <<: *common-php @@ -141,7 +146,5 @@ services: - neo4j extra_hosts: - "host.docker.internal:host-gateway" - depends_on: - - neo4j ports: - "9876:9876" diff --git a/neo4j-with-okta.conf b/neo4j-with-okta.conf deleted file mode 100644 index 3c8e189e..00000000 --- a/neo4j-with-okta.conf +++ /dev/null @@ -1,829 +0,0 @@ -#***************************************************************** -# Neo4j configuration -# -# For more details and a complete list of settings, please see -# https://neo4j.com/docs/operations-manual/current/reference/configuration-settings/ -#***************************************************************** - -# Paths of directories in the installation. -#server.directories.data=data -#server.directories.plugins=plugins -#server.directories.logs=logs -#server.directories.lib=lib -#server.directories.run=run -#server.directories.licenses=licenses -#server.directories.metrics=metrics -#server.directories.dumps.root=data/dumps -#server.directories.transaction.logs.root=data/transactions - -# This setting constrains all `LOAD CSV` import files to be under the `import` directory. Remove or comment it out to -# allow files to be loaded from anywhere in the filesystem; this introduces possible security problems. See the -# `LOAD CSV` section of the manual for details. -server.directories.import=import - -# Whether requests to Neo4j are authenticated. -# To disable authentication, uncomment this line -#dbms.security.auth_enabled=false - -# Number of databases in Neo4j is limited. -# To change this limit please uncomment and adapt following setting: -# dbms.max_databases=100 - -# Enable online backups to be taken from this database. -#server.backup.enabled=true - -# By default the backup service will only listen on localhost. -# To enable remote backups you will have to bind to an external -# network interface (e.g. 0.0.0.0 for all interfaces). -# The protocol running varies depending on deployment. In a cluster this is the -# same protocol that runs on server.cluster.listen_address. -#server.backup.listen_address=0.0.0.0:6362 - -# Anonymous usage data reporting -# To disable, uncomment this line -#dbms.usage_report.enabled=false - -#***************************************************************** -# Initial DBMS Settings -#***************************************************************** - -# Initial DBMS settings are picked up from the config file once, when a cluster first starts, and then transferred into -# the running DBMS. This means later changes to the values will not be seen. There are procedures to change the values -# after the initial start - -# Name of the default database (aliases are not supported). Can be changed with the 'dbms.setDefaultDatabase' procedure. -#initial.dbms.default_database=neo4j - -# Initial default number of primary and secondary instances of user databases. If the user does not specify the number -# of primaries and secondaries in 'CREATE DATABASE', these values will be used, unless they are overwritten with the -# 'dbms.setDefaultAllocationNumbers' procedure. -#initial.dbms.default_primaries_count=1 -#initial.dbms.default_secondaries_count=0 - -#******************************************************************** -# Memory Settings -#******************************************************************** -# -# Memory settings are specified kibibytes with the 'k' suffix, mebibytes with -# 'm' and gibibytes with 'g'. -# If Neo4j is running on a dedicated server, then it is generally recommended -# to leave about 2-4 gigabytes for the operating system, give the JVM enough -# heap to hold all your transaction state and query context, and then leave the -# rest for the page cache. - -# Java Heap Size: by default the Java heap size is dynamically calculated based -# on available system resources. Uncomment these lines to set specific initial -# and maximum heap size. -#server.memory.heap.initial_size=512m -#server.memory.heap.max_size=512m - -# The amount of memory to use for mapping the store files. -# The default page cache memory assumes the machine is dedicated to running -# Neo4j, and is heuristically set to 50% of RAM minus the Java heap size. -#server.memory.pagecache.size=10g - -# Limit the amount of memory that all of the running transaction can consume. -# The default value is 70% of the heap size limit. -#dbms.memory.transaction.total.max=256m - -# Limit the amount of memory that a single transaction can consume. -# By default there is no limit. -#db.memory.transaction.max=16m - -#***************************************************************** -# Network connector configuration -#***************************************************************** - -# With default configuration Neo4j only accepts local connections. -# Use 0.0.0.0 to bind to all network interfaces on the machine. If you want to only use a specific interface -# (such as a private IP address on AWS, for example) then use that IP address instead. -#server.default_listen_address=0.0.0.0 - -# You can also choose a specific network interface, and configure a non-default -# port for each connector, by setting their individual listen_address. - -# The address at which this server can be reached by its clients. This may be the server's IP address or DNS name, or -# it may be the address of a reverse proxy which sits in front of the server. This setting may be overridden for -# individual connectors below. -#server.default_advertised_address=localhost - -# You can also choose a specific advertised hostname or IP address, and -# configure an advertised port for each connector, by setting their -# individual advertised_address. - -# By default, encryption is turned off. -# To turn on encryption, an ssl policy for the connector needs to be configured -# Read more in SSL policy section in this file for how to define a SSL policy. - -# Bolt connector -server.bolt.enabled=true -#server.bolt.tls_level=DISABLED -#server.bolt.listen_address=:7687 -#server.bolt.advertised_address=:7687 - -# HTTP Connector. There can be zero or one HTTP connectors. -server.http.enabled=true -#server.http.listen_address=:7474 -#server.http.advertised_address=:7474 - -# HTTPS Connector. There can be zero or one HTTPS connectors. -server.https.enabled=false -#server.https.listen_address=:7473 -#server.https.advertised_address=:7473 - -# Number of Neo4j worker threads. -#server.threads.worker_count= - -#***************************************************************** -# SSL policy configuration -#***************************************************************** - -# Each policy is configured under a separate namespace, e.g. -# dbms.ssl.policy..* -# can be any of 'bolt', 'https', 'cluster' or 'backup' -# -# The scope is the name of the component where the policy will be used -# Each component where the use of an ssl policy is desired needs to declare at least one setting of the policy. -# Allowable values are 'bolt', 'https', 'cluster' or 'backup'. - -# E.g if bolt and https connectors should use the same policy, the following could be declared -# dbms.ssl.policy.bolt.base_directory=certificates/default -# dbms.ssl.policy.https.base_directory=certificates/default -# However, it's strongly encouraged to not use the same key pair for multiple scopes. -# -# N.B: Note that a connector must be configured to support/require -# SSL/TLS for the policy to actually be utilized. -# -# see: dbms.connector.*.tls_level - -# SSL settings (dbms.ssl.policy..*) -# .base_directory Base directory for SSL policies paths. All relative paths within the -# SSL configuration will be resolved from the base dir. -# -# .private_key A path to the key file relative to the '.base_directory'. -# -# .private_key_password The password for the private key. -# -# .public_certificate A path to the public certificate file relative to the '.base_directory'. -# -# .trusted_dir A path to a directory containing trusted certificates. -# -# .revoked_dir Path to the directory with Certificate Revocation Lists (CRLs). -# -# .verify_hostname If true, the server will verify the hostname that the client uses to connect with. In order -# for this to work, the server public certificate must have a valid CN and/or matching -# Subject Alternative Names. -# -# .client_auth How the client should be authorized. Possible values are: 'none', 'optional', 'require'. -# -# .tls_versions A comma-separated list of allowed TLS versions. By default only TLSv1.2 and TLSv1.3 are allowed. -# -# .trust_all Setting this to 'true' will ignore the trust truststore, trusting all clients and servers. -# Use of this mode is discouraged. It would offer encryption but no security. -# -# .ciphers A comma-separated list of allowed ciphers. The default ciphers are the defaults of -# the JVM platform. - -# Bolt SSL configuration -#dbms.ssl.policy.bolt.enabled=true -#dbms.ssl.policy.bolt.base_directory=certificates/bolt -#dbms.ssl.policy.bolt.private_key=private.key -#dbms.ssl.policy.bolt.public_certificate=public.crt -#dbms.ssl.policy.bolt.client_auth=NONE - -# Https SSL configuration -#dbms.ssl.policy.https.enabled=true -#dbms.ssl.policy.https.base_directory=certificates/https -#dbms.ssl.policy.https.private_key=private.key -#dbms.ssl.policy.https.public_certificate=public.crt -#dbms.ssl.policy.https.client_auth=NONE - -# Cluster SSL configuration -#dbms.ssl.policy.cluster.enabled=true -#dbms.ssl.policy.cluster.base_directory=certificates/cluster -#dbms.ssl.policy.cluster.private_key=private.key -#dbms.ssl.policy.cluster.public_certificate=public.crt - -# Backup SSL configuration -#dbms.ssl.policy.backup.enabled=true -#dbms.ssl.policy.backup.base_directory=certificates/backup -#dbms.ssl.policy.backup.private_key=private.key -#dbms.ssl.policy.backup.public_certificate=public.crt - -#***************************************************************** -# Logging configuration -#***************************************************************** - -# To enable HTTP logging, uncomment this line -#dbms.logs.http.enabled=true - -# To enable GC Logging, uncomment this line -#server.logs.gc.enabled=true - -# GC Logging Options -# see https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-BE93ABDC-999C-4CB5-A88B-1994AAAC74D5 -#server.logs.gc.options=-Xlog:gc*,safepoint,age*=trace - -# Number of GC logs to keep. -#server.logs.gc.rotation.keep_number=5 - -# Size of each GC log that is kept. -#server.logs.gc.rotation.size=20m - -# Log executed queries. One of OFF, INFO and VERBOSE. INFO logs queries longer than a given threshold, VERBOSE logs start and end of all queries. -#db.logs.query.enabled=VERBOSE - -# If the execution of query takes more time than this threshold, the query is logged. If set to zero then all queries -# are logged. Only used if `db.logs.query.enabled` is set to INFO -#db.logs.query.threshold=0 - -# Include parameters for the executed queries being logged (this is enabled by default). -#db.logs.query.parameter_logging_enabled=true - -# The security log is always enabled when `dbms.security.auth_enabled=true`, for addition -# configuration, look at $NEO4J_HOME/conf/server-logs.xml - -#***************************************************************** -# Cluster Configuration -#***************************************************************** - -# Uncomment and specify these lines for running Neo4j in a cluster. -# See the cluster documentation at https://neo4j.com/docs/ for details. - -# A comma-separated list of endpoints which a server should contact in order to discover other cluster members. It must -# be in the host:port format. For each machine in the cluster, the address will usually be the public ip address of -# that machine. The port will be the value used in the setting "server.cluster.advertised_address" of that server. -# WARN: If you use this, please also set dbms.cluster.discovery.version=V2_ONLY. -#dbms.cluster.discovery.v2.endpoints=localhost:6000,localhost:6001,localhost:6002 - -# The version of discovery to use for servers to find each other. We recommend V2_ONLY for new clusters, but if you need -# compatibility with existing clusters you can set V1_ONLY, which is the default. There are other options for use when -# moving from V1 to V2. -#dbms.cluster.discovery.version=V2_ONLY - -# Host and port to bind the cluster member V1 discovery management communication. -# If you need to use V1 discovery, uncomment this and also set the setting "dbms.cluster.discovery.endpoints" pointing -# to the values of "server.discovery.advertised_address" from all the servers. -#server.discovery.listen_address=:5000 -#server.discovery.advertised_address=:5000 - -# Network interface and port for various internal protocols to use, including the transaction shipping server. -# Please note that it is also possible to run the backup client against this port so always limit access to it via the -# firewall and configure an ssl policy. -# This is the setting to add to the collection of addresses in dbms.cluster.discovery.v2.endpoints if using V2 discovery. -#server.cluster.listen_address=:6000 -#server.cluster.advertised_address=:6000 - -# Network interface and port for the RAFT server to listen on. -#server.cluster.raft.listen_address=:7000 -#server.cluster.raft.advertised_address=:7000 - -# Network interface and port for server-side routing within the cluster. This allows requests to be forwarded -# from one cluster member to another, if the requests can't be satisfied by the first member (e.g. write requests -# received by a non-leader). -#server.routing.listen_address=:7688 -#server.routing.advertised_address=:7688 - -# List a set of names for groups to which this server should belong. This -# is a comma-separated list and names should only use alphanumericals -# and underscore. This can be used to identify groups of servers in the -# configuration for load balancing and replication policies. -# -# The main intention for this is to group servers, but it is possible to specify -# a unique identifier here as well which might be useful for troubleshooting -# or other special purposes. -#server.groups= - -#***************************************************************** -# Initial Server Settings -#***************************************************************** - -# Initial server settings are used as the default values when enabling a server, but can be overridden by specifying -# options when calling ENABLE (relevant for servers in a cluster *after* those that form the initial cluster). - -# Restrict the modes of database that can be hosted on this server -# Allowed values: -# PRIMARY - Host standalone databases, and members of the consensus quorum for a multi-primary database. -# SECONDARY - Only host read replicas, eventually-consistent read-only instances of databases. -# NONE - Can host any mode of database -#initial.server.mode_constraint=NONE - -#***************************************************************** -# Cluster Load Balancing -#***************************************************************** - -# N.B: Read the online documentation for a thorough explanation! - -# Selects the load balancing plugin that shall be enabled. -#dbms.routing.load_balancing.plugin=server_policies - -####### Examples for "server_policies" plugin ####### - -# Will select all available servers as the default policy, which is the -# policy used when the client does not specify a policy preference. The -# default configuration for the default policy is all(). -#dbms.routing.load_balancing.config.server_policies.default=all() - -# Will select servers in groups 'group1' or 'group2' under the default policy. -#dbms.routing.load_balancing.config.server_policies.default=groups(group1,group2) - -# Slightly more advanced example: -# Will select servers in 'group1', 'group2' or 'group3', but only if there are at least 2. -# This policy will be exposed under the name of 'mypolicy'. -#dbms.routing.load_balancing.config.server_policies.mypolicy=groups(group1,group2,group3) -> min(2) - -# Below will create an even more advanced policy named 'regionA' consisting of several rules -# yielding the following behaviour: -# -# select servers in regionA, if at least 2 are available -# otherwise: select servers in regionA and regionB, if at least 2 are available -# otherwise: select all servers -# -# The intention is to create a policy for a particular region which prefers -# a certain set of local servers, but which will fallback to other regions -# or all available servers as required. -# -# N.B: The following configuration uses the line-continuation character \ -# which allows you to construct an easily readable rule set spanning -# several lines. -# -#dbms.routing.load_balancing.config.server_policies.policyA=\ -#groups(regionA) -> min(2);\ -#groups(regionA,regionB) -> min(2); - -# Note that implicitly the last fallback is to always consider all() servers, -# but this can be prevented by specifying a halt() as the last rule. -# -#dbms.routing.load_balancing.config.server_policies.regionA_only=\ -#groups(regionA);\ -#halt(); - -#***************************************************************** -# Cluster Additional Configuration Options -#***************************************************************** -# The following settings are used less frequently. -# If you don't know what these are, you don't need to change these from their default values. - -# Cluster Routing Connector. Disable the opening of an additional port to allow -# for internal communication using the same security configuration as CLUSTER -#dbms.routing.enabled=false - -# The time window within which the loss of the leader is detected and the first re-election attempt is held. -# The window should be significantly larger than typical communication delays to make conflicts unlikely. -#dbms.cluster.raft.leader_failure_detection_window=20s-23s - -# The rate at which leader elections happen. Note that due to election conflicts it might take several attempts to -# find a leader. The window should be significantly larger than typical communication delays to make conflicts unlikely. -#dbms.cluster.raft.election_failure_detection_window=3s-6s - -# The time limit allowed for a new member to attempt to update its data to match the rest of the cluster. -#dbms.cluster.raft.membership.join_timeout=10m - -# Maximum amount of lag accepted for a new follower to join the Raft group. -#dbms.cluster.raft.membership.join_max_lag=10s - -# Raft log pruning frequency. -#dbms.cluster.raft.log.pruning_frequency=10m - -# The size to allow the raft log to grow before rotating. -#dbms.cluster.raft.log.rotation_size=250M - -# The name of a server_group whose members should be prioritized as leaders for the given database. -# This does not guarantee that members of this group will be leader at all times, but the cluster -# will attempt to transfer leadership to such a member when possible. -# N.B. the final portion of this config key is dynamic and refers to the name of the database being configured. -# You may specify multiple `db.cluster.raft.leader_transfer.priority_group.=` pairs: -#db.cluster.raft.leader_transfer.priority_group.foo= -#db.cluster.raft.leader_transfer.priority_group.neo4j= - -# Which strategy to use when transferring database leaderships around a cluster. -# This can be one of `equal_balancing` or `no_balancing`. -# `equal_balancing` automatically ensures that each Core server holds the leader role for an equal number of databases. -# `no_balancing` prevents any automatic balancing of the leader role. -# Note that if a `leadership_priority_group` is specified for a given database, -# the value of this setting will be ignored for that database. -#dbms.cluster.raft.leader_transfer.balancing_strategy=equal_balancing - -# The following setting controls how frequently a server hosting a secondary for a given database attempts to -# fetch an update from a server hosting a primary for that database -#db.cluster.catchup.pull_interval=1s - -#******************************************************************** -# Security Configuration -#******************************************************************** - -# The authentication and authorization providers that contains both users and roles. -# This can be one of the built-in `native` or `ldap` auth providers, -# or it can be an externally provided plugin, with a custom name prefixed by `plugin`, -# i.e. `plugin-`. -#dbms.security.authentication_providers=oidc-okta -#dbms.security.authorization_providers=oidc-okta - -# The time to live (TTL) for cached authentication and authorization info when using -# external auth providers (LDAP or plugin). Setting the TTL to 0 will -# disable auth caching. -#dbms.security.auth_cache_ttl=10m - -# The maximum capacity for authentication and authorization caches (respectively). -#dbms.security.auth_cache_max_capacity=10000 - -# Set to log successful authentication events to the security log. -# If this is set to `false` only failed authentication events will be logged, which -# could be useful if you find that the successful events spam the logs too much, -# and you do not require full auditing capability. -#dbms.security.log_successful_authentication=true - -#================================================ -# LDAP Auth Provider Configuration -#================================================ - -# URL of LDAP server to use for authentication and authorization. -# The format of the setting is `://:`, where hostname is the only required field. -# The supported values for protocol are `ldap` (default) and `ldaps`. -# The default port for `ldap` is 389 and for `ldaps` 636. -# For example: `ldaps://ldap.example.com:10389`. -# -# NOTE: You may want to consider using STARTTLS (`dbms.security.ldap.use_starttls`) instead of LDAPS -# for secure connections, in which case the correct protocol is `ldap`. -#dbms.security.ldap.host=localhost - -# Use secure communication with the LDAP server using opportunistic TLS. -# First an initial insecure connection will be made with the LDAP server, and then a STARTTLS command -# will be issued to negotiate an upgrade of the connection to TLS before initiating authentication. -#dbms.security.ldap.use_starttls=false - -# The LDAP referral behavior when creating a connection. This is one of `follow`, `ignore` or `throw`. -# `follow` automatically follows any referrals -# `ignore` ignores any referrals -# `throw` throws an exception, which will lead to authentication failure -#dbms.security.ldap.referral=follow - -# The timeout for establishing an LDAP connection. If a connection with the LDAP server cannot be -# established within the given time the attempt is aborted. -# A value of 0 means to use the network protocol's (i.e., TCP's) timeout value. -#dbms.security.ldap.connection_timeout=30s - -# The timeout for an LDAP read request (i.e. search). If the LDAP server does not respond within -# the given time the request will be aborted. A value of 0 means wait for a response indefinitely. -#dbms.security.ldap.read_timeout=30s - -#---------------------------------- -# LDAP Authentication Configuration -#---------------------------------- - -# LDAP authentication mechanism. This is one of `simple` or a SASL mechanism supported by JNDI, -# for example `DIGEST-MD5`. `simple` is basic username -# and password authentication and SASL is used for more advanced mechanisms. See RFC 2251 LDAPv3 -# documentation for more details. -#dbms.security.ldap.authentication.mechanism=simple - -# LDAP user DN template. An LDAP object is referenced by its distinguished name (DN), and a user DN is -# an LDAP fully-qualified unique user identifier. This setting is used to generate an LDAP DN that -# conforms with the LDAP directory's schema from the user principal that is submitted with the -# authentication token when logging in. -# The special token {0} is a placeholder where the user principal will be substituted into the DN string. -#dbms.security.ldap.authentication.user_dn_template=uid={0},ou=users,dc=example,dc=com - -# Determines if the result of authentication via the LDAP server should be cached or not. -# Caching is used to limit the number of LDAP requests that have to be made over the network -# for users that have already been authenticated successfully. A user can be authenticated against -# an existing cache entry (instead of via an LDAP server) as long as it is alive -# (see `dbms.security.auth_cache_ttl`). -# An important consequence of setting this to `true` is that -# Neo4j then needs to cache a hashed version of the credentials in order to perform credentials -# matching. This hashing is done using a cryptographic hash function together with a random salt. -# Preferably a conscious decision should be made if this method is considered acceptable by -# the security standards of the organization in which this Neo4j instance is deployed. -#dbms.security.ldap.authentication.cache_enabled=true - -#---------------------------------- -# LDAP Authorization Configuration -#---------------------------------- -# Authorization is performed by searching the directory for the groups that -# the user is a member of, and then map those groups to Neo4j roles. - -# Perform LDAP search for authorization info using a system account instead of the user's own account. -# -# If this is set to `false` (default), the search for group membership will be performed -# directly after authentication using the LDAP context bound with the user's own account. -# The mapped roles will be cached for the duration of `dbms.security.auth_cache_ttl`, -# and then expire, requiring re-authentication. To avoid frequently having to re-authenticate -# sessions you may want to set a relatively long auth cache expiration time together with this option. -# NOTE: This option will only work if the users are permitted to search for their -# own group membership attributes in the directory. -# -# If this is set to `true`, the search will be performed using a special system account user -# with read access to all the users in the directory. -# You need to specify the username and password using the settings -# `dbms.security.ldap.authorization.system_username` and -# `dbms.security.ldap.authorization.system_password` with this option. -# Note that this account only needs read access to the relevant parts of the LDAP directory -# and does not need to have access rights to Neo4j, or any other systems. -#dbms.security.ldap.authorization.use_system_account=false - -# An LDAP system account username to use for authorization searches when -# `dbms.security.ldap.authorization.use_system_account` is `true`. -# Note that the `dbms.security.ldap.authentication.user_dn_template` will not be applied to this username, -# so you may have to specify a full DN. -#dbms.security.ldap.authorization.system_username= - -# An LDAP system account password to use for authorization searches when -# `dbms.security.ldap.authorization.use_system_account` is `true`. -#dbms.security.ldap.authorization.system_password= - -# The name of the base object or named context to search for user objects when LDAP authorization is enabled. -# A common case is that this matches the last part of `dbms.security.ldap.authentication.user_dn_template`. -#dbms.security.ldap.authorization.user_search_base=ou=users,dc=example,dc=com - -# The LDAP search filter to search for a user principal when LDAP authorization is -# enabled. The filter should contain the placeholder token {0} which will be substituted for the -# user principal. -#dbms.security.ldap.authorization.user_search_filter=(&(objectClass=*)(uid={0})) - -# A list of attribute names on a user object that contains groups to be used for mapping to roles -# when LDAP authorization is enabled. This setting is ignored when `dbms.ldap_authorization_nested_groups_enabled` is `true`. -#dbms.security.ldap.authorization.group_membership_attributes=memberOf - -# This setting determines whether multiple LDAP search results will be processed (as is required for the lookup of nested groups). -# If set to `true` then instead of using attributes on the user object to determine group membership (as specified by -# `dbms.security.ldap.authorization.group_membership_attributes`), the `user` object will only be used to determine the user's -# Distinguished Name, which will subsequently be used with `dbms.security.ldap.authorization.user_search_filter` -# in order to perform a nested group search. The Distinguished Names of the resultant group search results will be used to determine roles. -#dbms.security.ldap.authorization.nested_groups_enabled=false - -# The search template which will be used to find the nested groups which the user is a member of. -# The filter should contain the placeholder token `{0}` which will be substituted with the user's -# Distinguished Name (which is found for the specified user principle using `dbms.security.ldap.authorization.user_search_filter`). -# The default value specifies Active Directory's LDAP_MATCHING_RULE_IN_CHAIN (aka 1.2.840.113556.1.4.1941) implementation -# which will walk the ancestry of group membership for the specified user. -#dbms.security.ldap.authorization.nested_groups_search_filter=(&(objectclass=group)(member:1.2.840.113556.1.4.1941:={0})) - -# An authorization mapping from LDAP group names to Neo4j role names. -# The map should be formatted as a semicolon separated list of key-value pairs, where the -# key is the LDAP group name and the value is a comma separated list of corresponding role names. -# For example: group1=role1;group2=role2;group3=role3,role4,role5 -# -# You could also use whitespaces and quotes around group names to make this mapping more readable, -# for example: dbms.security.ldap.authorization.group_to_role_mapping=\ -# "cn=Neo4j Read Only,cn=users,dc=example,dc=com" = reader; \ -# "cn=Neo4j Read-Write,cn=users,dc=example,dc=com" = publisher; \ -# "cn=Neo4j Schema Manager,cn=users,dc=example,dc=com" = architect; \ -# "cn=Neo4j Administrator,cn=users,dc=example,dc=com" = admin -#dbms.security.ldap.authorization.group_to_role_mapping= - -#***************************************************************** -# OpenID Connect configuration -#***************************************************************** - -# The display name for the provider. This will be displayed in clients such as Neo4j Browser and Bloom. -# dbms.security.oidc.okta.display_name=Okta - -# The OIDC auth_flow for clients such as Neo4j Browser and Bloom to use. Supported values are 'pkce' and 'implicit' -# dbms.security.oidc.okta.auth_flow=pkce - -# The OpenID Connect Discovery URL for the provider -# dbms.security.oidc.okta.well_known_discovery_uri=https://dev-vti54101110.okta.com/oauth2/default/.well-known/oauth-authorization-server - -# URL of the provider's Authorization Endpoint -# dbms.security.oidc.okta.auth_endpoint= - -# Parameters to use with the Authorization Endpoint. -#dbms.security.oidc..auth_params= - -# URL of the provider's OAuth 2.0 Token Endpoint -#dbms.security.oidc..token_endpoint= - -# Parameters to use with the Token Endpoint. -#dbms.security.oidc..token_params= - -# URL of the provider's JSON Web Key Set -#dbms.security.oidc..jwks_uri= - -# URL of the provider's UserInfo Endpoint -#dbms.security.oidc..user_info_uri= - -# URL that the provider asserts as its issuer identifier. This will be checked against the iss claim in the token -#dbms.security.oidc..issuer= - -# The expected value for the `aud` claim -# dbms.security.oidc.okta.audience=api://default - -# The client_id of this client as issued by the provider. -# dbms.security.oidc.okta.client_id=KnayoXYOnpMjKi6fulL5rRA0n2ghSAH4 - -# Whether to fetch the groups claim from the user info endpoint on the identity provider. The default is false, read it from the token. -#dbms.security.oidc..get_groups_from_user_info=false - -# Whether to fetch the username claim from the user info endpoint on the identity provider. The default is false, read it from the token. -#dbms.security.oidc..get_username_from_user_info=false - -# The claim to use for the database username. -# dbms.security.oidc.okta.claims.username=sub - -# The claim to use for the database roles. -# dbms.security.oidc.okta.claims.groups=groups - -# General parameters to use with the Identity Provider. -# dbms.security.oidc.okta.params=client_id=KnayoXYOnpMjKi6fulL5rRA0n2ghSAH4;response_type=code;scope=openid profile email - -# General config to use with the Identity Provider. -#dbms.security.oidc..config= - -# An authorization mapping from identity provider group names to Neo4j role names. See dbms.security.ldap.authorization.group_to_role_mapping above -# for the format. -#dbms.security.oidc.okta.authorization.group_to_role_mapping= "engineers" = admin; \ -# "collaborators" = reader - -# dbms.security.authentication_providers=oidc -# dbms.security.authorization_providers=oidc -# dbms.security.oidc.0.audience=0oaptrveuyV09M2Lh5d7 - -# dbms.security.oidc.0.display_name=Okta -# dbms.security.oidc.0.client_id=0oaptrveuyV09M2Lh5d7 -# dbms.security.oidc.0.discovery_uri=https://dev-00784916.okta.com/oauth2/default/.well-known/openid-configuration -# dbms.security.oidc.0.authorization_endpoint_params=scope=openid email profile - - -# === Enable OIDC === -dbms.security.authentication_providers=oidc-okta -dbms.security.authorization_providers=oidc-okta -dbms.security.oidc.okta.display_name=Okta -dbms.security.oidc.okta.auth_flow=pkce -dbms.security.oidc.okta.well_known_discovery_uri=https://dev-00784916.okta.com/oauth2/default/.well-known/openid-configuration -dbms.security.oidc.okta.audience=api://default -dbms.security.oidc.okta.claims.username=sub -dbms.security.oidc.okta.claims.groups=admin-groups -dbms.security.oidc.okta.params=client_id=0oaprtveuyV09M2Lh5d7;response_type=code;scope=openid profile email -dbms.security.oidc.okta.authorization.group_to_role_mapping= "engineers" = admin; \ - "collaborators" = reader - - -#***************************************************************** -# Miscellaneous configuration -#***************************************************************** - -# Compresses the metric archive files. -server.metrics.csv.rotation.compression=zip - -# Determines if Cypher will allow using file URLs when loading data using -# `LOAD CSV`. Setting this value to `false` will cause Neo4j to fail `LOAD CSV` -# clauses that load data from the file system. -#dbms.security.allow_csv_import_from_file_urls=true - - -# Value of the Access-Control-Allow-Origin header sent over any HTTP or HTTPS -# connector. This defaults to '*', which allows broadest compatibility. Note -# that any URI provided here limits HTTP/HTTPS access to that URI only. -#dbms.security.http_access_control_allow_origin=* - -# Value of the HTTP Strict-Transport-Security (HSTS) response header. This header -# tells browsers that a webpage should only be accessed using HTTPS instead of HTTP. -# It is attached to every HTTPS response. Setting is not set by default so -# 'Strict-Transport-Security' header is not sent. Value is expected to contain -# directives like 'max-age', 'includeSubDomains' and 'preload'. -#dbms.security.http_strict_transport_security= - -# Retention policy for transaction logs needed to perform recovery and backups. -db.tx_log.rotation.retention_policy=2 days 2G - -# Limit the number of IOs the background checkpoint process will consume per second. -# This setting is advisory, is ignored in Neo4j Community Edition, and is followed to -# best effort in Enterprise Edition. -# An IO is in this case a 8 KiB (mostly sequential) write. Limiting the write IO in -# this way will leave more bandwidth in the IO subsystem to service random-read IOs, -# which is important for the response time of queries when the database cannot fit -# entirely in memory. The only drawback of this setting is that longer checkpoint times -# may lead to slightly longer recovery times in case of a database or system crash. -# A lower number means lower IO pressure, and consequently longer checkpoint times. -# Set this to -1 to disable the IOPS limit and remove the limitation entirely, -# this will let the checkpointer flush data as fast as the hardware will go. -# Removing the setting, or commenting it out, will set the default value of 600. -# db.checkpoint.iops.limit=600 - -# Whether or not any database on this instance are read_only by default. -# If false, individual databases may be marked as read_only using dbms.database.read_only. -# If true, individual databases may be marked as writable using dbms.databases.writable. -#dbms.databases.default_to_read_only=false - -# Comma separated list of JAX-RS packages containing JAX-RS resources, one -# package name for each mountpoint. The listed package names will be loaded -# under the mountpoints specified. Uncomment this line to mount the -# org.neo4j.examples.server.unmanaged.HelloWorldResource.java from -# neo4j-server-examples under /examples/unmanaged, resulting in a final URL of -# http://localhost:7474/examples/unmanaged/helloworld/{nodeId} -#server.unmanaged_extension_classes=org.neo4j.examples.server.unmanaged=/examples/unmanaged - -# A comma separated list of procedures and user defined functions that are allowed -# full access to the database through unsupported/insecure internal APIs. -#dbms.security.procedures.unrestricted=my.extensions.example,my.procedures.* - -# A comma separated list of procedures to be loaded by default. -# Leaving this unconfigured will load all procedures found. -#dbms.security.procedures.allowlist=apoc.coll.*,apoc.load.*,gds.* - -# For how long should drivers cache the discovery data from -# the dbms.routing.getRoutingTable() procedure. Defaults to 300s. -#dbms.routing_ttl=300s - -#******************************************************************** -# JVM Parameters -#******************************************************************** - -# G1GC generally strikes a good balance between throughput and tail -# latency, without too much tuning. -server.jvm.additional=-XX:+UseG1GC - -# Have common exceptions keep producing stack traces, so they can be -# debugged regardless of how often logs are rotated. -server.jvm.additional=-XX:-OmitStackTraceInFastThrow - -# Make sure that `initmemory` is not only allocated, but committed to -# the process, before starting the database. This reduces memory -# fragmentation, increasing the effectiveness of transparent huge -# pages. It also reduces the possibility of seeing performance drop -# due to heap-growing GC events, where a decrease in available page -# cache leads to an increase in mean IO response time. -# Try reducing the heap memory, if this flag degrades performance. -server.jvm.additional=-XX:+AlwaysPreTouch - -# Trust that non-static final fields are really final. -# This allows more optimizations and improves overall performance. -# NOTE: Disable this if you use embedded mode, or have extensions or dependencies that may use reflection or -# serialization to change the value of final fields! -server.jvm.additional=-XX:+UnlockExperimentalVMOptions -server.jvm.additional=-XX:+TrustFinalNonStaticFields - -# Disable explicit garbage collection, which is occasionally invoked by the JDK itself. -server.jvm.additional=-XX:+DisableExplicitGC - -# Allow Neo4j to use @Contended annotation -server.jvm.additional=-XX:-RestrictContended - -# Restrict size of cached JDK buffers to 1 KB -server.jvm.additional=-Djdk.nio.maxCachedBufferSize=1024 - -# More efficient buffer allocation in Netty by allowing direct no cleaner buffers. -server.jvm.additional=-Dio.netty.tryReflectionSetAccessible=true - -# Exits JVM on the first occurrence of an out-of-memory error. Its preferable to restart VM in case of out of memory errors. -# server.jvm.additional=-XX:+ExitOnOutOfMemoryError - -# Expand Diffie Hellman (DH) key size from default 1024 to 2048 for DH-RSA cipher suites used in server TLS handshakes. -# This is to protect the server from any potential passive eavesdropping. -server.jvm.additional=-Djdk.tls.ephemeralDHKeySize=2048 - -# This mitigates a DDoS vector. -server.jvm.additional=-Djdk.tls.rejectClientInitiatedRenegotiation=true - -# Enable remote debugging -#server.jvm.additional=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 - -# This filter prevents deserialization of arbitrary objects via java object serialization, addressing potential vulnerabilities. -# By default this filter whitelists all neo4j classes, as well as classes from the hazelcast library and the java standard library. -# These defaults should only be modified by expert users! -# For more details (including filter syntax) see: https://openjdk.java.net/jeps/290 -#server.jvm.additional=-Djdk.serialFilter=java.**;org.neo4j.**;com.neo4j.**;com.hazelcast.**;net.sf.ehcache.Element;com.sun.proxy.*;org.openjdk.jmh.**;!* - -# Increase the default flight recorder stack sampling depth from 64 to 256, to avoid truncating frames when profiling. -server.jvm.additional=-XX:FlightRecorderOptions=stackdepth=256 - -# Allow profilers to sample between safepoints. Without this, sampling profilers may produce less accurate results. -server.jvm.additional=-XX:+UnlockDiagnosticVMOptions -server.jvm.additional=-XX:+DebugNonSafepoints - -# Open modules for neo4j to allow internal access -server.jvm.additional=--add-opens=java.base/java.nio=ALL-UNNAMED -server.jvm.additional=--add-opens=java.base/java.io=ALL-UNNAMED -server.jvm.additional=--add-opens=java.base/sun.nio.ch=ALL-UNNAMED - -# Enable native memory access -server.jvm.additional=--enable-native-access=ALL-UNNAMED - -# Enable access to JDK vector API -# server.jvm.additional=--add-modules=jdk.incubator.vector - -# Disable logging JMX endpoint. -server.jvm.additional=-Dlog4j2.disable.jmx=true - -# Increasing the JSON log string maximum length -server.jvm.additional=-Dlog4j.layout.jsonTemplate.maxStringLength=32768 - -# Limit JVM metaspace and code cache to allow garbage collection. Used by cypher for code generation and may grow indefinitely unless constrained. -# Useful for memory constrained environments -#server.jvm.additional=-XX:MaxMetaspaceSize=1024m -#server.jvm.additional=-XX:ReservedCodeCacheSize=512m - -# Allow big methods to be JIT compiled. -# Useful for big queries and big expressions where cypher code generation can create large methods. -#server.jvm.additional=-XX:-DontCompileHugeMethods - -#******************************************************************** -# Wrapper Windows NT/2000/XP Service Properties -#******************************************************************** -# WARNING - Do not modify any of these properties when an application -# using this configuration file has been installed as a service. -# Please uninstall the service before modifying this section. The -# service can then be reinstalled. - -# Name of the service -server.windows_service_name=neo4j diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index a1d7bdcc..3cc71d83 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -13,16 +13,10 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; use Exception; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltMessageFactory; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; use Psr\Http\Message\UriInterface; @@ -43,15 +37,15 @@ public function __construct( * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array + public function authenticateBolt(BoltConnection $connection, string $userAgent): array { - $factory = $this->createMessageFactory($protocol); + $factory = $this->createMessageFactory($connection); + $protocol = $connection->protocol(); if (method_exists($protocol, 'logon')) { $helloMetadata = ['user_agent' => $userAgent]; - $factory->createHelloMessage($helloMetadata)->send(); - $response = ResponseHelper::getResponse($protocol); + $responseHello = $factory->createHelloMessage($helloMetadata)->send()->getResponse(); $credentials = [ 'scheme' => 'basic', @@ -59,11 +53,10 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'credentials' => $this->password, ]; - $factory->createLogonMessage($credentials)->send(); - ResponseHelper::getResponse($protocol); + $response = $factory->createLogonMessage($credentials)->send()->getResponse(); /** @var array{server: string, connection_id: string, hints: list} */ - return $response->content; + return array_merge($responseHello->content, $response->content); } $helloMetadata = [ @@ -73,22 +66,15 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'credentials' => $this->password, ]; - $factory->createHelloMessage($helloMetadata)->send(); + $response = $factory->createHelloMessage($helloMetadata)->send()->getResponse(); /** @var array{server: string, connection_id: string, hints: list} */ - return ResponseHelper::getResponse($protocol)->content; + return $response->content; } /** * @throws Exception */ - public function logoff(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): void - { - $factory = $this->createMessageFactory($protocol); - $factory->createLogoffMessage()->send(); - ResponseHelper::getResponse($protocol); - } - public function toString(UriInterface $uri): string { return sprintf('Basic %s:%s@%s:%s', $this->username, '######', $uri->getHost(), $uri->getPort() ?? ''); @@ -97,8 +83,8 @@ public function toString(UriInterface $uri): string /** * Helper to create message factory. */ - private function createMessageFactory(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): BoltMessageFactory + private function createMessageFactory(BoltConnection $connection): BoltMessageFactory { - return new BoltMessageFactory($protocol, $this->logger); + return new BoltMessageFactory($connection, $this->logger); } } diff --git a/src/Authentication/KerberosAuth.php b/src/Authentication/KerberosAuth.php index e50a27dc..7b9f32a6 100644 --- a/src/Authentication/KerberosAuth.php +++ b/src/Authentication/KerberosAuth.php @@ -13,18 +13,11 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; use Exception; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltMessageFactory; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; use Psr\Log\LogLevel; @@ -41,38 +34,26 @@ public function __construct( ) { } - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface - { - $this->logger?->log(LogLevel::DEBUG, 'Authenticating using KerberosAuth'); - - return $request->withHeader('Authorization', 'Kerberos '.$this->token) - ->withHeader('User-Agent', $userAgent); - } - /** * @throws Exception * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array + public function authenticateBolt(BoltConnection $connection, string $userAgent): array { - $factory = $this->createMessageFactory($protocol); + $factory = $this->createMessageFactory($connection); $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]); - $factory->createHelloMessage(['user_agent' => $userAgent])->send(); - - $response = ResponseHelper::getResponse($protocol); + $factory->createHelloMessage(['user_agent' => $userAgent])->send()->getResponse(); $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'kerberos', 'principal' => '']); - $factory->createLogonMessage([ + $response = $factory->createLogonMessage([ 'scheme' => 'kerberos', 'principal' => '', 'credentials' => $this->token, - ])->send(); - - ResponseHelper::getResponse($protocol); + ])->send()->getResponse(); /** * @var array{server: string, connection_id: string, hints: list} @@ -88,8 +69,8 @@ public function toString(UriInterface $uri): string /** * Helper to create the message factory. */ - private function createMessageFactory(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): BoltMessageFactory + private function createMessageFactory(BoltConnection $connection): BoltMessageFactory { - return new BoltMessageFactory($protocol, $this->logger); + return new BoltMessageFactory($connection, $this->logger); } } diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php index 80b1b1b9..82210aee 100644 --- a/src/Authentication/NoAuth.php +++ b/src/Authentication/NoAuth.php @@ -13,20 +13,13 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; use Exception; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltMessageFactory; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Psr\Http\Message\RequestInterface; +use Laudis\Neo4j\Enum\ConnectionProtocol; use Psr\Http\Message\UriInterface; -use Psr\Log\LogLevel; use function sprintf; @@ -37,30 +30,21 @@ public function __construct( ) { } - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface - { - $this->logger?->log(LogLevel::DEBUG, 'Authentication disabled'); - - return $request->withHeader('User-Agent', $userAgent); - } - /** * @throws Exception * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array + public function authenticateBolt(BoltConnection $connection, string $userAgent): array { - $factory = $this->createMessageFactory($protocol); + $factory = $this->createMessageFactory($connection); - if (method_exists($protocol, 'logon')) { + if ($connection->getProtocol()->compare(ConnectionProtocol::BOLT_V5_1()) >= 0) { $helloMetadata = ['user_agent' => $userAgent]; - $factory->createHelloMessage($helloMetadata)->send(); - $response = ResponseHelper::getResponse($protocol); + $factory->createHelloMessage($helloMetadata)->send()->getResponse(); - $factory->createLogonMessage(['scheme' => 'none'])->send(); - ResponseHelper::getResponse($protocol); + $response = $factory->createLogonMessage(['scheme' => 'none'])->send()->getResponse(); /** @var array{server: string, connection_id: string, hints: list} */ return $response->content; @@ -71,17 +55,10 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'scheme' => 'none', ]; - $factory->createHelloMessage($helloMetadata)->send(); + $response = $factory->createHelloMessage($helloMetadata)->send()->getResponse(); /** @var array{server: string, connection_id: string, hints: list} */ - return ResponseHelper::getResponse($protocol)->content; - } - - public function logoff(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): void - { - $factory = $this->createMessageFactory($protocol); - $factory->createLogoffMessage()->send(); - ResponseHelper::getResponse($protocol); + return $response->content; } public function toString(UriInterface $uri): string @@ -89,8 +66,8 @@ public function toString(UriInterface $uri): string return sprintf('No Auth %s:%s', $uri->getHost(), $uri->getPort() ?? ''); } - private function createMessageFactory(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): BoltMessageFactory + private function createMessageFactory(BoltConnection $connection): BoltMessageFactory { - return new BoltMessageFactory($protocol, $this->logger); + return new BoltMessageFactory($connection, $this->logger); } } diff --git a/src/Authentication/OpenIDConnectAuth.php b/src/Authentication/OpenIDConnectAuth.php index bb7a134f..947bbcd6 100644 --- a/src/Authentication/OpenIDConnectAuth.php +++ b/src/Authentication/OpenIDConnectAuth.php @@ -13,16 +13,10 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; use Exception; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltMessageFactory; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; @@ -51,24 +45,20 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array + public function authenticateBolt(BoltConnection $connection, string $userAgent): array { - $factory = $this->createMessageFactory($protocol); + $factory = $this->createMessageFactory($connection); $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]); - $factory->createHelloMessage(['user_agent' => $userAgent])->send(); - - $response = ResponseHelper::getResponse($protocol); + $factory->createHelloMessage(['user_agent' => $userAgent])->send()->getResponse(); $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'bearer']); - $factory->createLogonMessage([ + $response = $factory->createLogonMessage([ 'scheme' => 'bearer', 'credentials' => $this->token, - ])->send(); - - ResponseHelper::getResponse($protocol); + ])->send()->getResponse(); /** * @var array{server: string, connection_id: string, hints: list} @@ -84,8 +74,8 @@ public function toString(UriInterface $uri): string /** * Helper to create the message factory. */ - public function createMessageFactory(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): BoltMessageFactory + public function createMessageFactory(BoltConnection $connection): BoltMessageFactory { - return new BoltMessageFactory($protocol, $this->logger); + return new BoltMessageFactory($connection, $this->logger); } } diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 4720d127..dcaf94fb 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -64,6 +64,12 @@ class BoltConnection implements ConnectionInterface * @var list> */ private array $subscribedResults = []; + private bool $inTransaction = false; + /** @var array Track if this connection was ever used for a query */ + private array $connectionUsed = [ + 'reader' => false, + 'writer' => false, + ]; /** * @return array{0: V4_4|V5|V5_1|V5_2|V5_3|V5_4|null, 1: Connection} @@ -85,7 +91,7 @@ public function __construct( private readonly ConnectionConfiguration $config, private readonly ?Neo4jLogger $logger, ) { - $this->messageFactory = new BoltMessageFactory($this->protocol(), $this->logger); + $this->messageFactory = new BoltMessageFactory($this, $this->logger); } public function getEncryptionLevel(): string @@ -109,14 +115,6 @@ public function getServerAddress(): UriInterface return $this->config->getServerAddress(); } - /** - * @psalm-mutation-free - */ - public function getServerVersion(): string - { - return $this->config->getServerVersion(); - } - /** * @psalm-mutation-free */ @@ -206,21 +204,29 @@ public function reset(): void $this->subscribedResults = []; } + private function prepareForBegin(): void + { + if (in_array($this->getServerState(), ['STREAMING', 'TX_STREAMING'], true)) { + $this->discardUnconsumedResults(); + } + } + /** * Begins a transaction. * * Any of the preconditioned states are: 'READY', 'INTERRUPTED'. * - * @param iterable|null $txMetaData + * @param array|null $txMetaData */ - public function begin(?string $database, ?float $timeout, BookmarkHolder $holder, ?iterable $txMetaData): void + public function begin(?string $database, ?float $timeout, BookmarkHolder $holder, ?array $txMetaData): void { $this->consumeResults(); - $extra = $this->buildRunExtra($database, $timeout, $holder, AccessMode::WRITE(), $txMetaData); + $extra = $this->buildRunExtra($database, $timeout, $holder, $this->getAccessMode(), $txMetaData); $message = $this->messageFactory->createBeginMessage($extra); $response = $message->send()->getResponse(); $this->assertNoFailure($response); + $this->inTransaction = true; } /** @@ -253,7 +259,17 @@ public function run( ?AccessMode $mode, ?iterable $tsxMetadata, ): array { - $extra = $this->buildRunExtra($database, $timeout, $holder, $mode, $tsxMetadata); + if ($mode === AccessMode::WRITE()) { + $this->connectionUsed['writer'] = true; + } else { + $this->connectionUsed['reader'] = true; + } + + if ($this->isInTransaction()) { + $extra = []; + } else { + $extra = $this->buildRunExtra($database, $timeout, $holder, $mode, $tsxMetadata, false); + } $message = $this->messageFactory->createRunMessage($text, $parameters, $extra); $response = $message->send()->getResponse(); $this->assertNoFailure($response); @@ -318,22 +334,33 @@ public function close(): void { try { if ($this->isOpen()) { - if ($this->isStreaming()) { - $this->consumeResults(); + if ($this->isStreaming() && (($this->connectionUsed['reader'] ?? false) || ($this->connectionUsed['writer'] ?? false))) { + $this->discardUnconsumedResults(); } - $message = $this->messageFactory->createGoodbyeMessage(); - $message->send(); + // Only send GOODBYE if the connection was ever used + if (($this->connectionUsed['reader'] ?? false) || ($this->connectionUsed['writer'] ?? false)) { + $message = $this->messageFactory->createGoodbyeMessage(); + $message->send(); + } - unset($this->boltProtocol); // has to be set to null as the sockets don't recover nicely contrary to what the underlying code might lead you to believe; + unset($this->boltProtocol); } } catch (Throwable) { + // ignore, but could log } } - private function buildRunExtra(?string $database, ?float $timeout, BookmarkHolder $holder, ?AccessMode $mode, ?iterable $metadata): array - { + private function buildRunExtra( + ?string $database, + ?float $timeout, + BookmarkHolder $holder, + ?AccessMode $mode, + ?iterable $metadata, + bool $forBegin = false, + ): array { $extra = []; + if ($database !== null) { $extra['db'] = $database; } @@ -341,17 +368,20 @@ private function buildRunExtra(?string $database, ?float $timeout, BookmarkHolde $extra['tx_timeout'] = (int) ($timeout * 1000); } - if (!$holder->getBookmark()->isEmpty()) { - $extra['bookmarks'] = $holder->getBookmark()->values(); + $bookmarks = $holder->getBookmark()->values(); + if (!empty($bookmarks)) { + $extra['bookmarks'] = $bookmarks; } - if ($mode) { - $extra['mode'] = AccessMode::WRITE() === $mode ? 'w' : 'r'; + if ($forBegin) { + if ($mode !== null) { + $extra['mode'] = $mode === AccessMode::WRITE() ? 'w' : 'r'; + } } if ($metadata !== null) { $metadataArray = $metadata instanceof Traversable ? iterator_to_array($metadata) : $metadata; - if (count($metadataArray) > 0) { + if (!empty($metadataArray)) { $extra['tx_metadata'] = $metadataArray; } } @@ -362,11 +392,13 @@ private function buildRunExtra(?string $database, ?float $timeout, BookmarkHolde private function buildResultExtra(?int $fetchSize, ?int $qid): array { $extra = []; + $fetchSize = 1000; + /** @psalm-suppress RedundantCondition */ if ($fetchSize !== null) { $extra['n'] = $fetchSize; } - if ($qid !== null) { + if ($qid !== null && $qid >= 0) { $extra['qid'] = $qid; } @@ -392,17 +424,77 @@ public function getUserAgent(): string return $this->userAgent; } - private function assertNoFailure(Response $response): void + public function assertNoFailure(Response $response): void { if ($response->signature === Signature::FAILURE) { $this->logger?->log(LogLevel::ERROR, 'FAILURE'); $message = $this->messageFactory->createResetMessage(); - $resetResponse = $message->send()->getResponse(); + + try { + $resetResponse = $message->send()->getResponse(); + } catch (Throwable $e) { + $this->subscribedResults = []; + throw Neo4jException::fromBoltResponse($response); + } + $this->subscribedResults = []; + if ($resetResponse->signature === Signature::FAILURE) { throw new Neo4jException([Neo4jError::fromBoltResponse($resetResponse), Neo4jError::fromBoltResponse($response)]); } + throw Neo4jException::fromBoltResponse($response); } } + + /** + * Discard unconsumed results - sends DISCARD to server for each subscribed result. + */ + public function discardUnconsumedResults(): void + { + $this->logger?->log(LogLevel::DEBUG, 'Discarding unconsumed results'); + + $this->subscribedResults = array_values(array_filter( + $this->subscribedResults, + static fn (WeakReference $ref): bool => $ref->get() !== null + )); + + if (empty($this->subscribedResults)) { + $this->logger?->log(LogLevel::DEBUG, 'No unconsumed results to discard'); + + return; + } + + $state = $this->getServerState(); + $this->logger?->log(LogLevel::DEBUG, "Server state before discard: {$state}"); + + // Skip discard if this connection was never used + // Skip discard if this connection was never used + if (!($this->connectionUsed['reader'] ?? false) && !($this->connectionUsed['writer'] ?? false)) { + $this->logger?->log(LogLevel::DEBUG, 'Skipping discard - connection never used'); + $this->subscribedResults = []; + + return; + } + + try { + if (in_array($state, ['STREAMING', 'TX_STREAMING'], true)) { + $this->discard(null); + $this->logger?->log(LogLevel::DEBUG, 'Sent DISCARD ALL for unconsumed results'); + } else { + $this->logger?->log(LogLevel::DEBUG, 'Skipping discard - server not in streaming state'); + } + } catch (Throwable $e) { + $this->logger?->log(LogLevel::ERROR, 'Failed to discard results', [ + 'exception' => $e->getMessage(), + ]); + } + + $this->subscribedResults = []; + } + + private function isInTransaction(): bool + { + return $this->inTransaction; + } } diff --git a/src/Bolt/BoltMessageFactory.php b/src/Bolt/BoltMessageFactory.php index cc1ea910..14a97c40 100644 --- a/src/Bolt/BoltMessageFactory.php +++ b/src/Bolt/BoltMessageFactory.php @@ -13,12 +13,6 @@ namespace Laudis\Neo4j\Bolt; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; use Laudis\Neo4j\Bolt\Messages\BoltBeginMessage; use Laudis\Neo4j\Bolt\Messages\BoltCommitMessage; use Laudis\Neo4j\Bolt\Messages\BoltDiscardMessage; @@ -39,67 +33,67 @@ class BoltMessageFactory { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + private readonly BoltConnection $connection, private readonly ?Neo4jLogger $logger = null, ) { } public function createResetMessage(): BoltResetMessage { - return new BoltResetMessage($this->protocol, $this->logger); + return new BoltResetMessage($this->connection, $this->logger); } public function createBeginMessage(array $extra): BoltBeginMessage { - return new BoltBeginMessage($this->protocol, $extra, $this->logger); + return new BoltBeginMessage($this->connection, $extra, $this->logger); } public function createDiscardMessage(array $extra): BoltDiscardMessage { - return new BoltDiscardMessage($this->protocol, $extra, $this->logger); + return new BoltDiscardMessage($this->connection, $extra, $this->logger); } public function createRunMessage(string $text, array $parameters, array $extra): BoltRunMessage { - return new BoltRunMessage($this->protocol, $text, $parameters, $extra, $this->logger); + return new BoltRunMessage($this->connection, $text, $parameters, $extra, $this->logger); } public function createCommitMessage(BookmarkHolder $bookmarkHolder): BoltCommitMessage { - return new BoltCommitMessage($this->protocol, $this->logger, $bookmarkHolder); + return new BoltCommitMessage($this->connection, $this->logger, $bookmarkHolder); } public function createRollbackMessage(): BoltRollbackMessage { - return new BoltRollbackMessage($this->protocol, $this->logger); + return new BoltRollbackMessage($this->connection, $this->logger); } public function createPullMessage(array $extra): BoltPullMessage { - return new BoltPullMessage($this->protocol, $extra, $this->logger); + return new BoltPullMessage($this->connection, $extra, $this->logger); } public function createHelloMessage(array $extra): BoltHelloMessage { /** @var array $extra */ - return new BoltHelloMessage($this->protocol, $extra, $this->logger); + return new BoltHelloMessage($this->connection, $extra, $this->logger); } public function createLogonMessage(array $credentials): BoltLogonMessage { /** @var array $credentials */ - return new BoltLogonMessage($this->protocol, $credentials, $this->logger); + return new BoltLogonMessage($this->connection, $credentials, $this->logger); } public function createLogoffMessage(): BoltLogoffMessage { - return new BoltLogoffMessage($this->protocol, $this->logger); + return new BoltLogoffMessage($this->connection, $this->logger); } public function createGoodbyeMessage(): BoltGoodbyeMessage { - return new BoltGoodbyeMessage($this->protocol, $this->logger); + return new BoltGoodbyeMessage($this->connection, $this->logger); } } diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index f01daf84..adaa5614 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -21,7 +21,7 @@ use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Enum\TransactionState; -use Laudis\Neo4j\Exception\ClientException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\ParameterHelper; use Laudis\Neo4j\Types\CypherList; @@ -58,7 +58,7 @@ public function __construct( /** * @param iterable $statements * - * @throws ClientException|Throwable + * @throws TransactionException|Throwable * * @return CypherList */ @@ -66,15 +66,15 @@ public function commit(iterable $statements = []): CypherList { if ($this->isFinished()) { if ($this->state === TransactionState::TERMINATED) { - throw new ClientException("Can't commit, transaction has been terminated"); + throw new TransactionException("Can't commit a terminated transaction."); } if ($this->state === TransactionState::COMMITTED) { - throw new ClientException("Can't commit, transaction has already been committed"); + throw new TransactionException("Can't commit a committed transaction."); } if ($this->state === TransactionState::ROLLED_BACK) { - throw new ClientException("Can't commit, transaction has already been rolled back"); + throw new TransactionException("Can't commit a committed transaction."); } } @@ -84,7 +84,7 @@ public function commit(iterable $statements = []): CypherList $list->preload(); }); - $this->messageFactory->createCommitMessage($this->bookmarkHolder)->send(); + $this->messageFactory->createCommitMessage($this->bookmarkHolder)->send()->getResponse(); $this->state = TransactionState::COMMITTED; return $tbr; @@ -93,16 +93,12 @@ public function commit(iterable $statements = []): CypherList public function rollback(): void { if ($this->isFinished()) { - if ($this->state === TransactionState::TERMINATED) { - throw new ClientException("Can't rollback, transaction has been terminated"); - } - if ($this->state === TransactionState::COMMITTED) { - throw new ClientException("Can't rollback, transaction has already been committed"); + throw new TransactionException("Can't rollback a committed transaction."); } if ($this->state === TransactionState::ROLLED_BACK) { - throw new ClientException("Can't rollback, transaction has already been rolled back"); + throw new TransactionException("Can't rollback a rolled back transaction."); } } @@ -115,6 +111,20 @@ public function rollback(): void */ public function run(string $statement, iterable $parameters = []): SummarizedResult { + if ($this->isFinished()) { + if ($this->state === TransactionState::TERMINATED) { + throw new TransactionException("Can't run a query on a terminated transaction."); + } + + if ($this->state === TransactionState::COMMITTED) { + throw new TransactionException("Can't run a query on a committed transaction."); + } + + if ($this->state === TransactionState::ROLLED_BACK) { + throw new TransactionException("Can't run a query on a rolled back transaction."); + } + } + return $this->runStatement(new Statement($statement, $parameters)); } @@ -127,7 +137,7 @@ public function runStatement(Statement $statement): SummarizedResult $start = microtime(true); $serverState = $this->connection->protocol()->serverState; - if (in_array($serverState, [ServerState::STREAMING, ServerState::TX_STREAMING])) { + if ($serverState === ServerState::STREAMING) { $this->connection->consumeResults(); } diff --git a/src/Bolt/ConnectionPool.php b/src/Bolt/ConnectionPool.php index 565d9b31..937cffa4 100644 --- a/src/Bolt/ConnectionPool.php +++ b/src/Bolt/ConnectionPool.php @@ -102,6 +102,7 @@ public function acquire(SessionConfiguration $config): Generator } $connection = $this->factory->createConnection($this->data, $config); + $this->activeConnections[] = $connection; return $connection; diff --git a/src/Bolt/Messages/BoltBeginMessage.php b/src/Bolt/Messages/BoltBeginMessage.php index 339c2e92..2dbf0933 100644 --- a/src/Bolt/Messages/BoltBeginMessage.php +++ b/src/Bolt/Messages/BoltBeginMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,17 +21,17 @@ final class BoltBeginMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly array $extra, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltBeginMessage { $this->logger?->log(LogLevel::DEBUG, 'BEGIN', $this->extra); - $this->protocol->begin($this->extra); + $this->connection->protocol()->begin($this->extra); return $this; } diff --git a/src/Bolt/Messages/BoltCommitMessage.php b/src/Bolt/Messages/BoltCommitMessage.php index 4514f388..e554f51a 100644 --- a/src/Bolt/Messages/BoltCommitMessage.php +++ b/src/Bolt/Messages/BoltCommitMessage.php @@ -14,12 +14,8 @@ namespace Laudis\Neo4j\Bolt\Messages; use Bolt\enum\ServerState; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Bolt\protocol\Response; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Laudis\Neo4j\Databags\Bookmark; @@ -29,23 +25,31 @@ final class BoltCommitMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly ?Neo4jLogger $logger, private readonly BookmarkHolder $bookmarks, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltCommitMessage { $this->logger?->log(LogLevel::DEBUG, 'COMMIT'); - $response = $this->protocol->commit()->getResponse(); + $this->connection->protocol()->commit(); + + return $this; + } + + public function getResponse(): Response + { + $response = parent::getResponse(); + // TODO: This is an issue with the underlying bolt library. // The serverState should be READY after a successful commit but // it's still in TX_STREAMING if the results were not consumed // // This should be removed once it's fixed - $this->protocol->serverState = ServerState::READY; + $this->connection->protocol()->serverState = ServerState::READY; /** @var array{bookmark?: string} $content */ $content = $response->content; @@ -55,8 +59,8 @@ public function send(): BoltCommitMessage $this->bookmarks->setBookmark(new Bookmark([$bookmark])); } - $this->protocol->serverState = ServerState::READY; + $this->connection->protocol()->serverState = ServerState::READY; - return $this; + return $response; } } diff --git a/src/Bolt/Messages/BoltDiscardMessage.php b/src/Bolt/Messages/BoltDiscardMessage.php index e35adfbc..74824f56 100644 --- a/src/Bolt/Messages/BoltDiscardMessage.php +++ b/src/Bolt/Messages/BoltDiscardMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,17 +21,17 @@ final class BoltDiscardMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly array $extra, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltDiscardMessage { $this->logger?->log(LogLevel::DEBUG, 'DISCARD', $this->extra); - $this->protocol->discard($this->extra); + $this->connection->protocol()->discard($this->extra); return $this; } diff --git a/src/Bolt/Messages/BoltGoodbyeMessage.php b/src/Bolt/Messages/BoltGoodbyeMessage.php index a944cadb..e79c2ae7 100644 --- a/src/Bolt/Messages/BoltGoodbyeMessage.php +++ b/src/Bolt/Messages/BoltGoodbyeMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,16 +21,16 @@ final class BoltGoodbyeMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltGoodbyeMessage { $this->logger?->log(LogLevel::DEBUG, 'GOODBYE'); - $this->protocol->goodbye(); + $this->connection->protocol()->goodbye(); return $this; } diff --git a/src/Bolt/Messages/BoltHelloMessage.php b/src/Bolt/Messages/BoltHelloMessage.php index cbc3d4be..42a355e7 100644 --- a/src/Bolt/Messages/BoltHelloMessage.php +++ b/src/Bolt/Messages/BoltHelloMessage.php @@ -14,12 +14,7 @@ namespace Laudis\Neo4j\Bolt\Messages; use Bolt\error\BoltException; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -29,16 +24,15 @@ final class BoltHelloMessage extends BoltMessage /** * Constructor for the BoltHelloMessage. * - * @param V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol The protocol connection - * @param array $metadata The metadata for the HELLO message (like user agent, supported versions) - * @param Neo4jLogger|null $logger Optional logger for debugging purposes + * @param array $metadata The metadata for the HELLO message (like user agent, supported versions) + * @param Neo4jLogger|null $logger Optional logger for debugging purposes */ public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly array $metadata, private readonly ?Neo4jLogger $logger = null, ) { - parent::__construct($protocol); + parent::__construct($connection); } /** @@ -50,7 +44,7 @@ public function send(): BoltHelloMessage { $this->logger?->log(LogLevel::DEBUG, 'HELLO', $this->metadata); - $this->protocol->hello($this->metadata); + $this->connection->protocol()->hello($this->metadata); return $this; } diff --git a/src/Bolt/Messages/BoltLogoffMessage.php b/src/Bolt/Messages/BoltLogoffMessage.php index 2eda2cd6..d6edda45 100644 --- a/src/Bolt/Messages/BoltLogoffMessage.php +++ b/src/Bolt/Messages/BoltLogoffMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -29,14 +24,13 @@ class BoltLogoffMessage extends BoltMessage { /** - * @param V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol The Bolt protocol version - * @param Neo4jLogger|null $logger Optional logger for logging purposes + * @param Neo4jLogger|null $logger Optional logger for logging purposes */ public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly ?Neo4jLogger $logger = null, ) { - parent::__construct($protocol); + parent::__construct($connection); } /** @@ -48,7 +42,7 @@ public function send(): BoltLogoffMessage { $this->logger?->log(LogLevel::DEBUG, 'LOGOFF', []); /** @psalm-suppress PossiblyUndefinedMethod */ - $this->protocol->logoff(); + $this->connection->protocol()->logoff(); return $this; } diff --git a/src/Bolt/Messages/BoltLogonMessage.php b/src/Bolt/Messages/BoltLogonMessage.php index eeeec0b2..e1e47e24 100644 --- a/src/Bolt/Messages/BoltLogonMessage.php +++ b/src/Bolt/Messages/BoltLogonMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -29,16 +24,15 @@ final class BoltLogonMessage extends BoltMessage { /** - * @param V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol The Bolt protocol version - * @param array $credentials The credentials for the LOGON request (e.g., username and password) - * @param Neo4jLogger|null $logger Optional logger for logging purposes + * @param array $credentials The credentials for the LOGON request (e.g., username and password) + * @param Neo4jLogger|null $logger Optional logger for logging purposes */ public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly array $credentials, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } /** @@ -53,7 +47,7 @@ public function send(): BoltLogonMessage $this->logger?->log(LogLevel::DEBUG, 'LOGON', $toLog); /** @psalm-suppress PossiblyUndefinedMethod */ - $this->protocol->logon($this->credentials); + $this->connection->protocol()->logon($this->credentials); return $this; } diff --git a/src/Bolt/Messages/BoltPullMessage.php b/src/Bolt/Messages/BoltPullMessage.php index ab141786..42339e97 100644 --- a/src/Bolt/Messages/BoltPullMessage.php +++ b/src/Bolt/Messages/BoltPullMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,17 +21,17 @@ final class BoltPullMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly array $extra, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltPullMessage { $this->logger?->log(LogLevel::DEBUG, 'PULL', $this->extra); - $this->protocol->pull($this->extra); + $this->connection->protocol()->pull($this->extra); return $this; } diff --git a/src/Bolt/Messages/BoltResetMessage.php b/src/Bolt/Messages/BoltResetMessage.php index 223e3d01..2ce0846a 100644 --- a/src/Bolt/Messages/BoltResetMessage.php +++ b/src/Bolt/Messages/BoltResetMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,16 +21,16 @@ final class BoltResetMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltResetMessage { $this->logger?->log(LogLevel::DEBUG, 'RESET'); - $this->protocol->reset(); + $this->connection->protocol()->reset(); return $this; } diff --git a/src/Bolt/Messages/BoltRollbackMessage.php b/src/Bolt/Messages/BoltRollbackMessage.php index 170d376c..4bed2bb6 100644 --- a/src/Bolt/Messages/BoltRollbackMessage.php +++ b/src/Bolt/Messages/BoltRollbackMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,16 +21,16 @@ final class BoltRollbackMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltRollbackMessage { $this->logger?->log(LogLevel::DEBUG, 'ROLLBACK'); - $this->protocol->rollback(); + $this->connection->protocol()->rollback(); return $this; } diff --git a/src/Bolt/Messages/BoltRunMessage.php b/src/Bolt/Messages/BoltRunMessage.php index b07911fe..02f84c6e 100644 --- a/src/Bolt/Messages/BoltRunMessage.php +++ b/src/Bolt/Messages/BoltRunMessage.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Bolt\Messages; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\BoltMessage; use Psr\Log\LogLevel; @@ -26,13 +21,13 @@ final class BoltRunMessage extends BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + BoltConnection $connection, private readonly string $text, private readonly array $parameters, private readonly array $extra, private readonly ?Neo4jLogger $logger, ) { - parent::__construct($protocol); + parent::__construct($connection); } public function send(): BoltRunMessage @@ -42,7 +37,7 @@ public function send(): BoltRunMessage 'parameters' => $this->parameters, 'extra' => $this->extra, ]); - $this->protocol->run($this->text, $this->parameters, $this->extra); + $this->connection->protocol()->run($this->text, $this->parameters, $this->extra); return $this; } diff --git a/src/Bolt/ProtocolFactory.php b/src/Bolt/ProtocolFactory.php index de983d8f..712991cc 100644 --- a/src/Bolt/ProtocolFactory.php +++ b/src/Bolt/ProtocolFactory.php @@ -21,15 +21,11 @@ use Bolt\protocol\V5_2; use Bolt\protocol\V5_3; use Bolt\protocol\V5_4; -use Laudis\Neo4j\Contracts\AuthenticateInterface; use RuntimeException; class ProtocolFactory { - /** - * @return array{0: V4_4|V5|V5_1|V5_2|V5_3|V5_4, 1: array{server: string, connection_id: string, hints: list}} - */ - public function createProtocol(IConnection $connection, AuthenticateInterface $auth, string $userAgent): array + public function createProtocol(IConnection $connection): V4_4|V5|V5_1|V5_2|V5_3|V5_4 { $boltOptoutEnv = getenv('BOLT_ANALYTICS_OPTOUT'); if ($boltOptoutEnv === false) { @@ -44,8 +40,6 @@ public function createProtocol(IConnection $connection, AuthenticateInterface $a throw new RuntimeException('Client only supports bolt version 4.4 to 5.4'); } - $response = $auth->authenticateBolt($protocol, $userAgent); - - return [$protocol, $response]; + return $protocol; } } diff --git a/src/Bolt/Session.php b/src/Bolt/Session.php index 8b161fa0..87bca34c 100644 --- a/src/Bolt/Session.php +++ b/src/Bolt/Session.php @@ -16,8 +16,8 @@ use Exception; use Laudis\Neo4j\Common\GeneratorHelper; use Laudis\Neo4j\Common\Neo4jLogger; +use Laudis\Neo4j\Common\TransactionHelper; use Laudis\Neo4j\Contracts\ConnectionPoolInterface; -use Laudis\Neo4j\Contracts\CypherSequence; use Laudis\Neo4j\Contracts\SessionInterface; use Laudis\Neo4j\Contracts\TransactionInterface; use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; @@ -39,9 +39,10 @@ */ final class Session implements SessionInterface { + /** @var list */ + private array $usedConnections = []; /** @psalm-readonly */ private readonly BookmarkHolder $bookmarkHolder; - private const ROLLBACK_CLASSIFICATIONS = ['ClientError', 'TransientError', 'DatabaseError']; /** * @param ConnectionPool|Neo4jConnectionPool $pool @@ -49,7 +50,6 @@ final class Session implements SessionInterface * @psalm-mutation-free */ public function __construct( - /** @psalm-readonly */ private readonly SessionConfiguration $config, private readonly ConnectionPoolInterface $pool, /** @@ -101,7 +101,10 @@ public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration $this->getLogger()?->log(LogLevel::INFO, 'Beginning write transaction', ['config' => $config]); $config = $this->mergeTsxConfig($config); - return $this->retry($tsxHandler, false, $config); + return TransactionHelper::retry( + fn () => $this->startTransaction($config, $this->config->withAccessMode(AccessMode::WRITE())), + $tsxHandler + ); } public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null) @@ -109,51 +112,10 @@ public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $this->getLogger()?->log(LogLevel::INFO, 'Beginning read transaction', ['config' => $config]); $config = $this->mergeTsxConfig($config); - return $this->retry($tsxHandler, true, $config); - } - - /** - * @template U - * - * @param callable(TransactionInterface):U $tsxHandler - * - * @return U - */ - private function retry(callable $tsxHandler, bool $read, TransactionConfiguration $config) - { - while (true) { - $transaction = null; - try { - if ($read) { - $transaction = $this->startTransaction($config, $this->config->withAccessMode(AccessMode::READ())); - } else { - $transaction = $this->startTransaction($config, $this->config->withAccessMode(AccessMode::WRITE())); - } - $tbr = $tsxHandler($transaction); - self::triggerLazyResult($tbr); - $transaction->commit(); - - return $tbr; - } catch (Neo4jException $e) { - if ($transaction && !in_array($e->getClassification(), self::ROLLBACK_CLASSIFICATIONS)) { - $transaction->rollback(); - } - - if ($e->getTitle() === 'NotALeader') { - // By closing the pool, we force the connection to be re-acquired and the routing table to be refetched - $this->pool->close(); - } elseif ($e->getClassification() !== 'TransientError') { - throw $e; - } - } - } - } - - private static function triggerLazyResult(mixed $tbr): void - { - if ($tbr instanceof CypherSequence) { - $tbr->preload(); - } + return TransactionHelper::retry( + fn () => $this->startTransaction($config, $this->config->withAccessMode(AccessMode::READ())), + $tsxHandler + ); } public function transaction(callable $tsxHandler, ?TransactionConfiguration $config = null) @@ -192,7 +154,7 @@ private function beginInstantTransaction( $this->config, $tsxConfig, $this->bookmarkHolder, - new BoltMessageFactory($connection->protocol(), $this->getLogger()), + new BoltMessageFactory($connection, $this->getLogger()), ); } @@ -217,6 +179,7 @@ private function acquireConnection(TransactionConfiguration $config, SessionConf $timeout = ($timeout < 30) ? 30 : $timeout; $connection->setTimeout($timeout + 2); } + $this->usedConnections[] = $connection; return $connection; } @@ -226,7 +189,7 @@ private function startTransaction(TransactionConfiguration $config, SessionConfi $this->getLogger()?->log(LogLevel::INFO, 'Starting transaction', ['config' => $config, 'sessionConfig' => $sessionConfig]); try { $connection = $this->acquireConnection($config, $sessionConfig); - + $connection->discardUnconsumedResults(); $connection->begin($this->config->getDatabase(), $config->getTimeout(), $this->bookmarkHolder, $config->getMetaData()); } catch (Neo4jException $e) { if (isset($connection) && $connection->getServerState() === 'FAILED') { @@ -234,6 +197,7 @@ private function startTransaction(TransactionConfiguration $config, SessionConfi } throw $e; } + error_log('>>> EXIT startTransaction()'); return new BoltUnmanagedTransaction( $this->config->getDatabase(), @@ -242,7 +206,7 @@ private function startTransaction(TransactionConfiguration $config, SessionConfi $this->config, $config, $this->bookmarkHolder, - new BoltMessageFactory($connection->protocol(), $this->getLogger()), + new BoltMessageFactory($connection, $this->getLogger()), ); } @@ -256,6 +220,14 @@ public function getLastBookmark(): Bookmark return $this->bookmarkHolder->getBookmark(); } + public function close(): void + { + foreach ($this->usedConnections as $connection) { + $connection->discardUnconsumedResults(); + } + $this->usedConnections = []; + } + private function getLogger(): ?Neo4jLogger { return $this->pool->getLogger(); diff --git a/src/BoltFactory.php b/src/BoltFactory.php index def0a8ff..afe1d744 100644 --- a/src/BoltFactory.php +++ b/src/BoltFactory.php @@ -13,8 +13,6 @@ namespace Laudis\Neo4j; -use function explode; - use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\ProtocolFactory; use Laudis\Neo4j\Bolt\SslConfigurationFactory; @@ -64,19 +62,24 @@ public function createConnection(ConnectionRequestData $data, SessionConfigurati ); $connection = $this->connectionFactory->create($uriConfig); - [$protocol, $authResponse] = $this->protocolFactory->createProtocol($connection->getIConnection(), $data->getAuth(), $data->getUserAgent()); + $protocol = $this->protocolFactory->createProtocol($connection->getIConnection()); $config = new ConnectionConfiguration( - $authResponse['server'], + '', $data->getUri(), - explode('/', $authResponse['server'])[1] ?? '', ConnectionProtocol::determineBoltVersion($protocol), $sessionConfig->getAccessMode(), $sessionConfig->getDatabase() === null ? null : new DatabaseInfo($sessionConfig->getDatabase()), $sslLevel ); - return new BoltConnection($protocol, $connection, $data->getAuth(), $data->getUserAgent(), $config, $this->logger); + $connection = new BoltConnection($protocol, $connection, $data->getAuth(), $data->getUserAgent(), $config, $this->logger); + + $response = $data->getAuth()->authenticateBolt($connection, $data->getUserAgent()); + + $config->setServerAgent($response['server']); + + return $connection; } public function canReuseConnection(ConnectionInterface $connection, SessionConfiguration $config): bool diff --git a/src/Common/ConnectionConfiguration.php b/src/Common/ConnectionConfiguration.php index 3d69fa43..3a458f91 100644 --- a/src/Common/ConnectionConfiguration.php +++ b/src/Common/ConnectionConfiguration.php @@ -18,18 +18,14 @@ use Laudis\Neo4j\Enum\ConnectionProtocol; use Psr\Http\Message\UriInterface; -/** - * @psalm-immutable - */ final class ConnectionConfiguration { /** * @param ''|'s'|'ssc' $encryptionLevel */ public function __construct( - private readonly string $serverAgent, + private string $serverAgent, private readonly UriInterface $serverAddress, - private readonly string $serverVersion, private readonly ConnectionProtocol $protocol, private readonly AccessMode $accessMode, private readonly ?DatabaseInfo $databaseInfo, @@ -42,14 +38,15 @@ public function getServerAgent(): string return $this->serverAgent; } - public function getServerAddress(): UriInterface + // We can only know the server agent once we have established a connection and gotten a re + public function setServerAgent(string $serverAgent): void { - return $this->serverAddress; + $this->serverAgent = $serverAgent; } - public function getServerVersion(): string + public function getServerAddress(): UriInterface { - return $this->serverVersion; + return $this->serverAddress; } public function getProtocol(): ConnectionProtocol diff --git a/src/Common/ResponseHelper.php b/src/Common/ResponseHelper.php deleted file mode 100644 index 9fb6a094..00000000 --- a/src/Common/ResponseHelper.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Common; - -use Bolt\enum\Signature; -use Bolt\protocol\Response; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; -use Laudis\Neo4j\Exception\Neo4jException; - -class ResponseHelper -{ - public static function getResponse(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): Response - { - $response = $protocol->getResponse(); - if ($response->signature === Signature::FAILURE) { - throw Neo4jException::fromBoltResponse($response); - } - - return $response; - } -} diff --git a/src/Common/TransactionHelper.php b/src/Common/TransactionHelper.php new file mode 100644 index 00000000..83c7e086 --- /dev/null +++ b/src/Common/TransactionHelper.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Common; + +use Laudis\Neo4j\Contracts\CypherSequence; +use Laudis\Neo4j\Contracts\TransactionInterface; +use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; +use Laudis\Neo4j\Exception\Neo4jException; + +final class TransactionHelper +{ + public const ROLLBACK_CLASSIFICATIONS = ['ClientError', 'TransientError', 'DatabaseError']; + + /** + * @template U + * + * @param callable():UnmanagedTransactionInterface $tsxFactory + * @param callable(TransactionInterface):U $tsxHandler + * + * @return U + */ + public static function retry(callable $tsxFactory, callable $tsxHandler) + { + while (true) { + $transaction = null; + try { + $transaction = $tsxFactory(); + $tbr = $tsxHandler($transaction); + self::triggerLazyResult($tbr); + $transaction->commit(); + + return $tbr; + } catch (Neo4jException $e) { + if ($transaction && !in_array($e->getClassification(), self::ROLLBACK_CLASSIFICATIONS)) { + $transaction->rollback(); + } + + if ($e->getClassification() !== 'TransientError') { + throw $e; + } + } + } + } + + private static function triggerLazyResult(mixed $tbr): void + { + if ($tbr instanceof CypherSequence) { + $tbr->preload(); + } + } +} diff --git a/src/Contracts/AuthenticateInterface.php b/src/Contracts/AuthenticateInterface.php index a597d72f..0e3d4433 100644 --- a/src/Contracts/AuthenticateInterface.php +++ b/src/Contracts/AuthenticateInterface.php @@ -13,12 +13,7 @@ namespace Laudis\Neo4j\Contracts; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\BoltConnection; use Psr\Http\Message\UriInterface; interface AuthenticateInterface @@ -28,7 +23,7 @@ interface AuthenticateInterface * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array; + public function authenticateBolt(BoltConnection $connection, string $userAgent): array; /** * Returns a string representation of the authentication. diff --git a/src/Contracts/BoltMessage.php b/src/Contracts/BoltMessage.php index 02eeeac1..ba7d7984 100644 --- a/src/Contracts/BoltMessage.php +++ b/src/Contracts/BoltMessage.php @@ -14,18 +14,13 @@ namespace Laudis\Neo4j\Contracts; use Bolt\protocol\Response; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; -use Bolt\protocol\V5_1; -use Bolt\protocol\V5_2; -use Bolt\protocol\V5_3; -use Bolt\protocol\V5_4; use Iterator; +use Laudis\Neo4j\Bolt\BoltConnection; abstract class BoltMessage { public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, + protected readonly BoltConnection $connection, ) { } @@ -36,7 +31,11 @@ abstract public function send(): BoltMessage; public function getResponse(): Response { - return $this->protocol->getResponse(); + $response = $this->connection->protocol()->getResponse(); + + $this->connection->assertNoFailure($response); + + return $response; } /** @@ -47,6 +46,6 @@ public function getResponses(): Iterator /** * @var Iterator */ - return $this->protocol->getResponses(); + return $this->connection->protocol()->getResponses(); } } diff --git a/src/Contracts/ConnectionInterface.php b/src/Contracts/ConnectionInterface.php index e95988b6..473758ac 100644 --- a/src/Contracts/ConnectionInterface.php +++ b/src/Contracts/ConnectionInterface.php @@ -51,13 +51,6 @@ public function getServerAgent(): string; */ public function getServerAddress(): UriInterface; - /** - * Returns the version of the neo4j server. - * - * @psalm-mutation-free - */ - public function getServerVersion(): string; - /** * Returns the assumed server state. */ diff --git a/src/Databags/Notification.php b/src/Databags/Notification.php index 08294ab5..6fb968e4 100644 --- a/src/Databags/Notification.php +++ b/src/Databags/Notification.php @@ -14,16 +14,22 @@ namespace Laudis\Neo4j\Databags; use InvalidArgumentException; +use Laudis\Neo4j\Types\AbstractCypherObject; -final class Notification +/** + * @psalm-immutable + * + * @template-extends AbstractCypherObject + */ +final class Notification extends AbstractCypherObject { public function __construct( - private string $severity, - private string $description, - private string $code, - private Position $position, - private string $title, - private string $category, + private readonly string $severity, + private readonly string $description, + private readonly string $code, + private readonly Position $position, + private readonly string $title, + private readonly string $category, ) { } @@ -92,9 +98,38 @@ public function getCategory(): string } /** + * Matches inherited return type: array. + * * @psalm-external-mutation-free + * + * @return array */ public function toArray(): array + { + return [ + 'severity' => $this->severity, + 'description' => $this->description, + 'code' => $this->code, + 'position' => $this->position, + 'title' => $this->title, + 'category' => $this->category, + ]; + } + + /** + * If you still want a version with the position converted to array, + * use this custom method instead of overriding toArray(). + * + * @return array{ + * severity: string, + * description: string, + * code: string, + * position: array, + * title: string, + * category: string + * } + */ + public function toSerializedArray(): array { return [ 'severity' => $this->severity, diff --git a/src/Databags/PlanArguments.php b/src/Databags/PlanArguments.php index 76a84501..a2e6b6d7 100644 --- a/src/Databags/PlanArguments.php +++ b/src/Databags/PlanArguments.php @@ -13,7 +13,14 @@ namespace Laudis\Neo4j\Databags; -final class PlanArguments +use Laudis\Neo4j\Types\AbstractCypherObject; + +/** + * @psalm-immutable + * + * @template-extends AbstractCypherObject + */ +final class PlanArguments extends AbstractCypherObject { public function __construct( public readonly ?int $globalMemory = null, diff --git a/src/Databags/Position.php b/src/Databags/Position.php index 474885b1..acfa6781 100644 --- a/src/Databags/Position.php +++ b/src/Databags/Position.php @@ -13,15 +13,19 @@ namespace Laudis\Neo4j\Databags; +use Laudis\Neo4j\Types\AbstractCypherObject; + /** * @psalm-immutable + * + *@template-extends AbstractCypherObject */ -final class Position +final class Position extends AbstractCypherObject { public function __construct( - private int $column, - private int $offset, - private int $line, + private readonly int $column, + private readonly int $offset, + private readonly int $line, ) { } diff --git a/src/Databags/ProfiledQueryPlan.php b/src/Databags/ProfiledQueryPlan.php index a0aec701..5fc6d75d 100644 --- a/src/Databags/ProfiledQueryPlan.php +++ b/src/Databags/ProfiledQueryPlan.php @@ -18,6 +18,8 @@ final class ProfiledQueryPlan /** * @param list $children * @param list $identifiers + * + * @psalm-immutable */ public function __construct( public readonly PlanArguments $arguments, diff --git a/src/Databags/SummarizedResult.php b/src/Databags/SummarizedResult.php index 15a4dd22..e97c54c8 100644 --- a/src/Databags/SummarizedResult.php +++ b/src/Databags/SummarizedResult.php @@ -28,16 +28,22 @@ final class SummarizedResult extends CypherList { private ?ResultSummary $summary = null; + /** + * @var list + */ + private array $keys; /** - * @param iterable>|callable():Generator> $iterable - * * @psalm-mutation-free + * + * @param iterable>|callable():Generator> $iterable + * @param list $keys */ - public function __construct(?ResultSummary &$summary, iterable|callable $iterable = []) + public function __construct(?ResultSummary &$summary, iterable|callable $iterable = [], array $keys = []) { parent::__construct($iterable); $this->summary = &$summary; + $this->keys = $keys; } /** @@ -71,4 +77,12 @@ public function jsonSerialize(): array 'result' => parent::jsonSerialize(), ]; } + + /** + * @return list + */ + public function keys(): array + { + return $this->keys; + } } diff --git a/src/Databags/TransactionConfiguration.php b/src/Databags/TransactionConfiguration.php index bc9a68d9..ba7df2b4 100644 --- a/src/Databags/TransactionConfiguration.php +++ b/src/Databags/TransactionConfiguration.php @@ -20,16 +20,16 @@ */ final class TransactionConfiguration { - public const DEFAULT_TIMEOUT = 60.0; + public const DEFAULT_TIMEOUT = 60 * 60 * 24; public const DEFAULT_METADATA = '[]'; /** - * @param float|null $timeout timeout in seconds - * @param iterable|null $metaData + * @param float|null $timeout timeout in seconds + * @param array|null $metaData */ public function __construct( private ?float $timeout = null, - private ?iterable $metaData = null, + private ?array $metaData = null, ) { } @@ -41,7 +41,7 @@ public function __construct( */ public static function create(?float $timeout = null, ?iterable $metaData = null): self { - return new self($timeout, $metaData); + return new self($timeout, $metaData !== null ? (array) $metaData : null); } /** @@ -53,11 +53,9 @@ public static function default(): self } /** - * Get the configured transaction metadata. - * - * @return iterable|null + * @return array|null */ - public function getMetaData(): ?iterable + public function getMetaData(): ?array { return $this->metaData; } @@ -87,7 +85,7 @@ public function withTimeout(?float $timeout): self */ public function withMetaData(?iterable $metaData): self { - return new self($this->timeout, $metaData); + return new self($this->timeout, $metaData !== null ? (array) $metaData : null); } /** @@ -101,6 +99,7 @@ public function merge(?TransactionConfiguration $config): self $metaData = $config->metaData; if ($metaData !== null) { + /** @psalm-suppress PossiblyInvalidArgument */ $tsxConfig = $tsxConfig->withMetaData($metaData); } $timeout = $config->timeout; diff --git a/src/Exception/SSLConnectionException.php b/src/Exception/SSLConnectionException.php deleted file mode 100644 index 26179ddb..00000000 --- a/src/Exception/SSLConnectionException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Exception; - -use RuntimeException; - -final class SSLConnectionException extends RuntimeException -{ -} diff --git a/src/Exception/TimeoutException.php b/src/Exception/TimeoutException.php deleted file mode 100644 index 10206f49..00000000 --- a/src/Exception/TimeoutException.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Exception; - -use RuntimeException; - -class TimeoutException extends RuntimeException -{ -} diff --git a/src/Exception/ClientException.php b/src/Exception/TransactionException.php similarity index 91% rename from src/Exception/ClientException.php rename to src/Exception/TransactionException.php index 81639c5a..161378a7 100644 --- a/src/Exception/ClientException.php +++ b/src/Exception/TransactionException.php @@ -23,7 +23,7 @@ * * @psalm-suppress MutableDependency */ -final class ClientException extends RuntimeException +final class TransactionException extends RuntimeException { public function __construct(string $message, ?Throwable $previous = null) { diff --git a/src/Formatter/Specialised/BoltOGMTranslator.php b/src/Formatter/Specialised/BoltOGMTranslator.php index 2096e02a..16165eab 100644 --- a/src/Formatter/Specialised/BoltOGMTranslator.php +++ b/src/Formatter/Specialised/BoltOGMTranslator.php @@ -178,9 +178,6 @@ private function makeFromBoltRelationship(BoltRelationship $rel): Relationship foreach ($rel->properties as $key => $property) { $map[$key] = $this->mapValueToType($property); } - /** @var string|null $elementId */ - $startNodeElementId = null; - $endNodeElementId = null; /** @var string|null $elementId */ $elementId = null; @@ -194,9 +191,7 @@ private function makeFromBoltRelationship(BoltRelationship $rel): Relationship $rel->endNodeId, $rel->type, new CypherMap($map), - $elementId, - $startNodeElementId, // Add this parameter - $endNodeElementId + $elementId ); } diff --git a/src/Formatter/SummarizedResultFormatter.php b/src/Formatter/SummarizedResultFormatter.php index 6a6d8531..5f8c36f6 100644 --- a/src/Formatter/SummarizedResultFormatter.php +++ b/src/Formatter/SummarizedResultFormatter.php @@ -86,8 +86,8 @@ * constraints-added?: int, * constraints-removed?: int, * contains-updates?: bool, - * contains-system-updates?: bool|int, - * system-updates?: int|bool, + * contains-system-updates?: bool, + * system-updates?: int, * db?: string * } * @psalm-type CypherError = array{code: string, message: string} @@ -138,21 +138,6 @@ public function formatBoltStats(array $response): SummaryCounters } } - $systemUpdates = $stats['system-updates'] ?? 0; - if (is_bool($systemUpdates)) { - $systemUpdates = (int) $systemUpdates; - } - - $containsSystemUpdates = $stats['contains-system-updates'] ?? null; - - if ($containsSystemUpdates === null) { - $containsSystemUpdates = $systemUpdates > 0; - } else { - if (!is_bool($containsSystemUpdates)) { - $containsSystemUpdates = (bool) $containsSystemUpdates; - } - } - return new SummaryCounters( $stats['nodes-created'] ?? 0, $stats['nodes-deleted'] ?? 0, @@ -166,8 +151,8 @@ public function formatBoltStats(array $response): SummaryCounters $stats['constraints-added'] ?? 0, $stats['constraints-removed'] ?? 0, $updateCount > 0, - $containsSystemUpdates, - $systemUpdates + ($stats['contains-system-updates'] ?? $stats['system-updates'] ?? 0) >= 1, + $stats['system-updates'] ?? 0 ); } @@ -212,9 +197,9 @@ function (mixed $response) use ($connection, $statement, $runStart, $resultAvail /** @var SummarizedResult */ $result = (new CypherList($formattedResult))->withCacheLimit($result->getFetchSize()); - // $keys = $meta['fields']; + $keys = $meta['fields']; - return new SummarizedResult($summary, $result); + return new SummarizedResult($summary, $result, $keys); } public function formatArgs(array $profiledPlanData): PlanArguments diff --git a/src/Types/CypherList.php b/src/Types/CypherList.php index 4ea30b35..6cf08fb3 100644 --- a/src/Types/CypherList.php +++ b/src/Types/CypherList.php @@ -42,17 +42,19 @@ class CypherList implements CypherSequence, Iterator, ArrayAccess * @use CypherSequenceTrait */ use CypherSequenceTrait; + private ?int $qid = null; /** * @param iterable|callable():Generator $iterable * * @psalm-mutation-free */ - public function __construct(iterable|callable $iterable = []) + public function __construct(iterable|callable $iterable = [], ?int $qid = null) { if (is_array($iterable)) { $iterable = new ArrayIterator($iterable); } + $this->qid = $qid; $this->generator = static function () use ($iterable): Generator { $i = 0; @@ -65,6 +67,11 @@ public function __construct(iterable|callable $iterable = []) }; } + public function getQid(): ?int + { + return $this->qid; + } + /** * @template Value * diff --git a/src/Types/Relationship.php b/src/Types/Relationship.php index 9f5cb239..f0b716a9 100644 --- a/src/Types/Relationship.php +++ b/src/Types/Relationship.php @@ -24,9 +24,6 @@ */ final class Relationship extends UnboundRelationship { - private string $startNodeElementId; - private string $endNodeElementId; - /** * @param CypherMap $properties */ @@ -37,17 +34,8 @@ public function __construct( string $type, CypherMap $properties, ?string $elementId, - int|string|null $startNodeElementId = null, - int|string|null $endNodeElementId = null, ) { parent::__construct($id, $type, $properties, $elementId); - $this->startNodeElementId = $startNodeElementId !== null - ? (string) $startNodeElementId - : (string) $startNodeId; - - $this->endNodeElementId = $endNodeElementId !== null - ? (string) $endNodeElementId - : (string) $endNodeId; } /** diff --git a/src/Types/UnboundRelationship.php b/src/Types/UnboundRelationship.php index 904237f2..cde5fdb1 100644 --- a/src/Types/UnboundRelationship.php +++ b/src/Types/UnboundRelationship.php @@ -32,15 +32,12 @@ class UnboundRelationship extends AbstractPropertyObject /** * @param CypherMap $properties */ - private string $elementId; - public function __construct( private readonly int $id, private readonly string $type, private readonly CypherMap $properties, - ?string $elementId = null, + private readonly ?string $elementId, ) { - $this->elementId = $elementId ?? (string) $id; } public function getElementId(): ?string @@ -58,9 +55,6 @@ public function getType(): string return $this->type; } - /** - * @psalm-suppress MixedReturnTypeCoercion - */ public function getProperties(): CypherMap { /** @psalm-suppress InvalidReturnStatement false positive with type alias. */ @@ -86,11 +80,7 @@ public function toArray(): array * * @return OGMTypes */ - - /** - * @psalm-suppress MixedReturnStatement - */ - public function getProperty(string $key): string + public function getProperty(string $key) { /** @psalm-suppress ImpureMethodCall */ if (!$this->properties->hasKey($key)) { diff --git a/test-basic.php b/test-basic.php deleted file mode 100644 index 5799e5f2..00000000 --- a/test-basic.php +++ /dev/null @@ -1,20 +0,0 @@ -withDriver('bolt', 'bolt://localhost:7687', - Authenticate::basic('neo4j', 'testtest') - ) - ->build(); - -try { - $result = $client->run('RETURN "Hello World" as message'); - echo "Connection successful!\n"; - print_r($result->first()->get('message')); -} catch (Exception $e) { - echo "Connection failed: " . $e->getMessage() . "\n"; -} -?> diff --git a/testkit-backend.sh b/testkit-backend.sh deleted file mode 100644 index 8e238b82..00000000 --- a/testkit-backend.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -docker compose up -d testkit_backend -docker compose logs -f testkit_backend diff --git a/testkit-backend/features.php b/testkit-backend/features.php index 77a1b98d..0efa8bd6 100644 --- a/testkit-backend/features.php +++ b/testkit-backend/features.php @@ -124,9 +124,9 @@ // The driver supports Bolt protocol version 5.6 'Feature:Bolt:5.6' => false, // The driver supports Bolt protocol version 5.7 - 'Feature:Bolt:5.7' => true, + 'Feature:Bolt:5.7' => false, // The driver supports Bolt protocol version 5.8 - 'Feature:Bolt:5.8' => true, + 'Feature:Bolt:5.8' => false, // The driver supports negotiating the Bolt protocol version with the server // using handshake manifest v1. 'Feature:Bolt:HandshakeManifestV1' => true, @@ -221,7 +221,7 @@ // time period. On timout, the driver should remove the server from its // routing table and assume all other connections to the server are dead // as well. - 'ConfHint:connection.recv_timeout_seconds' => true, + 'ConfHint:connection.recv_timeout_seconds' => false, // === BACKEND FEATURES FOR TESTING === // The backend understands the FakeTimeInstall, FakeTimeUninstall and diff --git a/testkit-backend/src/Handlers/AbstractRunner.php b/testkit-backend/src/Handlers/AbstractRunner.php index 81bd004c..140fc60d 100644 --- a/testkit-backend/src/Handlers/AbstractRunner.php +++ b/testkit-backend/src/Handlers/AbstractRunner.php @@ -13,14 +13,17 @@ namespace Laudis\Neo4j\TestkitBackend\Handlers; +use Exception; use Laudis\Neo4j\Contracts\SessionInterface; use Laudis\Neo4j\Contracts\TransactionInterface; use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; use Laudis\Neo4j\TestkitBackend\MainRepository; use Laudis\Neo4j\TestkitBackend\Requests\SessionRunRequest; +use Laudis\Neo4j\TestkitBackend\Requests\TransactionRunRequest; use Laudis\Neo4j\TestkitBackend\Responses\DriverErrorResponse; use Laudis\Neo4j\TestkitBackend\Responses\ResultResponse; use Laudis\Neo4j\Types\AbstractCypherObject; @@ -47,7 +50,7 @@ public function __construct(MainRepository $repository, LoggerInterface $logger) $this->logger = $logger; } - public function handle($request): ResultResponse + public function handle($request): ResultResponse|DriverErrorResponse { $session = $this->getRunner($request); $id = Uuid::v4(); @@ -77,16 +80,24 @@ public function handle($request): ResultResponse $this->repository->addRecords($id, $result); - return new ResultResponse($id, $result->isEmpty() ? [] : $result->first()->keys()); + return new ResultResponse($id, $result->keys()); } catch (Neo4jException $exception) { - $this->logger->debug($exception->__toString()); - $this->repository->addRecords($id, new DriverErrorResponse( - $this->getId($request), - $exception - )); - - return new ResultResponse($id, []); - } // NOTE: all other exceptions will be caught in the Backend + if ($request instanceof SessionRunRequest) { + return new DriverErrorResponse($request->getSessionId(), $exception); + } + if ($request instanceof TransactionRunRequest) { + return new DriverErrorResponse($request->getTxId(), $exception); + } + + throw new Exception('Unhandled neo4j exception for run request of type: '.get_class($request)); + } catch (TransactionException $exception) { + if ($request instanceof TransactionRunRequest) { + return new DriverErrorResponse($request->getTxId(), $exception); + } + + throw new Exception('Unhandled neo4j exception for run request of type: '.get_class($request)); + } + // NOTE: all other exceptions will be caught in the Backend } /** diff --git a/testkit-backend/src/Handlers/GetServerInfo.php b/testkit-backend/src/Handlers/GetServerInfo.php new file mode 100644 index 00000000..2a3c2a44 --- /dev/null +++ b/testkit-backend/src/Handlers/GetServerInfo.php @@ -0,0 +1,180 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\TestkitBackend\Handlers; + +use Exception; +use Laudis\Neo4j\Bolt\BoltDriver; +use Laudis\Neo4j\Common\GeneratorHelper; +use Laudis\Neo4j\Databags\Neo4jError; +use Laudis\Neo4j\Databags\ServerInfo; +use Laudis\Neo4j\Databags\SessionConfiguration; +use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Exception\TransactionException; +use Laudis\Neo4j\Neo4j\Neo4jDriver; +use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; +use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; +use Laudis\Neo4j\TestkitBackend\MainRepository; +use Laudis\Neo4j\TestkitBackend\Requests\GetServerInfoRequest; +use Laudis\Neo4j\TestkitBackend\Responses\DriverErrorResponse; +use Laudis\Neo4j\TestkitBackend\Responses\ServerInfoResponse; +use ReflectionClass; +use Symfony\Component\Uid\Uuid; + +/** + * @implements RequestHandlerInterface + */ +final class GetServerInfo implements RequestHandlerInterface +{ + public function __construct( + private MainRepository $repository, + ) { + } + + /** + * @param GetServerInfoRequest $request + */ + public function handle($request): TestkitResponseInterface + { + try { + $driver = $this->repository->getDriver($request->getDriverId()); + + if ($driver instanceof BoltDriver || $driver instanceof Neo4jDriver) { + return $this->getServerInfoFromDriver($driver); + } + + return $this->getServerInfoFromSession($driver); + } catch (Exception $e) { + $uuid = Uuid::v4(); + + if ($e instanceof Neo4jException || $e instanceof TransactionException) { + return new DriverErrorResponse($uuid, $e); + } + + $neo4jError = new Neo4jError( + $e->getMessage(), + (string) $e->getCode(), + 'DatabaseError', + 'Service', + 'Service Unavailable' + ); + + return new DriverErrorResponse($uuid, new Neo4jException([$neo4jError], $e)); + } + } + + private function getServerInfoFromDriver($driver): ServerInfoResponse + { + $connection = null; + $pool = null; + + try { + $pool = $this->getConnectionPool($driver); + $connection = $this->acquireConnectionFromPool($pool, SessionConfiguration::default()); + + return new ServerInfoResponse($this->extractServerInfo($connection)); + } finally { + if ($connection !== null && $pool !== null) { + $pool->release($connection); + } + } + } + + /** + * Extracts connection pool from driver using reflection. + */ + private function getConnectionPool($driver) + { + $reflection = new ReflectionClass($driver); + + foreach (['pool', 'connectionPool', '_pool', 'connections'] as $propertyName) { + if ($reflection->hasProperty($propertyName)) { + $property = $reflection->getProperty($propertyName); + $property->setAccessible(true); + $pool = $property->getValue($driver); + + if ($pool !== null) { + return $pool; + } + } + } + + throw new Exception('Could not access connection pool from driver'); + } + + /** + * Acquire a connection from the pool. + */ + private function acquireConnectionFromPool($pool, SessionConfiguration $sessionConfig) + { + // Fail early if routing table has no readers + if (method_exists($pool, 'getRoutingTable')) { + $routingTable = $pool->getRoutingTable(); + if ($routingTable !== null && empty($routingTable->getReaders())) { + throw new Neo4jException([new Neo4jError('No readers available in routing table', 'N/A', 'ClientError', 'Routing', 'RoutingTable')]); + } + } + + $connectionGenerator = $pool->acquire($sessionConfig); + $connection = GeneratorHelper::getReturnFromGenerator($connectionGenerator); + + if ($connection === null) { + throw new Exception('Connection pool returned no connections'); + } + + return $connection; + } + + /** + * Extract server information from an active connection. + */ + private function extractServerInfo($connection): ServerInfo + { + foreach (['getServerAddress', 'getServerAgent', 'getProtocol'] as $method) { + if (!method_exists($connection, $method)) { + throw new Exception("Connection does not support {$method}()"); + } + } + + $address = $connection->getServerAddress(); + $agent = $connection->getServerAgent(); + $protocol = $connection->getProtocol(); + + if (empty($address) || empty($agent)) { + throw new Exception('Server info is incomplete'); + } + + return new ServerInfo($address, $protocol, $agent); + } + + private function getServerInfoFromSession($driver): ServerInfoResponse + { + if (method_exists($driver, 'session')) { + $session = $driver->session(); + } elseif (method_exists($driver, 'createSession')) { + $session = $driver->createSession(); + } elseif (method_exists($driver, 'newSession')) { + $session = $driver->newSession(); + } else { + throw new Exception('No session creation method found on driver'); + } + + try { + $result = $session->run('RETURN 1'); + + return new ServerInfoResponse($result->summary()->getServerInfo()); + } finally { + $session->close(); + } + } +} diff --git a/testkit-backend/src/Handlers/NewClientCertificateProvider.php b/testkit-backend/src/Handlers/NewClientCertificateProvider.php deleted file mode 100644 index beb9f92b..00000000 --- a/testkit-backend/src/Handlers/NewClientCertificateProvider.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\TestkitBackend\Handlers; - -use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; -use Laudis\Neo4j\TestkitBackend\MainRepository; -use Laudis\Neo4j\TestkitBackend\Requests\NewClientCertificateProviderRequest; -use Laudis\Neo4j\TestkitBackend\Responses\ClientCertificateProviderResponse; -use Symfony\Component\Uid\Uuid; - -/** - * @implements RequestHandlerInterface - */ -final class NewClientCertificateProvider implements RequestHandlerInterface -{ - private MainRepository $repository; - - public function __construct(MainRepository $repository) - { - $this->repository = $repository; - } - - /** - * @param NewClientCertificateProviderRequest $request - */ - public function handle($request): ClientCertificateProviderResponse - { - $id = Uuid::v4(); - - // Initialize the certificate provider with proper structure - $certificateData = [ - 'certificates' => [], - 'current_index' => 0, - 'rotation_enabled' => true, - 'created_at' => time(), - ]; - - // Store the certificate provider in the repository - // This will be used later when certificates are requested - $this->repository->addClientCertificateProvider($id, $certificateData); - - return new ClientCertificateProviderResponse($id); - } -} diff --git a/testkit-backend/src/Handlers/ResultNext.php b/testkit-backend/src/Handlers/ResultNext.php index 0b8782f1..b074284c 100644 --- a/testkit-backend/src/Handlers/ResultNext.php +++ b/testkit-backend/src/Handlers/ResultNext.php @@ -48,7 +48,6 @@ public function handle($request): TestkitResponseInterface $iterator = $this->repository->getIterator($request->getResultId()); - // Advance iterator only if it’s not the first fetch if ($this->repository->getIteratorFetchedFirst($request->getResultId()) === true) { $iterator->next(); } diff --git a/testkit-backend/src/Handlers/RetryableNegative.php b/testkit-backend/src/Handlers/RetryableNegative.php index 7801b4e9..3b6504a1 100644 --- a/testkit-backend/src/Handlers/RetryableNegative.php +++ b/testkit-backend/src/Handlers/RetryableNegative.php @@ -16,7 +16,7 @@ use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; use Laudis\Neo4j\TestkitBackend\Requests\RetryableNegativeRequest; -use Laudis\Neo4j\TestkitBackend\Responses\BackendErrorResponse; +use Laudis\Neo4j\TestkitBackend\Responses\FrontendErrorResponse; /** * @implements RequestHandlerInterface @@ -28,6 +28,6 @@ final class RetryableNegative implements RequestHandlerInterface */ public function handle($request): TestkitResponseInterface { - return new BackendErrorResponse('Retryable negative not implemented yet'); // TODO + return new FrontendErrorResponse('Retryable negative not implemented yet'); // TODO } } diff --git a/testkit-backend/src/Handlers/RetryablePositive.php b/testkit-backend/src/Handlers/RetryablePositive.php index fa9b27d2..c68ffee6 100644 --- a/testkit-backend/src/Handlers/RetryablePositive.php +++ b/testkit-backend/src/Handlers/RetryablePositive.php @@ -13,10 +13,17 @@ namespace Laudis\Neo4j\TestkitBackend\Handlers; +use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; +use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; +use Laudis\Neo4j\TestkitBackend\MainRepository; use Laudis\Neo4j\TestkitBackend\Requests\RetryablePositiveRequest; +use Laudis\Neo4j\TestkitBackend\Responses\BackendErrorResponse; +use Laudis\Neo4j\TestkitBackend\Responses\DriverErrorResponse; use Laudis\Neo4j\TestkitBackend\Responses\RetryableDoneResponse; +use Throwable; /** * @implements RequestHandlerInterface @@ -26,8 +33,35 @@ final class RetryablePositive implements RequestHandlerInterface /** * @param RetryablePositiveRequest $request */ + private MainRepository $repository; + + public function __construct(MainRepository $repository) + { + $this->repository = $repository; + } + public function handle($request): TestkitResponseInterface { + $sessionId = $request->getSessionId(); + + try { + $transactionId = $this->repository->getTsxIdFromSession($sessionId); + } catch (Throwable $e) { + return new BackendErrorResponse('Transaction not found for session '.$sessionId->toRfc4122()); + } + + $tsx = $this->repository->getTransaction($transactionId); + + if (!$tsx instanceof UnmanagedTransactionInterface) { + return new BackendErrorResponse('Transaction not found '.$transactionId->toRfc4122()); + } + + try { + $tsx->commit(); + } catch (Neo4jException|TransactionException $e) { + return new DriverErrorResponse($transactionId, $e); + } + return new RetryableDoneResponse(); } } diff --git a/testkit-backend/src/Handlers/SessionBeginTransaction.php b/testkit-backend/src/Handlers/SessionBeginTransaction.php index fe2647ed..600821da 100644 --- a/testkit-backend/src/Handlers/SessionBeginTransaction.php +++ b/testkit-backend/src/Handlers/SessionBeginTransaction.php @@ -21,6 +21,9 @@ use Laudis\Neo4j\TestkitBackend\Requests\SessionBeginTransactionRequest; use Laudis\Neo4j\TestkitBackend\Responses\DriverErrorResponse; use Laudis\Neo4j\TestkitBackend\Responses\TransactionResponse; +use Laudis\Neo4j\Types\AbstractCypherObject; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\CypherMap; use Psr\Log\LoggerInterface; use Symfony\Component\Uid\Uuid; @@ -52,7 +55,14 @@ public function handle($request): TestkitResponseInterface } if ($request->getTxMeta()) { - $config = $config->withMetaData($request->getTxMeta()); + $metaData = $request->getTxMeta(); + $actualMeta = []; + if ($metaData !== null) { + foreach ($metaData as $key => $meta) { + $actualMeta[$key] = $this->decodeToValue($meta); + } + } + $config = $config->withMetaData($actualMeta); } // TODO - Create beginReadTransaction and beginWriteTransaction @@ -70,4 +80,45 @@ public function handle($request): TestkitResponseInterface return new TransactionResponse($id); } + + /** + * @param array{name: string, data: array{value: iterable|scalar|null}} $param + * + * @return scalar|AbstractCypherObject|iterable|null + */ + private function decodeToValue(array $param) + { + $value = $param['data']['value']; + if (is_iterable($value)) { + if ($param['name'] === 'CypherMap') { + /** @psalm-suppress MixedArgumentTypeCoercion */ + $map = []; + /** + * @var numeric $k + * @var mixed $v + */ + foreach ($value as $k => $v) { + /** @psalm-suppress MixedArgument */ + $map[(string) $k] = $this->decodeToValue($v); + } + + return new CypherMap($map); + } + + if ($param['name'] === 'CypherList') { + $list = []; + /** + * @var mixed $v + */ + foreach ($value as $v) { + /** @psalm-suppress MixedArgument */ + $list[] = $this->decodeToValue($v); + } + + return new CypherList($list); + } + } + + return $value; + } } diff --git a/testkit-backend/src/Handlers/SessionClose.php b/testkit-backend/src/Handlers/SessionClose.php index 0a938ed8..4b0cdebe 100644 --- a/testkit-backend/src/Handlers/SessionClose.php +++ b/testkit-backend/src/Handlers/SessionClose.php @@ -35,6 +35,11 @@ public function __construct(MainRepository $repository) */ public function handle($request): SessionResponse { + $session = $this->repository->getSession($request->getSessionId()); + + if ($session !== null) { + $session->close(); + } $this->repository->removeSession($request->getSessionId()); return new SessionResponse($request->getSessionId()); diff --git a/testkit-backend/src/Handlers/SessionLastBookmarks.php b/testkit-backend/src/Handlers/SessionLastBookmarks.php index ac91b323..282fb08c 100644 --- a/testkit-backend/src/Handlers/SessionLastBookmarks.php +++ b/testkit-backend/src/Handlers/SessionLastBookmarks.php @@ -39,7 +39,7 @@ public function handle($request): TestkitResponseInterface { $session = $this->repository->getSession($request->getSessionId()); - $bookmarks = $session->getLastBookmark()->values(); + $bookmarks = $session->getLastBookmark()->values() ?? []; return new BookmarksResponse($bookmarks); } diff --git a/testkit-backend/src/Handlers/SessionReadTransaction.php b/testkit-backend/src/Handlers/SessionReadTransaction.php index 52fe47ae..5bd1e42b 100644 --- a/testkit-backend/src/Handlers/SessionReadTransaction.php +++ b/testkit-backend/src/Handlers/SessionReadTransaction.php @@ -21,6 +21,8 @@ use Laudis\Neo4j\TestkitBackend\Requests\SessionReadTransactionRequest; use Laudis\Neo4j\TestkitBackend\Responses\DriverErrorResponse; use Laudis\Neo4j\TestkitBackend\Responses\RetryableTryResponse; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\CypherMap; use Symfony\Component\Uid\Uuid; /** @@ -49,7 +51,18 @@ public function handle($request): TestkitResponseInterface } if ($request->getTxMeta()) { - $config = $config->withMetaData($request->getTxMeta()); + $metaData = $request->getTxMeta(); + $actualMeta = []; + if ($metaData !== null) { + foreach ($metaData as $key => $meta) { + $actualMeta[$key] = $this->decodeToValue($meta); + } + } + $config = $config->withMetaData($actualMeta); + } + + if ($request->getBookmarks()) { + $config = $config->withBookmarks($request->getBookmarks()); } $id = Uuid::v4(); @@ -70,5 +83,41 @@ public function handle($request): TestkitResponseInterface return new RetryableTryResponse($id); } + // f1aa000cede64d6a8879513c97633777 + private function decodeToValue(array $param) + { + $value = $param['data']['value']; + if (is_iterable($value)) { + if ($param['name'] === 'CypherMap') { + /** @psalm-suppress MixedArgumentTypeCoercion */ + $map = []; + /** + * @var numeric $k + * @var mixed $v + */ + foreach ($value as $k => $v) { + /** @psalm-suppress MixedArgument */ + $map[(string) $k] = $this->decodeToValue($v); + } + + return new CypherMap($map); + } + + if ($param['name'] === 'CypherList') { + $list = []; + /** + * @var mixed $v + */ + foreach ($value as $v) { + /** @psalm-suppress MixedArgument */ + $list[] = $this->decodeToValue($v); + } + + return new CypherList($list); + } + } + + return $value; + } } diff --git a/testkit-backend/src/Handlers/SessionWriteTransaction.php b/testkit-backend/src/Handlers/SessionWriteTransaction.php index 7a7004d9..9afcf133 100644 --- a/testkit-backend/src/Handlers/SessionWriteTransaction.php +++ b/testkit-backend/src/Handlers/SessionWriteTransaction.php @@ -13,11 +13,16 @@ namespace Laudis\Neo4j\TestkitBackend\Handlers; +use Laudis\Neo4j\Databags\TransactionConfiguration; +use Laudis\Neo4j\Exception\Neo4jException; use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; use Laudis\Neo4j\TestkitBackend\MainRepository; use Laudis\Neo4j\TestkitBackend\Requests\SessionWriteTransactionRequest; +use Laudis\Neo4j\TestkitBackend\Responses\DriverErrorResponse; use Laudis\Neo4j\TestkitBackend\Responses\RetryableTryResponse; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\CypherMap; use Symfony\Component\Uid\Uuid; /** @@ -39,11 +44,75 @@ public function handle($request): TestkitResponseInterface { $session = $this->repository->getSession($request->getSessionId()); + $config = TransactionConfiguration::default(); + + if ($request->getTimeout()) { + $config = $config->withTimeout($request->getTimeout()); + } + + if ($request->getTxMeta()) { + $metaData = $request->getTxMeta(); + $actualMeta = []; + if ($metaData !== null) { + foreach ($metaData as $key => $meta) { + $actualMeta[$key] = $this->decodeToValue($meta); + } + } + $config = $config->withMetaData($actualMeta); + } + $id = Uuid::v4(); + try { + // TODO - Create beginReadTransaction and beginWriteTransaction + $transaction = $session->beginTransaction(null, $config); - $this->repository->addTransaction($id, $session); - $this->repository->bindTransactionToSession($request->getSessionId(), $id); + $this->repository->addTransaction($id, $transaction); + $this->repository->bindTransactionToSession($request->getSessionId(), $id); + } catch (Neo4jException $exception) { + $this->repository->addRecords($id, new DriverErrorResponse( + $id, + $exception + )); + + return new DriverErrorResponse($id, $exception); + } return new RetryableTryResponse($id); } + + private function decodeToValue(array $param) + { + $value = $param['data']['value']; + if (is_iterable($value)) { + if ($param['name'] === 'CypherMap') { + /** @psalm-suppress MixedArgumentTypeCoercion */ + $map = []; + /** + * @var numeric $k + * @var mixed $v + */ + foreach ($value as $k => $v) { + /** @psalm-suppress MixedArgument */ + $map[(string) $k] = $this->decodeToValue($v); + } + + return new CypherMap($map); + } + + if ($param['name'] === 'CypherList') { + $list = []; + /** + * @var mixed $v + */ + foreach ($value as $v) { + /** @psalm-suppress MixedArgument */ + $list[] = $this->decodeToValue($v); + } + + return new CypherList($list); + } + } + + return $value; + } } diff --git a/testkit-backend/src/Handlers/TransactionCommit.php b/testkit-backend/src/Handlers/TransactionCommit.php index 203487c2..ca6d1029 100644 --- a/testkit-backend/src/Handlers/TransactionCommit.php +++ b/testkit-backend/src/Handlers/TransactionCommit.php @@ -15,6 +15,7 @@ use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; use Laudis\Neo4j\TestkitBackend\MainRepository; @@ -48,7 +49,7 @@ public function handle($request): TestkitResponseInterface try { $tsx->commit(); - } catch (Neo4jException $e) { + } catch (Neo4jException|TransactionException $e) { return new DriverErrorResponse($request->getTxId(), $e); } diff --git a/testkit-backend/src/Handlers/TransactionRollback.php b/testkit-backend/src/Handlers/TransactionRollback.php index ac40879d..08e4f6bd 100644 --- a/testkit-backend/src/Handlers/TransactionRollback.php +++ b/testkit-backend/src/Handlers/TransactionRollback.php @@ -15,6 +15,7 @@ use Laudis\Neo4j\Contracts\TransactionInterface; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\TestkitBackend\Contracts\RequestHandlerInterface; use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; use Laudis\Neo4j\TestkitBackend\MainRepository; @@ -48,7 +49,7 @@ public function handle($request): TestkitResponseInterface try { $tsx->rollback(); - } catch (Neo4jException $e) { + } catch (Neo4jException|TransactionException $e) { return new DriverErrorResponse($request->getTxId(), $e); } diff --git a/testkit-backend/src/MainRepository.php b/testkit-backend/src/MainRepository.php index 848ca3b7..4235c1f1 100644 --- a/testkit-backend/src/MainRepository.php +++ b/testkit-backend/src/MainRepository.php @@ -126,11 +126,7 @@ public function addRecords(Uuid $id, $result): void $this->records[$id->toRfc4122()] = $result; if ($result instanceof SummarizedResult) { /** @var SummarizedResult> $result */ - $this->recordIterators[$id->toRfc4122()] = (function () use ($result) { - foreach ($result as $row) { - yield $row; - } - })(); + $this->recordIterators[$id->toRfc4122()] = $result; } } @@ -176,4 +172,9 @@ public function getTsxIdFromSession(Uuid $sessionId): Uuid { return $this->sessionToTransactions[$sessionId->toRfc4122()]; } + + public function addBufferedRecords(string $id, array $records): void + { + $this->records[$id] = $records; + } } diff --git a/testkit-backend/src/RequestFactory.php b/testkit-backend/src/RequestFactory.php index 9a179d3c..8dd921af 100644 --- a/testkit-backend/src/RequestFactory.php +++ b/testkit-backend/src/RequestFactory.php @@ -22,6 +22,7 @@ use Laudis\Neo4j\TestkitBackend\Requests\ForcedRoutingTableUpdateRequest; use Laudis\Neo4j\TestkitBackend\Requests\GetFeaturesRequest; use Laudis\Neo4j\TestkitBackend\Requests\GetRoutingTableRequest; +use Laudis\Neo4j\TestkitBackend\Requests\GetServerInfoRequest; use Laudis\Neo4j\TestkitBackend\Requests\NewDriverRequest; use Laudis\Neo4j\TestkitBackend\Requests\NewSessionRequest; use Laudis\Neo4j\TestkitBackend\Requests\ResolverResolutionCompletedRequest; @@ -70,6 +71,7 @@ final class RequestFactory 'RetryableNegative' => RetryableNegativeRequest::class, 'ForcedRoutingTableUpdate' => ForcedRoutingTableUpdateRequest::class, 'GetRoutingTable' => GetRoutingTableRequest::class, + 'GetServerInfo' => GetServerInfoRequest::class, ]; /** @@ -83,7 +85,7 @@ public function create(string $name, iterable $data): object return new AuthorizationTokenRequest( $data['scheme'], $data['realm'] ?? '', - $data['principal'], + $data['principal'] ?? '', $data['credentials'] ); } diff --git a/testkit-backend/src/Requests/ClientCertificateRequest.php b/testkit-backend/src/Requests/ClientCertificateRequest.php deleted file mode 100644 index 4dc8abeb..00000000 --- a/testkit-backend/src/Requests/ClientCertificateRequest.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\TestkitBackend\Requests; - -/** - * Represents a client certificate for mutual TLS authentication. - * - * This follows the same pattern as the official Neo4j Python and Java drivers: - * - certfile: Path to the client certificate file (PEM format) - * - keyfile: Path to the private key file (PEM format, PKCS#8) - * - password: Optional password for the private key - */ -class ClientCertificateRequest -{ - public function __construct( - public readonly string $certfile, - public readonly string $keyfile, - public readonly ?string $password = null, - ) { - } -} diff --git a/testkit-backend/src/Requests/NewClientCertificateProviderRequest.php b/testkit-backend/src/Requests/GetServerInfoRequest.php similarity index 55% rename from testkit-backend/src/Requests/NewClientCertificateProviderRequest.php rename to testkit-backend/src/Requests/GetServerInfoRequest.php index 769f819a..7dcb7811 100644 --- a/testkit-backend/src/Requests/NewClientCertificateProviderRequest.php +++ b/testkit-backend/src/Requests/GetServerInfoRequest.php @@ -13,10 +13,19 @@ namespace Laudis\Neo4j\TestkitBackend\Requests; -final class NewClientCertificateProviderRequest +use Symfony\Component\Uid\Uuid; + +final class GetServerInfoRequest { - public function __construct( - public readonly array $data = [], - ) { + private Uuid $driverId; + + public function __construct(Uuid $driverId) + { + $this->driverId = $driverId; + } + + public function getDriverId(): Uuid + { + return $this->driverId; } } diff --git a/testkit-backend/src/Requests/SessionBeginTransactionRequest.php b/testkit-backend/src/Requests/SessionBeginTransactionRequest.php index 8e9025d3..a8242624 100644 --- a/testkit-backend/src/Requests/SessionBeginTransactionRequest.php +++ b/testkit-backend/src/Requests/SessionBeginTransactionRequest.php @@ -13,7 +13,6 @@ namespace Laudis\Neo4j\TestkitBackend\Requests; -use Laudis\Neo4j\Databags\TransactionConfiguration; use Symfony\Component\Uid\Uuid; final class SessionBeginTransactionRequest @@ -49,8 +48,8 @@ public function getTxMeta(): iterable return $this->txMeta ?? []; } - public function getTimeout(): int + public function getTimeout(): ?int { - return (int) ($this->timeout ?? TransactionConfiguration::DEFAULT_TIMEOUT); + return $this->timeout; } } diff --git a/testkit-backend/src/Requests/SessionReadTransactionRequest.php b/testkit-backend/src/Requests/SessionReadTransactionRequest.php index c7ee3bd8..4e936d7a 100644 --- a/testkit-backend/src/Requests/SessionReadTransactionRequest.php +++ b/testkit-backend/src/Requests/SessionReadTransactionRequest.php @@ -21,6 +21,7 @@ final class SessionReadTransactionRequest /** @var iterable */ private iterable $txMeta; private ?int $timeout; + private array $bookmarks; // ADD THIS /** * @param iterable|null $txMeta @@ -29,10 +30,12 @@ public function __construct( Uuid $sessionId, ?iterable $txMeta = null, ?int $timeout = null, + array $bookmarks = [], // ADD THIS ) { $this->sessionId = $sessionId; $this->txMeta = $txMeta ?? []; $this->timeout = $timeout; + $this->bookmarks = $bookmarks; // ADD THIS } public function getSessionId(): Uuid @@ -52,4 +55,9 @@ public function getTimeout(): ?int { return $this->timeout; } + + public function getBookmarks(): array // ADD THIS + { + return $this->bookmarks; + } } diff --git a/testkit-backend/src/Responses/ClientCertificateProviderResponse.php b/testkit-backend/src/Responses/ClientCertificateProviderResponse.php deleted file mode 100644 index 2ade486f..00000000 --- a/testkit-backend/src/Responses/ClientCertificateProviderResponse.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\TestkitBackend\Responses; - -use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; -use Symfony\Component\Uid\Uuid; - -final class ClientCertificateProviderResponse implements TestkitResponseInterface -{ - public function __construct( - public readonly Uuid $id, - ) { - } - - /** - * @return array - */ - public function jsonSerialize(): array - { - return [ - 'name' => 'ClientCertificateProvider', - 'data' => [ - 'id' => $this->id->toRfc4122(), - ], - ]; - } -} diff --git a/testkit-backend/src/Responses/DriverErrorResponse.php b/testkit-backend/src/Responses/DriverErrorResponse.php index 781effa4..f471e74f 100644 --- a/testkit-backend/src/Responses/DriverErrorResponse.php +++ b/testkit-backend/src/Responses/DriverErrorResponse.php @@ -14,6 +14,7 @@ namespace Laudis\Neo4j\TestkitBackend\Responses; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; use Symfony\Component\Uid\Uuid; @@ -23,9 +24,9 @@ final class DriverErrorResponse implements TestkitResponseInterface { private Uuid $id; - private Neo4jException $exception; + private Neo4jException|TransactionException $exception; - public function __construct(Uuid $id, Neo4jException $exception) + public function __construct(Uuid $id, Neo4jException|TransactionException $exception) { $this->id = $id; $this->exception = $exception; @@ -33,12 +34,23 @@ public function __construct(Uuid $id, Neo4jException $exception) public function jsonSerialize(): array { + if ($this->exception instanceof Neo4jException) { + return [ + 'name' => 'DriverError', + 'data' => [ + 'id' => $this->id->toRfc4122(), + 'code' => $this->exception->getNeo4jCode(), + 'msg' => $this->exception->getNeo4jMessage(), + ], + ]; + } + return [ 'name' => 'DriverError', 'data' => [ 'id' => $this->id->toRfc4122(), - 'code' => $this->exception->getNeo4jCode(), - 'msg' => $this->exception->getNeo4jMessage(), + 'code' => $this->exception->getCode(), + 'msg' => $this->exception->getMessage(), ], ]; } diff --git a/testkit-backend/src/Responses/ServerInfoResponse.php b/testkit-backend/src/Responses/ServerInfoResponse.php new file mode 100644 index 00000000..ad74b9de --- /dev/null +++ b/testkit-backend/src/Responses/ServerInfoResponse.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\TestkitBackend\Responses; + +use Laudis\Neo4j\Databags\ServerInfo; +use Laudis\Neo4j\TestkitBackend\Contracts\TestkitResponseInterface; + +/** + * Response containing server information. + */ +final class ServerInfoResponse implements TestkitResponseInterface +{ + private string $address; + private string $agent; + private string $protocolVersion; + + public function __construct(ServerInfo $serverInfo) + { + $uri = $serverInfo->getAddress(); + $this->address = $uri->getHost().':'.$uri->getPort(); + + $this->agent = $serverInfo->getAgent(); + + $protocol = $serverInfo->getProtocol(); + if (method_exists($protocol, 'getValue')) { + $this->protocolVersion = (string) $protocol->getValue(); + } else { + $this->protocolVersion = (string) $protocol; + } + } + + public function jsonSerialize(): array + { + return [ + 'name' => 'ServerInfo', + 'data' => [ + 'address' => $this->address, + 'agent' => $this->agent, + 'protocolVersion' => $this->protocolVersion, + ], + ]; + } +} diff --git a/testkit-backend/src/Responses/Types/CypherObject.php b/testkit-backend/src/Responses/Types/CypherObject.php index 7058da88..9d1bb088 100644 --- a/testkit-backend/src/Responses/Types/CypherObject.php +++ b/testkit-backend/src/Responses/Types/CypherObject.php @@ -24,12 +24,18 @@ use Laudis\Neo4j\Types\UnboundRelationship; use RuntimeException; +/** + * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter + */ final class CypherObject implements TestkitResponseInterface { /** @var CypherList|CypherMap|int|bool|float|string|Node|Relationship|Path|null */ private $value; private string $name; + // Store element ID mappings for relationships created from paths + private static array $relationshipElementIdMap = []; + /** * @param CypherList|CypherMap|int|bool|float|string|Node|Relationship|Path|null $value */ @@ -47,6 +53,9 @@ public function getValue() return $this->value; } + /** + * @param OGMTypes $value + */ public static function autoDetect($value): TestkitResponseInterface { switch (get_debug_type($value)) { @@ -54,13 +63,16 @@ public static function autoDetect($value): TestkitResponseInterface $tbr = new CypherObject('CypherNull', $value); break; case CypherList::class: + /** @var CypherList $value */ $list = []; foreach ($value as $item) { $list[] = self::autoDetect($item); } + $tbr = new CypherObject('CypherList', new CypherList($list)); break; case CypherMap::class: + /** @var CypherMap $value */ if ($value->count() === 2 && $value->hasKey('name') && $value->hasKey('data')) { $tbr = new CypherObject('CypherMap', $value); } else { @@ -68,6 +80,7 @@ public static function autoDetect($value): TestkitResponseInterface foreach ($value as $key => $item) { $map[$key] = self::autoDetect($item); } + $tbr = new CypherObject('CypherMap', new CypherMap($map)); } break; @@ -90,12 +103,14 @@ public static function autoDetect($value): TestkitResponseInterface } $props = []; foreach ($value->getProperties() as $key => $property) { + /** @psalm-suppress MixedArgumentTypeCoercion */ $props[$key] = self::autoDetect($property); } $elementId = $value->getElementId(); if ($elementId === null) { $elementId = (string) $value->getId(); } + $tbr = new CypherNode( new CypherObject('CypherInt', $value->getId()), new CypherObject('CypherList', new CypherList($labels)), @@ -106,26 +121,20 @@ public static function autoDetect($value): TestkitResponseInterface case Relationship::class: $props = []; foreach ($value->getProperties() as $key => $property) { + /** @psalm-suppress MixedArgumentTypeCoercion */ $props[$key] = self::autoDetect($property); } + $elementId = $value->getElementId(); if ($elementId === null) { $elementId = (string) $value->getId(); } - $startNodeElementId = null; - $endNodeElementId = null; - if (method_exists($value, 'getStartNodeElementId')) { - $startNodeElementId = $value->getStartNodeElementId(); - } - if ($startNodeElementId === null) { - $startNodeElementId = (string) $value->getStartNodeId(); - } - if (method_exists($value, 'getEndNodeElementId')) { - $endNodeElementId = $value->getEndNodeElementId(); - } - if ($endNodeElementId === null) { - $endNodeElementId = (string) $value->getEndNodeId(); - } + + $relationshipKey = $value->getId().'_'.$value->getStartNodeId().'_'.$value->getEndNodeId(); + + $startNodeElementId = self::$relationshipElementIdMap[$relationshipKey]['startNodeElementId'] ?? (string) $value->getStartNodeId(); + $endNodeElementId = self::$relationshipElementIdMap[$relationshipKey]['endNodeElementId'] ?? (string) $value->getEndNodeId(); + $tbr = new CypherRelationship( new CypherObject('CypherInt', $value->getId()), new CypherObject('CypherInt', $value->getStartNodeId()), @@ -133,68 +142,53 @@ public static function autoDetect($value): TestkitResponseInterface new CypherObject('CypherString', $value->getType()), new CypherObject('CypherMap', new CypherMap($props)), new CypherObject('CypherString', $elementId), - new CypherObject('CypherString', $startNodeElementId), - new CypherObject('CypherString', $endNodeElementId) + new CypherObject('CypherString', $startNodeElementId), // Use stored element ID + new CypherObject('CypherString', $endNodeElementId) // Use stored element ID ); break; case Path::class: $nodes = []; foreach ($value->getNodes() as $node) { - $nodeElementId = $node->getElementId(); - if ($nodeElementId === null) { - $nodeElementId = (string) $node->getId(); - } - - $nodes[] = new CypherNode( - new CypherObject('CypherInt', $node->getId()), - self::autoDetect($node->getLabels()), - self::autoDetect($node->getProperties()), - new CypherObject('CypherString', $nodeElementId) - ); + $nodes[] = self::autoDetect($node); } - $nodeList = $value->getNodes(); - $relationshipList = $value->getRelationships(); - $nodeCount = count($nodeList); - $rels = []; - foreach ($relationshipList as $i => $rel) { - if ($i + 1 >= $nodeCount) { - break; - } - - $startNode = $nodeList->get($i); - $endNode = $nodeList->get($i + 1); - - if ($startNode !== null && $endNode !== null) { - $startNodeId = $startNode->getId(); - $endNodeId = $endNode->getId(); - - $startNodeElementId = $startNode->getElementId(); - if ($startNodeElementId === null) { - $startNodeElementId = (string) $startNodeId; - } - - $endNodeElementId = $endNode->getElementId(); - if ($endNodeElementId === null) { - $endNodeElementId = (string) $endNodeId; + $nodesList = $value->getNodes(); + + foreach ($value->getRelationships() as $i => $rel) { + $relElementId = $rel->getElementId() ?? (string) $rel->getId(); + + if ($rel instanceof UnboundRelationship) { + if ($i < $nodesList->count() - 1) { + $startNode = $nodesList->get($i); + $endNode = $nodesList->get($i + 1); + + $startNodeElementId = $startNode->getElementId() ?? (string) $startNode->getId(); + $endNodeElementId = $endNode->getElementId() ?? (string) $endNode->getId(); + + $boundRel = new Relationship( + $rel->getId(), + $startNode->getId(), + $endNode->getId(), + $rel->getType(), + $rel->getProperties(), + $relElementId + ); + + $relationshipKey = $boundRel->getId().'_'.$boundRel->getStartNodeId().'_'.$boundRel->getEndNodeId(); + self::$relationshipElementIdMap[$relationshipKey] = [ + 'startNodeElementId' => $startNodeElementId, + 'endNodeElementId' => $endNodeElementId, + ]; + + error_log('DEBUG PATH: Stored mapping for key: '.$relationshipKey); + error_log('DEBUG PATH: Stored startNodeElementId: '.$startNodeElementId); + error_log('DEBUG PATH: Stored endNodeElementId: '.$endNodeElementId); + + $rels[] = self::autoDetect($boundRel); } - - $relElementId = $rel->getElementId(); - if ($relElementId === null) { - $relElementId = (string) $rel->getId(); - } - - $rels[] = new CypherRelationship( - new CypherObject('CypherInt', $rel->getId()), - new CypherObject('CypherInt', $startNodeId), - new CypherObject('CypherInt', $endNodeId), - new CypherObject('CypherString', $rel->getType()), - new CypherObject('CypherMap', new CypherMap($rel->getProperties())), - new CypherObject('CypherString', $relElementId), - new CypherObject('CypherString', $startNodeElementId), - new CypherObject('CypherString', $endNodeElementId) - ); + } else { + $rels[] = self::autoDetect($rel); } } @@ -203,25 +197,22 @@ public static function autoDetect($value): TestkitResponseInterface new CypherObject('CypherList', new CypherList($rels)) ); break; - case UnboundRelationship::class: $props = []; foreach ($value->getProperties() as $key => $property) { + /** @psalm-suppress MixedArgumentTypeCoercion */ $props[$key] = self::autoDetect($property); } - $elementId = $value->getElementId(); - if ($elementId === null) { - $elementId = (string) $value->getId(); - } + $tbr = new CypherRelationship( new CypherObject('CypherInt', $value->getId()), - new CypherObject('CypherInt', $value->getId()), - new CypherObject('CypherInt', $value->getId()), + new CypherObject('CypherNull', null), + new CypherObject('CypherNull', null), new CypherObject('CypherString', $value->getType()), new CypherObject('CypherMap', new CypherMap($props)), - new CypherObject('CypherString', $elementId), - new CypherObject('CypherString', $elementId), - new CypherObject('CypherString', $elementId) + new CypherObject('CypherString', $value->getElementId()), + new CypherObject('CypherNull', null), + new CypherObject('CypherNull', null) ); break; default: diff --git a/testkit-backend/src/Responses/Types/CypherRelationship.php b/testkit-backend/src/Responses/Types/CypherRelationship.php index 3f50fc77..8c2d4e6e 100644 --- a/testkit-backend/src/Responses/Types/CypherRelationship.php +++ b/testkit-backend/src/Responses/Types/CypherRelationship.php @@ -23,10 +23,12 @@ final class CypherRelationship implements TestkitResponseInterface private CypherObject $type; private CypherObject $props; private CypherObject $elementId; + private CypherObject $startNodeElementId; private CypherObject $endNodeElementId; - public function __construct(CypherObject $id, CypherObject $startNodeId, CypherObject $endNodeId, CypherObject $type, CypherObject $props, CypherObject $elementId, CypherObject $startNodeElementId, CypherObject $endNodeElementId) + public function __construct(CypherObject $id, CypherObject $startNodeId, CypherObject $endNodeId, CypherObject $type, CypherObject $props, CypherObject $elementId, CypherObject $startNodeElementId, + CypherObject $endNodeElementId) { $this->id = $id; $this->startNodeId = $startNodeId; diff --git a/testkit-backend/src/Socket.php b/testkit-backend/src/Socket.php index c39328b9..a01133a4 100644 --- a/testkit-backend/src/Socket.php +++ b/testkit-backend/src/Socket.php @@ -78,6 +78,7 @@ public static function fromAddressAndPort(string $address, int $port): self { $bind = 'tcp://'.$address.':'.$port; $streamSocketServer = stream_socket_server($bind, $errorNumber, $errorString); + stream_set_timeout($streamSocketServer, 60 * 60 * 24); if ($streamSocketServer === false) { throw new RuntimeException('stream_socket_server() failed: reason: '.$errorNumber.':'.$errorString); } diff --git a/testkit-backend/testkit.sh b/testkit-backend/testkit.sh index 5e317dfe..e5759257 100755 --- a/testkit-backend/testkit.sh +++ b/testkit-backend/testkit.sh @@ -5,10 +5,9 @@ TESTKIT_VERSION=5.0 [ -z "$TEST_NEO4J_HOST" ] && export TEST_NEO4J_HOST=neo4j [ -z "$TEST_NEO4J_USER" ] && export TEST_NEO4J_USER=neo4j [ -z "$TEST_NEO4J_PASS" ] && export TEST_NEO4J_PASS=testtest -[ -z "$TEST_NEO4J_VERSION" ] && export TEST_NEO4J_VERSION=5.23 +[ -z "$TEST_NEO4J_VERSION" ] && export TEST_NEO4J_VERSION=5.26 [ -z "$TEST_DRIVER_NAME" ] && export TEST_DRIVER_NAME=php -[ -z "$TEST_STUB_HOST" ] && export TEST_STUB_HOST=host.docker.internal - +[ -z "$TEST_DEBUG_NO_BACKEND_TIMEOUT" ] && export TEST_DEBUG_NO_BACKEND_TIMEOUT=1 [ -z "$TEST_DRIVER_REPO" ] && TEST_DRIVER_REPO=$(realpath ..) && export TEST_DRIVER_REPO @@ -23,9 +22,10 @@ if [ ! -d testkit ]; then if [ "$(cd testkit && git branch --show-current)" != "${TESTKIT_VERSION}" ]; then (cd testkit && git checkout ${TESTKIT_VERSION}) fi -else - (cd testkit && git pull) fi +#else +# (cd testkit && git pull) +#fi cd testkit || (echo 'cannot cd into testkit' && exit 1) python3 -m venv venv @@ -37,50 +37,120 @@ pip install -r requirements.txt echo "Starting tests..." EXIT_CODE=0 -#neo4j -#test_authentication -python3 -m unittest tests.neo4j.test_authentication.TestAuthenticationBasic || EXIT_CODE=1 - -#test_bookmarks -python3 -m unittest tests.neo4j.test_bookmarks.TestBookmarks || EXIT_CODE=1 +#python3 -m unittest tests.stub.tx_run.TestTxRun +##neo4j +#test_authentication +python3 -m unittest tests.neo4j.test_authentication.TestAuthenticationBasic|| EXIT_CODE=1 + +##test_bookmarks +python3 -m unittest tests.neo4j.test_bookmarks.TestBookmarks.test_can_obtain_bookmark_after_commit || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_bookmarks.TestBookmarks.test_can_pass_bookmark_into_next_session || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_bookmarks.TestBookmarks.test_no_bookmark_after_rollback || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_bookmarks.TestBookmarks.test_fails_on_invalid_bookmark || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_bookmarks.TestBookmarks.test_fails_on_invalid_bookmark_using_tx_func || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_bookmarks.TestBookmarks.test_can_handle_multiple_bookmarks || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_bookmarks.TestBookmarks.test_can_pass_write_bookmark_into_write_session || EXIT_CODE=1 #test_session_run -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_iteration_smaller_than_fetch_size -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_can_return_node -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_can_return_relationship -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_can_return_path -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_autocommit_transactions_should_support_metadata -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_regex_in_parameter -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_regex_inline -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_iteration_larger_than_fetch_size -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_partial_iteration -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_simple_query -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_session_reuse -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_iteration_nested -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_recover_from_invalid_query -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_recover_from_fail_on_streaming -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_updates_last_bookmark -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_fails_on_bad_syntax -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_fails_on_missing_parameter -python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_long_string - -#test_direct_driver +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_iteration_smaller_than_fetch_size || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_can_return_node || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_can_return_relationship || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_can_return_path || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_autocommit_transactions_should_support_metadata || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_regex_in_parameter || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_regex_inline || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_iteration_larger_than_fetch_size || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_partial_iteration || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_simple_query || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_session_reuse || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_iteration_nested || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_recover_from_invalid_query || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_updates_last_bookmark || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_fails_on_bad_syntax || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_fails_on_missing_parameter || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_session_run.TestSessionRun.test_long_string || EXIT_CODE=1 + + +###test_direct_driver python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver.test_custom_resolver|| EXIT_CODE=1 python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver.test_fail_nicely_when_using_http_port|| EXIT_CODE=1 python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver.test_supports_multi_db|| EXIT_CODE=1 -python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver.test_multi_db_non_existing -python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver.test_multi_db|| EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver.test_multi_db_non_existing || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver.test_multi_db || EXIT_CODE=1 python3 -m unittest tests.neo4j.test_direct_driver.TestDirectDriver.test_multi_db_various_databases|| EXIT_CODE=1 -#test_summary +##test_summary python3 -m unittest tests.neo4j.test_summary.TestSummary -####stub -####test-basic-query -python3 -m unittest tests.stub.basic_query.test_basic_query.TestBasicQuery.test_5x0_populates_path_element_ids_with_string -python3 -m unittest tests.stub.basic_query.test_basic_query.TestBasicQuery.test_4x4_populates_node_element_id_with_id -python3 -m unittest tests.stub.basic_query.test_basic_query.TestBasicQuery.test_5x0_populates_node_element_id_with_string -python3 -m unittest tests.stub.basic_query.test_basic_query.TestBasicQuery.test_4x4_populates_rel_element_id_with_id -python3 -m unittest tests.stub.basic_query.test_basic_query.TestBasicQuery.test_4x4_populates_path_element_ids_with_long +##test_tx_run +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_simple_query || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_can_commit_transaction || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_can_rollback_transaction || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_updates_last_bookmark_on_commit || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_does_not_update_last_bookmark_on_rollback || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_does_not_update_last_bookmark_on_failure || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_should_be_able_to_rollback_a_failure || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_should_not_commit_a_failure || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_should_not_rollback_a_rollbacked_tx || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_should_not_rollback_a_commited_tx || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_should_not_commit_a_commited_tx || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_should_not_allow_run_on_a_commited_tx || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_should_not_allow_run_on_a_rollbacked_tx || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_should_not_run_valid_query_in_invalid_tx || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_should_fail_run_in_a_commited_tx || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_should_fail_run_in_a_rollbacked_tx || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_should_fail_to_run_query_for_invalid_bookmark || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_broken_transaction_should_not_break_session || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_tx_configuration || EXIT_CODE=1 //fail +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_consume_after_commit || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_parallel_queries || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_run.TestTxRun.test_unconsumed_result || EXIT_CODE=1 + +##test_tx_func_run +python3 -m unittest tests.neo4j.test_tx_func_run.TestTxFuncRun.test_simple_query || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_func_run.TestTxFuncRun.test_parameter || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_func_run.TestTxFuncRun.test_meta_data || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_func_run.TestTxFuncRun.test_iteration_nested || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_func_run.TestTxFuncRun.test_updates_last_bookmark_on_commit || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_func_run.TestTxFuncRun.test_does_not_update_last_bookmark_on_rollback || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_func_run.TestTxFuncRun.test_client_exception_rolls_back_change || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_func_run.TestTxFuncRun.test_tx_func_configuration || EXIT_CODE=1 +python3 -m unittest tests.neo4j.test_tx_func_run.TestTxFuncRun.test_tx_timeout || EXIT_CODE=1 + +##stub +##test-basic-query +python3 -m unittest tests.stub.basic_query.test_basic_query.TestBasicQuery.test_5x0_populates_path_element_ids_with_string || EXIT_CODE=1 +python3 -m unittest tests.stub.basic_query.test_basic_query.TestBasicQuery.test_4x4_populates_node_element_id_with_id || EXIT_CODE=1 +python3 -m unittest tests.stub.basic_query.test_basic_query.TestBasicQuery.test_5x0_populates_node_element_id_with_string || EXIT_CODE=1 +python3 -m unittest tests.stub.basic_query.test_basic_query.TestBasicQuery.test_4x4_populates_rel_element_id_with_id || EXIT_CODE=1 +python3 -m unittest tests.stub.basic_query.test_basic_query.TestBasicQuery.test_4x4_populates_path_element_ids_with_long || EXIT_CODE=1 + +##test-session-run +python3 -m unittest tests.stub.session_run.test_session_run.TestSessionRun.test_discard_on_session_close_untouched_result || EXIT_CODE=1 +python3 -m unittest tests.stub.session_run.test_session_run.TestSessionRun.test_discard_on_session_close_unfinished_result || EXIT_CODE=1 +python3 -m unittest tests.stub.session_run.test_session_run.TestSessionRun.test_no_discard_on_session_close_finished_result || EXIT_CODE=1 +python3 -m unittest tests.stub.session_run.test_session_run.TestSessionRun.test_raises_error_on_session_run || EXIT_CODE=1 + +##TestBookmarksV5 +python3 -m unittest tests.stub.bookmarks.test_bookmarks_v5.TestBookmarksV5.test_bookmarks_can_be_set || EXIT_CODE=1 +python3 -m unittest tests.stub.bookmarks.test_bookmarks_v5.TestBookmarksV5.test_last_bookmark || EXIT_CODE=1 +python3 -m unittest tests.stub.bookmarks.test_bookmarks_v5.TestBookmarksV5.test_send_and_receive_bookmarks_write_tx || EXIT_CODE=1 +python3 -m unittest tests.stub.bookmarks.test_bookmarks_v5.TestBookmarksV5.test_sequence_of_writing_and_reading_tx || EXIT_CODE=1 +python3 -m unittest tests.stub.bookmarks.test_bookmarks_v5.TestBookmarksV5.test_send_and_receive_multiple_bookmarks_write_tx || EXIT_CODE=1 + +##TestBookmarksV4 +python3 -m unittest tests.stub.bookmarks.test_bookmarks_v4.TestBookmarksV4.test_bookmarks_on_unused_sessions_are_returned || EXIT_CODE=1 +python3 -m unittest tests.stub.bookmarks.test_bookmarks_v4.TestBookmarksV4.test_bookmarks_session_run || EXIT_CODE=1 +python3 -m unittest tests.stub.bookmarks.test_bookmarks_v4.TestBookmarksV4.test_sequence_of_writing_and_reading_tx || EXIT_CODE=1 +python3 -m unittest tests.stub.bookmarks.test_bookmarks_v4.TestBookmarksV4.test_bookmarks_tx_run || EXIT_CODE=1 + +#connectivity_check +##get_server_info +python3 -m unittest tests.stub.connectivity_check.test_get_server_info.TestGetServerInfo.test_direct_no_server +python3 -m unittest tests.stub.connectivity_check.test_get_server_info.TestGetServerInfo.test_direct_raises_error +python3 -m unittest tests.stub.connectivity_check.test_get_server_info.TestGetServerInfo.test_direct +python3 -m unittest tests.stub.connectivity_check.test_get_server_info.TestGetServerInfo.test_routing_no_server + exit $EXIT_CODE + diff --git a/tests/Integration/ComplexQueryTest.php b/tests/Integration/ComplexQueryTest.php index 2b8485b0..7a526af6 100644 --- a/tests/Integration/ComplexQueryTest.php +++ b/tests/Integration/ComplexQueryTest.php @@ -13,6 +13,8 @@ namespace Laudis\Neo4j\Tests\Integration; +use Bolt\error\ConnectionTimeoutException; +use Exception; use Generator; use function getenv; @@ -75,7 +77,7 @@ public function testArrayParameterHelper(): void { $this->expectNotToPerformAssertions(); $this->getSession()->transaction(static fn (TSX $tsx) => $tsx->run(<<<'CYPHER' -MERGE (x:Node {slug: 'a'}) +MERGE (x:Node {slug: 'a'}) WITH x MATCH (x) WHERE x.slug IN $listOrMap RETURN x CYPHER, ['listOrMap' => []])); @@ -266,13 +268,32 @@ public function testDiscardAfterTimeout(): void $this->markTestSkipped('Memory bug in CI'); } + // First, let's debug what timeout value is actually being sent + $config = TransactionConfiguration::default()->withTimeout(2); + echo 'Config timeout: '.$config->getTimeout()." seconds\n"; + try { - $this->getSession(['bolt', 'neo4j']) - ->run('CALL apoc.util.sleep(2000000) RETURN 5 as x', [], TransactionConfiguration::default()->withTimeout(2)) - ->first() - ->get('x'); + $result = $this->getSession(['bolt', 'neo4j']) + ->run('CALL apoc.util.sleep(5000) RETURN 5 as x', [], $config); + + echo "Query started, attempting to get first result...\n"; + $firstResult = $result->first(); + echo "Got first result, attempting to get 'x' value...\n"; + $value = $firstResult->get('x'); + echo 'Successfully got value: '.$value."\n"; + + // If we reach here, no timeout occurred + $this->fail('Query completed successfully - no timeout occurred. This suggests the timeout is not being applied correctly.'); } catch (Neo4jException $e) { + echo 'Neo4jException caught: '.$e->getMessage()."\n"; + echo 'Neo4j Code: '.$e->getNeo4jCode()."\n"; self::assertStringContainsString('Neo.ClientError.Transaction.TransactionTimedOut', $e->getNeo4jCode()); + } catch (ConnectionTimeoutException $e) { + echo 'ConnectionTimeoutException: '.$e->getMessage()."\n"; + $this->fail('Connection timeout occurred instead of transaction timeout'); + } catch (Exception $e) { + echo 'Other exception: '.get_class($e).' - '.$e->getMessage()."\n"; + throw $e; // Re-throw for debugging } } diff --git a/tests/Integration/SummarizedResultFormatterTest.php b/tests/Integration/SummarizedResultFormatterTest.php index 1d83014a..0bc86131 100644 --- a/tests/Integration/SummarizedResultFormatterTest.php +++ b/tests/Integration/SummarizedResultFormatterTest.php @@ -24,8 +24,6 @@ use Laudis\Neo4j\Contracts\TransactionInterface; use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\SummaryCounters; -use Laudis\Neo4j\Formatter\Specialised\BoltOGMTranslator; -use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\Tests\EnvironmentAwareIntegrationTest; use Laudis\Neo4j\Types\CartesianPoint; use Laudis\Neo4j\Types\CypherList; @@ -523,98 +521,4 @@ private function articlesQuery(): string article.readingTime = duration(articleProperties.readingTime) CYPHER; } - - public function testFormatBoltStatsWithFalseSystemUpdates(): void - { - $formatter = new SummarizedResultFormatter(new BoltOGMTranslator()); - - $response = [ - 'stats' => [ - 'nodes-created' => 1, - 'nodes-deleted' => 0, - 'relationships-created' => 0, - 'relationships-deleted' => 0, - 'properties-set' => 2, - 'labels-added' => 1, - 'labels-removed' => 0, - 'indexes-added' => 0, - 'indexes-removed' => 0, - 'constraints-added' => 0, - 'constraints-removed' => 0, - 'contains-updates' => true, - 'contains-system-updates' => false, - 'system-updates' => false, - ], - ]; - - $counters = $formatter->formatBoltStats($response); - - self::assertInstanceOf(SummaryCounters::class, $counters); - self::assertEquals(1, $counters->nodesCreated()); - self::assertEquals(2, $counters->propertiesSet()); - self::assertSame(0, $counters->systemUpdates()); - } - - public function testSystemUpdatesWithPotentialFalseValues(): void - { - $this->getSession()->run('CREATE INDEX duplicate_test_index IF NOT EXISTS FOR (n:TestSystemUpdates) ON (n.duplicateProperty)'); - $result = $this->getSession()->run('CREATE INDEX duplicate_test_index IF NOT EXISTS FOR (n:TestSystemUpdates) ON (n.duplicateProperty)'); - - $summary = $result->getSummary(); - $counters = $summary->getCounters(); - - // For duplicate index creation (IF NOT EXISTS), might not create system updates - $this->assertGreaterThanOrEqual(0, $counters->systemUpdates()); - // containsSystemUpdates should be consistent with systemUpdates count - $this->assertEquals($counters->systemUpdates() > 0, $counters->containsSystemUpdates()); - - $result2 = $this->getSession()->run('DROP INDEX non_existent_test_index IF EXISTS'); - - $summary2 = $result2->getSummary(); - $counters2 = $summary2->getCounters(); - - // Dropping non-existent index should not create system updates - $this->assertEquals(0, $counters2->systemUpdates()); - $this->assertFalse($counters2->containsSystemUpdates()); - - $this->getSession()->run('DROP INDEX duplicate_test_index IF EXISTS'); - } - - public function testMultipleSystemOperationsForBug(): void - { - $operations = [ - 'CREATE INDEX multi_test_1 IF NOT EXISTS FOR (n:MultiTestNode) ON (n.prop1)', - 'CREATE INDEX multi_test_2 IF NOT EXISTS FOR (n:MultiTestNode) ON (n.prop2)', - 'CREATE CONSTRAINT multi_test_constraint IF NOT EXISTS FOR (n:MultiTestNode) REQUIRE n.id IS UNIQUE', - 'DROP INDEX multi_test_1 IF EXISTS', - 'DROP INDEX multi_test_2 IF EXISTS', - 'DROP CONSTRAINT multi_test_constraint IF EXISTS', - ]; - - foreach ($operations as $operation) { - $result = $this->getSession()->run($operation); - - $summary = $result->getSummary(); - $counters = $summary->getCounters(); - - // Test that system operations properly track system updates - $this->assertGreaterThanOrEqual(0, $counters->systemUpdates()); - // Verify consistency between systemUpdates count and containsSystemUpdates flag - $this->assertEquals($counters->systemUpdates() > 0, $counters->containsSystemUpdates()); - } - } - - public function testRegularDataOperationsStillWork(): void - { - $result = $this->getSession()->run('CREATE (n:RegularTestNode {name: "test", id: $id}) RETURN n', ['id' => bin2hex(random_bytes(8))]); - - $summary = $result->getSummary(); - $counters = $summary->getCounters(); - - // Regular data operations should not involve system updates - $this->assertEquals(0, $counters->systemUpdates()); - $this->assertFalse($counters->containsSystemUpdates()); - - $this->getSession()->run('MATCH (n:RegularTestNode) DELETE n'); - } } diff --git a/tests/Integration/TransactionIntegrationTest.php b/tests/Integration/TransactionIntegrationTest.php index 4d8707cc..d8c84bbf 100644 --- a/tests/Integration/TransactionIntegrationTest.php +++ b/tests/Integration/TransactionIntegrationTest.php @@ -14,8 +14,8 @@ namespace Laudis\Neo4j\Tests\Integration; use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Exception\ClientException; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Exception\TransactionException; use Laudis\Neo4j\Tests\EnvironmentAwareIntegrationTest; use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; @@ -227,15 +227,11 @@ public function testCommitInvalid(): void $exception = null; try { $tsx->commit(); - } catch (ClientException|Neo4jException $e) { + } catch (TransactionException|Neo4jException $e) { $exception = $e; } - if (str_starts_with($_ENV['CONNECTION'] ?? '', 'http')) { - self::assertTrue($exception instanceof Neo4jException); - } else { - self::assertTrue($exception instanceof ClientException); - } + self::assertTrue($exception instanceof TransactionException); self::assertTrue($tsx->isFinished()); self::assertFalse($tsx->isRolledBack()); @@ -265,15 +261,11 @@ public function testRollbackInvalid(): void $exception = null; try { $tsx->rollback(); - } catch (ClientException|Neo4jException $e) { + } catch (TransactionException|Neo4jException $e) { $exception = $e; } - if (str_starts_with($_ENV['CONNECTION'] ?? '', 'http')) { - self::assertTrue($exception instanceof Neo4jException); - } else { - self::assertTrue($exception instanceof ClientException); - } + self::assertTrue($exception instanceof TransactionException); self::assertTrue($tsx->isFinished()); self::assertTrue($tsx->isRolledBack()); @@ -324,6 +316,17 @@ public function testTransactionRunNoConsumeResult(): void $tsx->commit(); } + public function testRunAfterCommit(): void + { + $tsx = $this->getSession()->beginTransaction([]); + $tsx->run('MATCH (x) RETURN x'); + $tsx->run('MATCH (x) RETURN x'); + $tsx->commit(); + + $this->expectException(TransactionException::class); + $tsx->run('MATCH (x) RETURN x'); + } + #[DoesNotPerformAssertions] public function testTransactionRunNoConsumeButSaveResult(): void { diff --git a/tests/Unit/BasicAuthTest.php b/tests/Unit/BasicAuthTest.php index 626a93a0..6a91b85b 100644 --- a/tests/Unit/BasicAuthTest.php +++ b/tests/Unit/BasicAuthTest.php @@ -13,13 +13,8 @@ namespace Laudis\Neo4j\Tests\Unit; -use Bolt\enum\Message; -use Bolt\enum\Signature; -use Bolt\protocol\Response; -use Bolt\protocol\V5; use Laudis\Neo4j\Authentication\BasicAuth; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Exception\Neo4jException; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\TestCase; use Psr\Http\Message\UriInterface; @@ -51,44 +46,6 @@ public function testToString(): void * @throws Exception * @throws \Exception */ - public function testAuthenticateBoltSuccess(): void - { - $userAgent = 'neo4j-client/1.0'; - - $protocol = $this->createMock(V5::class); - - $response = new Response( - Message::HELLO, - Signature::SUCCESS, - ['server' => 'neo4j-server', 'connection_id' => '12345', 'hints' => []] - ); - - $protocol->expects($this->once()) - ->method('getResponse') - ->willReturn($response); - - $result = $this->auth->authenticateBolt($protocol, $userAgent); - $this->assertArrayHasKey('server', $result); - $this->assertSame('neo4j-server', $result['server']); - $this->assertSame('12345', $result['connection_id']); - } - - public function testAuthenticateBoltFailure(): void - { - $this->expectException(Neo4jException::class); - - $protocol = $this->createMock(V5::class); - $response = new Response( - Message::HELLO, - Signature::FAILURE, - ['code' => 'Neo.ClientError.Security.Unauthorized', 'message' => 'Invalid credentials'] - ); - - $protocol->method('getResponse')->willReturn($response); - - $this->auth->authenticateBolt($protocol, 'neo4j-client/1.0'); - } - public function testEmptyCredentials(): void { $emptyAuth = new BasicAuth('', '', null); diff --git a/tests/Unit/BoltFactoryTest.php b/tests/Unit/BoltFactoryTest.php deleted file mode 100644 index 4794e3e0..00000000 --- a/tests/Unit/BoltFactoryTest.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Tests\Unit; - -use Bolt\connection\IConnection; -use Bolt\enum\ServerState; -use Bolt\protocol\V5; -use Laudis\Neo4j\Authentication\Authenticate; -use Laudis\Neo4j\Bolt\BoltConnection; -use Laudis\Neo4j\Bolt\Connection; -use Laudis\Neo4j\Bolt\ProtocolFactory; -use Laudis\Neo4j\Bolt\SslConfigurationFactory; -use Laudis\Neo4j\BoltFactory; -use Laudis\Neo4j\Common\Uri; -use Laudis\Neo4j\Contracts\BasicConnectionFactoryInterface; -use Laudis\Neo4j\Databags\ConnectionRequestData; -use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Databags\SslConfiguration; -use PHPUnit\Framework\TestCase; - -final class BoltFactoryTest extends TestCase -{ - private BoltFactory $factory; - - protected function setUp(): void - { - parent::setUp(); - $basicConnectionFactory = $this->createMock(BasicConnectionFactoryInterface::class); - $basicConnectionFactory->method('create') - ->willReturn(new Connection($this->createMock(IConnection::class), '')); - - $protocolFactory = $this->createMock(ProtocolFactory::class); - $protocolFactory->method('createProtocol') - ->willReturnCallback(static function (IConnection $connection) { - $protocol = new V5(1, $connection); - $protocol->serverState = ServerState::READY; - - return [ - $protocol, - ['server' => 'abc', 'connection_id' => 'i'], - ]; - }); - - $this->factory = new BoltFactory( - $basicConnectionFactory, - $protocolFactory, - new SslConfigurationFactory() - ); - } - - public function testCreateBasic(): void - { - $connection = $this->factory->createConnection( - new ConnectionRequestData('', Uri::create(''), Authenticate::disabled(), '', SslConfiguration::default()), - SessionConfiguration::default() - ); - - self::assertInstanceOf(BoltConnection::class, $connection); - self::assertEquals('', $connection->getEncryptionLevel()); - self::assertInstanceOf(V5::class, $connection->getImplementation()[0]); - self::assertInstanceOf(Connection::class, - $connection->getImplementation()[1]); - } -} diff --git a/tests/Unit/ConnectionPoolTest.php b/tests/Unit/ConnectionPoolTest.php deleted file mode 100644 index 4e39e048..00000000 --- a/tests/Unit/ConnectionPoolTest.php +++ /dev/null @@ -1,146 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Tests\Unit; - -use Bolt\error\ConnectionTimeoutException; -use Laudis\Neo4j\Bolt\BoltConnection; -use Laudis\Neo4j\Bolt\ConnectionPool; -use Laudis\Neo4j\BoltFactory; -use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Contracts\SemaphoreInterface; -use Laudis\Neo4j\Databags\ConnectionRequestData; -use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Databags\SslConfiguration; -use Laudis\Neo4j\Enum\SslMode; -use Laudis\Neo4j\Exception\TimeoutException; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Psr\Http\Message\UriInterface; -use ReflectionClass; - -class ConnectionPoolTest extends TestCase -{ - private MockObject&SemaphoreInterface $semaphore; - private MockObject&BoltFactory $factory; - private ConnectionRequestData $requestData; - private MockObject&Neo4jLogger $logger; - private SessionConfiguration $sessionConfig; - private MockObject&UriInterface $uri; - private MockObject&AuthenticateInterface $auth; - - protected function setUp(): void - { - $this->semaphore = $this->createMock(SemaphoreInterface::class); - $this->factory = $this->createMock(BoltFactory::class); - $this->sessionConfig = SessionConfiguration::default(); - $this->logger = $this->createMock(Neo4jLogger::class); - $this->uri = $this->createMock(UriInterface::class); - $this->auth = $this->createMock(AuthenticateInterface::class); - - $this->uri->method('getHost')->willReturn('localhost'); - - $this->requestData = new ConnectionRequestData( - 'localhost', - $this->uri, - $this->auth, - 'test-user-agent', - new SslConfiguration(SslMode::DISABLE(), false) - ); - } - - public function testTimeoutExceptionIsThrown(): void - { - $this->expectException(TimeoutException::class); - $this->expectExceptionMessage('Connection timed out'); - - $this->semaphore->method('wait')->willReturn((function () { - yield 0.1; - })()); - - $this->factory->method('createConnection') - ->willThrowException(new ConnectionTimeoutException('Connection timed out')); - - $pool = new ConnectionPool( - $this->semaphore, - $this->factory, - $this->requestData, - $this->logger, - 0.5 - ); - - $generator = $pool->acquire($this->sessionConfig); - - try { - while ($generator->valid()) { - $generator->send(true); - } - $generator->getReturn(); - } catch (ConnectionTimeoutException $e) { - // Wrap Bolt exception into driver-specific TimeoutException - throw new TimeoutException($e->getMessage(), 0, $e); - } - } - - public function testReuseConnectionIfPossibleReturnsReusableConnection(): void - { - $connection = $this->createMock(BoltConnection::class); - $connection->method('getServerState')->willReturn('READY'); - $this->factory->method('canReuseConnection')->willReturn(true); - $this->factory->method('reuseConnection')->willReturn($connection); - - // Use real ConnectionPool instance without mocking isConnectionExpired - $pool = new ConnectionPool( - $this->semaphore, - $this->factory, - $this->requestData, - $this->logger, - 1.0 - ); - - $reflection = new ReflectionClass(ConnectionPool::class); - $property = $reflection->getProperty('activeConnections'); - $property->setValue($pool, [$connection]); - - $method = $reflection->getMethod('reuseConnectionIfPossible'); - $result = $method->invoke($pool, $this->sessionConfig); - - $this->assertSame($connection, $result); - } - - public function testReuseConnectionIfPossibleReturnsNullWhenNoReusableConnectionFound(): void - { - $connection = $this->createMock(BoltConnection::class); - $connection->method('getServerState')->willReturn('READY'); - $this->factory->method('canReuseConnection')->willReturn(false); - - // Use real ConnectionPool instance without mocking isConnectionExpired - $pool = new ConnectionPool( - $this->semaphore, - $this->factory, - $this->requestData, - $this->logger, - 1.0 - ); - - $reflection = new ReflectionClass(ConnectionPool::class); - $property = $reflection->getProperty('activeConnections'); - $property->setValue($pool, [$connection]); - - $method = $reflection->getMethod('reuseConnectionIfPossible'); - $result = $method->invoke($pool, $this->sessionConfig); - - $this->assertNull($result); - } -} diff --git a/tests/Unit/KerberosAuthTest.php b/tests/Unit/KerberosAuthTest.php index d60a359e..586efebd 100644 --- a/tests/Unit/KerberosAuthTest.php +++ b/tests/Unit/KerberosAuthTest.php @@ -13,16 +13,9 @@ namespace Laudis\Neo4j\Tests\Unit; -use Bolt\enum\Message; -use Bolt\enum\Signature; -use Bolt\protocol\Response; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; use Laudis\Neo4j\Authentication\KerberosAuth; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Exception\Neo4jException; use PHPUnit\Framework\TestCase; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; class KerberosAuthTest extends TestCase @@ -35,55 +28,6 @@ protected function setUp(): void $this->auth = new KerberosAuth('test-token', $logger); } - public function testAuthenticateHttpSuccess(): void - { - $request = $this->createMock(RequestInterface::class); - $request->expects($this->exactly(2)) - ->method('withHeader') - ->willReturnSelf(); - - $uri = $this->createMock(UriInterface::class); - $uri->method('getHost')->willReturn('localhost'); - $uri->method('getPort')->willReturn(7687); - - $auth = new KerberosAuth('test-token', null); - $result = $auth->authenticateHttp($request, $uri, 'neo4j-client/1.0'); - - $this->assertSame($request, $result); - } - - public function testAuthenticateBoltFailureV5(): void - { - $this->expectException(Neo4jException::class); - - $protocol = $this->createMock(V5::class); - $response = new Response( - Message::HELLO, - Signature::FAILURE, - ['code' => 'Neo.ClientError.Security.Unauthorized', 'message' => 'Invalid credentials'] - ); - - $protocol->method('getResponse')->willReturn($response); - - $this->auth->authenticateBolt($protocol, 'neo4j-client/1.0'); - } - - public function testAuthenticateBoltFailureV4(): void - { - $this->expectException(Neo4jException::class); - - $protocol = $this->createMock(V4_4::class); - $response = new Response( - Message::HELLO, - Signature::FAILURE, - ['code' => 'Neo.ClientError.Security.Unauthorized', 'message' => 'Invalid credentials'] - ); - - $protocol->method('getResponse')->willReturn($response); - - $this->auth->authenticateBolt($protocol, 'neo4j-client/1.0'); - } - public function testToString(): void { $uri = $this->createMock(UriInterface::class); diff --git a/tests/Unit/NoAuthTest.php b/tests/Unit/NoAuthTest.php index ddfc4274..bfb46bd6 100644 --- a/tests/Unit/NoAuthTest.php +++ b/tests/Unit/NoAuthTest.php @@ -13,16 +13,9 @@ namespace Laudis\Neo4j\Tests\Unit; -use Bolt\enum\Message; -use Bolt\enum\Signature; -use Bolt\protocol\Response; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; use Laudis\Neo4j\Authentication\NoAuth; use Laudis\Neo4j\Common\Neo4jLogger; -use Laudis\Neo4j\Exception\Neo4jException; use PHPUnit\Framework\TestCase; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; class NoAuthTest extends TestCase @@ -35,82 +28,6 @@ protected function setUp(): void $this->auth = new NoAuth($logger); } - public function testAuthenticateHttpSuccess(): void - { - $request = $this->createMock(RequestInterface::class); - $request->expects($this->once()) - ->method('withHeader') - ->with('User-Agent', 'neo4j-client/1.0') - ->willReturnSelf(); - - $uri = $this->createMock(UriInterface::class); - $uri->method('getHost')->willReturn('localhost'); - $uri->method('getPort')->willReturn(7687); - - $result = $this->auth->authenticateHttp($request, $uri, 'neo4j-client/1.0'); - $this->assertSame($request, $result); - } - - public function testAuthenticateBoltSuccessV5(): void - { - $userAgent = 'neo4j-client/1.0'; - - $protocol = $this->createMock(V5::class); - - $response = new Response( - Message::HELLO, - Signature::SUCCESS, - ['server' => 'neo4j-server', 'connection_id' => '12345', 'hints' => []] - ); - - $protocol->expects($this->once()) - ->method('getResponse') - ->willReturn($response); - - $result = $this->auth->authenticateBolt($protocol, $userAgent); - $this->assertArrayHasKey('server', $result); - $this->assertSame('neo4j-server', $result['server']); - $this->assertSame('12345', $result['connection_id']); - } - - public function testAuthenticateBoltFailureV5(): void - { - $this->expectException(Neo4jException::class); - - $protocol = $this->createMock(V5::class); - $response = new Response( - Message::HELLO, - Signature::FAILURE, - ['code' => 'Neo.ClientError.Security.Unauthorized', 'message' => 'Invalid credentials'] - ); - - $protocol->method('getResponse')->willReturn($response); - - $this->auth->authenticateBolt($protocol, 'neo4j-client/1.0'); - } - - public function testAuthenticateBoltSuccessV4(): void - { - $userAgent = 'neo4j-client/1.0'; - - $protocol = $this->createMock(V4_4::class); - - $response = new Response( - Message::HELLO, - Signature::SUCCESS, - ['server' => 'neo4j-server', 'connection_id' => '12345', 'hints' => []] - ); - - $protocol->expects($this->once()) - ->method('getResponse') - ->willReturn($response); - - $result = $this->auth->authenticateBolt($protocol, $userAgent); - $this->assertArrayHasKey('server', $result); - $this->assertSame('neo4j-server', $result['server']); - $this->assertSame('12345', $result['connection_id']); - } - public function testToString(): void { $uri = $this->createMock(UriInterface::class);