Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/Constructorio_NET.Tests/client/modules/AutocompleteTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,92 @@ public async Task GetAutocompleteResultsShouldReturnResultWithMultipleFilters()
Assert.GreaterOrEqual(res.Sections["Products"].Count, 1, "Results exist");
}

[Test]
public async Task GetAutocompleteResultsShouldReturnResultWithPreFilterExpression()
{
ValuePreFilterExpression filterByGroupId = new ValuePreFilterExpression("group_id", "All");
AutocompleteRequest req = new AutocompleteRequest("item")
{
UserInfo = UserInfo,
PreFilterExpression = filterByGroupId,
};
ConstructorIO constructorio = new ConstructorIO(this.Config);
AutocompleteResponse res = await constructorio.Autocomplete.GetAutocompleteResults(req);

Assert.NotNull(res.ResultId, "Result id exists");
res.Request.TryGetValue("pre_filter_expression", out object reqPreFilterExpression);
Assert.AreEqual(
JObject.Parse(filterByGroupId.GetExpression()),
reqPreFilterExpression,
"Pre Filter Expression is sent in request"
);
}

[Test]
public async Task GetAutocompleteResultsShouldReturnResultWithPreFilterExpressionPerSection()
{
ValuePreFilterExpression filterByGroupId = new ValuePreFilterExpression("group_id", "All");
AutocompleteRequest req = new AutocompleteRequest("item")
{
UserInfo = UserInfo,
PreFilterExpressionPerSection = new List<PreFilterExpressionPerSection>
{
new PreFilterExpressionPerSection("Products", filterByGroupId),
},
};
ConstructorIO constructorio = new ConstructorIO(this.Config);
AutocompleteResponse res = await constructorio.Autocomplete.GetAutocompleteResults(req);

Assert.NotNull(res.ResultId, "Result id exists");
res.Request.TryGetValue("pre_filter_expression", out object reqPreFilterExpression);
JObject parsedPreFilterExpression = (JObject)reqPreFilterExpression;
Assert.NotNull(parsedPreFilterExpression, "Pre filter expression exists in response");
Assert.AreEqual(
JObject.Parse(filterByGroupId.GetExpression()),
parsedPreFilterExpression["Products"],
"Per-section pre filter expression is sent in request"
);
}

[Test]
public async Task GetAutocompleteResultsShouldReturnResultWithMultiplePreFilterExpressionsPerSection()
{
ValuePreFilterExpression filterByGroupId = new ValuePreFilterExpression("group_id", "All");
AndPreFilterExpression filterByPriceAndBrand = new AndPreFilterExpression(
new List<PreFilterExpression>
{
new RangePreFilterExpression("price", new List<string> { "10", "100" }),
new ValuePreFilterExpression("brand", "Nike"),
}
);
AutocompleteRequest req = new AutocompleteRequest("item")
{
UserInfo = UserInfo,
PreFilterExpressionPerSection = new List<PreFilterExpressionPerSection>
{
new PreFilterExpressionPerSection("Products", filterByGroupId),
new PreFilterExpressionPerSection("Search Suggestions", filterByPriceAndBrand),
},
};
ConstructorIO constructorio = new ConstructorIO(this.Config);
AutocompleteResponse res = await constructorio.Autocomplete.GetAutocompleteResults(req);

Assert.NotNull(res.ResultId, "Result id exists");
res.Request.TryGetValue("pre_filter_expression", out object reqPreFilterExpression);
JObject parsedPreFilterExpression = (JObject)reqPreFilterExpression;
Assert.NotNull(parsedPreFilterExpression, "Pre filter expression exists in response");
Assert.AreEqual(
JObject.Parse(filterByGroupId.GetExpression()),
parsedPreFilterExpression["Products"],
"Value per-section pre filter expression is sent in request"
);
Assert.AreEqual(
JObject.Parse(filterByPriceAndBrand.GetExpression()),
parsedPreFilterExpression["Search Suggestions"],
"Composite per-section pre filter expression is sent in request"
);
}

[Test]
public async Task GetAutocompleteResultsShouldReturnResultWithFiltersPerSection()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,53 @@ public void GetRequestParameters()
Assert.AreEqual(this.FiltersPerSection, requestParameters[Constants.FILTERS_PER_SECTION]);
}

[Test]
public void GetRequestParametersWithPreFilterExpression()
{
ValuePreFilterExpression filterByBrand = new ValuePreFilterExpression("Brand", "XYZ");
AutocompleteRequest req = new AutocompleteRequest(this.Query)
{
PreFilterExpression = filterByBrand,
};

Hashtable requestParameters = req.GetRequestParameters();
Assert.IsNotNull(requestParameters[Constants.PRE_FILTER_EXPRESSION]);
Assert.IsTrue(requestParameters[Constants.PRE_FILTER_EXPRESSION].ToString().Contains("Brand"));
Assert.IsTrue(requestParameters[Constants.PRE_FILTER_EXPRESSION].ToString().Contains("XYZ"));
}

[Test]
public void GetRequestParametersWithPreFilterExpressionPerSection()
{
ValuePreFilterExpression filterProducts = new ValuePreFilterExpression("Brand", "XYZ");
ValuePreFilterExpression filterSuggestions = new ValuePreFilterExpression("group_id", "All");
AutocompleteRequest req = new AutocompleteRequest(this.Query)
{
PreFilterExpressionPerSection = new List<PreFilterExpressionPerSection>
{
new PreFilterExpressionPerSection("Products", filterProducts),
new PreFilterExpressionPerSection("Search Suggestions", filterSuggestions),
},
};

Hashtable requestParameters = req.GetRequestParameters();
List<PreFilterExpressionPerSection> perSection = (List<PreFilterExpressionPerSection>)requestParameters[Constants.PRE_FILTER_EXPRESSION_PER_SECTION];
Assert.IsNotNull(perSection);
Assert.AreEqual(2, perSection.Count);
Assert.AreEqual("Products", perSection[0].Section);
Assert.AreEqual("Search Suggestions", perSection[1].Section);
}

[Test]
public void GetRequestParametersWithoutPreFilterExpression()
{
AutocompleteRequest req = new AutocompleteRequest(this.Query);

Hashtable requestParameters = req.GetRequestParameters();
Assert.IsFalse(requestParameters.ContainsKey(Constants.PRE_FILTER_EXPRESSION));
Assert.IsFalse(requestParameters.ContainsKey(Constants.PRE_FILTER_EXPRESSION_PER_SECTION));
}

[Test]
public void GetRequestHeaders()
{
Expand Down
22 changes: 22 additions & 0 deletions src/Constructorio_NET.Tests/utils/HelpersTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,28 @@ public void MakeUrlSearchWithFiltersPerSection()
Assert.That(hasColorBlueFilter && hasColorGreenFilter, "url is properly formed and has all filters applied");
}

[Test]
public void MakeUrlAutocompleteWithPreFilterExpressionPerSection()
{
List<string> paths = new List<string> { "autocomplete", this.Query };
List<PreFilterExpressionPerSection> preFilterExpressionPerSection = new List<PreFilterExpressionPerSection>
{
new PreFilterExpressionPerSection("Products", new ValuePreFilterExpression("Brand", "XYZ")),
new PreFilterExpressionPerSection("Search Suggestions", new ValuePreFilterExpression("group_id", "All")),
};
Hashtable queryParams = new Hashtable()
{
{ Constants.PRE_FILTER_EXPRESSION_PER_SECTION, preFilterExpressionPerSection },
};

string url = MakeUrl(this.Options, paths, queryParams);
bool hasProductsKey = Regex.Match(url, "&pre_filter_expression%5BProducts%5D=").Success;
bool hasSuggestionsKey = Regex.Match(url, "&pre_filter_expression%5BSearch%20Suggestions%5D=").Success;
bool hasBrandExpression = url.Contains(OurEscapeDataString("XYZ"));
bool hasGroupExpression = url.Contains(OurEscapeDataString("All"));
Assert.That(hasProductsKey && hasSuggestionsKey && hasBrandExpression && hasGroupExpression, "url should have bracketed per-section pre_filter_expression");
Comment on lines +378 to +382

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It passes even if we emit the JSON unescaped
Also, there is one `Assert. ', it hides which part failed, maybe we can make a few asserts as the fmt_options test above?

Suggested change
bool hasProductsKey = Regex.Match(url, "&pre_filter_expression%5BProducts%5D=").Success;
bool hasSuggestionsKey = Regex.Match(url, "&pre_filter_expression%5BSearch%20Suggestions%5D=").Success;
bool hasBrandExpression = url.Contains(OurEscapeDataString("XYZ"));
bool hasGroupExpression = url.Contains(OurEscapeDataString("All"));
Assert.That(hasProductsKey && hasSuggestionsKey && hasBrandExpression && hasGroupExpression, "url should have bracketed per-section pre_filter_expression");
string productsParameter = "&pre_filter_expression%5BProducts%5D=" + OurEscapeDataString(filterProducts.GetExpression());
string suggestionsParameter = "&pre_filter_expression%5BSearch%20Suggestions%5D=" + OurEscapeDataString(filterSuggestions.GetExpression());
Assert.That(url, Does.Contain(productsParameter), "url should have the escaped Products expression");
Assert.That(url, Does.Contain(suggestionsParameter), "url should have the escaped Search Suggestions expression");
Assert.That(url, Does.Not.Contain("{"), "url should not contain unescaped json");
Assert.That(url, Does.Not.Contain("&pre_filter_expression="), "url should not have the non per-section form");

}

[Test]
public async Task TestCreateRequest()
{
Expand Down
21 changes: 21 additions & 0 deletions src/constructor.io/models/Autocomplete/AutocompleteRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ public class AutocompleteRequest : IFilterable, IUserDetails
/// </summary>
public Dictionary<string, Dictionary<string, List<string>>> FiltersPerSection { get; set; }

/// <summary>
/// Gets or sets the filter expression used to scope results across all sections.
/// </summary>
public PreFilterExpression PreFilterExpression { get; set; }

/// <summary>
/// Gets or sets per-section filter expressions used to scope results for specific sections.
/// Serialized as pre_filter_expression[Section]={...}.
/// </summary>
public List<PreFilterExpressionPerSection> PreFilterExpressionPerSection { get; set; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public List<PreFilterExpressionPerSection> PreFilterExpressionPerSection { get; set; }
public Dictionary<string, PreFilterExpression> PreFilterExpressionPerSection { get; set; }

List<> lets you pass the same section twice:

{
    "message": "pre_filter_expression: Products: Invalid JSON string"
}


/// <summary>
/// Gets or sets the format options used to refine result groups.
/// </summary>
Expand Down Expand Up @@ -105,6 +116,16 @@ public Hashtable GetRequestParameters()
parameters.Add(Constants.FILTERS_PER_SECTION, this.FiltersPerSection);
}

if (this.PreFilterExpression != null)
{
parameters.Add(Constants.PRE_FILTER_EXPRESSION, this.PreFilterExpression.GetExpression());
}

if (this.PreFilterExpressionPerSection != null && this.PreFilterExpressionPerSection.Count > 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we would set together pre_filter_expression= and pre_filter_expression[Section]=- it will throw

{
    "message": "Both dictionary and non dictionary value is supplied for pre_filter_expression key."
}

Can we throw here, or at least say "mutually exclusive" in the doc?

{
parameters.Add(Constants.PRE_FILTER_EXPRESSION_PER_SECTION, this.PreFilterExpressionPerSection);
}

if (this.TestCells != null)
{
parameters.Add(Constants.TEST_CELLS, this.TestCells);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Constructorio_NET.Models
{
/// <summary>
/// Associates a <see cref="PreFilterExpression"/> with a specific autocomplete section.
/// Serialized to the documented bracket-notation shape (e.g. pre_filter_expression[Products]={...}).
/// </summary>
public class PreFilterExpressionPerSection
{
/// <summary>
/// Gets or sets the section the expression applies to (e.g. "Products", "Search Suggestions").
/// </summary>
public string Section { get; set; }

/// <summary>
/// Gets or sets the pre-filter expression to scope results for the section.
/// </summary>
public PreFilterExpression Expression { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="PreFilterExpressionPerSection"/> class.
/// </summary>
/// <param name="section">Section the expression applies to.</param>
/// <param name="expression">Pre-filter expression for the section.</param>
public PreFilterExpressionPerSection(string section, PreFilterExpression expression)
{
this.Section = section;
this.Expression = expression;
}

/// <summary>
/// Initializes a new instance of the <see cref="PreFilterExpressionPerSection"/> class.
/// </summary>
public PreFilterExpressionPerSection()
{
}
}
}
1 change: 1 addition & 0 deletions src/constructor.io/utils/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public static class Constants
public const string QUIZ_VERSION_ID = "quiz_version_id";
public const string QUIZ_SESSION_ID = "quiz_session_id";
public const string PRE_FILTER_EXPRESSION = "pre_filter_expression";
public const string PRE_FILTER_EXPRESSION_PER_SECTION = "pre_filter_expression_per_section";
public const string QS_PARAM = "qs";
public const string ON_MISSING = "on_missing";
public const string FORMAT = "format";
Expand Down
19 changes: 19 additions & 0 deletions src/constructor.io/utils/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,25 @@ protected static string MakeUrl(Hashtable options, List<string> paths, Hashtable
}
}

if (queryParams.Contains(Constants.PRE_FILTER_EXPRESSION_PER_SECTION))
{
List<PreFilterExpressionPerSection> preFilterExpressionPerSection = (List<PreFilterExpressionPerSection>)queryParams[Constants.PRE_FILTER_EXPRESSION_PER_SECTION];
queryParams.Remove(Constants.PRE_FILTER_EXPRESSION_PER_SECTION);

foreach (var sectionExpression in preFilterExpressionPerSection)
{
if (sectionExpression?.Section == null || sectionExpression.Expression == null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Section == null lets "" and " " through, so we send pre_filter_expression[ ]=..., the API accepts it, but should we validate it?

{
continue;
}

url.Append("&" + Constants.PRE_FILTER_EXPRESSION + UrlEscapedStartSquareBracket)
.Append(OurEscapeDataString(sectionExpression.Section))
.Append(UrlEscapedEndSquareBracket + "=")
.Append(OurEscapeDataString(sectionExpression.Expression.GetExpression()));
}
}

// Add test cells to query string
if (queryParams.Contains(Constants.TEST_CELLS))
{
Expand Down
Loading