Skip to content

Commit

Permalink
Added tests, added STOP_WHEN_FOUND as environment variable to either …
Browse files Browse the repository at this point in the history
…stop ar continue the script
  • Loading branch information
renekann committed Jun 1, 2023
1 parent 87ea62c commit 6223c4b
Show file tree
Hide file tree
Showing 10 changed files with 3,255 additions and 111 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ APPOINTMENT_URL=
TIMESPAN_DAYS=30
SLACK_WEBHOOK_URL=
SCHEDULE=*/30 * * * *
STOP_WHEN_FOUND=false
DOCTOR_BOOKING_URL=
15 changes: 15 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,22 @@ env:
IMAGE_NAME: doctolib-appointment-finder

jobs:
run-tests:
runs-on: ubuntu-latest
environment: prod
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '14' # specify your node version
- name: Install dependencies
run: yarn install # or npm install
- name: Run tests
run: yarn test # or npm run test

docker-build-and-push:
needs: run-tests
runs-on: ubuntu-latest
environment: prod

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Doctolib Appointment Finder

The Doctolib Appointment Finder is a Node.js application which checks for available appointments for a specific doctor on Doctolib and notifies you via a Slack message if there's an available appointment within a given timespan.
The Doctolib Appointment Finder is a Node.js application which checks for available appointments for a specific doctor on Doctolib and notifies you via a Slack message if there's an available appointment within a given timespan. It also gives an option to automatically stop checking once an appointment is found.

## Setup

Expand All @@ -26,6 +26,7 @@ The configuration is done using environment variables, which can be set in the `
- `SCHEDULE`: A cron expression that defines how often the availability of appointments should be checked.
- `SLACK_WEBHOOK_URL`: The URL of your Slack incoming webhook. This will be used to send notifications.
- `DOCTOR_BOOKING_URL`: The URL of the doctor's booking page. This is optional, but can be used to provide a direct booking link in the Slack notification.
- `STOP_WHEN_FOUND`: (optional) If set to 'true', the script will automatically stop checking for new appointments once an appointment is found. Default is 'false', which means the script will keep running and checking for appointments even after one is found.

## Extracting the APPOINTMENT_URL

Expand All @@ -40,4 +41,4 @@ Follow these steps to extract the `APPOINTMENT_URL`:
5. In the filter box, type `availabilities.json`.
6. From the list of `availabilities.json` requests, find the one whose Request URL contains `start_date`.
7. Click on that `availabilities.json` request in the list.
8. Copy the Request URL. This is your `APPOINTMENT_URL`. The `start_date` value in the URL will be replaced with today's date when the script runs.
8. Copy the Request URL. This is your `APPOINTMENT_URL`. The `start_date` value in the URL will be replaced with today's date when the script runs.
6 changes: 6 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-typescript',
],
};
100 changes: 100 additions & 0 deletions index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import axios from 'axios';
import moment from 'moment';
import {fetchAvailabilities, availableAppointment, sendSlackNotification} from './index';

jest.mock('axios');

// Mock the environment variables
let originalEnv: NodeJS.ProcessEnv;

beforeEach(() => {
// Store the original environment variables
originalEnv = { ...process.env };

// Override environment variables for test
process.env.APPOINTMENT_URL = 'http://test.com/availabilities.json?start_date=2020-01-01';
process.env.TIMESPAN_DAYS = '7';
process.env.SCHEDULE = '* * * * *';
process.env.SLACK_WEBHOOK_URL = 'http://test.com/slack-webhook-url';
process.env.DOCTOR_BOOKING_URL = 'http://test.com/doctor-booking-url';
});

afterEach(() => {
// Restore the original environment variables
process.env = originalEnv;
});

describe("fetchAvailabilities", () => {
test("Fetches availabilities from API and returns available dates", async () => {
// setup
const expectedDate = moment().add(1, 'days').format('YYYY-MM-DD');
const url = 'http://test.com/availabilities.json?start_date=2020-01-01';

(axios.get as jest.MockedFunction<typeof axios.get>).mockResolvedValueOnce({
data: { next_slot: expectedDate }
});

const today = moment().format('YYYY-MM-DD');
const replacedUrl = url.replace(/start_date=\d{4}-\d{2}-\d{2}/, `start_date=${today}`);

// work
const dates = await fetchAvailabilities();

// expect
expect(dates).toEqual([expectedDate]);
expect(axios.get).toHaveBeenCalledWith(replacedUrl);
expect(axios.get).toHaveBeenCalledTimes(1);
});
});

describe("availableAppointment", () => {
test("Checks available dates and returns an appointment within timespan", async () => {
// setup
const now = moment().format('YYYY-MM-DD');
const futureDate = moment().add(3, 'days').format('YYYY-MM-DD');
const dates = [now, futureDate];

// work
const date = await availableAppointment(dates, 3);

// expect
expect(date).toEqual(now);
});

test("Returns empty string when no appointments are available", async () => {
// setup
const futureDate = moment().add(5, 'days').format('YYYY-MM-DD');
const dates = [futureDate];

// work
const date = await availableAppointment(dates, 3);

// expect
expect(date).toEqual('');
});
});

describe("sendSlackNotification", () => {
test("Sends a notification to Slack", async () => {
// setup
const date = moment().format('YYYY-MM-DD');
const message = {
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `:pill: An appointment is available on *${date}* :calendar:. You can book it here: http://test.com/doctor-booking-url`
}
},
]
};

