From 13460c7cb0363b7991f979bcef99a4a9a8a03d49 Mon Sep 17 00:00:00 2001 From: proshunsuke Date: Thu, 11 Mar 2021 19:28:27 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BB=96=E3=81=AE=E3=82=AB=E3=83=AC?= =?UTF-8?q?=E3=83=B3=E3=83=80=E3=83=BC=E3=82=92=E7=99=BB=E9=8C=B2=E5=87=BA?= =?UTF-8?q?=E6=9D=A5=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E6=8A=BD=E8=B1=A1?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 9 +-- gcpFunctions/getKeyakiSchedule.js | 10 ++- src/calendar.ts | 40 ++++++----- src/calendarInterface.ts | 13 ++++ src/{keyakizaka => }/fetchUrl.ts | 0 .../keyakiSchedule.ts => oneMonthSchedule.ts} | 47 ++++++------ src/schedule.ts | 41 +++-------- src/{ => sites}/keyakizaka/keyakiObjects.ts | 30 +++----- src/sites/keyakizaka/keyakiSiteSchedule.ts | 15 ++++ src/sites/siteSchedule.ts | 51 +++++++++++++ tests/calendar.test.ts | 71 ++++++++++++++----- ...edule.test.ts => oneMonthSchedule.test.ts} | 16 ++--- tests/schedule.test.ts | 12 ++-- .../keyakizaka/keyakiSitesSchedule.test.ts | 18 +++++ 14 files changed, 247 insertions(+), 126 deletions(-) create mode 100644 src/calendarInterface.ts rename src/{keyakizaka => }/fetchUrl.ts (100%) rename src/{keyakizaka/keyakiSchedule.ts => oneMonthSchedule.ts} (68%) rename src/{ => sites}/keyakizaka/keyakiObjects.ts (67%) create mode 100644 src/sites/keyakizaka/keyakiSiteSchedule.ts create mode 100644 src/sites/siteSchedule.ts rename tests/{keyakizaka/keyakiSchedule.test.ts => oneMonthSchedule.test.ts} (82%) create mode 100644 tests/sites/keyakizaka/keyakiSitesSchedule.test.ts diff --git a/.eslintrc.js b/.eslintrc.js index 4d08d8f..10fff79 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,11 +18,12 @@ module.exports = { 'prettier', ], rules: { - quotes: ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }], + quotes: ['error', 'single', {'avoidEscape': true, 'allowTemplateLiterals': false}], semi: ['error', 'always'], 'import/no-extraneous-dependencies': 'off', - 'no-void': ['error', { allowAsStatement: true }], - 'no-console': ['error', { allow: ['info', 'error'] }], - '@typescript-eslint/unbound-method': 'off' + 'no-void': ['error', {allowAsStatement: true}], + 'no-console': ['error', {allow: ['info', 'error']}], + '@typescript-eslint/unbound-method': 'off', + 'no-restricted-syntax': ['error', {allow: ['ForOfStatement']}] }, }; diff --git a/gcpFunctions/getKeyakiSchedule.js b/gcpFunctions/getKeyakiSchedule.js index 4e6f777..d661b74 100644 --- a/gcpFunctions/getKeyakiSchedule.js +++ b/gcpFunctions/getKeyakiSchedule.js @@ -15,7 +15,15 @@ exports.getKeyakiSchedule = async (req, res) => { await page.goto('https://www.keyakizaka46.com/s/k46o/media/list?dy=' + req.query['date']); - const result = await page.evaluate("scheduleEvents"); + const response = await page.evaluate("scheduleEvents"); + + const result = response.map((scheduleEvent) => { + return { + title: scheduleEvent.title, + date: scheduleEvent.start, + type: scheduleEvent.className + } + }); res.send(result); }; diff --git a/src/calendar.ts b/src/calendar.ts index ce6a1b6..f969fa7 100644 --- a/src/calendar.ts +++ b/src/calendar.ts @@ -1,8 +1,7 @@ import { - ScheduleObj, - KeyakiCalendarObj, - keyakiCalendarIds, -} from './keyakizaka/keyakiObjects'; + ScheduleInterface, + SiteCalendarInterface, +} from './calendarInterface'; import Retry from './lib/retry'; export default class Calendar { @@ -21,32 +20,41 @@ export default class Calendar { /** * - * @param {ScheduleObj} schedule + * @param {ScheduleInterface} schedule + * @param calendarIds */ - static createEvent(schedule: ScheduleObj): void { - const keyakiCalendarId: - | KeyakiCalendarObj - | undefined = keyakiCalendarIds.find( - (id) => id.kind === schedule.className + static createEvent(schedule: ScheduleInterface, calendarIds: SiteCalendarInterface[]): void { + const siteCalendarId: + | SiteCalendarInterface + | undefined = calendarIds.find( + (id) => id.type === schedule.type ); - if (typeof keyakiCalendarId === 'undefined') { + if (typeof siteCalendarId === 'undefined') { console.info('スケジュールの内容: '); console.info(schedule); throw new Error( - `存在しない種類のスケジュールです。className: ${schedule.className}` + `存在しない種類のスケジュールです。type: ${schedule.type}` ); } - const { calendarId } = keyakiCalendarId; + const { calendarId } = siteCalendarId; Retry.retryable(3, () => { - if (process.env.ENV === 'production') { + if (process.env.ENV !== 'production') return; + if (schedule.startTime && schedule.endTime) { + CalendarApp.getCalendarById(calendarId).createEvent( + schedule.title, + new Date(schedule.startTime), + new Date(schedule.endTime), + {description: schedule.description} + ); + } else { CalendarApp.getCalendarById(calendarId).createAllDayEvent( schedule.title, - new Date(schedule.start) + new Date(schedule.date) ); } }); console.info( - `予定を作成しました。日付: ${schedule.start}, タイトル: ${schedule.title}` + `予定を作成しました。日付: ${schedule.date}, タイトル: ${schedule.title}` ); if (process.env.ENV === 'production') { Utilities.sleep(500); // API制限に引っかかってそうなのでsleepする diff --git a/src/calendarInterface.ts b/src/calendarInterface.ts new file mode 100644 index 0000000..e6a3dc8 --- /dev/null +++ b/src/calendarInterface.ts @@ -0,0 +1,13 @@ +export interface ScheduleInterface { + title: string; + date: string; + startTime?: string; + endTime?: string; + description?: string; + type: string; +} + +export interface SiteCalendarInterface { + type: string; + calendarId: string; +} diff --git a/src/keyakizaka/fetchUrl.ts b/src/fetchUrl.ts similarity index 100% rename from src/keyakizaka/fetchUrl.ts rename to src/fetchUrl.ts diff --git a/src/keyakizaka/keyakiSchedule.ts b/src/oneMonthSchedule.ts similarity index 68% rename from src/keyakizaka/keyakiSchedule.ts rename to src/oneMonthSchedule.ts index 10e79a3..b5d885a 100644 --- a/src/keyakizaka/keyakiSchedule.ts +++ b/src/oneMonthSchedule.ts @@ -1,30 +1,30 @@ import dayjs from 'dayjs'; -import Calendar from '../calendar'; +import Calendar from './calendar'; import { - ScheduleObj, - KeyakiCalendarObj, - getKeyakiCalendarUrl, - keyakiCalendarIds, -} from './keyakiObjects'; -import Retry from '../lib/retry'; + ScheduleInterface, + SiteCalendarInterface, +} from './calendarInterface'; +import Retry from './lib/retry'; import 'regenerator-runtime'; -export default class KeyakiSchedule { +export default class OneMonthSchedule { /** * * @param {dayjs.Dayjs} date + * @param calendarUrl + * @param siteCalendarIds * @returns {Promise} */ - static async setSchedule(date: dayjs.Dayjs): Promise { - const customUrl: string = getKeyakiCalendarUrl + date.format('YYYYMMDD'); + static async setSchedule(date: dayjs.Dayjs, calendarUrl: string, siteCalendarIds: SiteCalendarInterface[]): Promise { + const customUrl: string = calendarUrl + date.format('YYYYMMDD'); - const scheduleJson = await KeyakiSchedule.getScheduleJson(customUrl); + const scheduleJson = await OneMonthSchedule.getScheduleJson(customUrl); - const scheduleList = JSON.parse(scheduleJson) as ScheduleObj[]; + const scheduleList = JSON.parse(scheduleJson) as ScheduleInterface[]; console.info(`${date.format('YYYY年MM月')}分の予定を更新します`); - KeyakiSchedule.delete1MonthCalendarEvents(date); - KeyakiSchedule.create1MonthEvents(scheduleList, date); + OneMonthSchedule.delete1MonthCalendarEvents(date, siteCalendarIds); + OneMonthSchedule.create1MonthEvents(scheduleList, date, siteCalendarIds); console.info(`${date.format('YYYY年MM月')}分の予定を更新しました`); } @@ -50,13 +50,14 @@ export default class KeyakiSchedule { /** * * @param {dayjs.Dayjs} date + * @param siteCalendarIds */ - private static delete1MonthCalendarEvents(date: dayjs.Dayjs) { + private static delete1MonthCalendarEvents(date: dayjs.Dayjs, siteCalendarIds: SiteCalendarInterface[]) { let deleteEventCallCount = 0; - keyakiCalendarIds.forEach((keyakiCalendarObj: KeyakiCalendarObj) => { + siteCalendarIds.forEach((siteCalendarId) => { if (process.env.ENV !== 'production') return; const calendarApp = Retry.retryable(3, () => - CalendarApp.getCalendarById(keyakiCalendarObj.calendarId) + CalendarApp.getCalendarById(siteCalendarId.calendarId) ); const targetDateBeginningOfMonth = date; const targetDateBeginningOfNextMonth = date.add(1, 'month'); @@ -87,18 +88,20 @@ export default class KeyakiSchedule { /** * - * @param {ScheduleObj[]} scheduleList + * @param {ScheduleInterface[]} scheduleList * @param {dayjs.Dayjs} date + * @param calendarIds */ private static create1MonthEvents( - scheduleList: ScheduleObj[], - date: dayjs.Dayjs + scheduleList: ScheduleInterface[], + date: dayjs.Dayjs, + calendarIds: SiteCalendarInterface[] ) { let createEventCallCount = 0; - scheduleList.forEach((schedule: ScheduleObj) => { + scheduleList.forEach((schedule: ScheduleInterface) => { createEventCallCount += 1; try { - Calendar.createEvent(schedule); + Calendar.createEvent(schedule, calendarIds); } catch (e) { console.error( `カレンダー作成に失敗しました。失敗するまでに実行された回数: ${createEventCallCount.toString()}` diff --git a/src/schedule.ts b/src/schedule.ts index 152ca3d..a709753 100644 --- a/src/schedule.ts +++ b/src/schedule.ts @@ -1,39 +1,14 @@ -import dayjs from 'dayjs'; -import KeyakiSchedule from './keyakizaka/keyakiSchedule'; -import Trigger, { TERMINATION_MINUTES } from './lib/trigger'; +import KeyakiSiteSchedule from './sites/keyakizaka/keyakiSiteSchedule'; +import {SiteScheduleInterface} from './sites/siteSchedule'; export default class Schedule { static async setSchedule(): Promise { - const beginningOfNexYearMonth = dayjs().startOf('month').add(1, 'year'); - let targetBeginningOfMonth = Schedule.getTargetBeginningOfMonth(); - const startDate = dayjs(); - - while (targetBeginningOfMonth.isBefore(beginningOfNexYearMonth)) { - // eslint-disable-next-line no-await-in-loop - await KeyakiSchedule.setSchedule(targetBeginningOfMonth); - targetBeginningOfMonth = targetBeginningOfMonth.add(1, 'month'); - if (Trigger.hasExceededTerminationMinutes(startDate)) { - Trigger.setTrigger(targetBeginningOfMonth); - console.info( - `${TERMINATION_MINUTES}分以上経過したので次のトリガーをセットして終了します。次実行開始する月: ${targetBeginningOfMonth.format( - 'YYYY-MM-DD' - )}` - ); - return; - } + const siteScheduleList: SiteScheduleInterface[] = [ + new KeyakiSiteSchedule, + // new KeyakiSiteSchedule + ]; + for await(const siteSchedule of siteScheduleList) { + await siteSchedule.setSiteSchedule(); } - Trigger.deleteTargetDateProperty(); - Trigger.deleteTriggers(); - } - - /** - * - * @returns {dayjs.Dayjs} - */ - private static getTargetBeginningOfMonth(): dayjs.Dayjs { - const targetDateStr: string | null = Trigger.getTargetDateProperty(); - return targetDateStr - ? dayjs(targetDateStr).startOf('month') - : dayjs().startOf('month'); } } diff --git a/src/keyakizaka/keyakiObjects.ts b/src/sites/keyakizaka/keyakiObjects.ts similarity index 67% rename from src/keyakizaka/keyakiObjects.ts rename to src/sites/keyakizaka/keyakiObjects.ts index b4cfa6a..8fe7860 100644 --- a/src/keyakizaka/keyakiObjects.ts +++ b/src/sites/keyakizaka/keyakiObjects.ts @@ -1,51 +1,41 @@ -export interface ScheduleObj { - title: string; - start: string; - description: string; - className: string; -} - -export interface KeyakiCalendarObj { - kind: string; - calendarId: string; -} +import {SiteCalendarInterface} from '../../calendarInterface'; export const getKeyakiCalendarUrl = process.env.ENV === 'production' ? 'https://us-central1-augc-260709.cloudfunctions.net/getKeyakiSchedule?date=' : 'http://localhost:8080?date='; -export const keyakiCalendarIds: KeyakiCalendarObj[] = [ +export const keyakiCalendarIds: SiteCalendarInterface[] = [ { - kind: 'shakehands', + type: 'shakehands', calendarId: 'jdnc8uf21242be7qjm5nmj7uok@group.calendar.google.com', }, { - kind: 'event', + type: 'event', calendarId: 'eh0boh68ai7r2v15m38k2ms1lg@group.calendar.google.com', }, { - kind: 'goods', + type: 'goods', calendarId: '8l4srrnd9c6vge51k6cclsdsmc@group.calendar.google.com', }, { - kind: 'release', + type: 'release', calendarId: '8tc88j0j9gmr95qa81r8t2210c@group.calendar.google.com', }, { - kind: 'ticket', + type: 'ticket', calendarId: 'f4bcp8sqv66sugk9m06gb1ioeg@group.calendar.google.com', }, { - kind: 'media', + type: 'media', calendarId: '9beck0tqd2096b3b5utkh0jg8g@group.calendar.google.com', }, { - kind: 'birthday', + type: 'birthday', calendarId: 'lihum5fsldhsspa3r8altr01ns@group.calendar.google.com', }, { - kind: 'other', + type: 'other', calendarId: 'efhfvac7iii073suf8v16tlmic@group.calendar.google.com', }, ]; diff --git a/src/sites/keyakizaka/keyakiSiteSchedule.ts b/src/sites/keyakizaka/keyakiSiteSchedule.ts new file mode 100644 index 0000000..ffff478 --- /dev/null +++ b/src/sites/keyakizaka/keyakiSiteSchedule.ts @@ -0,0 +1,15 @@ +import SiteSchedule from '../siteSchedule'; +import {getKeyakiCalendarUrl, keyakiCalendarIds} from './keyakiObjects'; +import {SiteCalendarInterface} from '../../calendarInterface'; + +export default class KeyakiSiteSchedule extends SiteSchedule { + // eslint-disable-next-line class-methods-use-this + siteCalendarUrl(): string { + return getKeyakiCalendarUrl; + } + + // eslint-disable-next-line class-methods-use-this + siteCalendarIds(): SiteCalendarInterface[] { + return keyakiCalendarIds; + } +} diff --git a/src/sites/siteSchedule.ts b/src/sites/siteSchedule.ts new file mode 100644 index 0000000..481fb65 --- /dev/null +++ b/src/sites/siteSchedule.ts @@ -0,0 +1,51 @@ +import dayjs from 'dayjs'; +import OneMonthSchedule from '../oneMonthSchedule'; +import Trigger, { TERMINATION_MINUTES } from '../lib/trigger'; +import {SiteCalendarInterface} from '../calendarInterface'; + +export interface SiteScheduleInterface { + setSiteSchedule(): Promise; + siteCalendarUrl(): string; + siteCalendarIds(): SiteCalendarInterface[]; +} + +export default abstract class SiteSchedule implements SiteScheduleInterface{ + async setSiteSchedule(): Promise { + const beginningOfNexYearMonth = dayjs().startOf('month').add(1, 'year'); + let targetBeginningOfMonth = SiteSchedule.getTargetBeginningOfMonth(); + const startDate = dayjs(); + + while (targetBeginningOfMonth.isBefore(beginningOfNexYearMonth)) { + // eslint-disable-next-line no-await-in-loop + await OneMonthSchedule.setSchedule(targetBeginningOfMonth, this.siteCalendarUrl(), this.siteCalendarIds()); + targetBeginningOfMonth = targetBeginningOfMonth.add(1, 'month'); + if (Trigger.hasExceededTerminationMinutes(startDate)) { + Trigger.setTrigger(targetBeginningOfMonth); + console.info( + `${TERMINATION_MINUTES}分以上経過したので次のトリガーをセットして終了します。次実行開始する月: ${targetBeginningOfMonth.format( + 'YYYY-MM-DD' + )}` + ); + return; + } + } + Trigger.deleteTargetDateProperty(); + Trigger.deleteTriggers(); + } + + /** + * + * @returns {dayjs.Dayjs} + */ + private static getTargetBeginningOfMonth(): dayjs.Dayjs { + const targetDateStr: string | null = Trigger.getTargetDateProperty(); + return targetDateStr + ? dayjs(targetDateStr).startOf('month') + : dayjs().startOf('month'); + } + + abstract siteCalendarUrl(): string; + + abstract siteCalendarIds(): SiteCalendarInterface[]; +} + diff --git a/tests/calendar.test.ts b/tests/calendar.test.ts index a3f3d88..65db5f0 100644 --- a/tests/calendar.test.ts +++ b/tests/calendar.test.ts @@ -1,35 +1,51 @@ import Calendar from '../src/calendar'; -import { ScheduleObj } from '../src/keyakizaka/keyakiObjects'; +import { ScheduleInterface } from '../src/calendarInterface'; +import {keyakiCalendarIds} from '../src/sites/keyakizaka/keyakiObjects'; /** * * @param {string | undefined} title - * @param {string | undefined} start + * @param {string | undefined} date * @param {string | undefined} description - * @param {string | undefined} className - * @returns {ScheduleObj} + * @param {string | undefined} type + * @param startTime + * @param endTime + * @returns {ScheduleInterface} */ function defaultSchedule({ title = 'タイトル', - start = '2019-12-01', + date = '2019-12-01', description = '内容', - className = 'media', + type = 'media', + startTime = '2019-12-01 20:00:00', + endTime = '2019-12-01 22:00:00', }: { title?: string; - start?: string; + date?: string; description?: string; - className?: string; -}): ScheduleObj { + type?: string; + startTime?: string; + endTime?: string; +}): ScheduleInterface { return { title, - start, + date, description, - className, + type, + startTime, + endTime, }; } +const OLD_ENV = process.env; + beforeEach(() => { jest.spyOn(console, 'info').mockImplementation(); + process.env = { ...OLD_ENV }; +}); + +afterAll(() => { + process.env = OLD_ENV; }); describe('deleteEvent', (): void => { @@ -46,8 +62,10 @@ describe('deleteEvent', (): void => { }); describe('createEvent', (): void => { - describe('正常な値が引数に渡された場合', (): void => { + describe('期間の無い予定の場合', (): void => { const schedule = defaultSchedule({}); + delete schedule.startTime; + delete schedule.endTime; it('createAllDayEventが正常に呼ばれること', (): void => { const createAllDayEventMock = jest.fn().mockReturnThis(); @@ -55,16 +73,37 @@ describe('createEvent', (): void => { createAllDayEvent: createAllDayEventMock, })) as jest.Mock; Utilities.sleep = jest.fn().mockReturnThis(); - Calendar.createEvent(schedule); + Calendar.createEvent(schedule, keyakiCalendarIds); expect(createAllDayEventMock).toBeCalledTimes(1); }); }); + describe('期間のある予定の場合', (): void => { + const schedule = defaultSchedule({}); + it('createEventが正常に呼ばれること', (): void => { + const createEventMock = jest.fn().mockReturnThis(); + + CalendarApp.getCalendarById = jest.fn(() => ({ + createEvent: createEventMock, + })) as jest.Mock; + Utilities.sleep = jest.fn().mockReturnThis(); + Calendar.createEvent(schedule, keyakiCalendarIds); + expect(createEventMock).toBeCalledTimes(1); + }); + }); describe('存在しない種類の予定の場合', (): void => { - const schedule = defaultSchedule({ className: 'none' }); + const schedule = defaultSchedule({ type: 'none' }); it('例外が投げられること', (): void => { expect(() => { - Calendar.createEvent(schedule); - }).toThrow('存在しない種類のスケジュールです。className: none'); + Calendar.createEvent(schedule, keyakiCalendarIds); + }).toThrow('存在しない種類のスケジュールです。type: none'); }); }); + it('production環境では無かった場合にCalendarApp.getCalendarByIdが呼ばれないこと', () => { + process.env.ENV = 'local'; + CalendarApp.getCalendarById = jest.fn(); + + const schedule = defaultSchedule({}); + Calendar.createEvent(schedule, keyakiCalendarIds); + expect(CalendarApp.getCalendarById).not.toBeCalled(); + }); }); diff --git a/tests/keyakizaka/keyakiSchedule.test.ts b/tests/oneMonthSchedule.test.ts similarity index 82% rename from tests/keyakizaka/keyakiSchedule.test.ts rename to tests/oneMonthSchedule.test.ts index 7d99675..d9b8772 100644 --- a/tests/keyakizaka/keyakiSchedule.test.ts +++ b/tests/oneMonthSchedule.test.ts @@ -1,14 +1,14 @@ import dayjs from 'dayjs'; import fetchMock from 'jest-fetch-mock'; -import KeyakiSchedule from '../../src/keyakizaka/keyakiSchedule'; -import Calendar from '../../src/calendar'; -import { keyakiCalendarIds } from '../../src/keyakizaka/keyakiObjects'; +import OneMonthSchedule from '../src/oneMonthSchedule'; +import Calendar from '../src/calendar'; +import {getKeyakiCalendarUrl, keyakiCalendarIds} from '../src/sites/keyakizaka/keyakiObjects'; function getScheduleJson() { return '[{"title":"欅坂46 こちら有楽町星空放送局","start":"2019-12-01","className":"media","description":"欅坂46 こちら有楽町星空放送局"},{"title":"テレビ東京「欅って、書けない?」","start":"2019-12-01","className":"media","description":"テレビ東京「欅って、書けない?」"}]'; } -jest.mock('../../src/calendar'); +jest.mock('../src/calendar'); describe('setSchedule', () => { const OLD_ENV = process.env; beforeEach(() => { @@ -35,7 +35,7 @@ describe('setSchedule', () => { })) as jest.Mock; const date = dayjs('2019-12-01'); - await expect(KeyakiSchedule.setSchedule(date)).resolves.not.toThrow(); + await expect(OneMonthSchedule.setSchedule(date, getKeyakiCalendarUrl, keyakiCalendarIds)).resolves.not.toThrow(); expect(Calendar.deleteEvent).toBeCalledTimes( keyakiCalendarIds.length * date.endOf('month').date() ); @@ -61,7 +61,7 @@ describe('setSchedule', () => { }); const date = dayjs('2019-12-01'); - await expect(KeyakiSchedule.setSchedule(date)).rejects.toThrow(); + await expect(OneMonthSchedule.setSchedule(date, getKeyakiCalendarUrl, keyakiCalendarIds)).rejects.toThrow(); expect(Calendar.createEvent).not.toBeCalled(); }); it('カレンダーの作成に失敗した場合に例外が起きて後続の処理が止まること', async () => { @@ -83,7 +83,7 @@ describe('setSchedule', () => { }); const date = dayjs('2019-12-01'); - await expect(KeyakiSchedule.setSchedule(date)).rejects.toThrow(); + await expect(OneMonthSchedule.setSchedule(date, getKeyakiCalendarUrl, keyakiCalendarIds)).rejects.toThrow(); }); it('production環境では無かった場合にUrlFetchApp.fetchが呼ばれないこと', async () => { process.env.ENV = 'local'; @@ -91,7 +91,7 @@ describe('setSchedule', () => { fetchMock.mockOnce(getScheduleJson()); const date = dayjs('2019-12-01'); - await KeyakiSchedule.setSchedule(date); + await OneMonthSchedule.setSchedule(date, getKeyakiCalendarUrl, keyakiCalendarIds); expect(UrlFetchApp.fetch).not.toBeCalled(); }); }); diff --git a/tests/schedule.test.ts b/tests/schedule.test.ts index 72609f9..e31a8dc 100644 --- a/tests/schedule.test.ts +++ b/tests/schedule.test.ts @@ -1,9 +1,9 @@ import MockDate from 'mockdate'; import Schedule from '../src/schedule'; -import KeyakiSchedule from '../src/keyakizaka/keyakiSchedule'; +import OneMonthSchedule from '../src/oneMonthSchedule'; import Trigger from '../src/lib/trigger'; -jest.mock('../src/keyakizaka/keyakiSchedule'); +jest.mock('../src/oneMonthSchedule'); jest.mock('../src/lib/trigger'); describe('setSchedule', (): void => { beforeEach(() => { @@ -13,12 +13,12 @@ describe('setSchedule', (): void => { }); it('setScheduleが12回呼ばれ1年分の予定が作成されること', async () => { await Schedule.setSchedule(); - expect(KeyakiSchedule.setSchedule).toBeCalledTimes(12); + expect(OneMonthSchedule.setSchedule).toBeCalledTimes(12); }); it('propertiesに日付がセットされていた場合にその日付からスケジュール登録が始まること', async () => { Trigger.getTargetDateProperty = jest.fn().mockReturnValueOnce('2020-01-01'); await Schedule.setSchedule(); - expect(KeyakiSchedule.setSchedule).toBeCalledTimes(11); + expect(OneMonthSchedule.setSchedule).toBeCalledTimes(11); }); it('実行時間が指定時間を過ぎていたらトリガーがセットされ処理が終わること', async () => { Trigger.hasExceededTerminationMinutes = jest @@ -27,7 +27,7 @@ describe('setSchedule', (): void => { .mockReturnValue(true); Trigger.setTrigger = jest.fn().mockReturnThis(); await Schedule.setSchedule(); - expect(KeyakiSchedule.setSchedule).toBeCalledTimes(2); + expect(OneMonthSchedule.setSchedule).toBeCalledTimes(2); expect(Trigger.setTrigger).toBeCalledTimes(1); }); it('指定時間内に全て実行出来たらdeleteTargetDatePropertyとdeleteTriggersが呼ばれること', async () => { @@ -36,7 +36,7 @@ describe('setSchedule', (): void => { Trigger.deleteTargetDateProperty = jest.fn().mockReturnThis(); Trigger.deleteTriggers = jest.fn().mockReturnThis(); await Schedule.setSchedule(); - expect(KeyakiSchedule.setSchedule).toBeCalledTimes(12); + expect(OneMonthSchedule.setSchedule).toBeCalledTimes(12); expect(Trigger.setTrigger).not.toBeCalled(); expect(Trigger.deleteTargetDateProperty).toBeCalledTimes(1); expect(Trigger.deleteTriggers).toBeCalledTimes(1); diff --git a/tests/sites/keyakizaka/keyakiSitesSchedule.test.ts b/tests/sites/keyakizaka/keyakiSitesSchedule.test.ts new file mode 100644 index 0000000..2ed24ec --- /dev/null +++ b/tests/sites/keyakizaka/keyakiSitesSchedule.test.ts @@ -0,0 +1,18 @@ +import MockDate from 'mockdate'; +import KeyakiSiteSchedule from '../../../src/sites/keyakizaka/keyakiSiteSchedule'; +import OneMonthSchedule from '../../../src/oneMonthSchedule'; + +jest.mock('../../../src/oneMonthSchedule'); +jest.mock('../../../src/lib/trigger'); +describe('setSiteSchedule', (): void => { + beforeEach(() => { + MockDate.set(new Date('2019-12-10')); + jest.spyOn(console, 'info').mockImplementation(); + jest.resetAllMocks(); + }); + it('setScheduleが12回呼ばれ1年分の予定が作成されること', async () => { + const keyakiSiteSchedule = new KeyakiSiteSchedule(); + await keyakiSiteSchedule.setSiteSchedule(); + expect(OneMonthSchedule.setSchedule).toBeCalledTimes(12); + }); +}); From 1694b1ddf68534c18f2fa1b9608698b890f6a56e Mon Sep 17 00:00:00 2001 From: proshunsuke Date: Thu, 11 Mar 2021 19:34:01 +0900 Subject: [PATCH 2/2] =?UTF-8?q?linter=E3=81=AE=E4=BF=AE=E6=AD=A3=E3=81=A8?= =?UTF-8?q?=E3=82=B3=E3=83=BC=E3=83=89=E6=95=B4=E5=BD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 20 ++++++++++++- src/calendar.ts | 16 +++++----- src/oneMonthSchedule.ts | 16 ++++++---- src/schedule.ts | 6 ++-- src/sites/keyakizaka/keyakiObjects.ts | 2 +- src/sites/keyakizaka/keyakiSiteSchedule.ts | 4 +-- src/sites/siteSchedule.ts | 11 ++++--- tests/calendar.test.ts | 2 +- tests/oneMonthSchedule.test.ts | 35 ++++++++++++++++++---- 9 files changed, 80 insertions(+), 32 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 10fff79..809afa3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,6 +24,24 @@ module.exports = { 'no-void': ['error', {allowAsStatement: true}], 'no-console': ['error', {allow: ['info', 'error']}], '@typescript-eslint/unbound-method': 'off', - 'no-restricted-syntax': ['error', {allow: ['ForOfStatement']}] + 'no-restricted-syntax': [ + 'error', + { + selector: 'ForInStatement', + message: 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.', + }, + // { + // selector: 'ForOfStatement', + // message: 'iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations.', + // }, + { + selector: 'LabeledStatement', + message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', + }, + { + selector: 'WithStatement', + message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.', + }, + ], }, }; diff --git a/src/calendar.ts b/src/calendar.ts index f969fa7..e1faa72 100644 --- a/src/calendar.ts +++ b/src/calendar.ts @@ -1,7 +1,4 @@ -import { - ScheduleInterface, - SiteCalendarInterface, -} from './calendarInterface'; +import { ScheduleInterface, SiteCalendarInterface } from './calendarInterface'; import Retry from './lib/retry'; export default class Calendar { @@ -23,10 +20,11 @@ export default class Calendar { * @param {ScheduleInterface} schedule * @param calendarIds */ - static createEvent(schedule: ScheduleInterface, calendarIds: SiteCalendarInterface[]): void { - const siteCalendarId: - | SiteCalendarInterface - | undefined = calendarIds.find( + static createEvent( + schedule: ScheduleInterface, + calendarIds: SiteCalendarInterface[] + ): void { + const siteCalendarId: SiteCalendarInterface | undefined = calendarIds.find( (id) => id.type === schedule.type ); if (typeof siteCalendarId === 'undefined') { @@ -44,7 +42,7 @@ export default class Calendar { schedule.title, new Date(schedule.startTime), new Date(schedule.endTime), - {description: schedule.description} + { description: schedule.description } ); } else { CalendarApp.getCalendarById(calendarId).createAllDayEvent( diff --git a/src/oneMonthSchedule.ts b/src/oneMonthSchedule.ts index b5d885a..8407c36 100644 --- a/src/oneMonthSchedule.ts +++ b/src/oneMonthSchedule.ts @@ -1,9 +1,6 @@ import dayjs from 'dayjs'; import Calendar from './calendar'; -import { - ScheduleInterface, - SiteCalendarInterface, -} from './calendarInterface'; +import { ScheduleInterface, SiteCalendarInterface } from './calendarInterface'; import Retry from './lib/retry'; import 'regenerator-runtime'; @@ -15,7 +12,11 @@ export default class OneMonthSchedule { * @param siteCalendarIds * @returns {Promise} */ - static async setSchedule(date: dayjs.Dayjs, calendarUrl: string, siteCalendarIds: SiteCalendarInterface[]): Promise { + static async setSchedule( + date: dayjs.Dayjs, + calendarUrl: string, + siteCalendarIds: SiteCalendarInterface[] + ): Promise { const customUrl: string = calendarUrl + date.format('YYYYMMDD'); const scheduleJson = await OneMonthSchedule.getScheduleJson(customUrl); @@ -52,7 +53,10 @@ export default class OneMonthSchedule { * @param {dayjs.Dayjs} date * @param siteCalendarIds */ - private static delete1MonthCalendarEvents(date: dayjs.Dayjs, siteCalendarIds: SiteCalendarInterface[]) { + private static delete1MonthCalendarEvents( + date: dayjs.Dayjs, + siteCalendarIds: SiteCalendarInterface[] + ) { let deleteEventCallCount = 0; siteCalendarIds.forEach((siteCalendarId) => { if (process.env.ENV !== 'production') return; diff --git a/src/schedule.ts b/src/schedule.ts index a709753..ea3af63 100644 --- a/src/schedule.ts +++ b/src/schedule.ts @@ -1,13 +1,13 @@ import KeyakiSiteSchedule from './sites/keyakizaka/keyakiSiteSchedule'; -import {SiteScheduleInterface} from './sites/siteSchedule'; +import { SiteScheduleInterface } from './sites/siteSchedule'; export default class Schedule { static async setSchedule(): Promise { const siteScheduleList: SiteScheduleInterface[] = [ - new KeyakiSiteSchedule, + new KeyakiSiteSchedule(), // new KeyakiSiteSchedule ]; - for await(const siteSchedule of siteScheduleList) { + for await (const siteSchedule of siteScheduleList) { await siteSchedule.setSiteSchedule(); } } diff --git a/src/sites/keyakizaka/keyakiObjects.ts b/src/sites/keyakizaka/keyakiObjects.ts index 8fe7860..68c07e9 100644 --- a/src/sites/keyakizaka/keyakiObjects.ts +++ b/src/sites/keyakizaka/keyakiObjects.ts @@ -1,4 +1,4 @@ -import {SiteCalendarInterface} from '../../calendarInterface'; +import { SiteCalendarInterface } from '../../calendarInterface'; export const getKeyakiCalendarUrl = process.env.ENV === 'production' diff --git a/src/sites/keyakizaka/keyakiSiteSchedule.ts b/src/sites/keyakizaka/keyakiSiteSchedule.ts index ffff478..89bd676 100644 --- a/src/sites/keyakizaka/keyakiSiteSchedule.ts +++ b/src/sites/keyakizaka/keyakiSiteSchedule.ts @@ -1,6 +1,6 @@ import SiteSchedule from '../siteSchedule'; -import {getKeyakiCalendarUrl, keyakiCalendarIds} from './keyakiObjects'; -import {SiteCalendarInterface} from '../../calendarInterface'; +import { getKeyakiCalendarUrl, keyakiCalendarIds } from './keyakiObjects'; +import { SiteCalendarInterface } from '../../calendarInterface'; export default class KeyakiSiteSchedule extends SiteSchedule { // eslint-disable-next-line class-methods-use-this diff --git a/src/sites/siteSchedule.ts b/src/sites/siteSchedule.ts index 481fb65..1ecd0e5 100644 --- a/src/sites/siteSchedule.ts +++ b/src/sites/siteSchedule.ts @@ -1,7 +1,7 @@ import dayjs from 'dayjs'; import OneMonthSchedule from '../oneMonthSchedule'; import Trigger, { TERMINATION_MINUTES } from '../lib/trigger'; -import {SiteCalendarInterface} from '../calendarInterface'; +import { SiteCalendarInterface } from '../calendarInterface'; export interface SiteScheduleInterface { setSiteSchedule(): Promise; @@ -9,7 +9,7 @@ export interface SiteScheduleInterface { siteCalendarIds(): SiteCalendarInterface[]; } -export default abstract class SiteSchedule implements SiteScheduleInterface{ +export default abstract class SiteSchedule implements SiteScheduleInterface { async setSiteSchedule(): Promise { const beginningOfNexYearMonth = dayjs().startOf('month').add(1, 'year'); let targetBeginningOfMonth = SiteSchedule.getTargetBeginningOfMonth(); @@ -17,7 +17,11 @@ export default abstract class SiteSchedule implements SiteScheduleInterface{ while (targetBeginningOfMonth.isBefore(beginningOfNexYearMonth)) { // eslint-disable-next-line no-await-in-loop - await OneMonthSchedule.setSchedule(targetBeginningOfMonth, this.siteCalendarUrl(), this.siteCalendarIds()); + await OneMonthSchedule.setSchedule( + targetBeginningOfMonth, + this.siteCalendarUrl(), + this.siteCalendarIds() + ); targetBeginningOfMonth = targetBeginningOfMonth.add(1, 'month'); if (Trigger.hasExceededTerminationMinutes(startDate)) { Trigger.setTrigger(targetBeginningOfMonth); @@ -48,4 +52,3 @@ export default abstract class SiteSchedule implements SiteScheduleInterface{ abstract siteCalendarIds(): SiteCalendarInterface[]; } - diff --git a/tests/calendar.test.ts b/tests/calendar.test.ts index 65db5f0..5169d95 100644 --- a/tests/calendar.test.ts +++ b/tests/calendar.test.ts @@ -1,6 +1,6 @@ import Calendar from '../src/calendar'; import { ScheduleInterface } from '../src/calendarInterface'; -import {keyakiCalendarIds} from '../src/sites/keyakizaka/keyakiObjects'; +import { keyakiCalendarIds } from '../src/sites/keyakizaka/keyakiObjects'; /** * diff --git a/tests/oneMonthSchedule.test.ts b/tests/oneMonthSchedule.test.ts index d9b8772..0794339 100644 --- a/tests/oneMonthSchedule.test.ts +++ b/tests/oneMonthSchedule.test.ts @@ -2,7 +2,10 @@ import dayjs from 'dayjs'; import fetchMock from 'jest-fetch-mock'; import OneMonthSchedule from '../src/oneMonthSchedule'; import Calendar from '../src/calendar'; -import {getKeyakiCalendarUrl, keyakiCalendarIds} from '../src/sites/keyakizaka/keyakiObjects'; +import { + getKeyakiCalendarUrl, + keyakiCalendarIds, +} from '../src/sites/keyakizaka/keyakiObjects'; function getScheduleJson() { return '[{"title":"欅坂46 こちら有楽町星空放送局","start":"2019-12-01","className":"media","description":"欅坂46 こちら有楽町星空放送局"},{"title":"テレビ東京「欅って、書けない?」","start":"2019-12-01","className":"media","description":"テレビ東京「欅って、書けない?」"}]'; @@ -35,7 +38,13 @@ describe('setSchedule', () => { })) as jest.Mock; const date = dayjs('2019-12-01'); - await expect(OneMonthSchedule.setSchedule(date, getKeyakiCalendarUrl, keyakiCalendarIds)).resolves.not.toThrow(); + await expect( + OneMonthSchedule.setSchedule( + date, + getKeyakiCalendarUrl, + keyakiCalendarIds + ) + ).resolves.not.toThrow(); expect(Calendar.deleteEvent).toBeCalledTimes( keyakiCalendarIds.length * date.endOf('month').date() ); @@ -61,7 +70,13 @@ describe('setSchedule', () => { }); const date = dayjs('2019-12-01'); - await expect(OneMonthSchedule.setSchedule(date, getKeyakiCalendarUrl, keyakiCalendarIds)).rejects.toThrow(); + await expect( + OneMonthSchedule.setSchedule( + date, + getKeyakiCalendarUrl, + keyakiCalendarIds + ) + ).rejects.toThrow(); expect(Calendar.createEvent).not.toBeCalled(); }); it('カレンダーの作成に失敗した場合に例外が起きて後続の処理が止まること', async () => { @@ -83,7 +98,13 @@ describe('setSchedule', () => { }); const date = dayjs('2019-12-01'); - await expect(OneMonthSchedule.setSchedule(date, getKeyakiCalendarUrl, keyakiCalendarIds)).rejects.toThrow(); + await expect( + OneMonthSchedule.setSchedule( + date, + getKeyakiCalendarUrl, + keyakiCalendarIds + ) + ).rejects.toThrow(); }); it('production環境では無かった場合にUrlFetchApp.fetchが呼ばれないこと', async () => { process.env.ENV = 'local'; @@ -91,7 +112,11 @@ describe('setSchedule', () => { fetchMock.mockOnce(getScheduleJson()); const date = dayjs('2019-12-01'); - await OneMonthSchedule.setSchedule(date, getKeyakiCalendarUrl, keyakiCalendarIds); + await OneMonthSchedule.setSchedule( + date, + getKeyakiCalendarUrl, + keyakiCalendarIds + ); expect(UrlFetchApp.fetch).not.toBeCalled(); }); });