Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

5 changed files with 87 additions and 79 deletions

3
.gitignore vendored
View file

@ -1,5 +1,4 @@
node_modules/ node_modules/
test/calendar/ test/calendar/
calendar/ calendar/
logs/ logs/
.qodo

BIN
.qodo/history.sqlite Normal file

Binary file not shown.

View file

@ -15,18 +15,33 @@ 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');
if (isFilePath) {
// logger.debug(`Reading calendar from file: ${calendar.url}`);
return { data: fs.readFileSync(path.resolve(calendar.url), 'utf-8'), ...calendar };
}
try { try {
if (isFilePath) { // First try the original URL
// logger.debug(`Reading calendar from file: ${calendar.url}`); const initialResponse = await axios.get(calendar.url);
return { data: fs.readFileSync(path.resolve(calendar.url), 'utf-8'), ...calendar }; return { data: initialResponse.data, ...calendar };
} else { } catch (initialError) {
// logger.debug(`Fetching calendar from URL: ${calendar.url}`); logger.debug(`Initial fetch failed, trying extension adjustment for: ${calendar.url}`);
const response = await axios.get(calendar.url);
return { data: response.data, ...calendar }; // 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`);
} }
} catch (error) {
logger.error(`Error retrieving calendar from ${calendar.url}: ${error.message}`);
throw new Error(`Error retrieving calendar from ${calendar.url}: ${error.message}`);
} }
} }

View file

@ -57,15 +57,9 @@ 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) => {
// Extract the calendar name and remove .ics extension if present const calendarName = req.params.name;
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
console.log(`Serving calendar for: ${calendarName}`); console.log(`Serving calendar for: ${calendarName}`);
@ -79,11 +73,7 @@ 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) {
@ -92,4 +82,4 @@ router.get('/calendar/:name', async (req, res) => {
} }
}); });
export default router; export default router;

View file

@ -2,12 +2,15 @@ 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(__dirname, 'calendar'); const CALENDARS_DIR = path.join(process.cwd(), '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');
@ -26,9 +29,8 @@ 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.rmdirSync(CALENDARS_DIR, { recursive: true }); fs.rmSync(CALENDARS_DIR, { recursive: true, force: true });
} }
// Optional: Add a delay to ensure all handles are released // Optional: Add a delay to ensure all handles are released
@ -280,58 +282,60 @@ describe('Calendar Merging API', () => {
expect(actualOutput).toBe(expectedOutput); expect(actualOutput).toBe(expectedOutput);
}); });
// Test accessing calendar with and without .ics extension describe('Smart URL Handling', () => {
test('Access calendar with and without .ics extension', async () => { let mockAxios;
// Create a test calendar
const response = await request(server) beforeAll(() => {
.post('/merge') mockAxios = new MockAdapter(axios);
.send({ });
linkGroupName: 'Extension Test Calendar',
calendars: [
{
url: getTestCalendarFilename('sf_public_holidays.ics'),
prefix: 'Test',
override: false,
},
],
});
expect(response.status).toBe(200); afterEach(() => {
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);
});
// Test 404 response for non-existent calendar afterAll(() => {
test('Return 404 for non-existent calendar with and without .ics extension', async () => { mockAxios.restore();
// 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 () => {
const validUrl = 'https://cals.ftt.gmbh/calendar/germanholithunder';
expect(responseWithoutExtension.status).toBe(404); mockAxios.onGet(validUrl).reply(200, 'VALID_CALENDAR');
// Test accessing non-existent calendar with .ics extension const result = await fetchCalendarData({ url: validUrl });
const responseWithExtension = await request(server) expect(result.data).toBe('VALID_CALENDAR');
.get('/calendar/NonExistentCalendar.ics'); });
expect(responseWithExtension.status).toBe(404); test('should try .ics version when original fails', async () => {
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');
});
}); });
}); });