@zonuexe/techbook-mcp 0.1.0

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 (174) hide show
  1. package/.claude/settings.local.json +23 -0
  2. package/.github/workflows/test.yml +36 -0
  3. package/AGENTS.md +72 -0
  4. package/CLAUDE.md +2 -0
  5. package/LICENSE +661 -0
  6. package/README.md +154 -0
  7. package/dist/adapters/cache/memory-cache.d.ts +8 -0
  8. package/dist/adapters/cache/memory-cache.d.ts.map +1 -0
  9. package/dist/adapters/cache/memory-cache.js +23 -0
  10. package/dist/adapters/cache/memory-cache.js.map +1 -0
  11. package/dist/adapters/cache/null-cache.d.ts +8 -0
  12. package/dist/adapters/cache/null-cache.d.ts.map +1 -0
  13. package/dist/adapters/cache/null-cache.js +7 -0
  14. package/dist/adapters/cache/null-cache.js.map +1 -0
  15. package/dist/adapters/html/cheerio-parser.d.ts +5 -0
  16. package/dist/adapters/html/cheerio-parser.d.ts.map +1 -0
  17. package/dist/adapters/html/cheerio-parser.js +45 -0
  18. package/dist/adapters/html/cheerio-parser.js.map +1 -0
  19. package/dist/adapters/http/fetch-client.d.ts +6 -0
  20. package/dist/adapters/http/fetch-client.d.ts.map +1 -0
  21. package/dist/adapters/http/fetch-client.js +43 -0
  22. package/dist/adapters/http/fetch-client.js.map +1 -0
  23. package/dist/adapters/http/mock-client.d.ts +19 -0
  24. package/dist/adapters/http/mock-client.d.ts.map +1 -0
  25. package/dist/adapters/http/mock-client.js +59 -0
  26. package/dist/adapters/http/mock-client.js.map +1 -0
  27. package/dist/adapters/publishers/base.d.ts +24 -0
  28. package/dist/adapters/publishers/base.d.ts.map +1 -0
  29. package/dist/adapters/publishers/base.js +88 -0
  30. package/dist/adapters/publishers/base.js.map +1 -0
  31. package/dist/adapters/publishers/gihyo.d.ts +3 -0
  32. package/dist/adapters/publishers/gihyo.d.ts.map +1 -0
  33. package/dist/adapters/publishers/gihyo.js +75 -0
  34. package/dist/adapters/publishers/gihyo.js.map +1 -0
  35. package/dist/adapters/publishers/lambdanote.d.ts +3 -0
  36. package/dist/adapters/publishers/lambdanote.d.ts.map +1 -0
  37. package/dist/adapters/publishers/lambdanote.js +113 -0
  38. package/dist/adapters/publishers/lambdanote.js.map +1 -0
  39. package/dist/adapters/publishers/registry.d.ts +3 -0
  40. package/dist/adapters/publishers/registry.d.ts.map +1 -0
  41. package/dist/adapters/publishers/registry.js +11 -0
  42. package/dist/adapters/publishers/registry.js.map +1 -0
  43. package/dist/adapters/publishers/tatsu-zine.d.ts +3 -0
  44. package/dist/adapters/publishers/tatsu-zine.d.ts.map +1 -0
  45. package/dist/adapters/publishers/tatsu-zine.js +110 -0
  46. package/dist/adapters/publishers/tatsu-zine.js.map +1 -0
  47. package/dist/adapters/publishers/techbookfest.d.ts +3 -0
  48. package/dist/adapters/publishers/techbookfest.d.ts.map +1 -0
  49. package/dist/adapters/publishers/techbookfest.js +134 -0
  50. package/dist/adapters/publishers/techbookfest.js.map +1 -0
  51. package/dist/application/get-book-detail.d.ts +4 -0
  52. package/dist/application/get-book-detail.d.ts.map +1 -0
  53. package/dist/application/get-book-detail.js +9 -0
  54. package/dist/application/get-book-detail.js.map +1 -0
  55. package/dist/application/search-books.d.ts +11 -0
  56. package/dist/application/search-books.d.ts.map +1 -0
  57. package/dist/application/search-books.js +23 -0
  58. package/dist/application/search-books.js.map +1 -0
  59. package/dist/domain/book.d.ts +32 -0
  60. package/dist/domain/book.d.ts.map +1 -0
  61. package/dist/domain/book.js +2 -0
  62. package/dist/domain/book.js.map +1 -0
  63. package/dist/domain/publisher.d.ts +17 -0
  64. package/dist/domain/publisher.d.ts.map +1 -0
  65. package/dist/domain/publisher.js +2 -0
  66. package/dist/domain/publisher.js.map +1 -0
  67. package/dist/main.d.ts +2 -0
  68. package/dist/main.d.ts.map +1 -0
  69. package/dist/main.js +12 -0
  70. package/dist/main.js.map +1 -0
  71. package/dist/mcp/server.d.ts +5 -0
  72. package/dist/mcp/server.d.ts.map +1 -0
  73. package/dist/mcp/server.js +79 -0
  74. package/dist/mcp/server.js.map +1 -0
  75. package/dist/mcp/tools.d.ts +47 -0
  76. package/dist/mcp/tools.d.ts.map +1 -0
  77. package/dist/mcp/tools.js +53 -0
  78. package/dist/mcp/tools.js.map +1 -0
  79. package/dist/ports/cache.d.ts +6 -0
  80. package/dist/ports/cache.d.ts.map +1 -0
  81. package/dist/ports/cache.js +2 -0
  82. package/dist/ports/cache.js.map +1 -0
  83. package/dist/ports/html-parser.d.ts +14 -0
  84. package/dist/ports/html-parser.d.ts.map +1 -0
  85. package/dist/ports/html-parser.js +2 -0
  86. package/dist/ports/html-parser.js.map +1 -0
  87. package/dist/ports/http.d.ts +16 -0
  88. package/dist/ports/http.d.ts.map +1 -0
  89. package/dist/ports/http.js +2 -0
  90. package/dist/ports/http.js.map +1 -0
  91. package/docs/design-doc.md +365 -0
  92. package/flake.nix +50 -0
  93. package/package.json +29 -0
  94. package/src/adapters/cache/memory-cache.ts +31 -0
  95. package/src/adapters/cache/null-cache.ts +8 -0
  96. package/src/adapters/html/cheerio-parser.ts +49 -0
  97. package/src/adapters/http/fetch-client.ts +47 -0
  98. package/src/adapters/http/mock-client.ts +77 -0
  99. package/src/adapters/publishers/base.ts +129 -0
  100. package/src/adapters/publishers/book-tech.ts +117 -0
  101. package/src/adapters/publishers/born-digital.ts +158 -0
  102. package/src/adapters/publishers/coronasha.ts +139 -0
  103. package/src/adapters/publishers/gihyo.ts +120 -0
  104. package/src/adapters/publishers/lambdanote.ts +146 -0
  105. package/src/adapters/publishers/manatee.ts +112 -0
  106. package/src/adapters/publishers/maruzen-publishing.ts +141 -0
  107. package/src/adapters/publishers/optronics.ts +113 -0
  108. package/src/adapters/publishers/oreilly-japan.ts +138 -0
  109. package/src/adapters/publishers/peaks.ts +98 -0
  110. package/src/adapters/publishers/personal-media.ts +168 -0
  111. package/src/adapters/publishers/registry.ts +36 -0
  112. package/src/adapters/publishers/rutles.ts +161 -0
  113. package/src/adapters/publishers/saiensu.ts +149 -0
  114. package/src/adapters/publishers/seshop.ts +121 -0
  115. package/src/adapters/publishers/tatsu-zine.ts +129 -0
  116. package/src/adapters/publishers/techbookfest.ts +179 -0
  117. package/src/application/get-book-detail.ts +17 -0
  118. package/src/application/search-books.ts +39 -0
  119. package/src/domain/book.ts +35 -0
  120. package/src/domain/publisher.ts +18 -0
  121. package/src/main.ts +13 -0
  122. package/src/mcp/server.ts +103 -0
  123. package/src/mcp/tools.ts +54 -0
  124. package/src/ports/cache.ts +5 -0
  125. package/src/ports/html-parser.ts +15 -0
  126. package/src/ports/http.ts +17 -0
  127. package/tests/fixtures/book-tech-detail.html +51 -0
  128. package/tests/fixtures/book-tech-search.html +91 -0
  129. package/tests/fixtures/born-digital-detail.html +62 -0
  130. package/tests/fixtures/born-digital-search.html +51 -0
  131. package/tests/fixtures/coronasha-detail.html +41 -0
  132. package/tests/fixtures/coronasha-search.html +61 -0
  133. package/tests/fixtures/gihyo-detail.html +42 -0
  134. package/tests/fixtures/gihyo-search.json +54 -0
  135. package/tests/fixtures/lambdanote-search.html +66 -0
  136. package/tests/fixtures/manatee-detail.html +53 -0
  137. package/tests/fixtures/manatee-search.html +59 -0
  138. package/tests/fixtures/maruzen-detail.html +51 -0
  139. package/tests/fixtures/maruzen-search.html +60 -0
  140. package/tests/fixtures/optronics-detail.html +30 -0
  141. package/tests/fixtures/optronics-search.html +75 -0
  142. package/tests/fixtures/oreilly-detail.html +52 -0
  143. package/tests/fixtures/oreilly-ebook-list.html +53 -0
  144. package/tests/fixtures/peaks-detail.html +39 -0
  145. package/tests/fixtures/peaks-top.html +50 -0
  146. package/tests/fixtures/personal-media-detail.html +32 -0
  147. package/tests/fixtures/personal-media-search.html +39 -0
  148. package/tests/fixtures/rutles-detail.html +32 -0
  149. package/tests/fixtures/rutles-search.html +62 -0
  150. package/tests/fixtures/saiensu-detail.html +41 -0
  151. package/tests/fixtures/saiensu-search.html +65 -0
  152. package/tests/fixtures/seshop-detail.html +45 -0
  153. package/tests/fixtures/seshop-search.html +58 -0
  154. package/tests/fixtures/tatsu-zine-detail-free.html +22 -0
  155. package/tests/fixtures/tatsu-zine-search.html +24 -0
  156. package/tests/fixtures/techbookfest-search.json +73 -0
  157. package/tests/unit/adapters/publishers/book-tech.test.ts +183 -0
  158. package/tests/unit/adapters/publishers/born-digital.test.ts +191 -0
  159. package/tests/unit/adapters/publishers/coronasha.test.ts +201 -0
  160. package/tests/unit/adapters/publishers/gihyo.test.ts +135 -0
  161. package/tests/unit/adapters/publishers/lambdanote.test.ts +84 -0
  162. package/tests/unit/adapters/publishers/manatee.test.ts +163 -0
  163. package/tests/unit/adapters/publishers/maruzen-publishing.test.ts +177 -0
  164. package/tests/unit/adapters/publishers/optronics.test.ts +205 -0
  165. package/tests/unit/adapters/publishers/oreilly-japan.test.ts +191 -0
  166. package/tests/unit/adapters/publishers/peaks.test.ts +174 -0
  167. package/tests/unit/adapters/publishers/personal-media.test.ts +196 -0
  168. package/tests/unit/adapters/publishers/rutles.test.ts +170 -0
  169. package/tests/unit/adapters/publishers/saiensu.test.ts +167 -0
  170. package/tests/unit/adapters/publishers/seshop.test.ts +171 -0
  171. package/tests/unit/adapters/publishers/tatsu-zine.test.ts +130 -0
  172. package/tests/unit/adapters/publishers/techbookfest.test.ts +93 -0
  173. package/tsconfig.json +17 -0
  174. package/vitest.config.ts +8 -0
