forked from ryanmwangi/CalMerger
Compare commits
3 commits
Author | SHA1 | Date | |
---|---|---|---|
f7b72efd06 | |||
eb0f2f145c | |||
55826b8dde |
5 changed files with 79 additions and 87 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@ node_modules/
|
||||||
test/calendar/
|
test/calendar/
|
||||||
calendar/
|
calendar/
|
||||||
logs/
|
logs/
|
||||||
|
.qodo
|
||||||
|
|
Binary file not shown.
|
@ -15,33 +15,18 @@ export const sanitizeFilename = (filename) => filename.replace(/[<>:"/\\|?* ]/g,
|
||||||
// Fetch calendar data from URL or file
|
// Fetch calendar data from URL or file
|
||||||
export async function fetchCalendarData(calendar) {
|
export async function fetchCalendarData(calendar) {
|
||||||
const isFilePath = !calendar.url.startsWith('http');
|
const isFilePath = !calendar.url.startsWith('http');
|
||||||
|
try {
|
||||||
if (isFilePath) {
|
if (isFilePath) {
|
||||||
// logger.debug(`Reading calendar from file: ${calendar.url}`);
|
// logger.debug(`Reading calendar from file: ${calendar.url}`);
|
||||||
return { data: fs.readFileSync(path.resolve(calendar.url), 'utf-8'), ...calendar };
|
return { data: fs.readFileSync(path.resolve(calendar.url), 'utf-8'), ...calendar };
|
||||||
|
} else {
|
||||||
|
// logger.debug(`Fetching calendar from URL: ${calendar.url}`);
|
||||||
|
const response = await axios.get(calendar.url);
|
||||||
|
return { data: response.data, ...calendar };
|
||||||
}
|
}
|
||||||
try {
|
} catch (error) {
|
||||||
// First try the original URL
|
logger.error(`Error retrieving calendar from ${calendar.url}: ${error.message}`);
|
||||||
const initialResponse = await axios.get(calendar.url);
|
throw new Error(`Error retrieving calendar from ${calendar.url}: ${error.message}`);
|
||||||
return { data: initialResponse.data, ...calendar };
|
|
||||||
} catch (initialError) {
|
|
||||||
logger.debug(`Initial fetch failed, trying extension adjustment for: ${calendar.url}`);
|
|
||||||
|
|
||||||
// Determine alternate URL version
|
|
||||||
const altUrl = calendar.url.endsWith('.ics')
|
|
||||||
? calendar.url.slice(0, -4) // Remove .ics
|
|
||||||
: calendar.url + '.ics'; // Add .ics
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Try the alternate version
|
|
||||||
const altResponse = await axios.get(altUrl);
|
|
||||||
logger.debug(`Success with adjusted URL: ${altUrl}`);
|
|
||||||
return { data: altResponse.data, ...calendar };
|
|
||||||
} catch (altError) {
|
|
||||||
logger.error(`Both URL versions failed:
|
|
||||||
Original: ${calendar.url}
|
|
||||||
Adjusted: ${altUrl}`);
|
|
||||||
throw new Error(`Calendar fetch failed for both URL versions`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,8 +57,14 @@ async function refreshCalendarData(calendarName) {
|
||||||
|
|
||||||
// Serve the merged calendar file and refresh if older than an hour
|
// Serve the merged calendar file and refresh if older than an hour
|
||||||
router.get('/calendar/:name', async (req, res) => {
|
router.get('/calendar/:name', async (req, res) => {
|
||||||
const calendarName = req.params.name;
|
// Extract the calendar name and remove .ics extension if present
|
||||||
|
let calendarName = req.params.name;
|
||||||
|
if (calendarName.endsWith('.ics')) {
|
||||||
|
calendarName = calendarName.slice(0, -4);
|
||||||
|
}
|
||||||
|
|
||||||
const icsFilePath = path.join(MERGED_CALENDARS_DIR, `${calendarName}.ics`);
|
const icsFilePath = path.join(MERGED_CALENDARS_DIR, `${calendarName}.ics`);
|
||||||
|
const fullUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if the .ics file exists
|
// Check if the .ics file exists
|
||||||
|
@ -73,7 +79,11 @@ router.get('/calendar/:name', async (req, res) => {
|
||||||
}
|
}
|
||||||
res.setHeader('Content-Type', 'text/calendar');
|
res.setHeader('Content-Type', 'text/calendar');
|
||||||
res.sendFile(icsFilePath);
|
res.sendFile(icsFilePath);
|
||||||
|
|
||||||
|
// Log the successful request with URL format and status code
|
||||||
|
console.log(`Serving calendar ${calendarName} for ${fullUrl}: 200`);
|
||||||
} else {
|
} else {
|
||||||
|
console.log(`Calendar not found: ${calendarName} for ${fullUrl}: 404`);
|
||||||
res.status(404).json({ error: 'Calendar not found.' });
|
res.status(404).json({ error: 'Calendar not found.' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -2,15 +2,12 @@ import request from 'supertest';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import axios from 'axios';
|
|
||||||
import MockAdapter from 'axios-mock-adapter';
|
|
||||||
import { fetchCalendarData } from '../src/calendarUtil.js';
|
|
||||||
|
|
||||||
// ESM equivalent of __dirname
|
// ESM equivalent of __dirname
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const CALENDARS_DIR = path.join(process.cwd(), 'calendar');
|
const CALENDARS_DIR = path.join(__dirname, 'calendar');
|
||||||
const TEST_CALENDARS_DIR = path.join(__dirname, 'test_calendars');
|
const TEST_CALENDARS_DIR = path.join(__dirname, 'test_calendars');
|
||||||
const EXPECTED_OUTPUTS_DIR = path.join(__dirname, 'expected_outputs');
|
const EXPECTED_OUTPUTS_DIR = path.join(__dirname, 'expected_outputs');
|
||||||
|
|
||||||
|
@ -29,8 +26,9 @@ describe('Calendar Merging API', () => {
|
||||||
await new Promise(resolve => server.close(resolve));
|
await new Promise(resolve => server.close(resolve));
|
||||||
|
|
||||||
// Clean up the merged calendars directory after tests
|
// Clean up the merged calendars directory after tests
|
||||||
|
fs.rmdirSync(CALENDARS_DIR, { recursive: true });
|
||||||
if (fs.existsSync(CALENDARS_DIR)) {
|
if (fs.existsSync(CALENDARS_DIR)) {
|
||||||
fs.rmSync(CALENDARS_DIR, { recursive: true, force: true });
|
fs.rmdirSync(CALENDARS_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: Add a delay to ensure all handles are released
|
// Optional: Add a delay to ensure all handles are released
|
||||||
|
@ -282,60 +280,58 @@ describe('Calendar Merging API', () => {
|
||||||
expect(actualOutput).toBe(expectedOutput);
|
expect(actualOutput).toBe(expectedOutput);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Smart URL Handling', () => {
|
// Test accessing calendar with and without .ics extension
|
||||||
let mockAxios;
|
test('Access calendar with and without .ics extension', async () => {
|
||||||
|
// Create a test calendar
|
||||||
beforeAll(() => {
|
const response = await request(server)
|
||||||
mockAxios = new MockAdapter(axios);
|
.post('/merge')
|
||||||
|
.send({
|
||||||
|
linkGroupName: 'Extension Test Calendar',
|
||||||
|
calendars: [
|
||||||
|
{
|
||||||
|
url: getTestCalendarFilename('sf_public_holidays.ics'),
|
||||||
|
prefix: 'Test',
|
||||||
|
override: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
expect(response.status).toBe(200);
|
||||||
mockAxios.reset();
|
|
||||||
|
// Check if the file was created
|
||||||
|
const filePath = path.join(CALENDARS_DIR, 'Extension_Test_Calendar.ics');
|
||||||
|
expect(fs.existsSync(filePath)).toBe(true);
|
||||||
|
|
||||||
|
// Test accessing without .ics extension
|
||||||
|
const responseWithoutExtension = await request(server)
|
||||||
|
.get('/calendar/Extension_Test_Calendar');
|
||||||
|
|
||||||
|
expect(responseWithoutExtension.status).toBe(200);
|
||||||
|
expect(responseWithoutExtension.headers['content-type']).toMatch(/text\/calendar/);
|
||||||
|
|
||||||
|
// Test accessing with .ics extension
|
||||||
|
const responseWithExtension = await request(server)
|
||||||
|
.get('/calendar/Extension_Test_Calendar.ics');
|
||||||
|
|
||||||
|
expect(responseWithExtension.status).toBe(200);
|
||||||
|
expect(responseWithExtension.headers['content-type']).toMatch(/text\/calendar/);
|
||||||
|
|
||||||
|
// Verify both responses contain the same content
|
||||||
|
expect(responseWithoutExtension.text).toBe(responseWithExtension.text);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
// Test 404 response for non-existent calendar
|
||||||
mockAxios.restore();
|
test('Return 404 for non-existent calendar with and without .ics extension', async () => {
|
||||||
});
|
// Test accessing non-existent calendar without .ics extension
|
||||||
|
const responseWithoutExtension = await request(server)
|
||||||
|
.get('/calendar/NonExistentCalendar');
|
||||||
|
|
||||||
test('should use original URL when valid without .ics', async () => {
|
expect(responseWithoutExtension.status).toBe(404);
|
||||||
const validUrl = 'https://cals.ftt.gmbh/calendar/germanholithunder';
|
|
||||||
mockAxios.onGet(validUrl).reply(200, 'VALID_CALENDAR');
|
|
||||||
|
|
||||||
const result = await fetchCalendarData({ url: validUrl });
|
// Test accessing non-existent calendar with .ics extension
|
||||||
expect(result.data).toBe('VALID_CALENDAR');
|
const responseWithExtension = await request(server)
|
||||||
});
|
.get('/calendar/NonExistentCalendar.ics');
|
||||||
|
|
||||||
test('should try .ics version when original fails', async () => {
|
expect(responseWithExtension.status).toBe(404);
|
||||||
const invalidUrl = 'https://cals.ftt.gmbh/calendar/germanholithunder.ics';
|
|
||||||
const validUrl = invalidUrl.slice(0, -4);
|
|
||||||
|
|
||||||
mockAxios
|
|
||||||
.onGet(invalidUrl).reply(404)
|
|
||||||
.onGet(validUrl).reply(200, 'VALID_CALENDAR');
|
|
||||||
|
|
||||||
const result = await fetchCalendarData({ url: invalidUrl });
|
|
||||||
expect(result.data).toBe('VALID_CALENDAR');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should preserve valid .ics URLs', async () => {
|
|
||||||
const googleUrl = 'https://calendar.google.com/.../basic.ics';
|
|
||||||
mockAxios.onGet(googleUrl).reply(200, 'GOOGLE_CALENDAR');
|
|
||||||
|
|
||||||
const result = await fetchCalendarData({ url: googleUrl });
|
|
||||||
expect(result.data).toBe('GOOGLE_CALENDAR');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should try both versions for ambiguous URLs', async () => {
|
|
||||||
const baseUrl = 'https://example.com/calendar';
|
|
||||||
const icsUrl = baseUrl + '.ics';
|
|
||||||
|
|
||||||
mockAxios
|
|
||||||
.onGet(baseUrl).reply(404)
|
|
||||||
.onGet(icsUrl).reply(200, 'ICS_CALENDAR');
|
|
||||||
|
|
||||||
const result = await fetchCalendarData({ url: baseUrl });
|
|
||||||
expect(result.data).toBe('ICS_CALENDAR');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue