refactor: share teams allowlist matching helpers
Co-authored-by: thewilloftheshadow <thewilloftheshadow@users.noreply.github.com>
This commit is contained in:
@@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
buildChannelKeyCandidates,
|
||||
normalizeChannelSlug,
|
||||
resolveChannelEntryMatch,
|
||||
resolveChannelEntryMatchWithFallback,
|
||||
resolveNestedAllowlistDecision,
|
||||
} from "./channel-config.js";
|
||||
|
||||
describe("buildChannelKeyCandidates", () => {
|
||||
@@ -12,6 +14,14 @@ describe("buildChannelKeyCandidates", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeChannelSlug", () => {
|
||||
it("normalizes names into slugs", () => {
|
||||
expect(normalizeChannelSlug("My Team")).toBe("my-team");
|
||||
expect(normalizeChannelSlug("#General Chat")).toBe("general-chat");
|
||||
expect(normalizeChannelSlug(" Dev__Chat ")).toBe("dev-chat");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveChannelEntryMatch", () => {
|
||||
it("returns matched entry and wildcard metadata", () => {
|
||||
const entries = { a: { allow: true }, "*": { allow: false } };
|
||||
@@ -66,4 +76,59 @@ describe("resolveChannelEntryMatchWithFallback", () => {
|
||||
expect(match.matchSource).toBe("wildcard");
|
||||
expect(match.matchKey).toBe("*");
|
||||
});
|
||||
|
||||
it("matches normalized keys when normalizeKey is provided", () => {
|
||||
const entries = { "My Team": { allow: true } };
|
||||
const match = resolveChannelEntryMatchWithFallback({
|
||||
entries,
|
||||
keys: ["my-team"],
|
||||
normalizeKey: normalizeChannelSlug,
|
||||
});
|
||||
expect(match.entry).toBe(entries["My Team"]);
|
||||
expect(match.matchSource).toBe("direct");
|
||||
expect(match.matchKey).toBe("My Team");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveNestedAllowlistDecision", () => {
|
||||
it("allows when outer allowlist is disabled", () => {
|
||||
expect(
|
||||
resolveNestedAllowlistDecision({
|
||||
outerConfigured: false,
|
||||
outerMatched: false,
|
||||
innerConfigured: false,
|
||||
innerMatched: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("blocks when outer allowlist is configured but missing match", () => {
|
||||
expect(
|
||||
resolveNestedAllowlistDecision({
|
||||
outerConfigured: true,
|
||||
outerMatched: false,
|
||||
innerConfigured: false,
|
||||
innerMatched: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("requires inner match when inner allowlist is configured", () => {
|
||||
expect(
|
||||
resolveNestedAllowlistDecision({
|
||||
outerConfigured: true,
|
||||
outerMatched: true,
|
||||
innerConfigured: true,
|
||||
innerMatched: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
resolveNestedAllowlistDecision({
|
||||
outerConfigured: true,
|
||||
outerMatched: true,
|
||||
innerConfigured: true,
|
||||
innerMatched: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
export type ChannelMatchSource = "direct" | "parent" | "wildcard";
|
||||
|
||||
export function buildChannelKeyCandidates(
|
||||
...keys: Array<string | undefined | null>
|
||||
): string[] {
|
||||
export type ChannelEntryMatch<T> = {
|
||||
entry?: T;
|
||||
key?: string;
|
||||
@@ -14,6 +11,15 @@ export type ChannelEntryMatch<T> = {
|
||||
matchSource?: ChannelMatchSource;
|
||||
};
|
||||
|
||||
export function normalizeChannelSlug(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^#/, "")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
export function buildChannelKeyCandidates(
|
||||
...keys: Array<string | undefined | null>
|
||||
): string[] {
|
||||
@@ -54,6 +60,7 @@ export function resolveChannelEntryMatchWithFallback<T>(params: {
|
||||
keys: string[];
|
||||
parentKeys?: string[];
|
||||
wildcardKey?: string;
|
||||
normalizeKey?: (value: string) => string;
|
||||
}): ChannelEntryMatch<T> {
|
||||
const direct = resolveChannelEntryMatch({
|
||||
entries: params.entries,
|
||||
@@ -65,6 +72,25 @@ export function resolveChannelEntryMatchWithFallback<T>(params: {
|
||||
return { ...direct, matchKey: direct.key, matchSource: "direct" };
|
||||
}
|
||||
|
||||
const normalizeKey = params.normalizeKey;
|
||||
if (normalizeKey) {
|
||||
const normalizedKeys = params.keys.map((key) => normalizeKey(key)).filter(Boolean);
|
||||
if (normalizedKeys.length > 0) {
|
||||
for (const [entryKey, entry] of Object.entries(params.entries ?? {})) {
|
||||
const normalizedEntry = normalizeKey(entryKey);
|
||||
if (normalizedEntry && normalizedKeys.includes(normalizedEntry)) {
|
||||
return {
|
||||
...direct,
|
||||
entry,
|
||||
key: entryKey,
|
||||
matchKey: entryKey,
|
||||
matchSource: "direct",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parentKeys = params.parentKeys ?? [];
|
||||
if (parentKeys.length > 0) {
|
||||
const parent = resolveChannelEntryMatch({ entries: params.entries, keys: parentKeys });
|
||||
@@ -79,6 +105,25 @@ export function resolveChannelEntryMatchWithFallback<T>(params: {
|
||||
matchSource: "parent",
|
||||
};
|
||||
}
|
||||
if (normalizeKey) {
|
||||
const normalizedParentKeys = parentKeys.map((key) => normalizeKey(key)).filter(Boolean);
|
||||
if (normalizedParentKeys.length > 0) {
|
||||
for (const [entryKey, entry] of Object.entries(params.entries ?? {})) {
|
||||
const normalizedEntry = normalizeKey(entryKey);
|
||||
if (normalizedEntry && normalizedParentKeys.includes(normalizedEntry)) {
|
||||
return {
|
||||
...direct,
|
||||
entry,
|
||||
key: entryKey,
|
||||
parentEntry: entry,
|
||||
parentKey: entryKey,
|
||||
matchKey: entryKey,
|
||||
matchSource: "parent",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (direct.wildcardEntry && direct.wildcardKey) {
|
||||
@@ -93,3 +138,15 @@ export function resolveChannelEntryMatchWithFallback<T>(params: {
|
||||
|
||||
return direct;
|
||||
}
|
||||
|
||||
export function resolveNestedAllowlistDecision(params: {
|
||||
outerConfigured: boolean;
|
||||
outerMatched: boolean;
|
||||
innerConfigured: boolean;
|
||||
innerMatched: boolean;
|
||||
}): boolean {
|
||||
if (!params.outerConfigured) return true;
|
||||
if (!params.outerMatched) return false;
|
||||
if (!params.innerConfigured) return true;
|
||||
return params.innerMatched;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export type { ChannelEntryMatch, ChannelMatchSource } from "../channel-config.js";
|
||||
export {
|
||||
buildChannelKeyCandidates,
|
||||
normalizeChannelSlug,
|
||||
resolveChannelEntryMatch,
|
||||
resolveChannelEntryMatchWithFallback,
|
||||
resolveNestedAllowlistDecision,
|
||||
} from "../channel-config.js";
|
||||
|
||||
@@ -86,8 +86,10 @@ export {
|
||||
} from "./directory-config.js";
|
||||
export {
|
||||
buildChannelKeyCandidates,
|
||||
normalizeChannelSlug,
|
||||
resolveChannelEntryMatch,
|
||||
resolveChannelEntryMatchWithFallback,
|
||||
resolveNestedAllowlistDecision,
|
||||
type ChannelEntryMatch,
|
||||
type ChannelMatchSource,
|
||||
} from "./channel-config.js";
|
||||
|
||||
Reference in New Issue
Block a user