@@ -0,0 +1,58 @@
1
+ <!DOCTYPE HTML>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>TypeScript 検索一覧 | SEshop| 翔泳社の本・電子書籍通販サイト</title>
6
+ </head>
7
+ <body>
8
+ <div class="row list">
9
+ <div class="col-md-4 col-sm-6">
10
+ <div class="inner">
11
+ <a href="/product/detail/26500">
12
+ <figure class="ribbon-corner">
13
+ <img class="img-responsive" src="/static/images/product/26500/L.png" alt="TypeScript入門【PDF版】">
14
+ </figure>
15
+ </a>
16
+ <div class="txt">
17
+ <p><a href="/product/detail/26500">TypeScript入門【PDF版】</a></p>
18
+ <p>3,520円(税込)</p>
19
+ <p class="price sale-price">320pt (10%)</p>
20
+ <span class="date">2024.06.10発売</span>
21
+ </div>
22
+ <div class="product-data" data-title="TypeScript入門【PDF版】" data-products-id="190001" data-price="3520" data-category="電子書籍/コンピュータ書" data-list="TypeScript一覧" data-position="1" style="display:none;width:0;height:0;"></div>
23
+ </div>
24
+ </div>
25
+ <div class="col-md-4 col-sm-6">
26
+ <div class="inner">
27
+ <a href="/product/detail/26501">
28
+ <figure class="ribbon-corner">
29
+ <img class="img-responsive" src="/static/images/product/26501/L.png" alt="実践TypeScript【PDF版】">
30
+ </figure>
31
+ </a>
32
+ <div class="txt">
33
+ <p><a href="/product/detail/26501">実践TypeScript【PDF版】</a></p>
34
+ <p>4,180円(税込)</p>
35
+ <p class="price sale-price">380pt (10%)</p>
36
+ <span class="date">2023.11.20発売</span>
37
+ </div>
38
+ <div class="product-data" data-title="実践TypeScript【PDF版】" data-products-id="190002" data-price="4180" data-category="電子書籍/コンピュータ書" data-list="TypeScript一覧" data-position="2" style="display:none;width:0;height:0;"></div>
39
+ </div>
40
+ </div>
41
+ <div class="col-md-4 col-sm-6">
42
+ <div class="inner">
43
+ <a href="/product/detail/26502">
44
+ <figure class="ribbon-corner">
45
+ <img class="img-responsive" src="/static/images/product/26502/L.png" alt="TypeScript実践プログラミング(紙書籍)">
46
+ </figure>
47
+ </a>
48
+ <div class="txt">
49
+ <p><a href="/product/detail/26502">TypeScript実践プログラミング</a></p>
50
+ <p>3,080円(税込)</p>
51
+ <span class="date">2023.05.15発売</span>
52
+ </div>
53
+ <div class="product-data" data-title="TypeScript実践プログラミング" data-products-id="190003" data-price="3080" data-category="コンピュータ書" data-list="TypeScript一覧" data-position="3" style="display:none;width:0;height:0;"></div>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </body>
58
+ </html>
@@ -0,0 +1,22 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Goプログラミング実践入門 - 達人出版会</title>
6
+ </head>
7
+ <body>
8
+ <h1>Goプログラミング実践入門</h1>
9
+ <img src="/images/books/123/cover.jpg" alt="Goプログラミング実践入門">
10
+ <dl>
11
+ <dt>著者</dt>
12
+ <dd>Sau Sheong Chang(著), 武舎 広幸(訳)</dd>
13
+ <dt>定価</dt>
14
+ <dd>3,520円 (3,200円+税)</dd>
15
+ <dt>出版社</dt>
16
+ <dd><a href="/books/pub/impress">インプレス</a></dd>
17
+ </dl>
18
+ <div class="description">
19
+ <p>Goを使ったシステムプログラミングを解説する実践書です。</p>
20
+ </div>
21
+ </body>
22
+ </html>
@@ -0,0 +1,24 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>書籍一覧 - 達人出版会</title>
6
+ </head>
7
+ <body>
8
+ <h1>書籍一覧</h1>
9
+ <h2>検索結果</h2>
10
+
11
+ <a href="/books/go-programming">
12
+ <img src="/images/books/123/cover_s.jpg" alt="Goプログラミング実践入門">
13
+ </a>
14
+ <h3><a href="/books/go-programming">Goプログラミング実践入門</a></h3>
15
+ <p>Sau Sheong Chang(著), 武舎 広幸(訳)</p>
16
+
17
+ <a href="/books/go-concurrency">
18
+ <img src="/images/books/456/cover_s.jpg" alt="Go言語による並行処理">
19
+ </a>
20
+ <h3><a href="/books/go-concurrency">Go言語による並行処理</a></h3>
21
+ <p>Katherine Cox-Buday(著), 山口 能迪(訳)</p>
22
+
23
+ </body>
24
+ </html>
@@ -0,0 +1,73 @@
1
+ {
2
+ "data": {
3
+ "searchProducts": {
4
+ "pageInfo": {
5
+ "hasNextPage": false,
6
+ "endCursor": "cursor123"
7
+ },
8
+ "edges": [
9
+ {
10
+ "node": {
11
+ "product": {
12
+ "id": "Product:01HXXXX1",
13
+ "databaseID": "01HXXXX1",
14
+ "name": "TypeScriptで学ぶデザインパターン",
15
+ "description": "GoFのデザインパターンをTypeScriptで解説する本です。",
16
+ "organization": {
17
+ "name": "サークル名A"
18
+ },
19
+ "coverImage": {
20
+ "url": "https://techbookfest.org/api/image/01HXXXX1.png"
21
+ },
22
+ "ebookVariant": {
23
+ "price": 1000
24
+ },
25
+ "firstPublishedAt": "2024-01-15T00:00:00.000Z",
26
+ "status": "AVAILABLE"
27
+ }
28
+ }
29
+ },
30
+ {
31
+ "node": {
32
+ "product": {
33
+ "id": "Product:01HXXXX2",
34
+ "databaseID": "01HXXXX2",
35
+ "name": "実践TypeScript型パズル",
36
+ "description": null,
37
+ "organization": {
38
+ "name": "サークル名B"
39
+ },
40
+ "coverImage": null,
41
+ "ebookVariant": {
42
+ "price": 500
43
+ },
44
+ "firstPublishedAt": "2023-09-01T00:00:00.000Z",
45
+ "status": "AVAILABLE"
46
+ }
47
+ }
48
+ },
49
+ {
50
+ "node": {
51
+ "product": {
52
+ "id": "Product:01HXXXX3",
53
+ "databaseID": "01HXXXX3",
54
+ "name": "無料のTypeScript本",
55
+ "description": "無料で読める本。",
56
+ "organization": {
57
+ "name": "サークル名C"
58
+ },
59
+ "coverImage": {
60
+ "url": "https://techbookfest.org/api/image/01HXXXX3.png"
61
+ },
62
+ "ebookVariant": {
63
+ "price": 0
64
+ },
65
+ "firstPublishedAt": "2023-05-01T00:00:00.000Z",
66
+ "status": "AVAILABLE"
67
+ }
68
+ }
69
+ }
70
+ ]
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { bookTechAdapter } from "../../../../src/adapters/publishers/book-tech.js";
5
+ import { MockHttpClient } from "../../../../src/adapters/http/mock-client.js";
6
+ import { CheerioHtmlParser } from "../../../../src/adapters/html/cheerio-parser.js";
7
+ import { NullCacheStore } from "../../../../src/adapters/cache/null-cache.js";
8
+
9
+ const FIXTURES_DIR = join(import.meta.dirname, "../../../fixtures");
10
+
11
+ function makeDeps(http: MockHttpClient) {
12
+ return { http, parser: new CheerioHtmlParser(), cache: new NullCacheStore() };
13
+ }
14
+
15
+ async function loadFixture(name: string): Promise<string> {
16
+ return readFile(join(FIXTURES_DIR, name), "utf-8");
17
+ }
18
+
19
+ describe("bookTechAdapter", () => {
20
+ describe("search()", () => {
21
+ it("BookRecord[] を返す", async () => {
22
+ const body = await loadFixture("book-tech-search.html");
23
+ const http = new MockHttpClient().addResponse(
24
+ "https://book-tech.com/books",
25
+ { status: 200, body },
26
+ );
27
+
28
+ const results = await bookTechAdapter.search({ title: "TypeScript" }, makeDeps(http));
29
+
30
+ expect(results).toHaveLength(2);
31
+ expect(results[0]).toMatchObject({
32
+ title: "次のステップへ!React実践開発 サクサク作って学ぶ UI/テスト/デプロイ",
33
+ publisher: "インプレス NextPublishing",
34
+ url: "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
35
+ price: 2178,
36
+ publishedAt: "2026-02-20",
37
+ });
38
+ });
39
+
40
+ it("著者名から役割語を除去する", async () => {
41
+ const body = await loadFixture("book-tech-search.html");
42
+ const http = new MockHttpClient().addResponse(
43
+ "https://book-tech.com/books",
44
+ { status: 200, body },
45
+ );
46
+
47
+ const results = await bookTechAdapter.search({ title: "TypeScript" }, makeDeps(http));
48
+
49
+ expect(results[0].authors).toEqual(["philosophy"]);
50
+ expect(results[1].authors).toEqual(["井手 優太"]);
51
+ });
52
+
53
+ it("coverImageUrl が設定される", async () => {
54
+ const body = await loadFixture("book-tech-search.html");
55
+ const http = new MockHttpClient().addResponse(
56
+ "https://book-tech.com/books",
57
+ { status: 200, body },
58
+ );
59
+
60
+ const results = await bookTechAdapter.search({ title: "TypeScript" }, makeDeps(http));
61
+
62
+ expect(results[0].coverImageUrl).toBe(
63
+ "https://booktech-share.s3-ap-northeast-1.amazonaws.com/books/d80ffe3d.webp",
64
+ );
65
+ });
66
+
67
+ it("ebookStores に BOOK TECH (social DRM) が含まれる", async () => {
68
+ const body = await loadFixture("book-tech-search.html");
69
+ const http = new MockHttpClient().addResponse(
70
+ "https://book-tech.com/books",
71
+ { status: 200, body },
72
+ );
73
+
74
+ const results = await bookTechAdapter.search({ title: "TypeScript" }, makeDeps(http));
75
+
76
+ expect(results[0].ebookStores).toEqual([
77
+ {
78
+ name: "BOOK TECH",
79
+ url: "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
80
+ drm: "social",
81
+ },
82
+ ]);
83
+ });
84
+
85
+ it("title も author も空の場合は [] を返しHTTPを呼ばない", async () => {
86
+ const http = new MockHttpClient();
87
+
88
+ const results = await bookTechAdapter.search({}, makeDeps(http));
89
+
90
+ expect(results).toEqual([]);
91
+ expect(http.calls).toHaveLength(0);
92
+ });
93
+
94
+ it("検索リクエストに q[...] パラメータが含まれる", async () => {
95
+ const body = await loadFixture("book-tech-search.html");
96
+ const http = new MockHttpClient().addResponse(
97
+ "https://book-tech.com/books",
98
+ { status: 200, body },
99
+ );
100
+
101
+ await bookTechAdapter.search({ title: "TypeScript" }, makeDeps(http));
102
+
103
+ expect(http.calls[0]).toContain("TypeScript");
104
+ expect(http.calls[0]).toContain("title_or_overview");
105
+ });
106
+ });
107
+
108
+ describe("getDetail()", () => {
109
+ it("詳細情報を返す", async () => {
110
+ const body = await loadFixture("book-tech-detail.html");
111
+ const http = new MockHttpClient().addResponse(
112
+ "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
113
+ { status: 200, body },
114
+ );
115
+
116
+ const book = await bookTechAdapter.getDetail(
117
+ "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
118
+ makeDeps(http),
119
+ );
120
+
121
+ expect(book).toMatchObject({
122
+ title: "次のステップへ!React実践開発 サクサク作って学ぶ UI/テスト/デプロイ",
123
+ publisher: "インプレス NextPublishing",
124
+ isbn: "9784295604136",
125
+ price: 2178,
126
+ publishedAt: "2026-02-20",
127
+ });
128
+ });
129
+
130
+ it("著者名から役割語を除去する", async () => {
131
+ const body = await loadFixture("book-tech-detail.html");
132
+ const http = new MockHttpClient().addResponse(
133
+ "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
134
+ { status: 200, body },
135
+ );
136
+
137
+ const book = await bookTechAdapter.getDetail(
138
+ "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
139
+ makeDeps(http),
140
+ );
141
+
142
+ expect(book.authors).toEqual(["philosophy"]);
143
+ });
144
+
145
+ it("coverImageUrl が設定される", async () => {
146
+ const body = await loadFixture("book-tech-detail.html");
147
+ const http = new MockHttpClient().addResponse(
148
+ "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
149
+ { status: 200, body },
150
+ );
151
+
152
+ const book = await bookTechAdapter.getDetail(
153
+ "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
154
+ makeDeps(http),
155
+ );
156
+
157
+ expect(book.coverImageUrl).toBe(
158
+ "https://booktech-share.s3-ap-northeast-1.amazonaws.com/books/d80ffe3d.webp",
159
+ );
160
+ });
161
+
162
+ it("ebookStores に BOOK TECH (social DRM) が含まれる", async () => {
163
+ const body = await loadFixture("book-tech-detail.html");
164
+ const http = new MockHttpClient().addResponse(
165
+ "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
166
+ { status: 200, body },
167
+ );
168
+
169
+ const book = await bookTechAdapter.getDetail(
170
+ "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
171
+ makeDeps(http),
172
+ );
173
+
174
+ expect(book.ebookStores).toEqual([
175
+ {
176
+ name: "BOOK TECH",
177
+ url: "https://book-tech.com/books/d80ffe3d-f3fe-458b-95ee-b4dd3327fab2",
178
+ drm: "social",
179
+ },
180
+ ]);
181
+ });
182
+ });
183
+ });
@@ -0,0 +1,191 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { bornDigitalAdapter } from "../../../../src/adapters/publishers/born-digital.js";
5
+ import { MockHttpClient } from "../../../../src/adapters/http/mock-client.js";
6
+ import { CheerioHtmlParser } from "../../../../src/adapters/html/cheerio-parser.js";
7
+ import { NullCacheStore } from "../../../../src/adapters/cache/null-cache.js";
8
+
9
+ const FIXTURES_DIR = join(import.meta.dirname, "../../../fixtures");
10
+
11
+ function makeDeps(http: MockHttpClient) {
12
+ return { http, parser: new CheerioHtmlParser(), cache: new NullCacheStore() };
13
+ }
14
+
15
+ async function loadFixture(name: string): Promise<string> {
16
+ return readFile(join(FIXTURES_DIR, name), "utf-8");
17
+ }
18
+
19
+ describe("bornDigitalAdapter", () => {
20
+ describe("search()", () => {
21
+ it("電子書籍のみ BookRecord[] を返す(紙書籍は除外)", async () => {
22
+ const body = await loadFixture("born-digital-search.html");
23
+ const http = new MockHttpClient().addResponse(
24
+ "https://wgn-obs.shop-pro.jp/",
25
+ { status: 200, body },
26
+ );
27
+
28
+ const results = await bornDigitalAdapter.search({ title: "HTML" }, makeDeps(http));
29
+
30
+ // フィクスチャには電子2件・紙1件あり、電子のみ返す
31
+ expect(results).toHaveLength(2);
32
+ expect(results[0]).toMatchObject({
33
+ title: "【PDFダウンロード版】HTML解体新書 ー仕様から紐解く本格入門",
34
+ publisher: "ボーンデジタル",
35
+ url: "https://wgn-obs.shop-pro.jp/?pid=167400957",
36
+ price: 3520,
37
+ });
38
+ expect(results[1]).toMatchObject({
39
+ title: "【電子書籍版】インクルーシブHTML+CSS & JavaScript",
40
+ publisher: "ボーンデジタル",
41
+ url: "https://wgn-obs.shop-pro.jp/?pid=144269584",
42
+ price: 2640,
43
+ });
44
+ });
45
+
46
+ it("coverImageUrl が設定される", async () => {
47
+ const body = await loadFixture("born-digital-search.html");
48
+ const http = new MockHttpClient().addResponse(
49
+ "https://wgn-obs.shop-pro.jp/",
50
+ { status: 200, body },
51
+ );
52
+
53
+ const results = await bornDigitalAdapter.search({ title: "HTML" }, makeDeps(http));
54
+
55
+ expect(results[0].coverImageUrl).toBe(
56
+ "https://img07.shop-pro.jp/PA01427/945/product/167400957_th.png?cmsp_timestamp=20220328140327",
57
+ );
58
+ });
59
+
60
+ it("ebookStores に ボーンデジタル (social DRM) が含まれる", async () => {
61
+ const body = await loadFixture("born-digital-search.html");
62
+ const http = new MockHttpClient().addResponse(
63
+ "https://wgn-obs.shop-pro.jp/",
64
+ { status: 200, body },
65
+ );
66
+
67
+ const results = await bornDigitalAdapter.search({ title: "HTML" }, makeDeps(http));
68
+
69
+ expect(results[0].ebookStores).toEqual([
70
+ {
71
+ name: "ボーンデジタル",
72
+ url: "https://wgn-obs.shop-pro.jp/?pid=167400957",
73
+ drm: "social",
74
+ },
75
+ ]);
76
+ });
77
+
78
+ it("title も author も空の場合は [] を返しHTTPを呼ばない", async () => {
79
+ const http = new MockHttpClient();
80
+
81
+ const results = await bornDigitalAdapter.search({}, makeDeps(http));
82
+
83
+ expect(results).toEqual([]);
84
+ expect(http.calls).toHaveLength(0);
85
+ });
86
+
87
+ it("検索リクエストに EUC-JP エンコードされた keyword が含まれる", async () => {
88
+ const body = await loadFixture("born-digital-search.html");
89
+ const http = new MockHttpClient().addResponse(
90
+ "https://wgn-obs.shop-pro.jp/",
91
+ { status: 200, body },
92
+ );
93
+
94
+ await bornDigitalAdapter.search({ title: "HTML" }, makeDeps(http));
95
+
96
+ expect(http.calls[0]).toContain("mode=srh");
97
+ // ASCII は EUC-JP でも同じバイト列だがパーセントエンコードされる
98
+ expect(http.calls[0]).toContain("keyword=%48%54%4D%4C");
99
+ });
100
+ });
101
+
102
+ describe("getDetail()", () => {
103
+ it("詳細情報を返す", async () => {
104
+ const body = await loadFixture("born-digital-detail.html");
105
+ const http = new MockHttpClient().addResponse(
106
+ "https://wgn-obs.shop-pro.jp/?pid=167400957",
107
+ { status: 200, body },
108
+ );
109
+
110
+ const book = await bornDigitalAdapter.getDetail(
111
+ "https://wgn-obs.shop-pro.jp/?pid=167400957",
112
+ makeDeps(http),
113
+ );
114
+
115
+ expect(book).toMatchObject({
116
+ title: "【PDFダウンロード版】HTML解体新書 ー仕様から紐解く本格入門",
117
+ publisher: "ボーンデジタル",
118
+ price: 3520,
119
+ publishedAt: "2022-04-07",
120
+ });
121
+ });
122
+
123
+ it("著者を取得する", async () => {
124
+ const body = await loadFixture("born-digital-detail.html");
125
+ const http = new MockHttpClient().addResponse(
126
+ "https://wgn-obs.shop-pro.jp/?pid=167400957",
127
+ { status: 200, body },
128
+ );
129
+
130
+ const book = await bornDigitalAdapter.getDetail(
131
+ "https://wgn-obs.shop-pro.jp/?pid=167400957",
132
+ makeDeps(http),
133
+ );
134
+
135
+ expect(book.authors).toEqual(["太田 良典", "中村 直樹"]);
136
+ });
137
+
138
+ it("Colorme JSON から価格を取得する", async () => {
139
+ const body = await loadFixture("born-digital-detail.html");
140
+ const http = new MockHttpClient().addResponse(
141
+ "https://wgn-obs.shop-pro.jp/?pid=167400957",
142
+ { status: 200, body },
143
+ );
144
+
145
+ const book = await bornDigitalAdapter.getDetail(
146
+ "https://wgn-obs.shop-pro.jp/?pid=167400957",
147
+ makeDeps(http),
148
+ );
149
+
150
+ expect(book.price).toBe(3520);
151
+ });
152
+
153
+ it("coverImageUrl が設定される", async () => {
154
+ const body = await loadFixture("born-digital-detail.html");
155
+ const http = new MockHttpClient().addResponse(
156
+ "https://wgn-obs.shop-pro.jp/?pid=167400957",
157
+ { status: 200, body },
158
+ );
159
+
160
+ const book = await bornDigitalAdapter.getDetail(
161
+ "https://wgn-obs.shop-pro.jp/?pid=167400957",
162
+ makeDeps(http),
163
+ );
164
+
165
+ expect(book.coverImageUrl).toBe(
166
+ "https://img07.shop-pro.jp/PA01427/945/product/167400957.png?cmsp_timestamp=20220328140327",
167
+ );
168
+ });
169
+
170
+ it("ebookStores に ボーンデジタル (social DRM) が含まれる", async () => {
171
+ const body = await loadFixture("born-digital-detail.html");
172
+ const http = new MockHttpClient().addResponse(
173
+ "https://wgn-obs.shop-pro.jp/?pid=167400957",
174
+ { status: 200, body },
175
+ );
176
+
177
+ const book = await bornDigitalAdapter.getDetail(
178
+ "https://wgn-obs.shop-pro.jp/?pid=167400957",
179
+ makeDeps(http),
180
+ );
181
+
182
+ expect(book.ebookStores).toEqual([
183
+ {
184
+ name: "ボーンデジタル",
185
+ url: "https://wgn-obs.shop-pro.jp/?pid=167400957",
186
+ drm: "social",
187
+ },
188
+ ]);
189
+ });
190
+ });
191
+ });