Webhookのメッセージをカスタマイズできるように
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createApp } from '../src/server/app.js';
|
||||
import { query } from '../src/server/db/pool.js';
|
||||
import { pool, query } from '../src/server/db/pool.js';
|
||||
import { getCertificateExpiry } from '../src/server/modules/monitoring/certificate.js';
|
||||
|
||||
vi.mock('../src/server/db/pool.js', () => ({
|
||||
@@ -50,6 +50,7 @@ function mockAuthenticatedUser() {
|
||||
describe('API security boundaries', () => {
|
||||
beforeEach(() => {
|
||||
query.mockReset();
|
||||
pool.connect.mockReset();
|
||||
getCertificateExpiry.mockReset();
|
||||
});
|
||||
|
||||
@@ -161,6 +162,10 @@ describe('API security boundaries', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
}).mockImplementationOnce(async (sql, params) => {
|
||||
expect(sql).toContain('FROM user_notification_settings');
|
||||
expect(params).toEqual([USER_ID]);
|
||||
return { rows: [] };
|
||||
});
|
||||
|
||||
const app = createApp();
|
||||
@@ -174,6 +179,168 @@ describe('API security boundaries', () => {
|
||||
const data = await response.json();
|
||||
expect(data.webhooks).toHaveLength(1);
|
||||
expect(data).not.toHaveProperty('pushSubscriptions');
|
||||
expect(data.webhookMessageSettings).toMatchObject({
|
||||
timezone: 'Asia/Tokyo',
|
||||
usesDefault: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('requires a CSRF token when saving webhook message settings', async () => {
|
||||
const app = createApp();
|
||||
|
||||
const response = await app.request('/api/notification-methods/webhook-message-settings', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
webhookMessageTemplate: '{{siteDomain}}',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(query).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects invalid webhook message templates before persistence', async () => {
|
||||
mockAuthenticatedUser();
|
||||
const app = createApp();
|
||||
|
||||
const unsupportedVariable = await app.request('/api/notification-methods/webhook-message-settings', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: csrfCookie(),
|
||||
'x-csrf-token': 'csrf-token',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
webhookMessageTemplate: '{{unknownValue}}',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(unsupportedVariable.status).toBe(400);
|
||||
expect(query).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('rejects invalid account timezone before persistence', async () => {
|
||||
mockAuthenticatedUser();
|
||||
const app = createApp();
|
||||
|
||||
const invalidTimezone = await app.request('/api/account/profile', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: csrfCookie(),
|
||||
'x-csrf-token': 'csrf-token',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
displayName: 'Alice',
|
||||
timezone: 'Mars/Base',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(invalidTimezone.status).toBe(400);
|
||||
expect(query).toHaveBeenCalledTimes(1);
|
||||
expect(pool.connect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('saves account timezone for the session user only', async () => {
|
||||
const client = {
|
||||
query: vi.fn(async (sql, params) => {
|
||||
if (sql === 'BEGIN' || sql === 'COMMIT') return { rows: [] };
|
||||
expect(sql).toContain('INSERT INTO user_notification_settings');
|
||||
expect(params).toEqual([USER_ID, 'Alice', 'America/New_York']);
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
user_id: USER_ID,
|
||||
username: 'alice',
|
||||
display_name: 'Alice',
|
||||
otp_secret: null,
|
||||
timezone: 'America/New_York',
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
release: vi.fn(),
|
||||
};
|
||||
pool.connect.mockResolvedValue(client);
|
||||
mockAuthenticatedUser();
|
||||
|
||||
const app = createApp();
|
||||
const response = await app.request('/api/account/profile', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: csrfCookie(),
|
||||
'x-csrf-token': 'csrf-token',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
displayName: 'Alice',
|
||||
timezone: 'America/New_York',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
await expect(response.json()).resolves.toMatchObject({
|
||||
account: { timezone: 'America/New_York' },
|
||||
});
|
||||
expect(client.query).toHaveBeenCalledWith('COMMIT');
|
||||
expect(client.release).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('saves and deletes webhook message settings for the session user only', async () => {
|
||||
mockAuthenticatedUser();
|
||||
query
|
||||
.mockImplementationOnce(query.getMockImplementation())
|
||||
.mockImplementationOnce(async (sql, params) => {
|
||||
expect(sql).toContain('INSERT INTO user_notification_settings');
|
||||
expect(params).toEqual([USER_ID, '{{siteDomain}}']);
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
webhook_message_template: '{{siteDomain}}',
|
||||
timezone: 'Asia/Tokyo',
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const app = createApp();
|
||||
const saveResponse = await app.request('/api/notification-methods/webhook-message-settings', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: csrfCookie(),
|
||||
'x-csrf-token': 'csrf-token',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
webhookMessageTemplate: '{{siteDomain}}',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(saveResponse.status).toBe(200);
|
||||
|
||||
query.mockReset();
|
||||
mockAuthenticatedUser();
|
||||
query
|
||||
.mockImplementationOnce(query.getMockImplementation())
|
||||
.mockImplementationOnce(async (sql, params) => {
|
||||
expect(sql).toContain('UPDATE user_notification_settings');
|
||||
expect(sql).toContain('webhook_message_template = NULL');
|
||||
expect(params).toEqual([USER_ID]);
|
||||
return { rows: [{ webhook_message_template: null, timezone: 'Asia/Tokyo' }] };
|
||||
});
|
||||
|
||||
const deleteResponse = await app.request('/api/notification-methods/webhook-message-settings', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Cookie: csrfCookie(),
|
||||
'x-csrf-token': 'csrf-token',
|
||||
},
|
||||
});
|
||||
|
||||
expect(deleteResponse.status).toBe(200);
|
||||
});
|
||||
|
||||
it('reports the current browser push subscription as registered for the session user', async () => {
|
||||
|
||||
@@ -102,6 +102,11 @@ describe('certificate monitoring', () => {
|
||||
};
|
||||
}
|
||||
|
||||
if (sql.includes('FROM user_notification_settings')) {
|
||||
expect(params).toEqual([USER_ID]);
|
||||
return { rows: [] };
|
||||
}
|
||||
|
||||
if (sql.includes('FROM notification_methods') && sql.includes("notification_type = 'push'")) {
|
||||
expect(params).toEqual([USER_ID]);
|
||||
return {
|
||||
@@ -138,7 +143,12 @@ describe('certificate monitoring', () => {
|
||||
|
||||
expect(pool.connect).toHaveBeenCalledOnce();
|
||||
expect(getCertificateExpiry).toHaveBeenCalledWith('https://example.com/');
|
||||
expect(deliverNotifications).toHaveBeenCalledOnce();
|
||||
expect(deliverNotifications).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
webhookMessageBody:
|
||||
'example.com の有効期限が 12時間前 になりました。\n2026/05/25 20:30:00 に期限切れになります。',
|
||||
}),
|
||||
);
|
||||
expect(mocks.client.release).toHaveBeenCalledOnce();
|
||||
expect(result).toMatchObject({
|
||||
checkedSites: 1,
|
||||
@@ -490,4 +500,95 @@ describe('certificate monitoring', () => {
|
||||
results: [{ siteId: SITE_ID, ok: true, alertsCreated: 1 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a custom webhook message with the configured timezone and timing labels', async () => {
|
||||
vi.setSystemTime(new Date('2026-12-24T05:36:07.000Z'));
|
||||
const issuer = 'C = US, O = Example CA, CN = Example Root';
|
||||
const issuedAt = new Date('2026-01-01T00:00:00.000Z');
|
||||
const expiresAt = new Date('2026-12-31T05:06:07.000Z');
|
||||
|
||||
getCertificateExpiry.mockResolvedValue({
|
||||
issuer,
|
||||
issuedAt,
|
||||
expiresAt,
|
||||
hoursUntilExpiry: 168,
|
||||
});
|
||||
deliverNotifications.mockResolvedValue({
|
||||
webhook: [],
|
||||
push: { ok: false, skipped: true },
|
||||
});
|
||||
|
||||
mocks.client.query.mockImplementation(async (sql, params) => {
|
||||
if (sql.includes('FROM sites s') && sql.includes('LEFT JOIN site_alert_conditions')) {
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
site_id: SITE_ID,
|
||||
user_id: USER_ID,
|
||||
url: 'https://example.com/',
|
||||
alias: 'Example Site',
|
||||
conditions: [
|
||||
{
|
||||
site_alert_condition_id: CONDITION_ID,
|
||||
threshold_hours: 168,
|
||||
webhook_method_ids: [WEBHOOK_ID],
|
||||
push_enabled: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (sql.includes('SET certificate_issuer')) {
|
||||
expect(params).toEqual([SITE_ID, issuer, issuedAt, expiresAt]);
|
||||
return { rows: [] };
|
||||
}
|
||||
|
||||
if (sql.includes('FROM notification_methods') && sql.includes("notification_type = 'webhook'")) {
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
notification_method_id: WEBHOOK_ID,
|
||||
alias: 'Deploy hook',
|
||||
url: 'https://hooks.example.com/',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (sql.includes('FROM user_notification_settings')) {
|
||||
expect(params).toEqual([USER_ID]);
|
||||
return {
|
||||
rows: [
|
||||
{
|
||||
webhook_message_template:
|
||||
'{{siteName}}/{{siteDomain}}/{{condTiming}}/{{expiryDate}}/{{expiryTime}}/{{expiryDateTime}}',
|
||||
timezone: 'America/New_York',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (sql.includes('INSERT INTO alert_history')) {
|
||||
return { rows: [{ alert_id: '77777777-7777-4777-8777-777777777777' }] };
|
||||
}
|
||||
|
||||
if (sql.includes('UPDATE site_alert_conditions')) {
|
||||
expect(params).toEqual([CONDITION_ID, expiresAt]);
|
||||
return { rows: [] };
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected query: ${sql}`);
|
||||
});
|
||||
|
||||
await runCertificateMonitoring({ concurrency: 1 });
|
||||
|
||||
expect(deliverNotifications).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
webhookMessageBody:
|
||||
'Example Site/example.com/1週間前/2026/12/31/00:06:07/2026/12/31 00:06:07',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user