@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.
- package/.claude/settings.local.json +23 -0
- package/.github/workflows/test.yml +36 -0
- package/AGENTS.md +72 -0
- package/CLAUDE.md +2 -0
- package/LICENSE +661 -0
- package/README.md +154 -0
- package/dist/adapters/cache/memory-cache.d.ts +8 -0
- package/dist/adapters/cache/memory-cache.d.ts.map +1 -0
- package/dist/adapters/cache/memory-cache.js +23 -0
- package/dist/adapters/cache/memory-cache.js.map +1 -0
- package/dist/adapters/cache/null-cache.d.ts +8 -0
- package/dist/adapters/cache/null-cache.d.ts.map +1 -0
- package/dist/adapters/cache/null-cache.js +7 -0
- package/dist/adapters/cache/null-cache.js.map +1 -0
- package/dist/adapters/html/cheerio-parser.d.ts +5 -0
- package/dist/adapters/html/cheerio-parser.d.ts.map +1 -0
- package/dist/adapters/html/cheerio-parser.js +45 -0
- package/dist/adapters/html/cheerio-parser.js.map +1 -0
- package/dist/adapters/http/fetch-client.d.ts +6 -0
- package/dist/adapters/http/fetch-client.d.ts.map +1 -0
- package/dist/adapters/http/fetch-client.js +43 -0
- package/dist/adapters/http/fetch-client.js.map +1 -0
- package/dist/adapters/http/mock-client.d.ts +19 -0
- package/dist/adapters/http/mock-client.d.ts.map +1 -0
- package/dist/adapters/http/mock-client.js +59 -0
- package/dist/adapters/http/mock-client.js.map +1 -0
- package/dist/adapters/publishers/base.d.ts +24 -0
- package/dist/adapters/publishers/base.d.ts.map +1 -0
- package/dist/adapters/publishers/base.js +88 -0
- package/dist/adapters/publishers/base.js.map +1 -0
- package/dist/adapters/publishers/gihyo.d.ts +3 -0
- package/dist/adapters/publishers/gihyo.d.ts.map +1 -0
- package/dist/adapters/publishers/gihyo.js +75 -0
- package/dist/adapters/publishers/gihyo.js.map +1 -0
- package/dist/adapters/publishers/lambdanote.d.ts +3 -0
- package/dist/adapters/publishers/lambdanote.d.ts.map +1 -0
- package/dist/adapters/publishers/lambdanote.js +113 -0
- package/dist/adapters/publishers/lambdanote.js.map +1 -0
- package/dist/adapters/publishers/registry.d.ts +3 -0
- package/dist/adapters/publishers/registry.d.ts.map +1 -0
- package/dist/adapters/publishers/registry.js +11 -0
- package/dist/adapters/publishers/registry.js.map +1 -0
- package/dist/adapters/publishers/tatsu-zine.d.ts +3 -0
- package/dist/adapters/publishers/tatsu-zine.d.ts.map +1 -0
- package/dist/adapters/publishers/tatsu-zine.js +110 -0
- package/dist/adapters/publishers/tatsu-zine.js.map +1 -0
- package/dist/adapters/publishers/techbookfest.d.ts +3 -0
- package/dist/adapters/publishers/techbookfest.d.ts.map +1 -0
- package/dist/adapters/publishers/techbookfest.js +134 -0
- package/dist/adapters/publishers/techbookfest.js.map +1 -0
- package/dist/application/get-book-detail.d.ts +4 -0
- package/dist/application/get-book-detail.d.ts.map +1 -0
- package/dist/application/get-book-detail.js +9 -0
- package/dist/application/get-book-detail.js.map +1 -0
- package/dist/application/search-books.d.ts +11 -0
- package/dist/application/search-books.d.ts.map +1 -0
- package/dist/application/search-books.js +23 -0
- package/dist/application/search-books.js.map +1 -0
- package/dist/domain/book.d.ts +32 -0
- package/dist/domain/book.d.ts.map +1 -0
- package/dist/domain/book.js +2 -0
- package/dist/domain/book.js.map +1 -0
- package/dist/domain/publisher.d.ts +17 -0
- package/dist/domain/publisher.d.ts.map +1 -0
- package/dist/domain/publisher.js +2 -0
- package/dist/domain/publisher.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -0
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +79 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +47 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +53 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/ports/cache.d.ts +6 -0
- package/dist/ports/cache.d.ts.map +1 -0
- package/dist/ports/cache.js +2 -0
- package/dist/ports/cache.js.map +1 -0
- package/dist/ports/html-parser.d.ts +14 -0
- package/dist/ports/html-parser.d.ts.map +1 -0
- package/dist/ports/html-parser.js +2 -0
- package/dist/ports/html-parser.js.map +1 -0
- package/dist/ports/http.d.ts +16 -0
- package/dist/ports/http.d.ts.map +1 -0
- package/dist/ports/http.js +2 -0
- package/dist/ports/http.js.map +1 -0
- package/docs/design-doc.md +365 -0
- package/flake.nix +50 -0
- package/package.json +29 -0
- package/src/adapters/cache/memory-cache.ts +31 -0
- package/src/adapters/cache/null-cache.ts +8 -0
- package/src/adapters/html/cheerio-parser.ts +49 -0
- package/src/adapters/http/fetch-client.ts +47 -0
- package/src/adapters/http/mock-client.ts +77 -0
- package/src/adapters/publishers/base.ts +129 -0
- package/src/adapters/publishers/book-tech.ts +117 -0
- package/src/adapters/publishers/born-digital.ts +158 -0
- package/src/adapters/publishers/coronasha.ts +139 -0
- package/src/adapters/publishers/gihyo.ts +120 -0
- package/src/adapters/publishers/lambdanote.ts +146 -0
- package/src/adapters/publishers/manatee.ts +112 -0
- package/src/adapters/publishers/maruzen-publishing.ts +141 -0
- package/src/adapters/publishers/optronics.ts +113 -0
- package/src/adapters/publishers/oreilly-japan.ts +138 -0
- package/src/adapters/publishers/peaks.ts +98 -0
- package/src/adapters/publishers/personal-media.ts +168 -0
- package/src/adapters/publishers/registry.ts +36 -0
- package/src/adapters/publishers/rutles.ts +161 -0
- package/src/adapters/publishers/saiensu.ts +149 -0
- package/src/adapters/publishers/seshop.ts +121 -0
- package/src/adapters/publishers/tatsu-zine.ts +129 -0
- package/src/adapters/publishers/techbookfest.ts +179 -0
- package/src/application/get-book-detail.ts +17 -0
- package/src/application/search-books.ts +39 -0
- package/src/domain/book.ts +35 -0
- package/src/domain/publisher.ts +18 -0
- package/src/main.ts +13 -0
- package/src/mcp/server.ts +103 -0
- package/src/mcp/tools.ts +54 -0
- package/src/ports/cache.ts +5 -0
- package/src/ports/html-parser.ts +15 -0
- package/src/ports/http.ts +17 -0
- package/tests/fixtures/book-tech-detail.html +51 -0
- package/tests/fixtures/book-tech-search.html +91 -0
- package/tests/fixtures/born-digital-detail.html +62 -0
- package/tests/fixtures/born-digital-search.html +51 -0
- package/tests/fixtures/coronasha-detail.html +41 -0
- package/tests/fixtures/coronasha-search.html +61 -0
- package/tests/fixtures/gihyo-detail.html +42 -0
- package/tests/fixtures/gihyo-search.json +54 -0
- package/tests/fixtures/lambdanote-search.html +66 -0
- package/tests/fixtures/manatee-detail.html +53 -0
- package/tests/fixtures/manatee-search.html +59 -0
- package/tests/fixtures/maruzen-detail.html +51 -0
- package/tests/fixtures/maruzen-search.html +60 -0
- package/tests/fixtures/optronics-detail.html +30 -0
- package/tests/fixtures/optronics-search.html +75 -0
- package/tests/fixtures/oreilly-detail.html +52 -0
- package/tests/fixtures/oreilly-ebook-list.html +53 -0
- package/tests/fixtures/peaks-detail.html +39 -0
- package/tests/fixtures/peaks-top.html +50 -0
- package/tests/fixtures/personal-media-detail.html +32 -0
- package/tests/fixtures/personal-media-search.html +39 -0
- package/tests/fixtures/rutles-detail.html +32 -0
- package/tests/fixtures/rutles-search.html +62 -0
- package/tests/fixtures/saiensu-detail.html +41 -0
- package/tests/fixtures/saiensu-search.html +65 -0
- package/tests/fixtures/seshop-detail.html +45 -0
- package/tests/fixtures/seshop-search.html +58 -0
- package/tests/fixtures/tatsu-zine-detail-free.html +22 -0
- package/tests/fixtures/tatsu-zine-search.html +24 -0
- package/tests/fixtures/techbookfest-search.json +73 -0
- package/tests/unit/adapters/publishers/book-tech.test.ts +183 -0
- package/tests/unit/adapters/publishers/born-digital.test.ts +191 -0
- package/tests/unit/adapters/publishers/coronasha.test.ts +201 -0
- package/tests/unit/adapters/publishers/gihyo.test.ts +135 -0
- package/tests/unit/adapters/publishers/lambdanote.test.ts +84 -0
- package/tests/unit/adapters/publishers/manatee.test.ts +163 -0
- package/tests/unit/adapters/publishers/maruzen-publishing.test.ts +177 -0
- package/tests/unit/adapters/publishers/optronics.test.ts +205 -0
- package/tests/unit/adapters/publishers/oreilly-japan.test.ts +191 -0
- package/tests/unit/adapters/publishers/peaks.test.ts +174 -0
- package/tests/unit/adapters/publishers/personal-media.test.ts +196 -0
- package/tests/unit/adapters/publishers/rutles.test.ts +170 -0
- package/tests/unit/adapters/publishers/saiensu.test.ts +167 -0
- package/tests/unit/adapters/publishers/seshop.test.ts +171 -0
- package/tests/unit/adapters/publishers/tatsu-zine.test.ts +130 -0
- package/tests/unit/adapters/publishers/techbookfest.test.ts +93 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { maruzenPublishingAdapter } from "../../../../src/adapters/publishers/maruzen-publishing.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("maruzenPublishingAdapter", () => {
|
|
20
|
+
describe("search()", () => {
|
|
21
|
+
it("BookRecord[] を返す", async () => {
|
|
22
|
+
const body = await loadFixture("maruzen-search.html");
|
|
23
|
+
const http = new MockHttpClient().addResponse(
|
|
24
|
+
"https://www.maruzen-publishing.co.jp/search/",
|
|
25
|
+
{ status: 200, body },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const results = await maruzenPublishingAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
29
|
+
|
|
30
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
31
|
+
expect(results[0]).toMatchObject({
|
|
32
|
+
title: "プログラミングTypeScript",
|
|
33
|
+
publisher: "丸善出版",
|
|
34
|
+
url: "https://www.maruzen-publishing.co.jp/book/b10152370.html",
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("著者の役割語を除去する", async () => {
|
|
39
|
+
const body = await loadFixture("maruzen-search.html");
|
|
40
|
+
const http = new MockHttpClient().addResponse(
|
|
41
|
+
"https://www.maruzen-publishing.co.jp/search/",
|
|
42
|
+
{ status: 200, body },
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const results = await maruzenPublishingAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
46
|
+
|
|
47
|
+
expect(results[0].authors).toEqual(["ボリス・チェルニー", "折山文哉"]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("coverImageUrl が設定される", async () => {
|
|
51
|
+
const body = await loadFixture("maruzen-search.html");
|
|
52
|
+
const http = new MockHttpClient().addResponse(
|
|
53
|
+
"https://www.maruzen-publishing.co.jp/search/",
|
|
54
|
+
{ status: 200, body },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const results = await maruzenPublishingAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
58
|
+
|
|
59
|
+
expect(results[0].coverImageUrl).toBe(
|
|
60
|
+
"https://www.maruzen-publishing.co.jp/files/isbn/978-4-621-30855-1.jpg",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("title も author も空の場合は [] を返しHTTPを呼ばない", async () => {
|
|
65
|
+
const http = new MockHttpClient();
|
|
66
|
+
|
|
67
|
+
const results = await maruzenPublishingAdapter.search({}, makeDeps(http));
|
|
68
|
+
|
|
69
|
+
expect(results).toEqual([]);
|
|
70
|
+
expect(http.calls).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("検索リクエストに search_keyword が含まれる", async () => {
|
|
74
|
+
const body = await loadFixture("maruzen-search.html");
|
|
75
|
+
const http = new MockHttpClient().addResponse(
|
|
76
|
+
"https://www.maruzen-publishing.co.jp/search/",
|
|
77
|
+
{ status: 200, body },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
await maruzenPublishingAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
81
|
+
|
|
82
|
+
expect(http.calls[0]).toContain("search_keyword=TypeScript");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("検索リクエストに format=1 が含まれる", async () => {
|
|
86
|
+
const body = await loadFixture("maruzen-search.html");
|
|
87
|
+
const http = new MockHttpClient().addResponse(
|
|
88
|
+
"https://www.maruzen-publishing.co.jp/search/",
|
|
89
|
+
{ status: 200, body },
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
await maruzenPublishingAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
93
|
+
|
|
94
|
+
expect(http.calls[0]).toContain("format=1");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("3件取得できる", async () => {
|
|
98
|
+
const body = await loadFixture("maruzen-search.html");
|
|
99
|
+
const http = new MockHttpClient().addResponse(
|
|
100
|
+
"https://www.maruzen-publishing.co.jp/search/",
|
|
101
|
+
{ status: 200, body },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const results = await maruzenPublishingAdapter.search({ title: "プログラム" }, makeDeps(http));
|
|
105
|
+
|
|
106
|
+
expect(results).toHaveLength(3);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("監訳者の役割語も除去する", async () => {
|
|
110
|
+
const body = await loadFixture("maruzen-search.html");
|
|
111
|
+
const http = new MockHttpClient().addResponse(
|
|
112
|
+
"https://www.maruzen-publishing.co.jp/search/",
|
|
113
|
+
{ status: 200, body },
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const results = await maruzenPublishingAdapter.search({ title: "統計" }, makeDeps(http));
|
|
117
|
+
const statsBook = results.find(r => r.title.includes("統計"));
|
|
118
|
+
|
|
119
|
+
expect(statsBook?.authors).toEqual(["ピーター・ブルース", "アンドリュー・ブルース", "大橋真也"]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("getDetail()", () => {
|
|
124
|
+
it("詳細情報を返す", async () => {
|
|
125
|
+
const body = await loadFixture("maruzen-detail.html");
|
|
126
|
+
const http = new MockHttpClient().addResponse(
|
|
127
|
+
"https://www.maruzen-publishing.co.jp/book/b10152370.html",
|
|
128
|
+
{ status: 200, body },
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const book = await maruzenPublishingAdapter.getDetail(
|
|
132
|
+
"https://www.maruzen-publishing.co.jp/book/b10152370.html",
|
|
133
|
+
makeDeps(http),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(book).toMatchObject({
|
|
137
|
+
title: "プログラミングTypeScript",
|
|
138
|
+
publisher: "丸善出版",
|
|
139
|
+
publishedAt: "2020-03-31",
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("著者の役割語を除去する", async () => {
|
|
144
|
+
const body = await loadFixture("maruzen-detail.html");
|
|
145
|
+
const http = new MockHttpClient().addResponse(
|
|
146
|
+
"https://www.maruzen-publishing.co.jp/book/b10152370.html",
|
|
147
|
+
{ status: 200, body },
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const book = await maruzenPublishingAdapter.getDetail(
|
|
151
|
+
"https://www.maruzen-publishing.co.jp/book/b10152370.html",
|
|
152
|
+
makeDeps(http),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(book.authors).toEqual(["ボリス・チェルニー", "折山文哉"]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("ebookStores に Kindle と Kinoppy と honto が含まれ Knowledge Worker は除外される", async () => {
|
|
159
|
+
const body = await loadFixture("maruzen-detail.html");
|
|
160
|
+
const http = new MockHttpClient().addResponse(
|
|
161
|
+
"https://www.maruzen-publishing.co.jp/book/b10152370.html",
|
|
162
|
+
{ status: 200, body },
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const book = await maruzenPublishingAdapter.getDetail(
|
|
166
|
+
"https://www.maruzen-publishing.co.jp/book/b10152370.html",
|
|
167
|
+
makeDeps(http),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const storeNames = book.ebookStores?.map(s => s.name) ?? [];
|
|
171
|
+
expect(storeNames).toContain("Kindle");
|
|
172
|
+
expect(storeNames).toContain("Kinoppy");
|
|
173
|
+
expect(storeNames).toContain("honto");
|
|
174
|
+
expect(storeNames).not.toContain("Knowledge Worker");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { optronicsAdapter } from "../../../../src/adapters/publishers/optronics.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("optronicsAdapter", () => {
|
|
20
|
+
describe("search()", () => {
|
|
21
|
+
it("BookRecord[] を返す", async () => {
|
|
22
|
+
const body = await loadFixture("optronics-search.html");
|
|
23
|
+
const http = new MockHttpClient().addResponse(
|
|
24
|
+
"https://optronics-ebook.com/products/list.php",
|
|
25
|
+
{ status: 200, body },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const results = await optronicsAdapter.search({ title: "センシング" }, makeDeps(http));
|
|
29
|
+
|
|
30
|
+
expect(results).toHaveLength(2);
|
|
31
|
+
expect(results[0]).toMatchObject({
|
|
32
|
+
title: "感性計測&感覚センサ技術集成",
|
|
33
|
+
url: "https://optronics-ebook.com/products/detail.php?product_id=235",
|
|
34
|
+
price: 15000,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("listcomment から発行元を取得する", async () => {
|
|
39
|
+
const body = await loadFixture("optronics-search.html");
|
|
40
|
+
const http = new MockHttpClient().addResponse(
|
|
41
|
+
"https://optronics-ebook.com/products/list.php",
|
|
42
|
+
{ status: 200, body },
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const results = await optronicsAdapter.search({ title: "センシング" }, makeDeps(http));
|
|
46
|
+
|
|
47
|
+
expect(results[0].publisher).toBe("センシンディー株式会社");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("listcomment から著者を取得する", async () => {
|
|
51
|
+
const body = await loadFixture("optronics-search.html");
|
|
52
|
+
const http = new MockHttpClient().addResponse(
|
|
53
|
+
"https://optronics-ebook.com/products/list.php",
|
|
54
|
+
{ status: 200, body },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const results = await optronicsAdapter.search({ title: "センシング" }, makeDeps(http));
|
|
58
|
+
|
|
59
|
+
// 2件目は著者フィールドあり
|
|
60
|
+
expect(results[1].authors).toEqual(["波多腰 玄一"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("coverImageUrl が設定される", async () => {
|
|
64
|
+
const body = await loadFixture("optronics-search.html");
|
|
65
|
+
const http = new MockHttpClient().addResponse(
|
|
66
|
+
"https://optronics-ebook.com/products/list.php",
|
|
67
|
+
{ status: 200, body },
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const results = await optronicsAdapter.search({ title: "センシング" }, makeDeps(http));
|
|
71
|
+
|
|
72
|
+
expect(results[0].coverImageUrl).toBe(
|
|
73
|
+
"https://optronics-ebook.com/upload/save_image/03031025_69a6388916874.jpg",
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("ebookStores にオプトロニクス社 (DRMフリー) が含まれる", async () => {
|
|
78
|
+
const body = await loadFixture("optronics-search.html");
|
|
79
|
+
const http = new MockHttpClient().addResponse(
|
|
80
|
+
"https://optronics-ebook.com/products/list.php",
|
|
81
|
+
{ status: 200, body },
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const results = await optronicsAdapter.search({ title: "センシング" }, makeDeps(http));
|
|
85
|
+
|
|
86
|
+
expect(results[0].ebookStores).toEqual([
|
|
87
|
+
{
|
|
88
|
+
name: "オプトロニクス社",
|
|
89
|
+
url: "https://optronics-ebook.com/products/detail.php?product_id=235",
|
|
90
|
+
drm: "free",
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("title も author も空の場合は [] を返しHTTPを呼ばない", async () => {
|
|
96
|
+
const http = new MockHttpClient();
|
|
97
|
+
|
|
98
|
+
const results = await optronicsAdapter.search({}, makeDeps(http));
|
|
99
|
+
|
|
100
|
+
expect(results).toEqual([]);
|
|
101
|
+
expect(http.calls).toHaveLength(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("検索URLに name と category_id=1 が含まれる", async () => {
|
|
105
|
+
const body = await loadFixture("optronics-search.html");
|
|
106
|
+
const http = new MockHttpClient().addResponse(
|
|
107
|
+
"https://optronics-ebook.com/products/list.php",
|
|
108
|
+
{ status: 200, body },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
await optronicsAdapter.search({ title: "センシング" }, makeDeps(http));
|
|
112
|
+
|
|
113
|
+
expect(http.calls[0]).toContain("name=%E3%82%BB%E3%83%B3%E3%82%B7%E3%83%B3%E3%82%B0");
|
|
114
|
+
expect(http.calls[0]).toContain("category_id=1");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("getDetail()", () => {
|
|
119
|
+
it("詳細情報を返す", async () => {
|
|
120
|
+
const body = await loadFixture("optronics-detail.html");
|
|
121
|
+
const http = new MockHttpClient().addResponse(
|
|
122
|
+
"https://optronics-ebook.com/products/detail.php?product_id=210",
|
|
123
|
+
{ status: 200, body },
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const book = await optronicsAdapter.getDetail(
|
|
127
|
+
"https://optronics-ebook.com/products/detail.php?product_id=210",
|
|
128
|
+
makeDeps(http),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(book).toMatchObject({
|
|
132
|
+
title: "光センシング技術の最前線",
|
|
133
|
+
price: 20000,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("main_comment から著者を取得する", async () => {
|
|
138
|
+
const body = await loadFixture("optronics-detail.html");
|
|
139
|
+
const http = new MockHttpClient().addResponse(
|
|
140
|
+
"https://optronics-ebook.com/products/detail.php?product_id=210",
|
|
141
|
+
{ status: 200, body },
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const book = await optronicsAdapter.getDetail(
|
|
145
|
+
"https://optronics-ebook.com/products/detail.php?product_id=210",
|
|
146
|
+
makeDeps(http),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(book.authors).toEqual(["波多腰 玄一"]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("main_comment から発行元を取得し ㈱ を除去する", async () => {
|
|
153
|
+
const body = await loadFixture("optronics-detail.html");
|
|
154
|
+
const http = new MockHttpClient().addResponse(
|
|
155
|
+
"https://optronics-ebook.com/products/detail.php?product_id=210",
|
|
156
|
+
{ status: 200, body },
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const book = await optronicsAdapter.getDetail(
|
|
160
|
+
"https://optronics-ebook.com/products/detail.php?product_id=210",
|
|
161
|
+
makeDeps(http),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(book.publisher).toBe("オプトロニクス社");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("coverImageUrl が設定される", async () => {
|
|
168
|
+
const body = await loadFixture("optronics-detail.html");
|
|
169
|
+
const http = new MockHttpClient().addResponse(
|
|
170
|
+
"https://optronics-ebook.com/products/detail.php?product_id=210",
|
|
171
|
+
{ status: 200, body },
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const book = await optronicsAdapter.getDetail(
|
|
175
|
+
"https://optronics-ebook.com/products/detail.php?product_id=210",
|
|
176
|
+
makeDeps(http),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(book.coverImageUrl).toBe(
|
|
180
|
+
"https://optronics-ebook.com/upload/save_image/11141121_636a1e0f7bd39.jpg",
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("ebookStores にオプトロニクス社 (DRMフリー) が含まれる", async () => {
|
|
185
|
+
const body = await loadFixture("optronics-detail.html");
|
|
186
|
+
const http = new MockHttpClient().addResponse(
|
|
187
|
+
"https://optronics-ebook.com/products/detail.php?product_id=210",
|
|
188
|
+
{ status: 200, body },
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const book = await optronicsAdapter.getDetail(
|
|
192
|
+
"https://optronics-ebook.com/products/detail.php?product_id=210",
|
|
193
|
+
makeDeps(http),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(book.ebookStores).toEqual([
|
|
197
|
+
{
|
|
198
|
+
name: "オプトロニクス社",
|
|
199
|
+
url: "https://optronics-ebook.com/products/detail.php?product_id=210",
|
|
200
|
+
drm: "free",
|
|
201
|
+
},
|
|
202
|
+
]);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -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 { oreillyJapanAdapter } from "../../../../src/adapters/publishers/oreilly-japan.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("oreillyJapanAdapter", () => {
|
|
20
|
+
describe("search()", () => {
|
|
21
|
+
it("タイトルでフィルタリングして BookRecord[] を返す", async () => {
|
|
22
|
+
const body = await loadFixture("oreilly-ebook-list.html");
|
|
23
|
+
const http = new MockHttpClient().addResponse(
|
|
24
|
+
"https://www.oreilly.co.jp/ebook/",
|
|
25
|
+
{ status: 200, body },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const results = await oreillyJapanAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
29
|
+
|
|
30
|
+
expect(results).toHaveLength(1);
|
|
31
|
+
expect(results[0]).toMatchObject({
|
|
32
|
+
title: "Effective TypeScript 第2版",
|
|
33
|
+
publisher: "オライリー・ジャパン",
|
|
34
|
+
url: "https://www.oreilly.co.jp/books/9784814401093/",
|
|
35
|
+
isbn: "9784814401093",
|
|
36
|
+
price: 4620,
|
|
37
|
+
publishedAt: "2025-04-08",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("ebookStores にオライリー・ジャパン(DRMフリー)が含まれる", async () => {
|
|
42
|
+
const body = await loadFixture("oreilly-ebook-list.html");
|
|
43
|
+
const http = new MockHttpClient().addResponse(
|
|
44
|
+
"https://www.oreilly.co.jp/ebook/",
|
|
45
|
+
{ status: 200, body },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const results = await oreillyJapanAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
49
|
+
|
|
50
|
+
expect(results[0].ebookStores).toEqual([
|
|
51
|
+
{
|
|
52
|
+
name: "オライリー・ジャパン",
|
|
53
|
+
url: "https://www.oreilly.co.jp/books/9784814401093/",
|
|
54
|
+
drm: "free",
|
|
55
|
+
},
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("coverImageUrl が ISBN から構築される", async () => {
|
|
60
|
+
const body = await loadFixture("oreilly-ebook-list.html");
|
|
61
|
+
const http = new MockHttpClient().addResponse(
|
|
62
|
+
"https://www.oreilly.co.jp/ebook/",
|
|
63
|
+
{ status: 200, body },
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const results = await oreillyJapanAdapter.search({ title: "TypeScript" }, makeDeps(http));
|
|
67
|
+
|
|
68
|
+
expect(results[0].coverImageUrl).toBe(
|
|
69
|
+
"https://www.oreilly.co.jp/books/images/picture_large978-4-8144-0109-3.jpeg",
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("大文字小文字を区別せずにフィルタリングする", async () => {
|
|
74
|
+
const body = await loadFixture("oreilly-ebook-list.html");
|
|
75
|
+
const http = new MockHttpClient().addResponse(
|
|
76
|
+
"https://www.oreilly.co.jp/ebook/",
|
|
77
|
+
{ status: 200, body },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const results = await oreillyJapanAdapter.search({ title: "typescript" }, makeDeps(http));
|
|
81
|
+
|
|
82
|
+
expect(results).toHaveLength(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("limit を適用する", async () => {
|
|
86
|
+
const body = await loadFixture("oreilly-ebook-list.html");
|
|
87
|
+
const http = new MockHttpClient().addResponse(
|
|
88
|
+
"https://www.oreilly.co.jp/ebook/",
|
|
89
|
+
{ status: 200, body },
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const results = await oreillyJapanAdapter.search({ title: "の", limit: 1 }, makeDeps(http));
|
|
93
|
+
|
|
94
|
+
expect(results).toHaveLength(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("title が未指定の場合は [] を返しHTTPを呼ばない", async () => {
|
|
98
|
+
const http = new MockHttpClient();
|
|
99
|
+
|
|
100
|
+
const results = await oreillyJapanAdapter.search({}, makeDeps(http));
|
|
101
|
+
|
|
102
|
+
expect(results).toEqual([]);
|
|
103
|
+
expect(http.calls).toHaveLength(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("author のみの場合も [] を返しHTTPを呼ばない", async () => {
|
|
107
|
+
const http = new MockHttpClient();
|
|
108
|
+
|
|
109
|
+
const results = await oreillyJapanAdapter.search({ author: "Dan Vanderkam" }, makeDeps(http));
|
|
110
|
+
|
|
111
|
+
expect(results).toEqual([]);
|
|
112
|
+
expect(http.calls).toHaveLength(0);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("getDetail()", () => {
|
|
117
|
+
it("詳細情報を返す", async () => {
|
|
118
|
+
const body = await loadFixture("oreilly-detail.html");
|
|
119
|
+
const http = new MockHttpClient().addResponse(
|
|
120
|
+
"https://www.oreilly.co.jp/books/9784814401093/",
|
|
121
|
+
{ status: 200, body },
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const book = await oreillyJapanAdapter.getDetail(
|
|
125
|
+
"https://www.oreilly.co.jp/books/9784814401093/",
|
|
126
|
+
makeDeps(http),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(book).toMatchObject({
|
|
130
|
+
isbn: "9784814401093",
|
|
131
|
+
price: 4620,
|
|
132
|
+
publishedAt: "2025-04-08",
|
|
133
|
+
publisher: "オライリー・ジャパン",
|
|
134
|
+
});
|
|
135
|
+
expect(book.title).toContain("Effective TypeScript 第2版");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("著者が配列で返される(役割語を除去)", async () => {
|
|
139
|
+
const body = await loadFixture("oreilly-detail.html");
|
|
140
|
+
const http = new MockHttpClient().addResponse(
|
|
141
|
+
"https://www.oreilly.co.jp/books/9784814401093/",
|
|
142
|
+
{ status: 200, body },
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const book = await oreillyJapanAdapter.getDetail(
|
|
146
|
+
"https://www.oreilly.co.jp/books/9784814401093/",
|
|
147
|
+
makeDeps(http),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(book.authors).toEqual(["Dan Vanderkam", "今村 謙士"]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("coverImageUrl が取得される", async () => {
|
|
154
|
+
const body = await loadFixture("oreilly-detail.html");
|
|
155
|
+
const http = new MockHttpClient().addResponse(
|
|
156
|
+
"https://www.oreilly.co.jp/books/9784814401093/",
|
|
157
|
+
{ status: 200, body },
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const book = await oreillyJapanAdapter.getDetail(
|
|
161
|
+
"https://www.oreilly.co.jp/books/9784814401093/",
|
|
162
|
+
makeDeps(http),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(book.coverImageUrl).toBe(
|
|
166
|
+
"https://www.oreilly.co.jp/books/images/picture_large978-4-8144-0109-3.jpeg",
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("ebookStores にオライリー・ジャパン(DRMフリー)が含まれる", async () => {
|
|
171
|
+
const body = await loadFixture("oreilly-detail.html");
|
|
172
|
+
const http = new MockHttpClient().addResponse(
|
|
173
|
+
"https://www.oreilly.co.jp/books/9784814401093/",
|
|
174
|
+
{ status: 200, body },
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const book = await oreillyJapanAdapter.getDetail(
|
|
178
|
+
"https://www.oreilly.co.jp/books/9784814401093/",
|
|
179
|
+
makeDeps(http),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(book.ebookStores).toEqual([
|
|
183
|
+
{
|
|
184
|
+
name: "オライリー・ジャパン",
|
|
185
|
+
url: "https://www.oreilly.co.jp/books/9784814401093/",
|
|
186
|
+
drm: "free",
|
|
187
|
+
},
|
|
188
|
+
]);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|