Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scrapbox: 指定したページが更新されても通知が来ないようにする #229

Draft
wants to merge 45 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
3af7d47
scrapbox: dummy commit for draft pull request
pizzacat83 Jan 15, 2020
72aec15
scrapbox: implement mute of notifications
pizzacat83 Jan 15, 2020
6b48a20
scrapbox: implement maskAttachment
pizzacat83 Jan 15, 2020
1bf5b2c
scrapbox: type return value of maskAttachment
pizzacat83 Jan 15, 2020
c7dd4dd
scrapbox: implement isMuted
pizzacat83 Jan 15, 2020
8a2141e
scrapbox: don't show images
pizzacat83 Jan 15, 2020
5ca1d05
scrapbox: [WIP] add test for mute
pizzacat83 Jan 15, 2020
b6c6f6b
bcr: [Test Not Passing] mock axios
pizzacat83 Jan 17, 2020
4b93a9a
scrapbox: fix tests
pizzacat83 Jan 19, 2020
e5a0d0e
scrapbox: fix tests
pizzacat83 Jan 19, 2020
1444120
scrapbox: update .env.sample
pizzacat83 Jan 19, 2020
3c092fd
scrapbox: use (decode|encode)URIComponent
pizzacat83 Jan 27, 2020
2528f13
scrapbox: change URL of webhook
pizzacat83 Jan 27, 2020
13895cf
scrapbox: handle errors
pizzacat83 Jan 27, 2020
5ee0278
lib/fastify: add constructor of fastify for unit test
pizzacat83 Feb 22, 2020
fbe0c7e
scrapbox: 1 API request per notification
pizzacat83 Feb 23, 2020
b8d06ef
lib/fastify: add test for fastifyDevConstructor
pizzacat83 Feb 23, 2020
9b6a7fe
lib/scrapbox: add some functions
pizzacat83 Feb 24, 2020
70e3e86
add package eslint-plugin-jest
pizzacat83 Feb 25, 2020
6457b81
Merge remote-tracking branch 'origin/master' into scrapbox-mute-some-…
pizzacat83 Feb 25, 2020
3ac7c6b
lib/scrapbox: add scrapbox wrapper class
pizzacat83 Mar 3, 2020
6943983
lib/scrapbox: add tests for Page & fix bugs
pizzacat83 Mar 3, 2020
db48f9f
lib/scrapbox: add tests for fetchScrapboxUrl
pizzacat83 Mar 3, 2020
1c9178d
lib/scrapbox: add tests for getPageUrlRegExp
pizzacat83 Mar 3, 2020
f3aab4e
scrapbox: use lib/scrapbox
pizzacat83 Mar 3, 2020
c257e5c
welcome: use lib/scrapbox
pizzacat83 Mar 3, 2020
2d5045d
scrapbox: mock lib/scrapbox in tests
pizzacat83 Mar 3, 2020
a4c5590
scrapbox: implement splitAttachments
pizzacat83 Mar 4, 2020
1244809
lib/scrapbox: cache calls of getPageUrlRegExp
pizzacat83 Mar 4, 2020
d3b9f5a
scrapbox: add helper class for tests
pizzacat83 Mar 4, 2020
c83d003
scrapbox: reimplement mute
pizzacat83 Mar 4, 2020
5f6b6ad
lib/scrapbox: fix unintended nullable
pizzacat83 Mar 4, 2020
cbc5f2f
scrapbox: fix eslintrc
pizzacat83 Mar 4, 2020
93aadce
scrapbox: lint
pizzacat83 Mar 4, 2020
8ed67dc
lib/scrapbox: getPageUrlRegExp -> pageUrlRegExp and isPageOfProject
pizzacat83 Mar 4, 2020
6e16349
scrapbox: add doc
pizzacat83 Mar 4, 2020
2cead8b
scrapbox: split files
pizzacat83 Mar 5, 2020
a20913e
Merge remote-tracking branch 'origin/master' into scrapbox-mute-some-…
pizzacat83 Mar 5, 2020
f435b6d
scrapbox/mute: add text: '' in image attachments
pizzacat83 Mar 5, 2020
2e717b0
lib/scrapbox: refactoring
pizzacat83 Mar 9, 2020
90e2ba8
scrapbox/mute: add comments
pizzacat83 Mar 9, 2020
c2977de
scrapbox: delete unnecessary mock
pizzacat83 Mar 9, 2020
17642d4
scrapbox: remove dummy data not used in test
pizzacat83 Mar 9, 2020
2c20353
scrapbox/mute: better variable name
pizzacat83 Mar 9, 2020
c3f17a0
small refactoring
pizzacat83 Mar 9, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ SWARM_TOKEN=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
CHANNEL_SANDBOX=CXXXXXXXX
CHANNEL_ESOLANG=CXXXXXXXX
CHANNEL_PROCON=CXXXXXXXX
CHANNEL_SCRAPBOX=CXXXXXXXX
CHANNEL_RANDOM=CXXXXXXXX
USER_TSGBOT=UXXXXXXXX
GITHUB_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
GOOGLE_APPLICATION_CREDENTIALS=google_application_credentials.json
FIREBASE_ENDPOINT=https://hakata-shi.firebaseio.com
ACCUWEATHER_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SCRAPBOX_SID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SCRAPBOX_PROJECT_NAME=xxxxxxxxx
GITHUB_WEBHOOK_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SIGNING_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
PORT=21864
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const allBots = [
// ...(word2vecInstalled ? ['vocabwar'] : []),
'ricochet-robots',
'scrapbox',
'scrapbox/mute',
'slack-log',
'welcome',
'deploy',
Expand Down
24 changes: 24 additions & 0 deletions lib/fastify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import plugin from 'fastify-plugin';
import { fastifyDevConstructor } from './fastify';
import { FastifyInstance } from 'fastify';

describe('fastifyDevConstructor', () => {
it('throws error when error occures during request', () => {
const fastify: FastifyInstance = fastifyDevConstructor();
const msg = 'Dummy error.';
const path = '/path/to/somewhere';
fastify.register(plugin((fastify, opts, next) => {
fastify.get(path, (req) => {
throw Error(msg);
})
next();
}))
expect(
fastify.inject({
method: 'GET',
url: path,
payload: {something: 'hoge'},
})
).rejects.toThrow(msg);
})
});
34 changes: 34 additions & 0 deletions lib/fastify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import fastifyConstructor, { FastifyInstance } from 'fastify';

/**
* 単体テストに適した設定がなされたfastifyインスタンスを生成する
*
* @param opts fastifyConstructor に渡す引数
* @example
* import slack from '../lib/slackMock.js';
* import {fastifyDevConstructor} from '../lib/fastify';
* import {server} from './index';
*
* const fastify = fastifyDevConstructor();
* fastify.register(server(slack));
*/

export const fastifyDevConstructor = (opts?: Parameters<typeof fastifyConstructor>[0]): FastifyInstance => {
pizzacat83 marked this conversation as resolved.
Show resolved Hide resolved
// TODO: support generics of fastifyConstructor
/*
* Setting the return type to ReturnType<fastifyConstructor> causes typeerror
* because type Server is not compatible with type Http2SecureServer
* Maybe because of not handling the generics of fastifyConstructor
*/

const fastify = fastifyConstructor({ logger: true , ...opts });

/**
* デフォルトのエラーハンドラはエラーをログに出力して握り潰すため,
* 単体テストでexpectの失敗などによる例外をJestが検知することができない
* 発生した例外は全て再送出するように設定
*/
fastify.setErrorHandler((err) => { throw err; });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] うーむ⋯⋯⋯⋯ fastify, ルートハンドラがthrowした場合はちゃんとinjectの結果をrejectしてくれると思ってたけど、どうだろう⋯⋯? (実際にこの行をコメントアウトしてもfastify.test.tsのテストが通る)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

びっくりしているんですが,上にある throw Error をコメントアウトしても通っていて,少なくともテストコードはバグっています:ojigineko-superfast:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

え〜この行無くても動くな どうして昔握りつぶされてInternal Server Errorしてたんだ??????


return fastify;
};
184 changes: 184 additions & 0 deletions lib/scrapbox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
const defaultProjectName = 'default_proj';
process.env.SCRAPBOX_PROJECT_NAME = defaultProjectName;
const defaultToken = 'default_token';
process.env.SCRAPBOX_SID = defaultToken;

jest.mock('axios');
import _axios from 'axios';
const axios = _axios as jest.Mocked<typeof _axios>;

import { isPageOfProject, fetchScrapboxUrl, Page, pageUrlRegExp } from './scrapbox';

beforeEach(() => {
axios.get.mockReset();
})

describe('fetchScrapboxUrl', () => {
const data = {dummy: 'data'};
const url = 'dummy_url';
const token = 'dummy_token';

beforeEach(() => {
axios.get.mockResolvedValueOnce({ data });
});

it('fetches given URL with given token', async () => {
const res = await fetchScrapboxUrl({ url, token });
expect(res).toEqual(data);
expect(axios.get.mock.calls.length).toBe(1);
expect(axios.get.mock.calls[0][0]).toBe(url);
expect(axios.get.mock.calls[0][1]!.headers.Cookie).toContain(token);
});

it('uses default token if not specified', async () => {
await fetchScrapboxUrl({ url });
expect(axios.get.mock.calls[0][1]!.headers.Cookie).toContain(defaultToken);
});
});

describe('pageUrlRegExp', () => {
const projectName = 'proj';
const titleLc = 'タイトル';
const hash = 'hash';

it('parses URL without hash', () => {
const match = `https://scrapbox.io/${projectName}/${titleLc}`.match(pageUrlRegExp);
expect(match).not.toBeNull();
expect(match!.groups).toMatchObject({ projectName, titleLc });
});

it('parses URL with hash', () => {
const match = `https://scrapbox.io/${projectName}/${titleLc}#${hash}`.match(pageUrlRegExp);
expect(match).not.toBeNull();
expect(match!.groups).toMatchObject({ projectName, titleLc, hash });
});
});