// work
await sendSlackNotification(date);

// expect
expect(axios.post).toHaveBeenCalledTimes(1);
expect(axios.post).toHaveBeenCalledWith('http://test.com/slack-webhook-url', message);
});
});
38 changes: 22 additions & 16 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import cron, {ScheduledTask} from 'node-cron';

dotenv.config();

async function fetchAvailabilities(): Promise<string[]> {
export async function fetchAvailabilities(): Promise<string[]> {
try {
// Get the URL from the environment variable
let url = process.env.APPOINTMENT_URL || '';
Expand Down Expand Up @@ -51,7 +51,7 @@ async function fetchAvailabilities(): Promise<string[]> {
}
}

async function availableAppointment(dates: string[], timespan: number): Promise<string> {
export async function availableAppointment(dates: string[], timespan: number): Promise<string> {
// Check if the timespan is a valid number
if (isNaN(timespan)) {
throw new Error('The TIMESPAN_DAYS environment variable is not a valid number.');
Expand Down Expand Up @@ -80,7 +80,7 @@ async function availableAppointment(dates: string[], timespan: number): Promise<
return '';
}

async function sendSlackNotification(date: string): Promise<void> {
export async function sendSlackNotification(date: string): Promise<void> {
const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL;
const bookingUrl = process.env.DOCTOR_BOOKING_URL;

Expand Down Expand Up @@ -113,6 +113,7 @@ let task: ScheduledTask;
async function checkAppointmentAvailability() {
// Get the timespan from the environment variable
const timespan = Number(process.env.TIMESPAN_DAYS || '0');
const stopWhenFound = process.env.STOP_WHEN_FOUND?.toLowerCase() === 'true'

try {
const dates = await fetchAvailabilities();
Expand All @@ -125,7 +126,7 @@ async function checkAppointmentAvailability() {
});

// Stop the task once an appointment is found
if (task) {
if (task && stopWhenFound) {
console.log('Appointment found. Stopping the task...');
task.stop();
}
Expand All @@ -137,17 +138,22 @@ async function checkAppointmentAvailability() {
}
}

// Get the schedule from the environment variable
const schedule = process.env.SCHEDULE || '* * * * *';

// Schedule the function using node-cron
if (!cron.validate(schedule)) {
console.error('The SCHEDULE environment variable is not a valid cron expression.');
if (typeof jest !== 'undefined') {
console.log('Running in Jest environment');
} else {
console.log(`Scheduling appointment availability check every ${schedule}.`);
try {
task = cron.schedule(schedule, checkAppointmentAvailability);
} catch (error) {
console.error(`Error while scheduling appointment availability check: ${error}`);
// Get the schedule from the environment variable
const schedule = process.env.SCHEDULE || '* * * * *';

// Schedule the function using node-cron
if (!cron.validate(schedule)) {
console.error('The SCHEDULE environment variable is not a valid cron expression.');
} else {
console.log(`Scheduling appointment availability check every ${schedule}.`);
try {
task = cron.schedule(schedule, checkAppointmentAvailability);
} catch (error) {
console.error(`Error while scheduling appointment availability check: ${error}`);
}
}
}
}

15 changes: 15 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
"^.+\\.ts?$": "ts-jest"
},
moduleFileExtensions: [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
};
4 changes: 4 additions & 0 deletions mocks/axios.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default {
get: jest.fn(() => Promise.resolve({ data: {} })),
post: jest.fn(() => Promise.resolve({ data: {} })),
};
15 changes: 12 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "doctolib-appointment-finder",
"version": "1.0.1",
"version": "1.0.2",
"dependencies": {
"@types/node": "^20.2.5",
"@types/node-cron": "^3.0.7",
Expand All @@ -12,9 +12,18 @@
"typescript": "^5.0.4"
},
"scripts": {
"start": "ts-node index.ts"
"start": "ts-node index.ts",
"test": "jest"
},
"main": "index.js",
"author": "René Kann <[email protected]>",
"license": "MIT"
"license": "MIT",
"devDependencies": {
"@babel/preset-env": "^7.22.4",
"@babel/preset-typescript": "^7.21.5",
"@types/jest": "^29.5.2",
"jest": "^29.5.0",
"jest-mock-axios": "^4.7.2",
"ts-jest": "^29.1.0"
}
}
Loading

0 comments on commit 6223c4b

Please sign in to comment.