Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Fixed
- **Gym search failed with a MariaDB SQL syntax error** ([#260](https://github.com/PGAN-Dev/PoracleWeb.NET/issues/260)): the `LikeEscape` helper added in #232 used `\` as the LIKE-escape character, and `ScannerService.SearchGymsAsync` passed `\` to `EF.Functions.Like(name, pattern, "\\")`. MariaDB's default mode (`NO_BACKSLASH_ESCAPES=OFF`) treats `\` as a string-literal escape too, so any escaped backslash in the pattern (which `LikeEscape` itself produced for user-supplied backslashes) left an unbalanced quote and broke the query with `near ''\')`. Switched the escape character to `|` (added `LikeEscape.EscapeChar` constant) — it has no special meaning in MariaDB string literals so the LIKE pattern can no longer interact with quote escaping. Tests updated to match the new escape sequences.
- **Dependabot auto-merge workflow never fired on PRs**: `auto-merge-deps.yml` listed both `pull_request_target` and `push` as triggers, but in practice the workflow only ever ran for `push` events — every PR-event run for the last 100+ workflow runs was a `push` event, none were `pull_request_target`. Result: Dependabot PRs were never auto-approved (each one needed manual approval), and every push recorded a `failure` conclusion because the job's `if: github.event_name == 'pull_request_target'` gate skipped all steps. Removed the `push` trigger (matching `pr-labeler.yml`, which fires correctly with `pull_request_target` alone), dropped the job-level `if:`, and added a sentinel "Workflow ran" first step so non-Dependabot PRs record as success rather than zero-step failure. Follow-up to #231: that fix moved the gate to job level on the assumption GitHub would record skipped runs as success, but it records 0-job runs as failure regardless.
- **Frontend CI `npm ci` failures on Dependabot PRs**: CI used Node 22's bundled npm 10.9.7, which strictly requires nested `chokidar@4.0.3` / `readdirp@4.1.2` lockfile entries that `@angular-devkit/*` packages declare as optional peers. Dependabot regenerates `package-lock.json` with a newer npm that prunes those entries, producing lockfiles npm 10.9.7's `npm ci` rejected with `EUSAGE`. Pinned npm 11 in the `frontend` CI job so the install resolution matches what Dependabot produces. Affects PRs #248, #250, #256, #261, #262.

Expand Down
11 changes: 8 additions & 3 deletions Core/Pgan.PoracleWebNet.Core.Services/LikeEscape.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ namespace Pgan.PoracleWebNet.Core.Services;

public static class LikeEscape
{
// Use `|` instead of the more conventional `\` because MariaDB's default
// mode treats `\` as a string-literal escape too — a user-supplied `\` in
// the search term left an unbalanced quote and broke gym search (#260).
public const string EscapeChar = "|";

public static string Escape(string input) => input
.Replace("\\", "\\\\", StringComparison.Ordinal)
.Replace("%", "\\%", StringComparison.Ordinal)
.Replace("_", "\\_", StringComparison.Ordinal);
.Replace("|", "||", StringComparison.Ordinal)
.Replace("%", "|%", StringComparison.Ordinal)
.Replace("_", "|_", StringComparison.Ordinal);
}
2 changes: 1 addition & 1 deletion Core/Pgan.PoracleWebNet.Core.Services/ScannerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public async Task<IEnumerable<GymSearchResult>> SearchGymsAsync(string search, i

return await this._context.Gyms
.AsNoTracking()
.Where(g => g.Name != null && EF.Functions.Like(g.Name, pattern, "\\"))
.Where(g => g.Name != null && EF.Functions.Like(g.Name, pattern, LikeEscape.EscapeChar))
.OrderBy(g => g.Name)
.Take(safeLimit)
.Select(g => new GymSearchResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@
var sut = new ScannerController(this._logger.Object, service.Object);
SetupUser(sut);

var result = await sut.GetGymById(id);

Check warning on line 235 in Tests/Pgan.PoracleWebNet.Tests/Controllers/ScannerControllerTests.cs

View workflow job for this annotation

GitHub Actions / Backend (.NET)

Possible null reference argument for parameter 'id' in 'Task<IActionResult> ScannerController.GetGymById(string id)'.

Assert.IsType<NotFoundResult>(result);
service.Verify(s => s.GetGymByIdAsync(It.IsAny<string>()), Times.Never);
Expand Down Expand Up @@ -295,11 +295,12 @@

[Theory]
[InlineData("abc", "abc")]
[InlineData("100%", "100\\%")]
[InlineData("a_b", "a\\_b")]
[InlineData("back\\slash", "back\\\\slash")]
[InlineData("%_\\", "\\%\\_\\\\")]
public void EscapeLikePatternEscapesWildcardsAndBackslash(string input, string expected)
[InlineData("100%", "100|%")]
[InlineData("a_b", "a|_b")]
[InlineData("pipe|sep", "pipe||sep")]
[InlineData("%_|", "|%|_||")]
[InlineData("back\\slash", "back\\slash")] // backslash is no longer special
public void EscapeLikePatternEscapesWildcardsAndEscapeChar(string input, string expected)
{
var actual = Core.Services.LikeEscape.Escape(input);
Assert.Equal(expected, actual);
Expand Down
Loading