-
Notifications
You must be signed in to change notification settings - Fork 12
Scrapbox: 指定したページが更新されても通知が来ないようにする #229
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
base: master
Are you sure you want to change the base?
Changes from 41 commits
3af7d47
72aec15
6b48a20
1bf5b2c
c7dd4dd
8a2141e
5ca1d05
b6c6f6b
4b93a9a
e5a0d0e
1444120
3c092fd
2528f13
13895cf
5ee0278
fbe0c7e
b8d06ef
9b6a7fe
70e3e86
6457b81
3ac7c6b
6943983
db48f9f
1c9178d
f3aab4e
c257e5c
2d5045d
a4c5590
1244809
d3b9f5a
c83d003
5f6b6ad
cbc5f2f
93aadce
8ed67dc
6e16349
2cead8b
a20913e
f435b6d
2e717b0
90e2ba8
c2977de
17642d4
2c20353
c3f17a0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| }) | ||
| }); |
| 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 => { | ||
| // 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(Object.assign({}, { logger: true }, opts)); | ||
pizzacat83 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * デフォルトのエラーハンドラはエラーをログに出力して握り潰すため, | ||
| * 単体テストでexpectの失敗などによる例外をJestが検知することができない | ||
| * 発生した例外は全て再送出するように設定 | ||
| */ | ||
| fastify.setErrorHandler((err) => { throw err; }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [question] うーむ⋯⋯⋯⋯ fastify, ルートハンドラがthrowした場合はちゃんとinjectの結果をrejectしてくれると思ってたけど、どうだろう⋯⋯? (実際にこの行をコメントアウトしてもfastify.test.tsのテストが通る)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. びっくりしているんですが,上にある
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. え〜この行無くても動くな どうして昔握りつぶされてInternal Server Errorしてたんだ?????? |
||
|
|
||
| return fastify; | ||
| }; | ||
| 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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [question] strictNullChecksが入ってないので現状意味のない指定だと思いますが、大丈夫ですか
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 将来誰かがstrictNullChecksをしたくなった時用に
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| }); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.