@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.
Files changed (151) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +39 -20
  3. package/dist/adapters/calil.d.ts +10 -0
  4. package/dist/adapters/calil.d.ts.map +1 -0
  5. package/dist/adapters/calil.js +45 -0
  6. package/dist/adapters/calil.js.map +1 -0
  7. package/dist/adapters/openbd.d.ts +57 -0
  8. package/dist/adapters/openbd.d.ts.map +1 -0
  9. package/dist/adapters/openbd.js +87 -0
  10. package/dist/adapters/openbd.js.map +1 -0
  11. package/dist/adapters/publishers/google-books.d.ts +4 -0
  12. package/dist/adapters/publishers/google-books.d.ts.map +1 -0
  13. package/dist/adapters/publishers/google-books.js +75 -0
  14. package/dist/adapters/publishers/google-books.js.map +1 -0
  15. package/dist/adapters/publishers/isbn-publisher-codes.d.ts +21 -0
  16. package/dist/adapters/publishers/isbn-publisher-codes.d.ts.map +1 -0
  17. package/dist/adapters/publishers/isbn-publisher-codes.js +49 -0
  18. package/dist/adapters/publishers/isbn-publisher-codes.js.map +1 -0
  19. package/dist/adapters/publishers/juse-p.d.ts +3 -0
  20. package/dist/adapters/publishers/juse-p.d.ts.map +1 -0
  21. package/dist/adapters/publishers/juse-p.js +110 -0
  22. package/dist/adapters/publishers/juse-p.js.map +1 -0
  23. package/dist/adapters/publishers/registry.d.ts.map +1 -1
  24. package/dist/adapters/publishers/registry.js +4 -0
  25. package/dist/adapters/publishers/registry.js.map +1 -1
  26. package/dist/adapters/publishers/tatsu-zine.d.ts.map +1 -1
  27. package/dist/adapters/publishers/tatsu-zine.js +6 -18
  28. package/dist/adapters/publishers/tatsu-zine.js.map +1 -1
  29. package/dist/application/get-book-by-isbn.d.ts +13 -0
  30. package/dist/application/get-book-by-isbn.d.ts.map +1 -0
  31. package/dist/application/get-book-by-isbn.js +61 -0
  32. package/dist/application/get-book-by-isbn.js.map +1 -0
  33. package/dist/application/get-book-detail.d.ts.map +1 -1
  34. package/dist/application/get-book-detail.js +16 -1
  35. package/dist/application/get-book-detail.js.map +1 -1
  36. package/dist/application/search-books.d.ts.map +1 -1
  37. package/dist/application/search-books.js +20 -0
  38. package/dist/application/search-books.js.map +1 -1
  39. package/dist/config/credentials.d.ts +8 -0
  40. package/dist/config/credentials.d.ts.map +1 -0
  41. package/dist/config/credentials.js +32 -0
  42. package/dist/config/credentials.js.map +1 -0
  43. package/dist/main.js +15 -1
  44. package/dist/main.js.map +1 -1
  45. package/dist/mcp/server.d.ts.map +1 -1
  46. package/dist/mcp/server.js +10 -0
  47. package/dist/mcp/server.js.map +1 -1
  48. package/dist/mcp/tools.d.ts +13 -0
  49. package/dist/mcp/tools.d.ts.map +1 -1
  50. package/dist/mcp/tools.js +16 -0
  51. package/dist/mcp/tools.js.map +1 -1
  52. package/dist/setup.d.ts +2 -0
  53. package/dist/setup.d.ts.map +1 -0
  54. package/dist/setup.js +43 -0
  55. package/dist/setup.js.map +1 -0
  56. package/flake.lock +61 -0
  57. package/package.json +1 -1
  58. package/.claude/settings.local.json +0 -36
  59. package/.codex/skills/techbook-mcp-release-prep/SKILL.md +0 -105
  60. package/.github/workflows/test.yml +0 -72
  61. package/.oxlintrc.json +0 -12
  62. package/AGENTS.md +0 -100
  63. package/deno.json +0 -3
  64. package/src/adapters/cache/memory-cache.ts +0 -31
  65. package/src/adapters/cache/null-cache.ts +0 -8
  66. package/src/adapters/html/cheerio-parser.ts +0 -50
  67. package/src/adapters/http/fetch-client.ts +0 -47
  68. package/src/adapters/http/mock-client.ts +0 -77
  69. package/src/adapters/publishers/base.ts +0 -279
  70. package/src/adapters/publishers/book-tech.ts +0 -117
  71. package/src/adapters/publishers/born-digital.ts +0 -143
  72. package/src/adapters/publishers/coronasha.ts +0 -139
  73. package/src/adapters/publishers/gihyo.ts +0 -120
  74. package/src/adapters/publishers/impress.ts +0 -103
  75. package/src/adapters/publishers/lambdanote.ts +0 -146
  76. package/src/adapters/publishers/manatee.ts +0 -113
  77. package/src/adapters/publishers/maruzen-publishing.ts +0 -129
  78. package/src/adapters/publishers/optronics.ts +0 -113
  79. package/src/adapters/publishers/oreilly-japan.ts +0 -133
  80. package/src/adapters/publishers/peaks.ts +0 -98
  81. package/src/adapters/publishers/personal-media.ts +0 -168
  82. package/src/adapters/publishers/registry.ts +0 -38
  83. package/src/adapters/publishers/rutles.ts +0 -149
  84. package/src/adapters/publishers/saiensu.ts +0 -136
  85. package/src/adapters/publishers/seshop.ts +0 -121
  86. package/src/adapters/publishers/tatsu-zine.ts +0 -154
  87. package/src/adapters/publishers/techbookfest.ts +0 -179
  88. package/src/application/get-book-detail.ts +0 -24
  89. package/src/application/search-books.ts +0 -44
  90. package/src/domain/book.ts +0 -35
  91. package/src/domain/publisher.ts +0 -18
  92. package/src/main.ts +0 -14
  93. package/src/mcp/server.ts +0 -103
  94. package/src/mcp/tools.ts +0 -54
  95. package/src/ports/cache.ts +0 -5
  96. package/src/ports/html-parser.ts +0 -15
  97. package/src/ports/http.ts +0 -17
  98. package/tests/fixtures/book-tech-detail.html +0 -51
  99. package/tests/fixtures/book-tech-search.html +0 -91
  100. package/tests/fixtures/born-digital-detail.html +0 -62
  101. package/tests/fixtures/born-digital-search.html +0 -51
  102. package/tests/fixtures/coronasha-detail.html +0 -41
  103. package/tests/fixtures/coronasha-search.html +0 -61
  104. package/tests/fixtures/gihyo-detail.html +0 -42
  105. package/tests/fixtures/gihyo-search.json +0 -54
  106. package/tests/fixtures/impress-detail-epub.html +0 -746
  107. package/tests/fixtures/impress-detail-social.html +0 -689
  108. package/tests/fixtures/lambdanote-search.html +0 -66
  109. package/tests/fixtures/manatee-detail.html +0 -53
  110. package/tests/fixtures/manatee-search.html +0 -59
  111. package/tests/fixtures/maruzen-detail.html +0 -51
  112. package/tests/fixtures/maruzen-search.html +0 -60
  113. package/tests/fixtures/optronics-detail.html +0 -30
  114. package/tests/fixtures/optronics-search.html +0 -75
  115. package/tests/fixtures/oreilly-detail.html +0 -52
  116. package/tests/fixtures/oreilly-ebook-list.html +0 -53
  117. package/tests/fixtures/peaks-detail.html +0 -39
  118. package/tests/fixtures/peaks-top.html +0 -50
  119. package/tests/fixtures/personal-media-detail.html +0 -32
  120. package/tests/fixtures/personal-media-search.html +0 -39
  121. package/tests/fixtures/rutles-detail.html +0 -32
  122. package/tests/fixtures/rutles-search.html +0 -62
  123. package/tests/fixtures/saiensu-detail.html +0 -41
  124. package/tests/fixtures/saiensu-search.html +0 -65
  125. package/tests/fixtures/seshop-detail.html +0 -45
  126. package/tests/fixtures/seshop-search.html +0 -58
  127. package/tests/fixtures/tatsu-zine-detail-free.html +0 -22
  128. package/tests/fixtures/tatsu-zine-search.html +0 -40
  129. package/tests/fixtures/techbookfest-search.json +0 -73
  130. package/tests/unit/adapters/base.test.ts +0 -441
  131. package/tests/unit/adapters/publishers/book-tech.test.ts +0 -186
  132. package/tests/unit/adapters/publishers/born-digital.test.ts +0 -194
  133. package/tests/unit/adapters/publishers/coronasha.test.ts +0 -207
  134. package/tests/unit/adapters/publishers/gihyo.test.ts +0 -137
  135. package/tests/unit/adapters/publishers/impress.test.ts +0 -129
  136. package/tests/unit/adapters/publishers/lambdanote.test.ts +0 -85
  137. package/tests/unit/adapters/publishers/manatee.test.ts +0 -165
  138. package/tests/unit/adapters/publishers/maruzen-publishing.test.ts +0 -179
  139. package/tests/unit/adapters/publishers/optronics.test.ts +0 -208
  140. package/tests/unit/adapters/publishers/oreilly-japan.test.ts +0 -194
  141. package/tests/unit/adapters/publishers/peaks.test.ts +0 -177
  142. package/tests/unit/adapters/publishers/personal-media.test.ts +0 -199
  143. package/tests/unit/adapters/publishers/rutles.test.ts +0 -173
  144. package/tests/unit/adapters/publishers/saiensu.test.ts +0 -169
  145. package/tests/unit/adapters/publishers/seshop.test.ts +0 -174
  146. package/tests/unit/adapters/publishers/tatsu-zine.test.ts +0 -172
  147. package/tests/unit/adapters/publishers/techbookfest.test.ts +0 -94
  148. package/tests/unit/adapters/registry.test.ts +0 -37
  149. package/tests/unit/application/get-book-detail.test.ts +0 -102
  150. package/tests/unit/application/search-books.test.ts +0 -137
  151. package/tsconfig.json +0 -17
