@zonuexe/techbook-mcp 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -1
- package/README.md +39 -20
- package/dist/adapters/calil.d.ts +10 -0
- package/dist/adapters/calil.d.ts.map +1 -0
- package/dist/adapters/calil.js +45 -0
- package/dist/adapters/calil.js.map +1 -0
- package/dist/adapters/openbd.d.ts +57 -0
- package/dist/adapters/openbd.d.ts.map +1 -0
- package/dist/adapters/openbd.js +87 -0
- package/dist/adapters/openbd.js.map +1 -0
- package/dist/adapters/publishers/google-books.d.ts +4 -0
- package/dist/adapters/publishers/google-books.d.ts.map +1 -0
- package/dist/adapters/publishers/google-books.js +75 -0
- package/dist/adapters/publishers/google-books.js.map +1 -0
- package/dist/adapters/publishers/isbn-publisher-codes.d.ts +21 -0
- package/dist/adapters/publishers/isbn-publisher-codes.d.ts.map +1 -0
- package/dist/adapters/publishers/isbn-publisher-codes.js +49 -0
- package/dist/adapters/publishers/isbn-publisher-codes.js.map +1 -0
- package/dist/adapters/publishers/juse-p.d.ts +3 -0
- package/dist/adapters/publishers/juse-p.d.ts.map +1 -0
- package/dist/adapters/publishers/juse-p.js +110 -0
- package/dist/adapters/publishers/juse-p.js.map +1 -0
- package/dist/adapters/publishers/registry.d.ts.map +1 -1
- package/dist/adapters/publishers/registry.js +4 -0
- package/dist/adapters/publishers/registry.js.map +1 -1
- package/dist/adapters/publishers/tatsu-zine.d.ts.map +1 -1
- package/dist/adapters/publishers/tatsu-zine.js +6 -18
- package/dist/adapters/publishers/tatsu-zine.js.map +1 -1
- package/dist/application/get-book-by-isbn.d.ts +13 -0
- package/dist/application/get-book-by-isbn.d.ts.map +1 -0
- package/dist/application/get-book-by-isbn.js +61 -0
- package/dist/application/get-book-by-isbn.js.map +1 -0
- package/dist/application/get-book-detail.d.ts.map +1 -1
- package/dist/application/get-book-detail.js +16 -1
- package/dist/application/get-book-detail.js.map +1 -1
- package/dist/application/search-books.d.ts.map +1 -1
- package/dist/application/search-books.js +20 -0
- package/dist/application/search-books.js.map +1 -1
- package/dist/config/credentials.d.ts +8 -0
- package/dist/config/credentials.d.ts.map +1 -0
- package/dist/config/credentials.js +32 -0
- package/dist/config/credentials.js.map +1 -0
- package/dist/main.js +15 -1
- package/dist/main.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +10 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +13 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +16 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +43 -0
- package/dist/setup.js.map +1 -0
- package/flake.lock +61 -0
- package/package.json +1 -1
- package/.claude/settings.local.json +0 -36
- package/.codex/skills/techbook-mcp-release-prep/SKILL.md +0 -105
- package/.github/workflows/test.yml +0 -72
- package/.oxlintrc.json +0 -12
- package/AGENTS.md +0 -100
- package/deno.json +0 -3
- package/src/adapters/cache/memory-cache.ts +0 -31
- package/src/adapters/cache/null-cache.ts +0 -8
- package/src/adapters/html/cheerio-parser.ts +0 -50
- package/src/adapters/http/fetch-client.ts +0 -47
- package/src/adapters/http/mock-client.ts +0 -77
- package/src/adapters/publishers/base.ts +0 -279
- package/src/adapters/publishers/book-tech.ts +0 -117
- package/src/adapters/publishers/born-digital.ts +0 -143
- package/src/adapters/publishers/coronasha.ts +0 -139
- package/src/adapters/publishers/gihyo.ts +0 -120
- package/src/adapters/publishers/impress.ts +0 -103
- package/src/adapters/publishers/lambdanote.ts +0 -146
- package/src/adapters/publishers/manatee.ts +0 -113
- package/src/adapters/publishers/maruzen-publishing.ts +0 -129
- package/src/adapters/publishers/optronics.ts +0 -113
- package/src/adapters/publishers/oreilly-japan.ts +0 -133
- package/src/adapters/publishers/peaks.ts +0 -98
- package/src/adapters/publishers/personal-media.ts +0 -168
- package/src/adapters/publishers/registry.ts +0 -38
- package/src/adapters/publishers/rutles.ts +0 -149
- package/src/adapters/publishers/saiensu.ts +0 -136
- package/src/adapters/publishers/seshop.ts +0 -121
- package/src/adapters/publishers/tatsu-zine.ts +0 -154
- package/src/adapters/publishers/techbookfest.ts +0 -179
- package/src/application/get-book-detail.ts +0 -24
- package/src/application/search-books.ts +0 -44
- package/src/domain/book.ts +0 -35
- package/src/domain/publisher.ts +0 -18
- package/src/main.ts +0 -14
- package/src/mcp/server.ts +0 -103
- package/src/mcp/tools.ts +0 -54
- package/src/ports/cache.ts +0 -5
- package/src/ports/html-parser.ts +0 -15
- package/src/ports/http.ts +0 -17
- package/tests/fixtures/book-tech-detail.html +0 -51
- package/tests/fixtures/book-tech-search.html +0 -91
- package/tests/fixtures/born-digital-detail.html +0 -62
- package/tests/fixtures/born-digital-search.html +0 -51
- package/tests/fixtures/coronasha-detail.html +0 -41
- package/tests/fixtures/coronasha-search.html +0 -61
- package/tests/fixtures/gihyo-detail.html +0 -42
- package/tests/fixtures/gihyo-search.json +0 -54
- package/tests/fixtures/impress-detail-epub.html +0 -746
- package/tests/fixtures/impress-detail-social.html +0 -689
- package/tests/fixtures/lambdanote-search.html +0 -66
- package/tests/fixtures/manatee-detail.html +0 -53
- package/tests/fixtures/manatee-search.html +0 -59
- package/tests/fixtures/maruzen-detail.html +0 -51
- package/tests/fixtures/maruzen-search.html +0 -60
- package/tests/fixtures/optronics-detail.html +0 -30
- package/tests/fixtures/optronics-search.html +0 -75
- package/tests/fixtures/oreilly-detail.html +0 -52
- package/tests/fixtures/oreilly-ebook-list.html +0 -53
- package/tests/fixtures/peaks-detail.html +0 -39
- package/tests/fixtures/peaks-top.html +0 -50
- package/tests/fixtures/personal-media-detail.html +0 -32
- package/tests/fixtures/personal-media-search.html +0 -39
- package/tests/fixtures/rutles-detail.html +0 -32
- package/tests/fixtures/rutles-search.html +0 -62
- package/tests/fixtures/saiensu-detail.html +0 -41
- package/tests/fixtures/saiensu-search.html +0 -65
- package/tests/fixtures/seshop-detail.html +0 -45
- package/tests/fixtures/seshop-search.html +0 -58
- package/tests/fixtures/tatsu-zine-detail-free.html +0 -22
- package/tests/fixtures/tatsu-zine-search.html +0 -40
- package/tests/fixtures/techbookfest-search.json +0 -73
- package/tests/unit/adapters/base.test.ts +0 -441
- package/tests/unit/adapters/publishers/book-tech.test.ts +0 -186
- package/tests/unit/adapters/publishers/born-digital.test.ts +0 -194
- package/tests/unit/adapters/publishers/coronasha.test.ts +0 -207
- package/tests/unit/adapters/publishers/gihyo.test.ts +0 -137
- package/tests/unit/adapters/publishers/impress.test.ts +0 -129
- package/tests/unit/adapters/publishers/lambdanote.test.ts +0 -85
- package/tests/unit/adapters/publishers/manatee.test.ts +0 -165
- package/tests/unit/adapters/publishers/maruzen-publishing.test.ts +0 -179
- package/tests/unit/adapters/publishers/optronics.test.ts +0 -208
- package/tests/unit/adapters/publishers/oreilly-japan.test.ts +0 -194
- package/tests/unit/adapters/publishers/peaks.test.ts +0 -177
- package/tests/unit/adapters/publishers/personal-media.test.ts +0 -199
- package/tests/unit/adapters/publishers/rutles.test.ts +0 -173
- package/tests/unit/adapters/publishers/saiensu.test.ts +0 -169
- package/tests/unit/adapters/publishers/seshop.test.ts +0 -174
- package/tests/unit/adapters/publishers/tatsu-zine.test.ts +0 -172
- package/tests/unit/adapters/publishers/techbookfest.test.ts +0 -94
- package/tests/unit/adapters/registry.test.ts +0 -37
- package/tests/unit/application/get-book-detail.test.ts +0 -102
- package/tests/unit/application/search-books.test.ts +0 -137
- package/tsconfig.json +0 -17
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { techbookfestAdapter } from "../../../../src/adapters/publishers/techbookfest.js";
|
|
6
|
-
import { MockHttpClient } from "../../../../src/adapters/http/mock-client.js";
|
|
7
|
-
import { CheerioHtmlParser } from "../../../../src/adapters/html/cheerio-parser.js";
|
|
8
|
-
import { NullCacheStore } from "../../../../src/adapters/cache/null-cache.js";
|
|
9
|
-
|
|
10
|
-
const FIXTURES_DIR = join(import.meta.dirname, "../../../fixtures");
|
|
11
|
-
|
|
12
|
-
const XSRF_TOKEN = "test-xsrf-token-abc123";
|
|
13
|
-
|
|
14
|
-
function makeDeps(http: MockHttpClient) {
|
|
15
|
-
return { http, parser: new CheerioHtmlParser(), cache: new NullCacheStore() };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** トップページ GET → XSRF-TOKEN → GraphQL POST の 2 ステップを登録する */
|
|
19
|
-
async function makeSearchHttp(fixtureName: string): Promise<MockHttpClient> {
|
|
20
|
-
const body = await readFile(join(FIXTURES_DIR, fixtureName), "utf-8");
|
|
21
|
-
return new MockHttpClient()
|
|
22
|
-
.addResponse("https://techbookfest.org", {
|
|
23
|
-
status: 200,
|
|
24
|
-
body: "",
|
|
25
|
-
headers: { "set-cookie": `XSRF-TOKEN=${encodeURIComponent(XSRF_TOKEN)}; Path=/; Secure` },
|
|
26
|
-
})
|
|
27
|
-
.addPostResponse("https://techbookfest.org/api/graphql", { status: 200, body });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe("techbookfestAdapter", () => {
|
|
31
|
-
describe("search()", () => {
|
|
32
|
-
it("GraphQL レスポンスから BookRecord[] を返す", async () => {
|
|
33
|
-
const http = await makeSearchHttp("techbookfest-search.json");
|
|
34
|
-
|
|
35
|
-
const results = await techbookfestAdapter.search({ title: "TypeScript", limit: 10 }, makeDeps(http));
|
|
36
|
-
|
|
37
|
-
assert.strictEqual(results.length, 3);
|
|
38
|
-
assert.partialDeepStrictEqual(results[0], {
|
|
39
|
-
title: "TypeScriptで学ぶデザインパターン",
|
|
40
|
-
authors: ["サークル名A"],
|
|
41
|
-
publisher: "技術書典",
|
|
42
|
-
url: "https://techbookfest.org/product/01HXXXX1",
|
|
43
|
-
price: 1000,
|
|
44
|
-
publishedAt: "2024-01-15",
|
|
45
|
-
});
|
|
46
|
-
assert.strictEqual(results[0].coverImageUrl, "https://techbookfest.org/api/image/01HXXXX1.png");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("ebookStores に技術書典(DRMフリー)が含まれる", async () => {
|
|
50
|
-
const http = await makeSearchHttp("techbookfest-search.json");
|
|
51
|
-
|
|
52
|
-
const results = await techbookfestAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
53
|
-
|
|
54
|
-
assert.deepStrictEqual(results[0].ebookStores, [
|
|
55
|
-
{ name: "技術書典", url: "https://techbookfest.org/product/01HXXXX1", drm: "free" },
|
|
56
|
-
]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("coverImage が null の場合 coverImageUrl は undefined", async () => {
|
|
60
|
-
const http = await makeSearchHttp("techbookfest-search.json");
|
|
61
|
-
|
|
62
|
-
const results = await techbookfestAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
63
|
-
|
|
64
|
-
const book = results.find(b => b.url === "https://techbookfest.org/product/01HXXXX2");
|
|
65
|
-
assert.strictEqual(book?.coverImageUrl, undefined);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("title も author も空の場合は [] を返しHTTPを呼ばない", async () => {
|
|
69
|
-
const http = new MockHttpClient();
|
|
70
|
-
const results = await techbookfestAdapter.search({}, makeDeps(http));
|
|
71
|
-
|
|
72
|
-
assert.deepStrictEqual(results, []);
|
|
73
|
-
assert.strictEqual(http.calls.length, 0);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("トップページ GET で XSRF-TOKEN を取得してから GraphQL に POST する", async () => {
|
|
77
|
-
const http = await makeSearchHttp("techbookfest-search.json");
|
|
78
|
-
|
|
79
|
-
await techbookfestAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
80
|
-
|
|
81
|
-
assert.strictEqual(http.calls[0], "https://techbookfest.org");
|
|
82
|
-
assert.strictEqual(http.calls[1], "https://techbookfest.org/api/graphql");
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("price が 0 の書籍も返す", async () => {
|
|
86
|
-
const http = await makeSearchHttp("techbookfest-search.json");
|
|
87
|
-
|
|
88
|
-
const results = await techbookfestAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
89
|
-
|
|
90
|
-
const freeBook = results.find(b => b.url === "https://techbookfest.org/product/01HXXXX3");
|
|
91
|
-
assert.strictEqual(freeBook?.price, 0);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { DEFAULT_PUBLISHERS } from "../../../src/adapters/publishers/registry.js";
|
|
4
|
-
|
|
5
|
-
describe("DEFAULT_PUBLISHERS", () => {
|
|
6
|
-
it("アダプターが1件以上登録されている", () => {
|
|
7
|
-
assert.ok(DEFAULT_PUBLISHERS.length > 0);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("id がすべて一意である", () => {
|
|
11
|
-
const ids = DEFAULT_PUBLISHERS.map(p => p.id);
|
|
12
|
-
const uniqueIds = new Set(ids);
|
|
13
|
-
assert.strictEqual(uniqueIds.size, ids.length);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("baseUrl がすべて一意である", () => {
|
|
17
|
-
const urls = DEFAULT_PUBLISHERS.map(p => p.baseUrl);
|
|
18
|
-
const uniqueUrls = new Set(urls);
|
|
19
|
-
assert.strictEqual(uniqueUrls.size, urls.length);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("各アダプターが必須フィールドを持つ", () => {
|
|
23
|
-
for (const p of DEFAULT_PUBLISHERS) {
|
|
24
|
-
assert.ok(p.id, `${p.id}: id が空`);
|
|
25
|
-
assert.ok(p.name, `${p.id}: name が空`);
|
|
26
|
-
assert.ok(p.baseUrl, `${p.id}: baseUrl が空`);
|
|
27
|
-
assert.strictEqual(typeof p.search, "function", `${p.id}: search が関数でない`);
|
|
28
|
-
assert.strictEqual(typeof p.getDetail, "function", `${p.id}: getDetail が関数でない`);
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("baseUrl がすべて https:// で始まる", () => {
|
|
33
|
-
for (const p of DEFAULT_PUBLISHERS) {
|
|
34
|
-
assert.match(p.baseUrl, /^https:\/\//, `${p.id}: baseUrl が https でない`);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
});
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { getBookDetail } from "../../../src/application/get-book-detail.js";
|
|
4
|
-
import type { PublisherAdapter, PublisherDeps } from "../../../src/domain/publisher.js";
|
|
5
|
-
import type { BookRecord } from "../../../src/domain/book.js";
|
|
6
|
-
import { MockHttpClient } from "../../../src/adapters/http/mock-client.js";
|
|
7
|
-
import { CheerioHtmlParser } from "../../../src/adapters/html/cheerio-parser.js";
|
|
8
|
-
import { NullCacheStore } from "../../../src/adapters/cache/null-cache.js";
|
|
9
|
-
|
|
10
|
-
/** ランタイム非依存の最小モック関数 */
|
|
11
|
-
function mockFn<T>(impl: (...args: unknown[]) => T = () => undefined as T) {
|
|
12
|
-
const _calls: { arguments: unknown[] }[] = [];
|
|
13
|
-
const fn = Object.assign(
|
|
14
|
-
(...args: unknown[]) => {
|
|
15
|
-
_calls.push({ arguments: args });
|
|
16
|
-
return impl(...args);
|
|
17
|
-
},
|
|
18
|
-
{ mock: { calls: _calls, callCount: () => _calls.length } },
|
|
19
|
-
);
|
|
20
|
-
return fn;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function makeDeps(): PublisherDeps {
|
|
24
|
-
return {
|
|
25
|
-
http: new MockHttpClient(),
|
|
26
|
-
parser: new CheerioHtmlParser(),
|
|
27
|
-
cache: new NullCacheStore(),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function makeBook(overrides: Partial<BookRecord> = {}): BookRecord {
|
|
32
|
-
return {
|
|
33
|
-
title: "テスト本",
|
|
34
|
-
authors: ["著者名"],
|
|
35
|
-
publisher: "テスト社",
|
|
36
|
-
url: "https://example.com/book/1",
|
|
37
|
-
...overrides,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function makeAdapter(baseUrl: string, book: BookRecord): PublisherAdapter {
|
|
42
|
-
return {
|
|
43
|
-
id: "test",
|
|
44
|
-
name: "テスト社",
|
|
45
|
-
baseUrl,
|
|
46
|
-
search: mockFn(),
|
|
47
|
-
getDetail: mockFn(async () => book),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe("getBookDetail()", () => {
|
|
52
|
-
it("URLに対応するアダプターの getDetail() を呼んで結果を返す", async () => {
|
|
53
|
-
const book = makeBook({ title: "詳細情報テスト" });
|
|
54
|
-
const adapter = makeAdapter("https://example.com", book);
|
|
55
|
-
const url = "https://example.com/book/42";
|
|
56
|
-
|
|
57
|
-
const result = await getBookDetail(url, [adapter], makeDeps());
|
|
58
|
-
|
|
59
|
-
assert.deepStrictEqual(result, book);
|
|
60
|
-
assert.strictEqual(
|
|
61
|
-
(adapter.getDetail as ReturnType<typeof mockFn>).mock.calls[0].arguments[0],
|
|
62
|
-
url,
|
|
63
|
-
);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("baseUrl が前方一致するアダプターを選択する", async () => {
|
|
67
|
-
const bookA = makeBook({ title: "A社の本" });
|
|
68
|
-
const bookB = makeBook({ title: "B社の本" });
|
|
69
|
-
const adapterA = makeAdapter("https://a.example.com", bookA);
|
|
70
|
-
const adapterB = makeAdapter("https://b.example.com", bookB);
|
|
71
|
-
|
|
72
|
-
const result = await getBookDetail("https://b.example.com/book/1", [adapterA, adapterB], makeDeps());
|
|
73
|
-
|
|
74
|
-
assert.strictEqual(result.title, "B社の本");
|
|
75
|
-
assert.strictEqual((adapterA.getDetail as ReturnType<typeof mockFn>).mock.callCount(), 0);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("対応するアダプターがなければエラーをスローする", async () => {
|
|
79
|
-
const adapter = makeAdapter("https://other.example.com", makeBook());
|
|
80
|
-
|
|
81
|
-
await assert.rejects(
|
|
82
|
-
getBookDetail("https://unknown.example.com/book/1", [adapter], makeDeps()),
|
|
83
|
-
/このURLに対応する出版社アダプターがありません/,
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("エラーメッセージに対応URLリストを含む", async () => {
|
|
88
|
-
const adapter = makeAdapter("https://example.com", makeBook());
|
|
89
|
-
|
|
90
|
-
await assert.rejects(
|
|
91
|
-
getBookDetail("https://unknown.example.com/book/1", [adapter], makeDeps()),
|
|
92
|
-
/https:\/\/example\.com/,
|
|
93
|
-
);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("アダプターが空のときエラーをスローする", async () => {
|
|
97
|
-
await assert.rejects(
|
|
98
|
-
getBookDetail("https://example.com/book/1", [], makeDeps()),
|
|
99
|
-
/このURLに対応する出版社アダプターがありません/,
|
|
100
|
-
);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { describe, it } from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { searchBooks } from "../../../src/application/search-books.js";
|
|
4
|
-
import type { PublisherAdapter, PublisherDeps } from "../../../src/domain/publisher.js";
|
|
5
|
-
import type { BookRecord } from "../../../src/domain/book.js";
|
|
6
|
-
import { MockHttpClient } from "../../../src/adapters/http/mock-client.js";
|
|
7
|
-
import { CheerioHtmlParser } from "../../../src/adapters/html/cheerio-parser.js";
|
|
8
|
-
import { NullCacheStore } from "../../../src/adapters/cache/null-cache.js";
|
|
9
|
-
|
|
10
|
-
/** ランタイム非依存の最小モック関数 */
|
|
11
|
-
function mockFn<T>(impl: (...args: unknown[]) => T = () => undefined as T) {
|
|
12
|
-
const _calls: { arguments: unknown[] }[] = [];
|
|
13
|
-
const fn = Object.assign(
|
|
14
|
-
(...args: unknown[]) => {
|
|
15
|
-
_calls.push({ arguments: args });
|
|
16
|
-
return impl(...args);
|
|
17
|
-
},
|
|
18
|
-
{ mock: { calls: _calls, callCount: () => _calls.length } },
|
|
19
|
-
);
|
|
20
|
-
return fn;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function makeDeps(): PublisherDeps {
|
|
24
|
-
return {
|
|
25
|
-
http: new MockHttpClient(),
|
|
26
|
-
parser: new CheerioHtmlParser(),
|
|
27
|
-
cache: new NullCacheStore(),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function makeBook(overrides: Partial<BookRecord> = {}): BookRecord {
|
|
32
|
-
return {
|
|
33
|
-
title: "テスト本",
|
|
34
|
-
authors: ["著者名"],
|
|
35
|
-
publisher: "テスト社",
|
|
36
|
-
url: "https://example.com/book/1",
|
|
37
|
-
...overrides,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function makeAdapter(id: string, books: BookRecord[]): PublisherAdapter {
|
|
42
|
-
return {
|
|
43
|
-
id,
|
|
44
|
-
name: `${id} 出版社`,
|
|
45
|
-
baseUrl: `https://${id}.example.com`,
|
|
46
|
-
search: mockFn(async () => books),
|
|
47
|
-
getDetail: mockFn(),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe("searchBooks()", () => {
|
|
52
|
-
it("全アダプターの結果を結合して返す", async () => {
|
|
53
|
-
const book1 = makeBook({ title: "本A", url: "https://a.example.com/1" });
|
|
54
|
-
const book2 = makeBook({ title: "本B", url: "https://b.example.com/1" });
|
|
55
|
-
const publishers = [makeAdapter("a", [book1]), makeAdapter("b", [book2])];
|
|
56
|
-
|
|
57
|
-
const { books, errors } = await searchBooks({ title: "テスト" }, publishers, makeDeps());
|
|
58
|
-
|
|
59
|
-
assert.strictEqual(books.length, 2);
|
|
60
|
-
assert.strictEqual(books[0].title, "本A");
|
|
61
|
-
assert.strictEqual(books[1].title, "本B");
|
|
62
|
-
assert.strictEqual(errors.length, 0);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("publisherId が指定された場合は該当アダプターのみ呼ぶ", async () => {
|
|
66
|
-
const book = makeBook({ title: "本A" });
|
|
67
|
-
const adapterA = makeAdapter("a", [book]);
|
|
68
|
-
const adapterB = makeAdapter("b", []);
|
|
69
|
-
const publishers = [adapterA, adapterB];
|
|
70
|
-
|
|
71
|
-
const { books } = await searchBooks({ title: "テスト", publisherId: "a" }, publishers, makeDeps());
|
|
72
|
-
|
|
73
|
-
assert.strictEqual(books.length, 1);
|
|
74
|
-
assert.strictEqual((adapterA.search as ReturnType<typeof mockFn>).mock.callCount(), 1);
|
|
75
|
-
assert.strictEqual((adapterB.search as ReturnType<typeof mockFn>).mock.callCount(), 0);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("1つのアダプターが失敗しても他の結果は返す", async () => {
|
|
79
|
-
const book = makeBook({ title: "成功" });
|
|
80
|
-
const failingAdapter: PublisherAdapter = {
|
|
81
|
-
id: "fail",
|
|
82
|
-
name: "失敗社",
|
|
83
|
-
baseUrl: "https://fail.example.com",
|
|
84
|
-
search: mockFn(() => Promise.reject(new Error("network error"))),
|
|
85
|
-
getDetail: mockFn(),
|
|
86
|
-
};
|
|
87
|
-
const publishers = [failingAdapter, makeAdapter("ok", [book])];
|
|
88
|
-
|
|
89
|
-
const { books, errors } = await searchBooks({ title: "テスト" }, publishers, makeDeps());
|
|
90
|
-
|
|
91
|
-
assert.strictEqual(books.length, 1);
|
|
92
|
-
assert.strictEqual(books[0].title, "成功");
|
|
93
|
-
assert.strictEqual(errors.length, 1);
|
|
94
|
-
assert.deepStrictEqual(errors[0], { publisherId: "fail", message: "network error" });
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("全アダプターが失敗した場合は books が空で errors に全件入る", async () => {
|
|
98
|
-
const publishers = [
|
|
99
|
-
{ id: "a", name: "A社", baseUrl: "https://a.example.com", search: mockFn(() => Promise.reject(new Error("err A"))), getDetail: mockFn() },
|
|
100
|
-
{ id: "b", name: "B社", baseUrl: "https://b.example.com", search: mockFn(() => Promise.reject(new Error("err B"))), getDetail: mockFn() },
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
const { books, errors } = await searchBooks({ title: "テスト" }, publishers, makeDeps());
|
|
104
|
-
|
|
105
|
-
assert.strictEqual(books.length, 0);
|
|
106
|
-
assert.strictEqual(errors.length, 2);
|
|
107
|
-
assert.deepStrictEqual(errors.map(e => e.publisherId), ["a", "b"]);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("Error 以外の例外も文字列化して errors に入れる", async () => {
|
|
111
|
-
const publishers = [
|
|
112
|
-
{ id: "x", name: "X社", baseUrl: "https://x.example.com", search: mockFn(() => Promise.reject("string error")), getDetail: mockFn() },
|
|
113
|
-
];
|
|
114
|
-
|
|
115
|
-
const { errors } = await searchBooks({ title: "テスト" }, publishers, makeDeps());
|
|
116
|
-
|
|
117
|
-
assert.strictEqual(errors[0].message, "string error");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("アダプターが0件のとき空配列を返す", async () => {
|
|
121
|
-
const { books, errors } = await searchBooks({ title: "テスト" }, [], makeDeps());
|
|
122
|
-
assert.deepStrictEqual(books, []);
|
|
123
|
-
assert.deepStrictEqual(errors, []);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("クエリをそのまま各アダプターの search() に渡す", async () => {
|
|
127
|
-
const adapter = makeAdapter("a", []);
|
|
128
|
-
const query = { title: "TypeScript", author: "山田", limit: 5 };
|
|
129
|
-
|
|
130
|
-
await searchBooks(query, [adapter], makeDeps());
|
|
131
|
-
|
|
132
|
-
assert.deepStrictEqual(
|
|
133
|
-
(adapter.search as ReturnType<typeof mockFn>).mock.calls[0].arguments[0],
|
|
134
|
-
query,
|
|
135
|
-
);
|
|
136
|
-
});
|
|
137
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"outDir": "./dist",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"declaration": true,
|
|
12
|
-
"declarationMap": true,
|
|
13
|
-
"sourceMap": true
|
|
14
|
-
},
|
|
15
|
-
"include": ["src/**/*"],
|
|
16
|
-
"exclude": ["node_modules", "dist"]
|
|
17
|
-
}
|