Skip to content

Commit 3a80214

Browse files
authored
Issue 458 WithParam: throw exception if parameter starts with '$' (or… (#460)
* Issue 458 WithParam: throw exception if parameter starts with '$' (or any other not allowed character). Issue 459 WithParam: exeption when using already existing param name outputs "key" instead of param name * Added WithParams test method to ThrowsExceptionForDuplicateManualKey * fixes usage of params in ArgumentExceptions * added output of multiple invalid/duplicate parameters with tests * missing space
1 parent 50c8e41 commit 3a80214

File tree

2 files changed

+73
-10
lines changed

2 files changed

+73
-10
lines changed

Neo4jClient.Tests/Cypher/CypherFluentQueryWithParamTests.cs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System;
2+
using FluentAssertions;
23
using Neo4jClient.Cypher;
34
using Newtonsoft.Json.Serialization;
45
using NSubstitute;
6+
using NSubstitute.ExceptionExtensions;
57
using Xunit;
68

79
namespace Neo4jClient.Tests.Cypher
@@ -53,14 +55,27 @@ public void ThrowsExceptionForDuplicateManualKey()
5355
var client = Substitute.For<IRawGraphClient>();
5456
var query = new CypherFluentQuery(client)
5557
.Match("n")
56-
.WithParam("foo", 123);
58+
.WithParam("foo", 123)
59+
.WithParam("bar", 123);
5760

5861
// Assert
5962
var ex = Assert.Throws<ArgumentException>(
6063
() => query.WithParam("foo", 456)
6164
);
6265
Assert.Equal("key", ex.ParamName);
63-
Assert.Equal("A parameter with the given key is already defined in the query." + Environment.NewLine + "Parameter name: key", ex.Message);
66+
Assert.Equal("A parameter with the given key 'foo' is already defined in the query." + Environment.NewLine + "Parameter name: key", ex.Message);
67+
68+
ex = Assert.Throws<ArgumentException>(
69+
() => query.WithParams( new { foo = 456 })
70+
);
71+
Assert.Equal("parameters", ex.ParamName);
72+
Assert.Equal("A parameter with the given key 'foo' is already defined in the query." + Environment.NewLine + "Parameter name: parameters", ex.Message);
73+
74+
ex = Assert.Throws<ArgumentException>(
75+
() => query.WithParams(new { foo = 456, bar = 456 })
76+
);
77+
Assert.Equal("parameters", ex.ParamName);
78+
Assert.Equal("Parameters with the given keys 'foo, bar' are already defined in the query." + Environment.NewLine + "Parameter name: parameters", ex.Message);
6479
}
6580

6681
[Fact]
@@ -77,7 +92,39 @@ public void ThrowsExceptionForDuplicateOfAutoKey()
7792
() => query.WithParam("p0", 456)
7893
);
7994
Assert.Equal("key", ex.ParamName);
80-
Assert.Equal("A parameter with the given key is already defined in the query." + Environment.NewLine + "Parameter name: key", ex.Message);
95+
Assert.Equal("A parameter with the given key 'p0' is already defined in the query." + Environment.NewLine + "Parameter name: key", ex.Message);
96+
}
97+
98+
[Fact]
99+
//(Description = https://github.com/DotNet4Neo4j/Neo4jClient/issues/458)
100+
public void ThrowsExceptionForInvalidParamName()
101+
{
102+
// Arrange
103+
var client = Substitute.For<IRawGraphClient>();
104+
105+
// Assert WithParam
106+
var ex = Assert.Throws<ArgumentException>(() => new CypherFluentQuery(client).WithParam("$uuid", ""));
107+
ex.Should().NotBeNull();
108+
ex.Message.Should().Be("The parameter with the given key '$uuid' is not valid. Parameters may consist of letters and numbers, and any combination of these, but cannot start with a number or a currency symbol.\r\nParameter name: key");
109+
110+
ex = Assert.Throws<ArgumentException>(() => new CypherFluentQuery(client).WithParam("0uuid", ""));
111+
ex.Should().NotBeNull();
112+
ex.Message.Should().Be("The parameter with the given key '0uuid' is not valid. Parameters may consist of letters and numbers, and any combination of these, but cannot start with a number or a currency symbol.\r\nParameter name: key");
113+
114+
ex = Assert.Throws<ArgumentException>(() => new CypherFluentQuery(client).WithParam("{uuid}", ""));
115+
ex.Should().NotBeNull();
116+
ex.Message.Should().Be("The parameter with the given key '{uuid}' is not valid. Parameters may consist of letters and numbers, and any combination of these, but cannot start with a number or a currency symbol.\r\nParameter name: key");
117+
118+
// no exception for correct usage
119+
var ex2 = Record.Exception(() => new CypherFluentQuery(client).WithParam("uuid", ""));
120+
Assert.Null(ex2);
121+
122+
// Assert WithParams
123+
//ex = Assert.Throws<ArgumentException>(() => new CypherFluentQuery(client).WithParams(new { $uuid = "" })); // this will not compile anyways
124+
125+
// no exception for correct usage
126+
ex2 = Record.Exception(() => new CypherFluentQuery(client).WithParams(new { uuid = "" }));
127+
Assert.Null(ex2);
81128
}
82129