@@ -1,441 +0,0 @@
1
- import { describe, it } from "node:test";
2
- import assert from "node:assert/strict";
3
- import {
4
- parseJapanesePrice,
5
- stripHtmlTags,
6
- resolveUrl,
7
- extractAsin,
8
- classifyEbookStore,
9
- extractEbookStoresFromDoc,
10
- fetchText,
11
- encodeEucJp,
12
- parseJapaneseDateToISO,
13
- stripAuthorRole,
14
- checkRobotsTxt,
15
- ROBOTS_CACHE_TTL_SECONDS,
16
- } from "../../../src/adapters/publishers/base.js";
17
- import { MockHttpClient } from "../../../src/adapters/http/mock-client.js";
18
- import { CheerioHtmlParser } from "../../../src/adapters/html/cheerio-parser.js";
19
- import { NullCacheStore } from "../../../src/adapters/cache/null-cache.js";
20
- import { MemoryCacheStore } from "../../../src/adapters/cache/memory-cache.js";
21
-
22
- function makeDeps(http: MockHttpClient, cache = new NullCacheStore()) {
23
- return { http, parser: new CheerioHtmlParser(), cache };
24
- }
25
-
26
- // --- encodeEucJp ---
27
-
28
- describe("encodeEucJp()", () => {
29
- it("ASCII文字はそのままパーセントエンコードする", () => {
30
- assert.match(encodeEucJp("abc"), /^(%[0-9A-F]{2})+$/);
31
- });
32
-
33
- it("日本語をEUC-JPでエンコードする", () => {
34
- const result = encodeEucJp("TypeScript");
35
- // EUC-JPでエンコードされた結果は%XX形式
36
- assert.match(result, /^(%[0-9A-F]{2})+$/);
37
- });
38
-
39
- it("空文字列は空文字列を返す", () => {
40
- assert.strictEqual(encodeEucJp(""), "");
41
- });
42
- });
43
-
44
- // --- parseJapaneseDateToISO ---
45
-
46
- describe("parseJapaneseDateToISO()", () => {
47
- it("YYYY年M月D日 を YYYY-MM-DD に変換する", () => {
48
- assert.strictEqual(parseJapaneseDateToISO("2026年3月25日"), "2026-03-25");
49
- });
50
-
51
- it("1桁の月・日もゼロパディングする", () => {
52
- assert.strictEqual(parseJapaneseDateToISO("2024年1月5日"), "2024-01-05");
53
- });
54
-
55
- it("日付パターンがなければ undefined を返す", () => {
56
- assert.strictEqual(parseJapaneseDateToISO("発行:サイエンス社"), undefined);
57
- });
58
-
59
- it("テキスト中に埋め込まれていても抽出できる", () => {
60
- assert.strictEqual(parseJapaneseDateToISO("発行日:2026年3月25日"), "2026-03-25");
61
- });
62
- });
63
-
64
- // --- stripAuthorRole ---
65
-
66
- describe("stripAuthorRole()", () => {
67
- it("末尾の「著」を除去する", () => {
68
- assert.strictEqual(stripAuthorRole("Dan Vanderkam 著"), "Dan Vanderkam");
69
- });
70
-
71
- it("末尾の「訳」を除去する", () => {
72
- assert.strictEqual(stripAuthorRole("今村 謙士 訳"), "今村 謙士");
73
- });
74
-
75
- it("末尾の「監修」を除去する", () => {
76
- assert.strictEqual(stripAuthorRole("堀井俊佑 監修"), "堀井俊佑");
77
- });
78
-
79
- it("末尾の「著訳」を除去する", () => {
80
- assert.strictEqual(stripAuthorRole("島田浩二 著訳"), "島田浩二");
81
- });
82
-
83
- it("役割語がなければそのまま返す", () => {
84
- assert.strictEqual(stripAuthorRole("山田太郎"), "山田太郎");
85
- });
86
-
87
- it("前後の空白・全角スペースをトリムする", () => {
88
- assert.strictEqual(stripAuthorRole(" 著者名 "), "著者名");
89
- });
90
- });
91
-
92
- // --- parseJapanesePrice ---
93
-
94
- describe("parseJapanesePrice()", () => {
95
- it("カンマ区切りの円表記をパースする", () => {
96
- assert.strictEqual(parseJapanesePrice("3,960円(税込)"), 3960);
97
- });
98
-
99
- it("¥記号付きをパースする", () => {
100
- assert.strictEqual(parseJapanesePrice("¥3,960"), 3960);
101
- });
102
-
103
- it("カンマなし整数をパースする", () => {
104
- assert.strictEqual(parseJapanesePrice("1980円"), 1980);
105
- });
106
-
107
- it("数字がなければ undefined を返す", () => {
108
- assert.strictEqual(parseJapanesePrice("価格未定"), undefined);
109
- });
110
- });
111
-
112
- // --- stripHtmlTags ---
113
-
114
- describe("stripHtmlTags()", () => {
115
- it("HTMLタグを除去してテキストを返す", () => {
116
- assert.strictEqual(stripHtmlTags("<b>太字</b>テキスト"), "太字テキスト");
117
- });
118
-
119
- it("ruby タグを除去する(rt の中身は残る)", () => {
120
- // タグを除去するだけなので <rt> 内のテキストは残る
121
- assert.strictEqual(stripHtmlTags("<ruby>著者<rt>ちょしゃ</rt></ruby>名"), "著者ちょしゃ名");
122
- });
123
-
124
- it("タグがなければそのまま返す", () => {
125
- assert.strictEqual(stripHtmlTags("プレーンテキスト"), "プレーンテキスト");
126
- });
127
-
128
- it("空文字列はそのまま", () => {
129
- assert.strictEqual(stripHtmlTags(""), "");
130
- });
131
- });
132
-
133
- // --- resolveUrl ---
134
-
135
- describe("resolveUrl()", () => {
136
- it("相対パスを絶対URLに変換する", () => {
137
- assert.strictEqual(
138
- resolveUrl("https://example.com/books/", "../detail/1"),
139
- "https://example.com/detail/1",
140
- );
141
- });
142
-
143
- it("絶対URLはそのまま返す", () => {
144
- assert.strictEqual(
145
- resolveUrl("https://example.com/", "https://other.com/page"),
146
- "https://other.com/page",
147
- );
148
- });
149
-
150
- it("ルート相対パスを解決する", () => {
151
- assert.strictEqual(
152
- resolveUrl("https://example.com/books/123", "/about"),
153
- "https://example.com/about",
154
- );
155
- });
156
- });
157
-
158
- // --- extractAsin ---
159
-
160
- describe("extractAsin()", () => {
161
- it("/dp/ 形式から ASIN を抽出する", () => {
162
- assert.strictEqual(
163
- extractAsin("https://www.amazon.co.jp/dp/4873119464"),
164
- "4873119464",
165
- );
166
- });
167
-
168
- it("/gp/product/ 形式から ASIN を抽出する", () => {
169
- assert.strictEqual(
170
- extractAsin("https://www.amazon.co.jp/gp/product/4873119464"),
171
- "4873119464",
172
- );
173
- });
174
-
175
- it("Amazon URL でなければ undefined を返す", () => {
176
- assert.strictEqual(extractAsin("https://example.com/books/123"), undefined);
177
- });
178
-
179
- it("ASIN を含まなければ undefined を返す", () => {
180
- assert.strictEqual(extractAsin("https://www.amazon.co.jp/"), undefined);
181
- });
182
- });
183
-
184
- // --- classifyEbookStore ---
185
-
186
- describe("classifyEbookStore()", () => {
187
- it("技術書典URLは free を返す", () => {
188
- const store = classifyEbookStore("https://techbookfest.org/product/abc123");
189
- assert.partialDeepStrictEqual(store, { name: "技術書典", drm: "free" });
190
- });
191
-
192
- it("Kindle URLは drm を返す", () => {
193
- const store = classifyEbookStore("https://www.amazon.co.jp/dp/B0XXXXX");
194
- assert.partialDeepStrictEqual(store, { name: "Kindle", drm: "drm" });
195
- });
196
-
197
- it("サイエンス社は password_pdf を返す", () => {
198
- const store = classifyEbookStore("https://www.saiensu.co.jp/search/?isbn=978-4-7819-1234-5&y=2024#book");
199
- assert.partialDeepStrictEqual(store, { name: "サイエンス社", drm: "password_pdf" });
200
- });
201
-
202
- it("SEshop URLは social を返す", () => {
203
- const store = classifyEbookStore("https://www.seshop.com/product/detail/12345");
204
- assert.partialDeepStrictEqual(store, { name: "SEshop", drm: "social" });
205
- });
206
-
207
- it("未知のURLは null を返す", () => {
208
- assert.strictEqual(classifyEbookStore("https://unknown-store.example.com/book/1"), null);
209
- });
210
-
211
- it("URLを store.url に格納する", () => {
212
- const url = "https://techbookfest.org/product/abc123";
213
- assert.strictEqual(classifyEbookStore(url)?.url, url);
214
- });
215
- });
216
-
217
- // --- extractEbookStoresFromDoc ---
218
-
219
- describe("extractEbookStoresFromDoc()", () => {
220
- const parser = new CheerioHtmlParser();
221
-
222
- it("ページ内のストアリンクを抽出する", () => {
223
- const html = `<html><body>
224
- <a href="https://techbookfest.org/product/abc">技術書典</a>
225
- <a href="https://www.amazon.co.jp/dp/B0XXXXX">Kindle</a>
226
- </body></html>`;
227
- const doc = parser.parse(html);
228
- const stores = extractEbookStoresFromDoc(doc);
229
- assert.strictEqual(stores.length, 2);
230
- assert.partialDeepStrictEqual(stores[0], { name: "技術書典", drm: "free" });
231
- assert.partialDeepStrictEqual(stores[1], { name: "Kindle", drm: "drm" });
232
- });
233
-
234
- it("同一ストアのURLが複数あれば最初の1件のみ返す", () => {
235
- const html = `<html><body>
236
- <a href="https://www.amazon.co.jp/dp/B0AAAAA">Kindle A</a>
237
- <a href="https://www.amazon.co.jp/dp/B0BBBBB">Kindle B</a>
238
- </body></html>`;
239
- const doc = parser.parse(html);
240
- const stores = extractEbookStoresFromDoc(doc);
241
- assert.strictEqual(stores.length, 1);
242
- assert.strictEqual(stores[0].url, "https://www.amazon.co.jp/dp/B0AAAAA");
243
- });
244
-
245
- it("既知ストアへのリンクがなければ空配列を返す", () => {
246
- const html = `<html><body><a href="https://example.com/">不明</a></body></html>`;
247
- const doc = parser.parse(html);
248
- assert.deepStrictEqual(extractEbookStoresFromDoc(doc), []);
249
- });
250
-
251
- it("href を持たない a 要素は無視する", () => {
252
- const html = `<html><body><a>リンクなし</a></body></html>`;
253
- const doc = parser.parse(html);
254
- assert.deepStrictEqual(extractEbookStoresFromDoc(doc), []);
255
- });
256
- });
257
-
258
- // --- fetchText ---
259
-
260
- describe("fetchText()", () => {
261
- it("HTTPレスポンスのテキストを返す", async () => {
262
- const http = new MockHttpClient().addResponse(
263
- "https://example.com/page",
264
- { status: 200, body: "<html>hello</html>" },
265
- );
266
- const result = await fetchText("https://example.com/page", makeDeps(http));
267
- assert.strictEqual(result, "<html>hello</html>");
268
- });
269
-
270
- it("200以外はエラーをスローする", async () => {
271
- const http = new MockHttpClient().addResponse(
272
- "https://example.com/page",
273
- { status: 404, body: "Not Found" },
274
- );
275
- await assert.rejects(
276
- fetchText("https://example.com/page", makeDeps(http)),
277
- /HTTP 404/,
278
- );
279
- });
280
-
281
- it("キャッシュヒット時はHTTPを呼ばない", async () => {
282
- const cache = new MemoryCacheStore();
283
- await cache.set("https://example.com/page", "<html>cached</html>", 3600);
284
-
285
- const http = new MockHttpClient();
286
- const result = await fetchText("https://example.com/page", makeDeps(http, cache));
287
-
288
- assert.strictEqual(result, "<html>cached</html>");
289
- assert.strictEqual(http.calls.length, 0);
290
- });
291
-
292
- it("取得結果をキャッシュに保存する", async () => {
293
- const cache = new MemoryCacheStore();
294
- const http = new MockHttpClient().addResponse(
295
- "https://example.com/page",
296
- { status: 200, body: "<html>fresh</html>" },
297
- );
298
-
299
- await fetchText("https://example.com/page", makeDeps(http, cache));
300
- const cached = await cache.get("https://example.com/page");
301
- assert.strictEqual(cached, "<html>fresh</html>");
302
- });
303
-
304
- it("extraHeaders をリクエストに含める", async () => {
305
- // MockHttpClient は headers を直接検査できないが、HTTPが正常に呼ばれることを確認
306
- const http = new MockHttpClient().addResponse(
307
- "https://example.com/page",
308
- { status: 200, body: "ok" },
309
- );
310
- const result = await fetchText(
311
- "https://example.com/page",
312
- makeDeps(http),
313
- { Referer: "https://example.com/" },
314
- );
315
- assert.strictEqual(result, "ok");
316
- assert.strictEqual(http.calls.length, 1);
317
- });
318
- });
319
-
320
- // --- checkRobotsTxt ---
321
-
322
- describe("checkRobotsTxt()", () => {
323
- it("robots.txt が存在しない (404) 場合はアクセスを許可する", async () => {
324
- const http = new MockHttpClient().addResponse(
325
- "https://example.com/robots.txt",
326
- { status: 404, body: "Not Found" },
327
- );
328
- const result = await checkRobotsTxt("https://example.com/search?q=foo", makeDeps(http));
329
- assert.strictEqual(result, true);
330
- });
331
-
332
- it("HTTP エラー時はアクセスを許可する (fail-open)", async () => {
333
- // ハンドラー未登録 → MockHttpClient が例外をスロー
334
- const http = new MockHttpClient();
335
- const result = await checkRobotsTxt("https://example.com/search?q=foo", makeDeps(http));
336
- assert.strictEqual(result, true);
337
- });
338
-
339
- it("Disallow がない場合はアクセスを許可する", async () => {
340
- const http = new MockHttpClient().addResponse(
341
- "https://example.com/robots.txt",
342
- { status: 200, body: "User-agent: *\nDisallow:\n" },
343
- );
344
- const result = await checkRobotsTxt("https://example.com/search", makeDeps(http));
345
- assert.strictEqual(result, true);
346
- });
347
-
348
- it("Disallow: / はすべてのパスを禁止する", async () => {
349
- const http = new MockHttpClient().addResponse(
350
- "https://example.com/robots.txt",
351
- { status: 200, body: "User-agent: *\nDisallow: /\n" },
352
- );
353
- const result = await checkRobotsTxt("https://example.com/search?q=foo", makeDeps(http));
354
- assert.strictEqual(result, false);
355
- });
356
-
357
- it("特定パスの Disallow はそのパスだけを禁止する", async () => {
358
- const robotsTxt = "User-agent: *\nDisallow: /private/\n";
359
- const http = new MockHttpClient().addResponse(
360
- "https://example.com/robots.txt",
361
- { status: 200, body: robotsTxt },
362
- );
363
- const deps = makeDeps(http);
364
-
365
- assert.strictEqual(await checkRobotsTxt("https://example.com/private/data", deps), false);
366
- assert.strictEqual(await checkRobotsTxt("https://example.com/public/page", deps), true);
367
- });
368
-
369
- it("techbook-mcp 固有ルールがワイルドカードより優先される", async () => {
370
- const robotsTxt = [
371
- "User-agent: *",
372
- "Disallow: /",
373
- "",
374
- "User-agent: techbook-mcp",
375
- "Allow: /",
376
- ].join("\n");
377
- const http = new MockHttpClient().addResponse(
378
- "https://example.com/robots.txt",
379
- { status: 200, body: robotsTxt },
380
- );
381
- const result = await checkRobotsTxt("https://example.com/search", makeDeps(http));
382
- assert.strictEqual(result, true);
383
- });
384
-
385
- it("Allow が Disallow より長いプレフィックスで一致する場合は許可する", async () => {
386
- const robotsTxt = [
387
- "User-agent: *",
388
- "Disallow: /books/",
389
- "Allow: /books/detail/",
390
- ].join("\n");
391
- const http = new MockHttpClient().addResponse(
392
- "https://example.com/robots.txt",
393
- { status: 200, body: robotsTxt },
394
- );
395
- const deps = makeDeps(http);
396
-
397
- assert.strictEqual(await checkRobotsTxt("https://example.com/books/list", deps), false);
398
- assert.strictEqual(await checkRobotsTxt("https://example.com/books/detail/123", deps), true);
399
- });
400
-
401
- it("robots.txt の結果を ${ROBOTS_CACHE_TTL_SECONDS}秒キャッシュする", async () => {
402
- const http = new MockHttpClient().addResponse(
403
- "https://example.com/robots.txt",
404
- { status: 200, body: "User-agent: *\nDisallow: /\n" },
405
- );
406
- const cache = new MemoryCacheStore();
407
- const deps = makeDeps(http, cache);
408
-
409
- await checkRobotsTxt("https://example.com/page", deps);
410
-
411
- const cached = await cache.get("robots:https://example.com");
412
- assert.notEqual(cached, null);
413
- assert.strictEqual(http.calls.filter(u => u.includes("robots.txt")).length, 1);
414
- });
415
-
416
- it("キャッシュヒット時は HTTP を呼ばない", async () => {
417
- const cache = new MemoryCacheStore();
418
- await cache.set("robots:https://example.com", "", ROBOTS_CACHE_TTL_SECONDS);
419
-
420
- const http = new MockHttpClient();
421
- const result = await checkRobotsTxt("https://example.com/page", makeDeps(http, cache));
422
-
423
- assert.strictEqual(result, true);
424
- assert.strictEqual(http.calls.length, 0);
425
- });
426
-
427
- it("コメント行を無視する", async () => {
428
- const robotsTxt = [
429
- "# このサイトのクローラー設定",
430
- "User-agent: *",
431
- "# 検索ページを禁止",
432
- "Disallow: /search/",
433
- ].join("\n");
434
- const http = new MockHttpClient().addResponse(
435
- "https://example.com/robots.txt",
436
- { status: 200, body: robotsTxt },
437
- );
438
- const result = await checkRobotsTxt("https://example.com/search/query", makeDeps(http));
439
- assert.strictEqual(result, false);
440
- });
441
- });
@@ -1,186 +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 { bookTechAdapter } from "../../../../src/adapters/publishers/book-tech.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
- function makeDeps(http: MockHttpClient) {
13
- return { http, parser: new CheerioHtmlParser(), cache: new NullCacheStore() };
14
- }
15
-
16
- async function loadFixture(name: string): Promise<string> {
17
- return readFile(join(FIXTURES_DIR, name), "utf-8");
18
- }
19
-
20
- describe("bookTechAdapter", () => {
21
- describe("search()", () => {
22
- it("BookRecord[] を返す", async () => {
23
- const body = await loadFixture("book-tech-search.html");
24
- const http = new MockHttpClient().addResponse(
25
- "https://book-tech.com/books",
26
- { status: 200, body },
27
- );
28
-
29
- const results = await bookTechAdapter.search({ title: "TypeScript" }, makeDeps(http));
30
-
31
- assert.strictEqual(results.length, 2);
32
- assert.partialDeepStrictEqual(results[0], {
33
- title: "次のステップへ!React実践開発 サクサク作って学ぶ UI/テスト/デプロイ",
34
- publisher: "インプレス NextPublishing",
35
- url: "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
36
- price: 2178,
37
- publishedAt: "2026-02-20",
38
- });
39
- });
40
-
41
- it("著者名から役割語を除去する", async () => {
42
- const body = await loadFixture("book-tech-search.html");
43
- const http = new MockHttpClient().addResponse(
44
- "https://book-tech.com/books",
45
- { status: 200, body },
46
- );
47
-
48
- const results = await bookTechAdapter.search({ title: "TypeScript" }, makeDeps(http));
49
-
50
- assert.deepStrictEqual(results[0].authors, ["philosophy"]);
51
- assert.deepStrictEqual(results[1].authors, ["井手 優太"]);
52
- });
53
-
54
- it("coverImageUrl が設定される", async () => {
55
- const body = await loadFixture("book-tech-search.html");
56
- const http = new MockHttpClient().addResponse(
57
- "https://book-tech.com/books",
58
- { status: 200, body },
59
- );
60
-
61
- const results = await bookTechAdapter.search({ title: "TypeScript" }, makeDeps(http));
62
-
63
- assert.strictEqual(
64
- results[0].coverImageUrl,
65
- "https://booktech-share.s3-ap-northeast-1.amazonaws.com/books/d80ffe3d.webp",
66
- );
67
- });
68
-
69
- it("ebookStores に BOOK TECH (social DRM) が含まれる", async () => {
70
- const body = await loadFixture("book-tech-search.html");
71
- const http = new MockHttpClient().addResponse(
72
- "https://book-tech.com/books",
73
- { status: 200, body },
74
- );
75
-
76
- const results = await bookTechAdapter.search({ title: "TypeScript" }, makeDeps(http));
77
-
78
- assert.deepStrictEqual(results[0].ebookStores, [
79
- {
80
- name: "BOOK TECH",
81
- url: "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
82
- drm: "social",
83
- },
84
- ]);
85
- });
86
-
87
- it("title も author も空の場合は [] を返しHTTPを呼ばない", async () => {
88
- const http = new MockHttpClient();
89
-
90
- const results = await bookTechAdapter.search({}, makeDeps(http));
91
-
92
- assert.deepStrictEqual(results, []);
93
- assert.strictEqual(http.calls.length, 0);
94
- });
95
-
96
- it("検索リクエストに q[...] パラメータが含まれる", async () => {
97
- const body = await loadFixture("book-tech-search.html");
98
- const http = new MockHttpClient().addResponse(
99
- "https://book-tech.com/books",
100
- { status: 200, body },
101
- );
102
-
103
- await bookTechAdapter.search({ title: "TypeScript" }, makeDeps(http));
104
-
105
- assert.ok(http.calls[0].includes("TypeScript"));
106
- assert.ok(http.calls[0].includes("title_or_overview"));
107
- });
108
- });
109
-
110
- describe("getDetail()", () => {
111
- it("詳細情報を返す", async () => {
112
- const body = await loadFixture("book-tech-detail.html");
113
- const http = new MockHttpClient().addResponse(
114
- "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
115
- { status: 200, body },
116
- );
117
-
118
- const book = await bookTechAdapter.getDetail(
119
- "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
120
- makeDeps(http),
121
- );
122
-
123
- assert.partialDeepStrictEqual(book, {
124
- title: "次のステップへ!React実践開発 サクサク作って学ぶ UI/テスト/デプロイ",
125
- publisher: "インプレス NextPublishing",
126
- isbn: "9784295604136",
127
- price: 2178,
128
- publishedAt: "2026-02-20",
129
- });
130
- });
131
-
132
- it("著者名から役割語を除去する", async () => {
133
- const body = await loadFixture("book-tech-detail.html");
134
- const http = new MockHttpClient().addResponse(
135
- "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
136
- { status: 200, body },
137
- );
138
-
139
- const book = await bookTechAdapter.getDetail(
140
- "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
141
- makeDeps(http),
142
- );
143
-
144
- assert.deepStrictEqual(book.authors, ["philosophy"]);
145
- });
146
-
147
- it("coverImageUrl が設定される", async () => {
148
- const body = await loadFixture("book-tech-detail.html");
149
- const http = new MockHttpClient().addResponse(
150
- "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
151
- { status: 200, body },
152
- );
153
-
154
- const book = await bookTechAdapter.getDetail(
155
- "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
156
- makeDeps(http),
157
- );
158
-
159
- assert.strictEqual(
160
- book.coverImageUrl,
161
- "https://booktech-share.s3-ap-northeast-1.amazonaws.com/books/d80ffe3d.webp",
162
- );
163
- });
164
-
165
- it("ebookStores に BOOK TECH (social DRM) が含まれる", async () => {
166
- const body = await loadFixture("book-tech-detail.html");
167
- const http = new MockHttpClient().addResponse(
168
- "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
169
- { status: 200, body },
170
- );
171
-
172
- const book = await bookTechAdapter.getDetail(
173
- "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
174
- makeDeps(http),
175
- );
176
-
177
- assert.deepStrictEqual(book.ebookStores, [
178
- {
179
- name: "BOOK TECH",
180
- url: "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
181
- drm: "social",
182
- },
183
- ]);
184
- });
185
- });
186
- });