오류 수정

This commit is contained in:
2026-07-31 12:06:08 +09:00
parent d90a25919e
commit 616abf8fd2
3 changed files with 66 additions and 12 deletions
+19 -9
View File
@@ -1,6 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const dbQueryMock = vi.fn();
const { dbQueryMock } = vi.hoisted(() => ({
dbQueryMock: vi.fn(),
}));
vi.mock('../lib/database.js', () => ({
db: {
@@ -15,17 +17,25 @@ describe('resolveLinkTemplateId', () => {
dbQueryMock.mockReset();
});
it('returns null when the provided template id does not exist', async () => {
dbQueryMock.mockResolvedValueOnce({ rows: [] });
await expect(resolveLinkTemplateId('00000000-0000-0000-0000-000000000000', null)).resolves.toBeNull();
});
it('falls back to template slug when the provided id is not usable', async () => {
it('falls back to the default template when the provided id does not exist', async () => {
dbQueryMock.mockResolvedValueOnce({ rows: [] });
dbQueryMock.mockResolvedValueOnce({ rows: [{ id: '11111111-1111-1111-1111-111111111111' }] });
await expect(resolveLinkTemplateId('00000000-0000-0000-0000-000000000000', null)).resolves.toBe('11111111-1111-1111-1111-111111111111');
expect(dbQueryMock).toHaveBeenNthCalledWith(2, 'SELECT id FROM link_templates WHERE is_default = true LIMIT 1');
});
it('uses the template slug when it can be resolved', async () => {
dbQueryMock.mockResolvedValueOnce({ rows: [{ id: '11111111-1111-1111-1111-111111111111' }] });
await expect(resolveLinkTemplateId(null, 'default')).resolves.toBe('11111111-1111-1111-1111-111111111111');
expect(dbQueryMock).toHaveBeenNthCalledWith(2, 'SELECT id FROM link_templates WHERE slug = $1 LIMIT 1', ['default']);
expect(dbQueryMock).toHaveBeenCalledWith('SELECT id FROM link_templates WHERE slug = $1 LIMIT 1', ['default']);
});
it('falls back to the default template when the provided template id is invalid', async () => {
dbQueryMock.mockResolvedValueOnce({ rows: [{ id: '22222222-2222-2222-2222-222222222222' }] });
await expect(resolveLinkTemplateId('not-a-uuid', null)).resolves.toBe('22222222-2222-2222-2222-222222222222');
expect(dbQueryMock).toHaveBeenCalledWith('SELECT id FROM link_templates WHERE is_default = true LIMIT 1');
});
});
+44 -2
View File
@@ -13,6 +13,43 @@ async function getTemplateSlug(templateId: string | null): Promise<string | null
return result.rows[0]?.slug ?? null;
}
export async function resolveLinkTemplateId(
templateId: string | null | undefined,
templateSlug: string | null | undefined
): Promise<string | null> {
const normalizedTemplateId = templateId?.trim();
const normalizedSlug = templateSlug?.trim();
if (normalizedTemplateId) {
const parsed = z.string().uuid().safeParse(normalizedTemplateId);
if (parsed.success) {
const result = await db.query(
'SELECT id FROM link_templates WHERE id = $1 LIMIT 1',
[normalizedTemplateId]
);
if (result.rows[0]?.id) {
return result.rows[0].id;
}
}
}
if (normalizedSlug) {
const result = await db.query(
'SELECT id FROM link_templates WHERE slug = $1 LIMIT 1',
[normalizedSlug]
);
if (result.rows[0]?.id) {
return result.rows[0].id;
}
}
const defaultResult = await db.query(
'SELECT id FROM link_templates WHERE is_default = true LIMIT 1'
);
return defaultResult.rows[0]?.id ?? null;
}
const createLinkSchema = z.object({
userId: z.string().uuid().optional(),
templateId: z.string().uuid().optional(),
@@ -182,6 +219,7 @@ export async function linkRoutes(fastify: FastifyInstance) {
}
const originalUrl = data.originalUrl || 'https://angkorlifes.com';
const resolvedTemplateId = await resolveLinkTemplateId(data.templateId, null);
const result = await db.query(
`INSERT INTO links (
@@ -195,7 +233,7 @@ export async function linkRoutes(fastify: FastifyInstance) {
RETURNING *`,
[
data.userId || null,
data.templateId || null,
resolvedTemplateId,
shortCode,
originalUrl,
data.title || null,
@@ -238,6 +276,10 @@ export async function linkRoutes(fastify: FastifyInstance) {
const { userId } = request.query;
const data = updateLinkSchema.parse(request.body);
const resolvedData = {
...data,
templateId: data.templateId === undefined ? undefined : await resolveLinkTemplateId(data.templateId, null),
};
// Capture current identifiers for cache invalidation after the update
const oldLinkResult = await db.query(
@@ -250,7 +292,7 @@ export async function linkRoutes(fastify: FastifyInstance) {
const values: any[] = [];
let paramIndex = 1;
Object.entries(data).forEach(([key, value]) => {
Object.entries(resolvedData).forEach(([key, value]) => {
if (value !== undefined) {
if (key === 'utmParameters' || key === 'targetingRules' || key === 'deepLinkParameters') {
updates.push(`${key.replace(/([A-Z])/g, '_$1').toLowerCase()} = $${paramIndex}`);
+3 -1
View File
@@ -12,6 +12,7 @@ import { triggerWebhooks } from '../lib/webhook.js';
import { parseUserAgent, getLocationFromIP, detectDevice } from '../lib/utils.js';
import { emitClickEvent } from '../lib/event-emitter.js';
import { classifyBot, edgeBotSignal } from '../lib/bot-detection.js';
import { resolveLinkTemplateId } from './links.js';
/**
* SDK Routes - Mobile SDK endpoints for deferred deep linking
@@ -66,6 +67,7 @@ export async function sdkRoutes(fastify: FastifyInstance) {
const originalUrl = body.originalUrl || 'https://angkorlifes.com';
const shortCode = body.customCode || 'sdk-link';
const resolvedTemplateId = await resolveLinkTemplateId(body.templateId, null);
const result = await db.query(
`INSERT INTO links (
@@ -79,7 +81,7 @@ export async function sdkRoutes(fastify: FastifyInstance) {
RETURNING *`,
[
body.userId || null,
body.templateId || null,
resolvedTemplateId,
shortCode,
originalUrl,
body.title || null,