83130
public class ComplexObjForWithParamTest

Neo4jClient/Cypher/CypherFluentQuery.cs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,17 +148,33 @@ protected ICypherFluentQuery<TResult> Mutate<TResult>(Action<QueryWriter> callba
148148

149149
public ICypherFluentQuery WithParam(string key, object value)
150150
{
151+
if (string.IsNullOrWhiteSpace(key) || char.IsDigit(key[0]) || char.IsSymbol(key[0]) || key[0] == '{')
152+
throw new ArgumentException($"The parameter with the given key '{key}' is not valid. Parameters may consist of letters and numbers, and any combination of these, but cannot start with a number or a currency symbol.", nameof(key));
153+
151154
if (QueryWriter.ContainsParameterWithKey(key))
152-
throw new ArgumentException("A parameter with the given key is already defined in the query.", nameof(key));
155+
throw new ArgumentException($"A parameter with the given key '{key}' is already defined in the query.", nameof(key));
156+
153157
return Mutate(w => w.CreateParameter(key, value));
154158
}
155159

156160
public ICypherFluentQuery WithParams(IDictionary<string, object> parameters)
157161
{
158162
if (parameters == null || parameters.Count == 0) return this;
159163

160-
if (parameters.Keys.Any(key => QueryWriter.ContainsParameterWithKey(key)))
161-
throw new ArgumentException("A parameter with the given key is already defined in the query.", nameof(parameters));
164+
var invalidParameters = parameters.Keys.Where(key => string.IsNullOrWhiteSpace(key) || char.IsDigit(key[0]) || char.IsSymbol(key[0]) || key[0] == '{').ToList();
165+
166+
if (invalidParameters.Any())
167+
throw new ArgumentException((invalidParameters.Count == 1
168+
? $"The parameter with the given key '{invalidParameters.First()}' is not valid."
169+
: $"The parameters with the given keys '{string.Join(", ", invalidParameters)}' are not valid.") +
170+
" Parameters may consist of letters and numbers, and any combination of these, but cannot start with a number or a currency symbol.", nameof(parameters));
171+
172+
var duplicateParameters = parameters.Keys.Where(key => QueryWriter.ContainsParameterWithKey(key)).ToList();
173+
174+
if (duplicateParameters.Any())
175+
throw new ArgumentException(duplicateParameters.Count() == 1
176+
? $"A parameter with the given key '{duplicateParameters.First()}' is already defined in the query."
177+
: $"Parameters with the given keys '{string.Join(", ", duplicateParameters)}' are already defined in the query.", nameof(parameters));
162178

163179
return Mutate(w => w.CreateParameters(parameters));
164180
}
@@ -226,7 +242,7 @@ public ICypherFluentQuery Call(string storedProcedureText)
226242
if (!Client.CypherCapabilities.SupportsStoredProcedures)
227243
throw new InvalidOperationException("CALL not supported in Neo4j versions older than 3.0");
228244

229-
if(string.IsNullOrWhiteSpace(storedProcedureText))
245+
if(string.IsNullOrWhiteSpace(storedProcedureText))
230246
throw new ArgumentException("The stored procedure to call can't be null or whitespace.", nameof(storedProcedureText));
231247

232248
return Mutate(w =>
@@ -388,18 +404,18 @@ public ICypherFluentQuery LoadCsv(Uri fileUri, string identifier, bool withHeade
388404
}
389405

390406
string withHeadersEnabledText = string.Empty;
391-
string fieldSeperatorEnabledText = string.Empty;
407+
string fieldSeparatorEnabledText = string.Empty;
392408
if (withHeaders)
393409
{
394410
withHeadersEnabledText = " WITH HEADERS";
395411
}
396412

397413
if (!string.IsNullOrEmpty(fieldTerminator))
398414
{
399-
fieldSeperatorEnabledText = $" FIELDTERMINATOR '{fieldTerminator}'";
415+
fieldSeparatorEnabledText = $" FIELDTERMINATOR '{fieldTerminator}'";
400416
}
401417

402-
return Mutate(w => w.AppendClause($"{periodicCommitText} LOAD CSV{withHeadersEnabledText} FROM '{fileUri.AbsoluteUri}' AS {identifier}{fieldSeperatorEnabledText}".Trim()));
418+
return Mutate(w => w.AppendClause($"{periodicCommitText} LOAD CSV{withHeadersEnabledText} FROM '{fileUri.AbsoluteUri}' AS {identifier}{fieldSeparatorEnabledText}".Trim()));
403419
}
404420

405421
public ICypherFluentQuery Unwind(string collectionName, string columnName)

0 commit comments

Comments
 (0)