fix(redis): keep user wildcards in fuzzy key search

Refs: #9012
This commit is contained in:
Tara authored and GitHub committed 2026-09-16 20:41:50 +08:00
1 parent 5b60c2efcd
commit 9cb6b28a0f
2 files changed
+66 -3

No files matched your search

+55 -1
View File
@@ -1,5 +1,59 @@
import { describe, expect, it } from "vitest";
import { createRedisKeyPatternMatcher, redisGroupSubtreePattern, redisKeyMatchesPattern } from "@/lib/redis/redisKeyPattern";
import { createRedisKeyPatternMatcher, escapeRedisGlobText, redisGroupSubtreePattern, redisKeyMatchesPattern, redisKeySearchPattern } from "@/lib/redis/redisKeyPattern";
describe("redisKeySearchPattern", () => {
it("returns `*` for empty input regardless of fuzzy", () => {
expect(redisKeySearchPattern("", false)).toBe("*");
expect(redisKeySearchPattern(" ", true)).toBe("*");
});
it("non-fuzzy (pattern) mode returns the trimmed value verbatim", () => {
expect(redisKeySearchPattern("user:*", false)).toBe("user:*");
expect(redisKeySearchPattern(" prod:login_fail_count:* ", false)).toBe("prod:login_fail_count:*");
});
it("fuzzy mode wraps plain text as `*text*` for substring-contains matching", () => {
expect(redisKeySearchPattern("admin", true)).toBe("*admin*");
expect(redisKeySearchPattern("login_fail_count", true)).toBe("*login_fail_count*");
});
it("fuzzy mode keeps user `*` as a wildcard so explicit globs still match (#9012)", () => {
// `prod:login_fail_count:*` must NOT escape the `*` to `\*`; the key
// prod:login_fail_count:admin:2026-09-14 must be findable.
expect(redisKeySearchPattern("prod:login_fail_count:*", true)).toBe("*prod:login_fail_count:**");
expect(redisKeySearchPattern("prod:login_fail_count:admin:2026*", true)).toBe("*prod:login_fail_count:admin:2026**");
// A bare `2026*` becomes `*2026**` (substring contains, wildcard honoured).
expect(redisKeySearchPattern("2026*", true)).toBe("*2026**");
});
it("fuzzy mode keeps `?` as a single-char wildcard", () => {
expect(redisKeySearchPattern("user:?", true)).toBe("*user:?*");
});
it("fuzzy mode still escapes `[` / `]` / `\\` to avoid char-class / escape ambiguity", () => {
expect(redisKeySearchPattern("a[b", true)).toBe("*a\\[b*");
expect(redisKeySearchPattern("a]b", true)).toBe("*a\\]b*");
expect(redisKeySearchPattern("a\\b", true)).toBe("*a\\\\b*");
});
});
describe("escapeRedisGlobText", () => {
it("non-fuzzy (default) escapes the full glob metacharacter set", () => {
expect(escapeRedisGlobText("a*b")).toBe("a\\*b");
expect(escapeRedisGlobText("a?b")).toBe("a\\?b");
expect(escapeRedisGlobText("a[b")).toBe("a\\[b");
expect(escapeRedisGlobText("a]b")).toBe("a\\]b");
expect(escapeRedisGlobText("a\\b")).toBe("a\\\\b");
});
it("fuzzy escapes only `[` / `]` / `\\`, leaving `*` / `?` as wildcards", () => {
expect(escapeRedisGlobText("a*b", true)).toBe("a*b");
expect(escapeRedisGlobText("a?b", true)).toBe("a?b");
expect(escapeRedisGlobText("a[b", true)).toBe("a\\[b");
expect(escapeRedisGlobText("a]b", true)).toBe("a\\]b");
expect(escapeRedisGlobText("a\\b", true)).toBe("a\\\\b");
});
});
describe("redisGroupSubtreePattern", () => {
it("builds a prefix pattern for single and nested groups", () => {
+11 -2
View File
@@ -1,8 +1,17 @@
// Full glob metacharacter set — escaped when a value must match literally
// (group subtree patterns, non-fuzzy text).
const REDIS_GLOB_SPECIAL_CHARS = /[\\*?[\]]/g;
const REDIS_GLOB_SPECIAL_CHARS_FUZZY = /[\\*?[\]]/g;
// In fuzzy mode the user's `*` / `?` are intentional wildcards ("search keys
// containing this text"), so only `[` / `]` / `\` are escaped to avoid
// accidental character-class / escape ambiguity. The outer `*…*` wrap added
// by `redisKeySearchPattern` keeps the substring-contains contract, so inputs
// like `prod:*` or `2026*` match the same keys a SCAN with that glob would.
// (#9012: previously `*` was escaped too, so `prod:*` became a literal-`*`
// substring match and found nothing.)
const REDIS_GLOB_LITERAL_CHARS_FUZZY = /[\\[\]]/g;
export function escapeRedisGlobText(value: string, fuzzy = false): string {
return value.replace(fuzzy ? REDIS_GLOB_SPECIAL_CHARS_FUZZY : REDIS_GLOB_SPECIAL_CHARS, "\\$&");
return value.replace(fuzzy ? REDIS_GLOB_LITERAL_CHARS_FUZZY : REDIS_GLOB_SPECIAL_CHARS, "\\$&");
}
export function redisKeySearchPattern(value: string, fuzzy: boolean): string {