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
Binary file modified bun.lockb
Binary file not shown.
6 changes: 3 additions & 3 deletions src/lib/components/PostFilters.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@

const keywordData = $derived({
table: [
...posts.flatMap((post) =>
...posts.map((post) =>
post.keyword.map((keyword) => {
return { title: keyword.title, filtered: false };
})
),
...filteredPosts.flatMap((post) =>
...filteredPosts.map((post) =>
post.keyword.map((keyword) => {
return { title: keyword.title, filtered: true };
})
)
]
].flat()
});

const educationData = $derived({
Expand Down
4 changes: 1 addition & 3 deletions src/lib/components/ui/MultiSelect.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@

let detailsOpen = $state(false);

let valuesSet = $derived(new Set(values));

function updateValue(optionValue: string) {
if (values.includes(optionValue)) {
values = values.filter((v: string) => v !== optionValue);
Expand Down Expand Up @@ -107,7 +105,7 @@
</div>
<Checkbox
id={option.value}
checked={valuesSet.has(option.value)}
checked={values.includes(option.value)}
onchange={() => updateValue(option.value)}
/>
</label>
Expand Down
27 changes: 15 additions & 12 deletions src/routes/api/newsletter/daily-digest/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,21 @@ export const POST: RequestHandler = async ({ locals: { supabase }, request }) =>
// Common email body parts
const textBodyHeader = `Here are the new positions posted on vispositions in the last 24 hours:\n\n`;
const htmlBodyHeader = `<p>Here are the new positions posted on <a href="${siteUrl}">visPositions</a> in the last 24 hours:</p><ul>`;
const { postsText, linkedinText, postsHtmlItems } = posts.reduce(
(acc, post) => {
acc.postsText += `- ${post.title}\n ${post.description?.substring(0, 100)}...\n View: ${siteUrl}/jobs/${post.id}\n\n`;
acc.linkedinText += `- ${post.title}\n`;
const safeTitle = escapeHtml(post.title);
const safeDesc = post.description ? escapeHtml(post.description.substring(0, 100)) : '';
acc.postsHtmlItems += `<li><a href="${siteUrl}/jobs/${post.id}"><strong>${safeTitle}</strong></a><br/>${safeDesc}...</li>`;
return acc;
},
{ postsText: '', linkedinText: '', postsHtmlItems: '' }
);
const postsHtml = postsHtmlItems + `</ul>`;
const postsText = posts
.map(
(post) =>
`- ${post.title}\n ${post.description?.substring(0, 100)}...\n View: ${siteUrl}/jobs/${post.id}\n\n`
)
.join('');
const linkedinText = posts.map((post) => `- ${post.title}\n`).join('');
const postsHtml =
posts
.map((post) => {
const safeTitle = escapeHtml(post.title);
const safeDesc = post.description ? escapeHtml(post.description.substring(0, 100)) : '';
return `<li><a href="${siteUrl}/jobs/${post.id}"><strong>${safeTitle}</strong></a><br/>${safeDesc}...</li>`;
})
.join('') + `</ul>`;

const textBody =
`${textBodyHeader}${postsText}` +
Expand Down
12 changes: 0 additions & 12 deletions src/routes/api/post/[id]/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,6 @@ export const PATCH = async ({ locals: { supabase, safeGetSession }, params, requ

const data = await request.json();

if (
(data.title !== undefined && typeof data.title !== 'string') ||
(data.description !== undefined && typeof data.description !== 'string') ||
(data.contact !== undefined && typeof data.contact !== 'string') ||
(data.industry !== undefined && typeof data.industry !== 'boolean') ||
(data.education !== undefined && typeof data.education !== 'string') ||
(data.expiration_date !== undefined && typeof data.expiration_date !== 'string') ||
(data.keywords !== undefined && !Array.isArray(data.keywords))
) {
throw error(400, 'Invalid input data');
}

// Start a transaction to update both post and keywords
// Always include expiration_date in the update
const updateData = {
Expand Down
3 changes: 1 addition & 2 deletions src/routes/api/post/[id]/approve/+server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ADMIN_EMAIL } from '$env/static/private';
import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { error, json, type RequestHandler } from '@sveltejs/kit';

export const PATCH: RequestHandler = async ({ locals: { supabase, safeGetSession }, params }) => {
const { session } = await safeGetSession();
Expand Down
59 changes: 0 additions & 59 deletions src/routes/api/post/[id]/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,65 +12,6 @@ vi.mock('@sveltejs/kit', () => ({
text: vi.fn((message) => new Response(message))
}));

describe('PATCH /api/post/[id]', () => {
it('should throw 400 when invalid input is provided', async () => {
const mockSafeGetSession = vi.fn().mockResolvedValue({
session: {
user: {
email: 'test@example.com'
}
}
});

const locals = {
supabase: {},
safeGetSession: mockSafeGetSession
};

const params = {
id: '1'
};

// Test invalid title
const request1 = {
json: vi.fn().mockResolvedValue({
title: 123,
description: 'valid',
contact: 'valid',
industry: true,
education: 'none',
expiration_date: '2025-01-01',
keywords: []
})
};

const { PATCH } = await import('./+server');

await expect(
PATCH({ locals, params, request: request1 } as unknown as Parameters<typeof PATCH>[0])
).rejects.toThrow('Invalid input data');

// Test invalid keywords array
const request2 = {
json: vi.fn().mockResolvedValue({
title: 'valid',
description: 'valid',
contact: 'valid',
industry: true,
education: 'none',
expiration_date: '2025-01-01',
keywords: 'not-an-array'
})
};

await expect(
PATCH({ locals, params, request: request2 } as unknown as Parameters<typeof PATCH>[0])
).rejects.toThrow('Invalid input data');

expect(error).toHaveBeenCalledWith(400, 'Invalid input data');
});
});

describe('DELETE /api/post/[id]', () => {
it('should throw 404 when deleting a non-existent post', async () => {
// Create a mock chain for supabase
Expand Down
2 changes: 1 addition & 1 deletion src/routes/api/webhooks/google-sheets/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,6 @@ export const POST = async ({ request }) => {
return json({ success: true, insertedCount, skippedCount, errors });
} catch (error) {
console.error('Webhook error:', error);
return json({ error: 'Internal Server Error', details: String(error) }, { status: 500 });
return json({ error: 'Internal Server Error' }, { status: 500 });
}
};
26 changes: 11 additions & 15 deletions src/routes/api/webhooks/google-sheets/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,27 +159,23 @@ describe('Google Sheets Webhook API', () => {
});
});

it('should return 500 if an internal error occurs (catch block)', async () => {
const request = new Request('http://localhost/api/webhooks/google-sheets', {
method: 'POST',
headers: { Authorization: 'Bearer test_webhook_secret' }
});

// Force request.json to throw an error
request.json = vi.fn().mockRejectedValue(new Error('Simulated JSON parsing error'));

// Suppress console.error during this test
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
it('should return a generic 500 error without exposing internal details when an exception occurs', async () => {
// Create a mock request that will throw an exception during json() parsing
const request = {
headers: {
get: vi.fn().mockReturnValue('Bearer test_webhook_secret')
},
json: vi.fn().mockRejectedValue(new Error('Sensitive database connection string exposed!'))
};

const response = await POST({ request } as unknown as Parameters<typeof POST>[0]);
const data = await response.json();

expect(response.status).toBe(500);
expect(data).toEqual({
error: 'Internal Server Error',
details: 'Error: Simulated JSON parsing error'
error: 'Internal Server Error'
});

consoleSpy.mockRestore();
// Ensure that the detailed error message is not present in the response
expect(data.details).toBeUndefined();
});
});
24 changes: 0 additions & 24 deletions src/routes/api/webhooks/post/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,28 +65,4 @@ describe('Post Webhook API', () => {
expect(response.status).toBe(200);
expect(data).toEqual({ success: true });
});

it('should return 500 and success: false if an error occurs during processing', async () => {
const request = new Request('http://localhost/api/webhooks/post', {
method: 'POST',
headers: {
Authorization: 'Bearer test_secret'
},
body: JSON.stringify({ title: 'Test', description: 'Test desc' })
});

// Mock request.json() to throw an error
request.json = vi.fn().mockRejectedValue(new Error('Simulated processing error'));

const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

const response = await POST({ request } as unknown as Parameters<typeof POST>[0]);
const data = await response.json();

expect(response.status).toBe(500);
expect(data).toEqual({ success: false });
expect(consoleErrorSpy).toHaveBeenCalledWith('Webhook error:', expect.any(Error));

consoleErrorSpy.mockRestore();
});
});