describe('isPageOfProject', () => {
const projectName = 'proj';
const projectName2 = 'proj2';
const titleLc = 'タイトル';

it('returns true for Scrapbox URL of specified project', () => {
const url = `https://scrapbox.io/${projectName}/${titleLc}`;
expect(isPageOfProject({ url, projectName })).toBe(true);
});

it('returns false for Scrapbox URL of specified project', () => {
const url = `https://scrapbox.io/${projectName2}/${titleLc}`;
expect(isPageOfProject({ url, projectName })).toBe(false);
});

it('returns false for strings not Scrapbox URL', () => {
const url = 'hoge';
expect(isPageOfProject({ url, projectName })).toBe(false);
});

test.each([
{projectName: defaultProjectName, expected: true},
{projectName: projectName, expected: false}
])('uses projectName specified in envvar when not specified #%#', ({ projectName, expected }) => {
const url = `https://scrapbox.io/${projectName}/${titleLc}`;
expect(isPageOfProject({ url })).toBe(expected);
});
});

describe('Page', () => {
const projectName = 'proj';
const titleLc = 'タイトル';
const encodedTitleLc = encodeURIComponent(titleLc);
const hash = 'hash';
const token = 'token';

describe('constructor', () => {
const assertProperties = (page: Page): void => {
expect(page.token).toBe(token);
expect(page.projectName).toBe(projectName);
expect(page.titleLc).toBe(titleLc);
expect(page.encodedTitleLc).toBe(encodedTitleLc);
expect(page.hash).toBe(hash);
};

const generateURL = ({ projectName, titleLc, hash }: { projectName: string; titleLc: string; hash: string }) =>
`https://scrapbox.io/${projectName}/${titleLc}#${hash}`;

it('handles unencoded titleLc', () => {
assertProperties(new Page({ titleLc, projectName, hash, isEncoded: false, token }));
});

it('handles encoded titleLc', () => {
assertProperties(new Page({ titleLc: encodedTitleLc, projectName, hash, isEncoded: true, token }));
});

it('assumes unencoded titleLc to be unencoded', () => {
assertProperties(new Page({ titleLc, projectName, hash, isEncoded: undefined, token }));
});

it('assumes encoded titleLc to be encoded', () => {
assertProperties(new Page({ titleLc: encodedTitleLc, projectName, hash, isEncoded: undefined, token }));
});


it('handles unencoded URL', () => {
const url = generateURL({ projectName, titleLc, hash });
assertProperties(new Page({ url, isEncoded: false, token }));
});

it('handles encoded URL', () => {
const url = generateURL({ projectName, titleLc: encodedTitleLc, hash });
assertProperties(new Page({ url, isEncoded: true, token }));
});

it('assumes unencoded URL to be unencoded', () => {
const url = generateURL({ projectName, titleLc, hash });
assertProperties(new Page({ url, isEncoded: undefined, token }));
});

it('assumes encoded URL to be encoded', () => {
const url = generateURL({ projectName, titleLc: encodedTitleLc, hash });
assertProperties(new Page({ url, isEncoded: undefined, token }));
});
it('handles encoded URL', () => {
const url = generateURL({ projectName, titleLc: encodedTitleLc, hash });
assertProperties(new Page({ url, isEncoded: undefined, token }));
});

it('handles missing parameters when specified titleLc', () => {
const page = new Page({ titleLc });
expect(page.projectName).toBe(defaultProjectName);
expect(page.token).toBe(defaultToken);
expect(page.hash).toBeUndefined();
});

it('handles missing parameters when specified URL', () => {
const page = new Page({ titleLc });
expect(page.token).toBe(defaultToken);
});

it('throws error on invalid URL', () => {
expect(() => new Page({ url: 'hoge' })).toThrow();
})
});

describe('methods', () => {
let page: Page | null = null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] strictNullChecksが入ってないので現状意味のない指定だと思いますが、大丈夫ですか

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

将来誰かがstrictNullChecksをしたくなった時用に lib/scrapbox.* はstrictNullChecksでも通るコードになってるんですが,どこかにその旨書いた方がいいですかね

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほど、まあ意図があるなら大丈夫だけど、現状のプロジェクトの設定では検証できない制約をソースコードに課すのはあまり良くない気もする

beforeEach(() => {
page = new Page({ titleLc, projectName, hash, token });
});

test('.url is correct', () => {
expect(page!.url).toBe(`https://scrapbox.io/${projectName}/${encodedTitleLc}#${hash}`);
});

test('.infoUrl is correct', () => {
expect(page!.infoUrl).toBe(`https://scrapbox.io/api/pages/${projectName}/${encodedTitleLc}`);
});

test('.fetchInfo() fetches from correct URL', async () => {
axios.get.mockResolvedValueOnce({ data: {} });
await page!.fetchInfo();
expect(axios.get.mock.calls.length).toBe(1);
expect(axios.get.mock.calls[0][0]/* url of 0th call */).toBe(page!.infoUrl);
});
});
});
Loading