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
27 changes: 27 additions & 0 deletions src/app/api/gigs/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,33 @@ describe("GET /api/gigs", () => {
expect(json.pagination.total).toBe(1);
});

it.each(["human", "agent"])("filters gigs by the %s poster relationship", async (accountType) => {
const chain = chainResult({ data: null, error: null });
chain.range = vi.fn().mockResolvedValue({ data: [], error: null, count: 0 });
mockFrom.mockReturnValue(chain);

const res = await GET(makeGetRequest({ account_type: accountType }));

expect(res.status).toBe(200);
// Filtering the embedded object alone keeps unrelated parent rows.
// An inner join must constrain the gigs and their exact pagination count.
expect(chain.select.mock.calls[0][0]).toContain("poster:profiles!poster_id!inner");
expect(chain.eq).toHaveBeenCalledWith("poster.account_type", accountType);
expect(chain.eq).not.toHaveBeenCalledWith("poster:profiles!poster_id.account_type", accountType);
});

it("keeps the default left join when no account type is requested", async () => {
const chain = chainResult({ data: null, error: null });
chain.range = vi.fn().mockResolvedValue({ data: [], error: null, count: 0 });
mockFrom.mockReturnValue(chain);

const res = await GET(makeGetRequest());

expect(res.status).toBe(200);
expect(chain.select.mock.calls[0][0]).not.toContain("!inner");
expect(chain.eq.mock.calls.some(([column]) => column === "poster.account_type")).toBe(false);
});

it("caps huge page values before building the Supabase range", async () => {
const chain = chainResult({ data: null, error: null });
chain.select = vi.fn().mockReturnValue(chain);
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/gigs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export async function GET(request: NextRequest) {
.select(
`
*,
poster:profiles!poster_id (
poster:profiles!poster_id${account_type ? "!inner" : ""} (
id,
username,
full_name,
Expand Down Expand Up @@ -121,7 +121,7 @@ export async function GET(request: NextRequest) {
}

if (account_type) {
query = query.eq("poster:profiles!poster_id.account_type", account_type);
query = query.eq("poster.account_type", account_type);
}

// Apply sorting
Expand Down
7 changes: 7 additions & 0 deletions src/lib/github-links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import {
} from "./github-links";

describe("isGitHubPrLink", () => {
it("rejects a numeric prefix followed by a non-path suffix", () => {
for (const suffix of ["12abc", "12.5", "12-closed"]) {
const url = `https://github.com/org/repo/pull/${suffix}`;
expect(isGitHubPrLink(url)).toBe(false);
expect(parseGitHubPullUrl(url)).toBeNull();
}
});
it("accepts a single pull request URL", () => {
expect(isGitHubPrLink("https://github.com/profullstack/ugig.net/pull/42")).toBe(true);
expect(isGitHubPrLink("https://github.com/org/repo/pull/1/files")).toBe(true);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/github-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function isGitHubPrLink(value: string): boolean {
const path = url.pathname;
return (
// A single PR: /owner/repo/pull/123 (optionally /files, #discussion, …)
/^\/[^/]+\/[^/]+\/pull\/\d+/.test(path) ||
/^\/[^/]+\/[^/]+\/pull\/\d+(?:\/.*)?$/.test(path) ||
// A repo's PR list/search: /owner/repo/pulls
/^\/[^/]+\/[^/]+\/pulls\/?$/.test(path) ||
// The global PR search: /pulls
Expand Down
15 changes: 15 additions & 0 deletions src/lib/mentions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ describe("parseMentions", () => {
});

describe("parseContentWithMentions", () => {
it("keeps email addresses and URL userinfo as ordinary text", () => {
for (const content of ["Contact alice@example.com", "https://user@example.com/path"]) {
expect(parseContentWithMentions(content)).toEqual([{ type: "text", value: content }]);
expect(parseMentions(content)).toEqual([]);
}
});

it("preserves parentheses and whitespace around real mentions", () => {
expect(parseContentWithMentions("Hi (@Alice) and\n@bob")).toEqual([
{ type: "text", value: "Hi (" },
{ type: "mention", username: "Alice" },
{ type: "text", value: ") and\n" },
{ type: "mention", username: "bob" },
]);
});
it("parses text with mentions into segments", () => {
const segments = parseContentWithMentions("hey @alice check this");
expect(segments).toEqual([
Expand Down
11 changes: 7 additions & 4 deletions src/lib/mentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,18 @@ export type MentionSegment =

export function parseContentWithMentions(content: string): MentionSegment[] {
const segments: MentionSegment[] = [];
const regex = /@([a-zA-Z0-9_-]+)/g;
// Use the same token boundary as notification extraction. An email address
// must not become a link to the user named after its domain.
const regex = /(^|[\s(])@([a-zA-Z0-9_-]+)/g;
let lastIndex = 0;
let match: RegExpExecArray | null;

while ((match = regex.exec(content)) !== null) {
if (match.index > lastIndex) {
segments.push({ type: "text", value: content.slice(lastIndex, match.index) });
const mentionStart = match.index + match[1].length;
if (mentionStart > lastIndex) {
segments.push({ type: "text", value: content.slice(lastIndex, mentionStart) });
}
segments.push({ type: "mention", username: match[1] });
segments.push({ type: "mention", username: match[2] });
lastIndex = regex.lastIndex;
}

Expand Down
Loading