forked from ryanmwangi/CalMerger
Compare commits
9 commits
869beeb3d4
...
30622c611a
Author | SHA1 | Date | |
---|---|---|---|
30622c611a | |||
48548dadc8 | |||
927941abf9 | |||
0950ec772e | |||
67fc4a6987 | |||
9910e458f1 | |||
3abfe4c0e7 | |||
6ff8f24f10 | |||
ce8c6e66a5 |
12 changed files with 366 additions and 92 deletions
BIN
.qodo/history.sqlite
Normal file
BIN
.qodo/history.sqlite
Normal file
Binary file not shown.
17
README.md
17
README.md
|
@ -18,6 +18,17 @@ The application also generates a unique URL for the merged calendar and updates
|
||||||
- Generate a unique URL for the merged calendar
|
- Generate a unique URL for the merged calendar
|
||||||
- Automatically update the merged calendar every hour
|
- Automatically update the merged calendar every hour
|
||||||
|
|
||||||
|
## Calender Directory
|
||||||
|
|
||||||
|
#### `calendar/` (MERGED_CALENDARS_DIR)
|
||||||
|
- Location: Created in application's current working directory
|
||||||
|
- Purpose: Stores all generated calendar files
|
||||||
|
- Contains:
|
||||||
|
- `.ics` files - Final merged calendars in iCalendar format
|
||||||
|
- `.json` files - Configuration preserving original merge parameters
|
||||||
|
- Maintains both formats for each merged calendar group
|
||||||
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/) (version 14 or higher)
|
- [Node.js](https://nodejs.org/) (version 14 or higher)
|
||||||
|
@ -54,11 +65,11 @@ docker build -t calmerger-app .
|
||||||
To start the container, use:
|
To start the container, use:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d --name calmerger -p 3000:3000 calmerger-app
|
docker run -d --name calmerger -p 3012:3012 calmerger-app
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This maps the container's port `3000` to the host system's port `3000`. The application will be accessible at [http://localhost:3000](http://localhost:3000).
|
This maps the container's port `3012` to the host system's port `3012`. The application will be accessible at [http://localhost:3012](http://localhost:3012).
|
||||||
|
|
||||||
### 3. Using Docker Compose (Optional)
|
### 3. Using Docker Compose (Optional)
|
||||||
|
|
||||||
|
@ -114,7 +125,7 @@ This generates a `coverage` report, showing how much of the codebase is tested.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
1. Open a web browser and navigate to `http://localhost:3000`.
|
1. Open a web browser and navigate to `http://localhost:3012`.
|
||||||
2. Click the **Add Calendar** button to add a new calendar.
|
2. Click the **Add Calendar** button to add a new calendar.
|
||||||
3. Enter the Link Group Name, calendar URL, prefix, and override options (if needed).
|
3. Enter the Link Group Name, calendar URL, prefix, and override options (if needed).
|
||||||
4. Click the **Merge Calendars** button to generate the merged calendar.
|
4. Click the **Merge Calendars** button to generate the merged calendar.
|
||||||
|
|
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
version: "3.3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
calmerge:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "3012:3012"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- NODE_PORT=3012
|
||||||
|
volumes:
|
||||||
|
- ./calendar:/app/calendar
|
||||||
|
- ./logs:/app/logs
|
||||||
|
restart: unless-stopped
|
|
@ -8,13 +8,13 @@ WORKDIR /usr/src/app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN npm install --production
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
# Copy the rest of the project files
|
# Copy the rest of the project files
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Expose the port your application runs on (if applicable)
|
# Expose the port your application runs on (if applicable)
|
||||||
EXPOSE 3000
|
EXPOSE 3012
|
||||||
|
|
||||||
# Command to run the application
|
# Command to run the application
|
||||||
CMD ["node", "src/app.js"]
|
CMD ["node", "src/app.js"]
|
||||||
|
|
39
package-lock.json
generated
39
package-lock.json
generated
|
@ -22,6 +22,7 @@
|
||||||
"@babel/plugin-transform-modules-commonjs": "^7.25.9",
|
"@babel/plugin-transform-modules-commonjs": "^7.25.9",
|
||||||
"@babel/preset-env": "^7.26.0",
|
"@babel/preset-env": "^7.26.0",
|
||||||
"@babel/register": "^7.25.9",
|
"@babel/register": "^7.25.9",
|
||||||
|
"axios-mock-adapter": "^2.1.0",
|
||||||
"babel-jest": "^29.7.0",
|
"babel-jest": "^29.7.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"rewire": "^7.0.0"
|
"rewire": "^7.0.0"
|
||||||
|
@ -2679,6 +2680,20 @@
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios-mock-adapter": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-AZUe4OjECGCNNssH8SOdtneiQELsqTsat3SQQCWLPjN436/H+L9AjWfV7bF+Zg/YL9cgbhrz5671hoh+Tbn98w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"is-buffer": "^2.0.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"axios": ">= 0.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-jest": {
|
"node_modules/babel-jest": {
|
||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||||
|
@ -4502,6 +4517,30 @@
|
||||||
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
|
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/is-buffer": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-core-module": {
|
"node_modules/is-core-module": {
|
||||||
"version": "2.15.1",
|
"version": "2.15.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
"@babel/plugin-transform-modules-commonjs": "^7.25.9",
|
"@babel/plugin-transform-modules-commonjs": "^7.25.9",
|
||||||
"@babel/preset-env": "^7.26.0",
|
"@babel/preset-env": "^7.26.0",
|
||||||
"@babel/register": "^7.25.9",
|
"@babel/register": "^7.25.9",
|
||||||
|
"axios-mock-adapter": "^2.1.0",
|
||||||
"babel-jest": "^29.7.0",
|
"babel-jest": "^29.7.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"rewire": "^7.0.0"
|
"rewire": "^7.0.0"
|
||||||
|
|
|
@ -4,45 +4,47 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Calendar Merger</title>
|
<title>Calendar Merger</title>
|
||||||
<style>
|
<link rel="stylesheet" href="styles.css">
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
}
|
|
||||||
#calendars {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
#calendars .calendar {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
#calendars .calendar input[type="text"] {
|
|
||||||
width: 50%;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
#calendars .calendar input[type="url"] {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
#add-calendar {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Calendar Merger</h1>
|
<div class="container">
|
||||||
<form id="merge-form">
|
<h1>📅 Calendar Merger</h1>
|
||||||
<input type="text" id="link-group-name" placeholder="Link Group Name">
|
|
||||||
<div id="calendars">
|
<div class="form-card">
|
||||||
<div class="calendar">
|
<form id="merge-form">
|
||||||
<input type="text" id="prefix-0" placeholder="Prefix">
|
<div class="input-group">
|
||||||
|
<input type="text"
|
||||||
|
id="link-group-name"
|
||||||
|
placeholder="Enter collection name"
|
||||||
|
class="input-field">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="calendars">
|
||||||
|
<div class="calendar-entry">
|
||||||
|
<input type="url" id="url-0" placeholder="https://example.com/calendar.ics">
|
||||||
|
<input type="text" id="prefix-0" placeholder="Event prefix">
|
||||||
|
<div class="checkbox-group">
|
||||||
<input type="checkbox" id="override-0">
|
<input type="checkbox" id="override-0">
|
||||||
<label for="override-0">Override</label>
|
<label for="override-0">Override</label>
|
||||||
<input type="url" id="url-0" placeholder="Calendar URL">
|
</div>
|
||||||
|
<button type="button" class="remove-btn" title="Remove calendar"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="add-calendar" type="button">Add Calendar</button>
|
|
||||||
<button type="submit">Merge Calendars</button>
|
<div class="button-group">
|
||||||
</form>
|
<button type="button" id="add-calendar" class="button secondary-btn">
|
||||||
<div id="result"></div>
|
➕ Add Another Calendar
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="button primary-btn">
|
||||||
|
🔗 Merge Calendars
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="script.js"></script>
|
<script src="script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -14,12 +14,15 @@ function isValidUrl(url) {
|
||||||
|
|
||||||
addCalendarButton.addEventListener('click', () => {
|
addCalendarButton.addEventListener('click', () => {
|
||||||
const newCalendar = document.createElement('div');
|
const newCalendar = document.createElement('div');
|
||||||
newCalendar.className = 'calendar';
|
newCalendar.className = 'calendar-entry';
|
||||||
newCalendar.innerHTML = `
|
newCalendar.innerHTML = `
|
||||||
<input type="text" id="prefix-${calendarIndex}" placeholder="Prefix">
|
<input type="url" id="url-${calendarIndex}" placeholder="https://example.com/calendar.ics">
|
||||||
<input type="checkbox" id="override-${calendarIndex}">
|
<input type="text" id="prefix-${calendarIndex}" placeholder="Event prefix">
|
||||||
<label for="override-${calendarIndex}">Override</label>
|
<div class="checkbox-group">
|
||||||
<input type="url" id="url-${calendarIndex}" placeholder="Calendar URL">
|
<input type="checkbox" id="override-${calendarIndex}">
|
||||||
|
<label for="override-${calendarIndex}">Override</label>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="remove-btn" title="Remove calendar"></button>
|
||||||
`;
|
`;
|
||||||
calendars.appendChild(newCalendar);
|
calendars.appendChild(newCalendar);
|
||||||
calendarIndex++;
|
calendarIndex++;
|
||||||
|
@ -75,3 +78,12 @@ function isValidUrl(url) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
if (event.target.classList.contains('remove-btn')) {
|
||||||
|
const calendarEntry = event.target.closest('.calendar-entry');
|
||||||
|
if (calendarEntry) {
|
||||||
|
calendarEntry.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -1,37 +1,159 @@
|
||||||
body {
|
:root {
|
||||||
font-family: Arial, sans-serif;
|
--primary: #2563eb;
|
||||||
}
|
--primary-hover: #1d4ed8;
|
||||||
|
--background: #f8fafc;
|
||||||
#merge-form {
|
--surface: #ffffff;
|
||||||
max-width: 400px;
|
--border: #e2e8f0;
|
||||||
margin: 40px auto;
|
--text: #1e293b;
|
||||||
padding: 20px;
|
}
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 10px;
|
body {
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||||
}
|
background: var(--background);
|
||||||
|
color: var(--text);
|
||||||
label {
|
line-height: 1.6;
|
||||||
display: block;
|
margin: 0;
|
||||||
margin-bottom: 10px;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="url"], input[type="text"] {
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Styling */
|
||||||
|
.form-card {
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Entry Styling */
|
||||||
|
.calendar-entry {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--background);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-entry:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Elements */
|
||||||
|
input[type="text"],
|
||||||
|
input[type="url"] {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
transition: border-color 0.2s ease;
|
||||||
margin-bottom: 20px;
|
}
|
||||||
border: 1px solid #ccc;
|
|
||||||
}
|
input:focus {
|
||||||
|
outline: none;
|
||||||
button[type="submit"] {
|
border-color: var(--primary);
|
||||||
background-color: #4CAF50;
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||||
color: #fff;
|
}
|
||||||
padding: 10px 20px;
|
|
||||||
|
/* Checkbox Styling */
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="checkbox"] {
|
||||||
|
width: 1.2em;
|
||||||
|
height: 1.2em;
|
||||||
|
accent-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Styling */
|
||||||
|
.button {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
font-weight: 600;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn:hover {
|
||||||
|
background: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-btn {
|
||||||
|
background: var(--background);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Result Display */
|
||||||
|
#result {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border: 2px dashed var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
#result a {
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.calendar-entry {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button[type="submit"]:hover {
|
.remove-btn {
|
||||||
background-color: #3e8e41;
|
background: #fef2f2;
|
||||||
}
|
color: #dc2626;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn:hover {
|
||||||
|
background: #fee2e2;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn::before {
|
||||||
|
content: '×';
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import server from './server.js';
|
import server from './server.js';
|
||||||
|
|
||||||
const port = process.env.NODE_PORT || 3000;
|
const port = process.env.NODE_PORT || 3012;
|
||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
console.log(`Server started on port ${port}`);
|
console.log(`Server started on port ${port}`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,4 +282,60 @@ describe('Calendar Merging API', () => {
|
||||||
expect(actualOutput).toBe(expectedOutput);
|
expect(actualOutput).toBe(expectedOutput);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Smart URL Handling', () => {
|
||||||
|
let mockAxios;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
mockAxios = new MockAdapter(axios);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockAxios.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
mockAxios.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should use original URL when valid without .ics', async () => {
|
||||||
|
const validUrl = 'https://cals.ftt.gmbh/calendar/germanholithunder';
|
||||||
|
mockAxios.onGet(validUrl).reply(200, 'VALID_CALENDAR');
|
||||||
|
|
||||||
|
const result = await fetchCalendarData({ url: validUrl });
|
||||||
|
expect(result.data).toBe('VALID_CALENDAR');
|
||||||
|
});
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue