@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,194 +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 { bornDigitalAdapter } from "../../../../src/adapters/publishers/born-digital.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("bornDigitalAdapter", () => {
|
|
21
|
-
describe("search()", () => {
|
|
22
|
-
it("電子書籍のみ BookRecord[] を返す(紙書籍は除外)", async () => {
|
|
23
|
-
const body = await loadFixture("born-digital-search.html");
|
|
24
|
-
const http = new MockHttpClient().addResponse(
|
|
25
|
-
"https://wgn-obs.shop-pro.jp/",
|
|
26
|
-
{ status: 200, body },
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
const results = await bornDigitalAdapter.search({ title: "HTML" }, makeDeps(http));
|
|
30
|
-
|
|
31
|
-
// フィクスチャには電子2件・紙1件あり、電子のみ返す
|
|
32
|
-
assert.strictEqual(results.length, 2);
|
|
33
|
-
assert.partialDeepStrictEqual(results[0], {
|
|
34
|
-
title: "【PDFダウンロード版】HTML解体新書 ー仕様から紐解く本格入門",
|
|
35
|
-
publisher: "ボーンデジタル",
|
|
36
|
-
url: "https://wgn-obs.shop-pro.jp/?pid=167400957",
|
|
37
|
-
price: 3520,
|
|
38
|
-
});
|
|
39
|
-
assert.partialDeepStrictEqual(results[1], {
|
|
40
|
-
title: "【電子書籍版】インクルーシブHTML+CSS & JavaScript",
|
|
41
|
-
publisher: "ボーンデジタル",
|
|
42
|
-
url: "https://wgn-obs.shop-pro.jp/?pid=144269584",
|
|
43
|
-
price: 2640,
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("coverImageUrl が設定される", async () => {
|
|
48
|
-
const body = await loadFixture("born-digital-search.html");
|
|
49
|
-
const http = new MockHttpClient().addResponse(
|
|
50
|
-
"https://wgn-obs.shop-pro.jp/",
|
|
51
|
-
{ status: 200, body },
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const results = await bornDigitalAdapter.search({ title: "HTML" }, makeDeps(http));
|
|
55
|
-
|
|
56
|
-
assert.strictEqual(
|
|
57
|
-
results[0].coverImageUrl,
|
|
58
|
-
"https://img07.shop-pro.jp/PA01427/945/product/167400957_th.png?cmsp_timestamp=20220328140327",
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("ebookStores に ボーンデジタル (social DRM) が含まれる", async () => {
|
|
63
|
-
const body = await loadFixture("born-digital-search.html");
|
|
64
|
-
const http = new MockHttpClient().addResponse(
|
|
65
|
-
"https://wgn-obs.shop-pro.jp/",
|
|
66
|
-
{ status: 200, body },
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
const results = await bornDigitalAdapter.search({ title: "HTML" }, makeDeps(http));
|
|
70
|
-
|
|
71
|
-
assert.deepStrictEqual(results[0].ebookStores, [
|
|
72
|
-
{
|
|
73
|
-
name: "ボーンデジタル",
|
|
74
|
-
url: "https://wgn-obs.shop-pro.jp/?pid=167400957",
|
|
75
|
-
drm: "social",
|
|
76
|
-
},
|
|
77
|
-
]);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("title も author も空の場合は [] を返しHTTPを呼ばない", async () => {
|
|
81
|
-
const http = new MockHttpClient();
|
|
82
|
-
|
|
83
|
-
const results = await bornDigitalAdapter.search({}, makeDeps(http));
|
|
84
|
-
|
|
85
|
-
assert.deepStrictEqual(results, []);
|
|
86
|
-
assert.strictEqual(http.calls.length, 0);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("検索リクエストに EUC-JP エンコードされた keyword が含まれる", async () => {
|
|
90
|
-
const body = await loadFixture("born-digital-search.html");
|
|
91
|
-
const http = new MockHttpClient().addResponse(
|
|
92
|
-
"https://wgn-obs.shop-pro.jp/",
|
|
93
|
-
{ status: 200, body },
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
await bornDigitalAdapter.search({ title: "HTML" }, makeDeps(http));
|
|
97
|
-
|
|
98
|
-
assert.ok(http.calls[0].includes("mode=srh"));
|
|
99
|
-
// ASCII は EUC-JP でも同じバイト列だがパーセントエンコードされる
|
|
100
|
-
assert.ok(http.calls[0].includes("keyword=%48%54%4D%4C"));
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe("getDetail()", () => {
|
|
105
|
-
it("詳細情報を返す", async () => {
|
|
106
|
-
const body = await loadFixture("born-digital-detail.html");
|
|
107
|
-
const http = new MockHttpClient().addResponse(
|
|
108
|
-
"https://wgn-obs.shop-pro.jp/?pid=167400957",
|
|
109
|
-
{ status: 200, body },
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
const book = await bornDigitalAdapter.getDetail(
|
|
113
|
-
"https://wgn-obs.shop-pro.jp/?pid=167400957",
|
|
114
|
-
makeDeps(http),
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
assert.partialDeepStrictEqual(book, {
|
|
118
|
-
title: "【PDFダウンロード版】HTML解体新書 ー仕様から紐解く本格入門",
|
|
119
|
-
publisher: "ボーンデジタル",
|
|
120
|
-
price: 3520,
|
|
121
|
-
publishedAt: "2022-04-07",
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("著者を取得する", async () => {
|
|
126
|
-
const body = await loadFixture("born-digital-detail.html");
|
|
127
|
-
const http = new MockHttpClient().addResponse(
|
|
128
|
-
"https://wgn-obs.shop-pro.jp/?pid=167400957",
|
|
129
|
-
{ status: 200, body },
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
const book = await bornDigitalAdapter.getDetail(
|
|
133
|
-
"https://wgn-obs.shop-pro.jp/?pid=167400957",
|
|
134
|
-
makeDeps(http),
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
assert.deepStrictEqual(book.authors, ["太田 良典", "中村 直樹"]);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("Colorme JSON から価格を取得する", async () => {
|
|
141
|
-
const body = await loadFixture("born-digital-detail.html");
|
|
142
|
-
const http = new MockHttpClient().addResponse(
|
|
143
|
-
"https://wgn-obs.shop-pro.jp/?pid=167400957",
|
|
144
|
-
{ status: 200, body },
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
const book = await bornDigitalAdapter.getDetail(
|
|
148
|
-
"https://wgn-obs.shop-pro.jp/?pid=167400957",
|
|
149
|
-
makeDeps(http),
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
assert.strictEqual(book.price, 3520);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("coverImageUrl が設定される", async () => {
|
|
156
|
-
const body = await loadFixture("born-digital-detail.html");
|
|
157
|
-
const http = new MockHttpClient().addResponse(
|
|
158
|
-
"https://wgn-obs.shop-pro.jp/?pid=167400957",
|
|
159
|
-
{ status: 200, body },
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
const book = await bornDigitalAdapter.getDetail(
|
|
163
|
-
"https://wgn-obs.shop-pro.jp/?pid=167400957",
|
|
164
|
-
makeDeps(http),
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
assert.strictEqual(
|
|
168
|
-
book.coverImageUrl,
|
|
169
|
-
"https://img07.shop-pro.jp/PA01427/945/product/167400957.png?cmsp_timestamp=20220328140327",
|
|
170
|
-
);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("ebookStores に ボーンデジタル (social DRM) が含まれる", async () => {
|
|
174
|
-
const body = await loadFixture("born-digital-detail.html");
|
|
175
|
-
const http = new MockHttpClient().addResponse(
|
|
176
|
-
"https://wgn-obs.shop-pro.jp/?pid=167400957",
|
|
177
|
-
{ status: 200, body },
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
const book = await bornDigitalAdapter.getDetail(
|
|
181
|
-
"https://wgn-obs.shop-pro.jp/?pid=167400957",
|
|
182
|
-
makeDeps(http),
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
assert.deepStrictEqual(book.ebookStores, [
|
|
186
|
-
{
|
|
187
|
-
name: "ボーンデジタル",
|
|
188
|
-
url: "https://wgn-obs.shop-pro.jp/?pid=167400957",
|
|
189
|
-
drm: "social",
|
|
190
|
-
},
|
|
191
|
-
]);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
});
|
|
@@ -1,207 +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 { coronashaAdapter } from "../../../../src/adapters/publishers/coronasha.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("coronashaAdapter", () => {
|
|
21
|
-
describe("search()", () => {
|
|
22
|
-
it("電子版ありの書籍のみ返す(電子版なしは除外)", async () => {
|
|
23
|
-
const body = await loadFixture("coronasha-search.html");
|
|
24
|
-
const http = new MockHttpClient().addResponse(
|
|
25
|
-
"https://www.coronasha.co.jp/np/result.html",
|
|
26
|
-
{ status: 200, body },
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
const results = await coronashaAdapter.search({ title: "Julia" }, makeDeps(http));
|
|
30
|
-
|
|
31
|
-
// フィクスチャには電子版あり1件・電子版なし1件あり、電子版のみ返す
|
|
32
|
-
assert.strictEqual(results.length, 1);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("タイトルを tunogaki + book-title で組み立てる", async () => {
|
|
36
|
-
const body = await loadFixture("coronasha-search.html");
|
|
37
|
-
const http = new MockHttpClient().addResponse(
|
|
38
|
-
"https://www.coronasha.co.jp/np/result.html",
|
|
39
|
-
{ status: 200, body },
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
const results = await coronashaAdapter.search({ title: "Julia" }, makeDeps(http));
|
|
43
|
-
|
|
44
|
-
assert.strictEqual(results[0].title, "1から始める Juliaプログラミング大全");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("著者一覧を返す", async () => {
|
|
48
|
-
const body = await loadFixture("coronasha-search.html");
|
|
49
|
-
const http = new MockHttpClient().addResponse(
|
|
50
|
-
"https://www.coronasha.co.jp/np/result.html",
|
|
51
|
-
{ status: 200, body },
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const results = await coronashaAdapter.search({ title: "Julia" }, makeDeps(http));
|
|
55
|
-
|
|
56
|
-
assert.deepStrictEqual(results[0].authors, ["進藤 裕之", "佐藤 建太"]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("価格・ISBN・発行日を返す", async () => {
|
|
60
|
-
const body = await loadFixture("coronasha-search.html");
|
|
61
|
-
const http = new MockHttpClient().addResponse(
|
|
62
|
-
"https://www.coronasha.co.jp/np/result.html",
|
|
63
|
-
{ status: 200, body },
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
const results = await coronashaAdapter.search({ title: "Julia" }, makeDeps(http));
|
|
67
|
-
|
|
68
|
-
assert.partialDeepStrictEqual(results[0], {
|
|
69
|
-
price: 3630,
|
|
70
|
-
isbn: "9784339029345",
|
|
71
|
-
publishedAt: "2023-05-01",
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("coverImageUrl が絶対URLになる", async () => {
|
|
76
|
-
const body = await loadFixture("coronasha-search.html");
|
|
77
|
-
const http = new MockHttpClient().addResponse(
|
|
78
|
-
"https://www.coronasha.co.jp/np/result.html",
|
|
79
|
-
{ status: 200, body },
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const results = await coronashaAdapter.search({ title: "Julia" }, makeDeps(http));
|
|
83
|
-
|
|
84
|
-
assert.strictEqual(
|
|
85
|
-
results[0].coverImageUrl,
|
|
86
|
-
"https://www.coronasha.co.jp/np/images/isbn/9784339029345_main.jpg",
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("publisher が コロナ社 になる", async () => {
|
|
91
|
-
const body = await loadFixture("coronasha-search.html");
|
|
92
|
-
const http = new MockHttpClient().addResponse(
|
|
93
|
-
"https://www.coronasha.co.jp/np/result.html",
|
|
94
|
-
{ status: 200, body },
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
const results = await coronashaAdapter.search({ title: "Julia" }, makeDeps(http));
|
|
98
|
-
|
|
99
|
-
assert.strictEqual(results[0].publisher, "コロナ社");
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("title も author も空の場合は [] を返しHTTPを呼ばない", async () => {
|
|
103
|
-
const http = new MockHttpClient();
|
|
104
|
-
|
|
105
|
-
const results = await coronashaAdapter.search({}, makeDeps(http));
|
|
106
|
-
|
|
107
|
-
assert.deepStrictEqual(results, []);
|
|
108
|
-
assert.strictEqual(http.calls.length, 0);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("getDetail()", () => {
|
|
113
|
-
it("詳細情報を返す", async () => {
|
|
114
|
-
const body = await loadFixture("coronasha-detail.html");
|
|
115
|
-
const http = new MockHttpClient().addResponse(
|
|
116
|
-
"https://www.coronasha.co.jp/np/isbn/9784339029345/",
|
|
117
|
-
{ status: 200, body },
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
const book = await coronashaAdapter.getDetail(
|
|
121
|
-
"https://www.coronasha.co.jp/np/isbn/9784339029345/",
|
|
122
|
-
makeDeps(http),
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
assert.partialDeepStrictEqual(book, {
|
|
126
|
-
title: "1から始める Juliaプログラミング大全",
|
|
127
|
-
publisher: "コロナ社",
|
|
128
|
-
price: 3630,
|
|
129
|
-
publishedAt: "2023-05-01",
|
|
130
|
-
isbn: "9784339029345",
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("著者一覧を返す", async () => {
|
|
135
|
-
const body = await loadFixture("coronasha-detail.html");
|
|
136
|
-
const http = new MockHttpClient().addResponse(
|
|
137
|
-
"https://www.coronasha.co.jp/np/isbn/9784339029345/",
|
|
138
|
-
{ status: 200, body },
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
const book = await coronashaAdapter.getDetail(
|
|
142
|
-
"https://www.coronasha.co.jp/np/isbn/9784339029345/",
|
|
143
|
-
makeDeps(http),
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
assert.deepStrictEqual(book.authors, ["進藤 裕之", "佐藤 建太"]);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("ebookStores に Kindle・Kinoppy・VarsityWave eBooks が含まれる", async () => {
|
|
150
|
-
const body = await loadFixture("coronasha-detail.html");
|
|
151
|
-
const http = new MockHttpClient().addResponse(
|
|
152
|
-
"https://www.coronasha.co.jp/np/isbn/9784339029345/",
|
|
153
|
-
{ status: 200, body },
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
const book = await coronashaAdapter.getDetail(
|
|
157
|
-
"https://www.coronasha.co.jp/np/isbn/9784339029345/",
|
|
158
|
-
makeDeps(http),
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
for (const { name, drm } of [
|
|
162
|
-
{ name: "Kindle", drm: "drm" },
|
|
163
|
-
{ name: "Kinoppy", drm: "drm" },
|
|
164
|
-
{ name: "VarsityWave eBooks", drm: "drm" },
|
|
165
|
-
]) {
|
|
166
|
-
assert.ok(
|
|
167
|
-
book.ebookStores.some(s => s.name === name && s.drm === drm),
|
|
168
|
-
`Expected store: ${name} (${drm})`,
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("Knowledge Worker (kw.maruzen.co.jp) は ebookStores に含まれない", async () => {
|
|
174
|
-
const body = await loadFixture("coronasha-detail.html");
|
|
175
|
-
const http = new MockHttpClient().addResponse(
|
|
176
|
-
"https://www.coronasha.co.jp/np/isbn/9784339029345/",
|
|
177
|
-
{ status: 200, body },
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
const book = await coronashaAdapter.getDetail(
|
|
181
|
-
"https://www.coronasha.co.jp/np/isbn/9784339029345/",
|
|
182
|
-
makeDeps(http),
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
const names = book.ebookStores.map(s => s.name);
|
|
186
|
-
assert.ok(!names.includes("Knowledge Worker"));
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("coverImageUrl が絶対URLになる", async () => {
|
|
190
|
-
const body = await loadFixture("coronasha-detail.html");
|
|
191
|
-
const http = new MockHttpClient().addResponse(
|
|
192
|
-
"https://www.coronasha.co.jp/np/isbn/9784339029345/",
|
|
193
|
-
{ status: 200, body },
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
const book = await coronashaAdapter.getDetail(
|
|
197
|
-
"https://www.coronasha.co.jp/np/isbn/9784339029345/",
|
|
198
|
-
makeDeps(http),
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
assert.strictEqual(
|
|
202
|
-
book.coverImageUrl,
|
|
203
|
-
"https://www.coronasha.co.jp/np/images/isbn/9784339029345_main.jpg",
|
|
204
|
-
);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
});
|
|
@@ -1,137 +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 { gihyoAdapter } from "../../../../src/adapters/publishers/gihyo.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
|
-
import type { HtmlParser } from "../../../../src/ports/html-parser.js";
|
|
10
|
-
|
|
11
|
-
// search() は JSON API のみ使用するためパーサー不要
|
|
12
|
-
const noopParser: HtmlParser = {
|
|
13
|
-
parse(_html) {
|
|
14
|
-
throw new Error("gihyo adapter must not call HtmlParser during search()");
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const FIXTURES_DIR = join(import.meta.dirname, "../../../fixtures");
|
|
19
|
-
|
|
20
|
-
describe("gihyoAdapter", () => {
|
|
21
|
-
async function makeSearchDeps(fixtureName: string) {
|
|
22
|
-
const body = await readFile(join(FIXTURES_DIR, fixtureName), "utf-8");
|
|
23
|
-
const http = new MockHttpClient().addResponse(
|
|
24
|
-
"https://gihyo.jp/api_gh/site/search",
|
|
25
|
-
{ status: 200, body },
|
|
26
|
-
);
|
|
27
|
-
return { http, parser: noopParser, cache: new NullCacheStore() };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function makeDetailDeps() {
|
|
31
|
-
const [apiBody, htmlBody] = await Promise.all([
|
|
32
|
-
readFile(join(FIXTURES_DIR, "gihyo-search.json"), "utf-8"),
|
|
33
|
-
readFile(join(FIXTURES_DIR, "gihyo-detail.html"), "utf-8"),
|
|
34
|
-
]);
|
|
35
|
-
const http = new MockHttpClient()
|
|
36
|
-
.addResponse("https://gihyo.jp/api_gh/site/search", { status: 200, body: apiBody })
|
|
37
|
-
.addResponse("https://gihyo.jp/book/", { status: 200, body: htmlBody });
|
|
38
|
-
return { http, parser: new CheerioHtmlParser(), cache: new NullCacheStore() };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
it("search() が JSON API レスポンスから BookRecord[] を返す", async () => {
|
|
42
|
-
const deps = await makeSearchDeps("gihyo-search.json");
|
|
43
|
-
|
|
44
|
-
const results = await gihyoAdapter.search({ title: "TypeScript", limit: 10 }, deps);
|
|
45
|
-
|
|
46
|
-
assert.strictEqual(results.length, 2);
|
|
47
|
-
assert.partialDeepStrictEqual(results[0], {
|
|
48
|
-
title: "プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで",
|
|
49
|
-
authors: ["uhyo"],
|
|
50
|
-
publisher: "技術評論社",
|
|
51
|
-
isbn: "9784297128152",
|
|
52
|
-
price: 3740,
|
|
53
|
-
publishedAt: "2022-04-01",
|
|
54
|
-
});
|
|
55
|
-
assert.strictEqual(results[0].url, "https://gihyo.jp/book/2022/978-4-297-12815-2");
|
|
56
|
-
assert.strictEqual(
|
|
57
|
-
results[0].coverImageUrl,
|
|
58
|
-
"https://gihyo.jp/assets/images/cover/2022/9784297128152.jpg",
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("サブタイトルなし書籍はタイトルのみになる", async () => {
|
|
63
|
-
const deps = await makeSearchDeps("gihyo-search.json");
|
|
64
|
-
const results = await gihyoAdapter.search({ title: "TypeScript" }, deps);
|
|
65
|
-
|
|
66
|
-
const book = results.find(b => b.isbn === "9784297136010");
|
|
67
|
-
assert.strictEqual(book?.title, "TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("複数著者が配列になる", async () => {
|
|
71
|
-
const deps = await makeSearchDeps("gihyo-search.json");
|
|
72
|
-
const results = await gihyoAdapter.search({ title: "TypeScript" }, deps);
|
|
73
|
-
|
|
74
|
-
const book = results.find(b => b.isbn === "9784297136010");
|
|
75
|
-
assert.deepStrictEqual(book?.authors, ["手島拓也", "吉田健人", "高林佳稀"]);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("title も author も空の場合は [] を返しHTTPを呼ばない", async () => {
|
|
79
|
-
const http = new MockHttpClient();
|
|
80
|
-
const deps = { http, parser: noopParser, cache: new NullCacheStore() };
|
|
81
|
-
|
|
82
|
-
const results = await gihyoAdapter.search({}, deps);
|
|
83
|
-
|
|
84
|
-
assert.deepStrictEqual(results, []);
|
|
85
|
-
assert.strictEqual(http.calls.length, 0);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("search() のリクエストURLにクエリが含まれる", async () => {
|
|
89
|
-
const deps = await makeSearchDeps("gihyo-search.json");
|
|
90
|
-
await gihyoAdapter.search({ title: "TypeScript" }, deps);
|
|
91
|
-
|
|
92
|
-
assert.ok(deps.http.calls[0].includes("search=TypeScript"));
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("author クエリが API に渡される", async () => {
|
|
96
|
-
const deps = await makeSearchDeps("gihyo-search.json");
|
|
97
|
-
await gihyoAdapter.search({ author: "uhyo" }, deps);
|
|
98
|
-
|
|
99
|
-
assert.ok(deps.http.calls[0].includes("search=uhyo"));
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe("getDetail()", () => {
|
|
103
|
-
it("ebookStores にソーシャルDRMとDRM付きが含まれる", async () => {
|
|
104
|
-
const deps = await makeDetailDeps();
|
|
105
|
-
const book = await gihyoAdapter.getDetail(
|
|
106
|
-
"https://gihyo.jp/book/2022/978-4-297-12815-2",
|
|
107
|
-
deps,
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
assert.ok(book.ebookStores !== undefined);
|
|
111
|
-
const socialStores = book.ebookStores!.filter(s => s.drm === "social");
|
|
112
|
-
const drmStores = book.ebookStores!.filter(s => s.drm === "drm");
|
|
113
|
-
|
|
114
|
-
// Gihyo Digital Publishing は見えない購入者情報を埋め込むソーシャルDRM
|
|
115
|
-
assert.strictEqual(socialStores.length, 1);
|
|
116
|
-
assert.partialDeepStrictEqual(socialStores[0], {
|
|
117
|
-
name: "Gihyo Digital Publishing",
|
|
118
|
-
drm: "social",
|
|
119
|
-
});
|
|
120
|
-
assert.ok(socialStores[0].url.includes("gihyo.jp/dp/ebook/"));
|
|
121
|
-
|
|
122
|
-
for (const name of ["Kindle", "楽天Kobo", "BookLive", "honto"]) {
|
|
123
|
-
assert.ok(drmStores.some(s => s.name === name), `Expected DRM store: ${name}`);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("ASIN が Amazon リンクから抽出される", async () => {
|
|
128
|
-
const deps = await makeDetailDeps();
|
|
129
|
-
const book = await gihyoAdapter.getDetail(
|
|
130
|
-
"https://gihyo.jp/book/2022/978-4-297-12815-2",
|
|
131
|
-
deps,
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
assert.strictEqual(book.asin, "B09YGZ18ZK");
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
});
|
|
@@ -1,129 +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 { impressBooksAdapter } from "../../../../src/adapters/publishers/impress.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
|
-
const DETAIL_SOCIAL_URL = "https://book.impress.co.jp/books/1125101113";
|
|
12
|
-
const DETAIL_EPUB_URL = "https://book.impress.co.jp/books/1124101031";
|
|
13
|
-
|
|
14
|
-
function makeDeps(http: MockHttpClient) {
|
|
15
|
-
return { http, parser: new CheerioHtmlParser(), cache: new NullCacheStore() };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function loadFixture(name: string): Promise<string> {
|
|
19
|
-
return readFile(join(FIXTURES_DIR, name), "utf-8");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe("impressBooksAdapter", () => {
|
|
23
|
-
describe("search()", () => {
|
|
24
|
-
it("検索APIがないため常に [] を返しHTTPを呼ばない", async () => {
|
|
25
|
-
const http = new MockHttpClient();
|
|
26
|
-
const results = await impressBooksAdapter.search({ title: "Python" }, makeDeps(http));
|
|
27
|
-
assert.deepStrictEqual(results, []);
|
|
28
|
-
assert.strictEqual(http.calls.length, 0);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("クエリが空でも [] を返しHTTPを呼ばない", async () => {
|
|
32
|
-
const http = new MockHttpClient();
|
|
33
|
-
const results = await impressBooksAdapter.search({}, makeDeps(http));
|
|
34
|
-
assert.deepStrictEqual(results, []);
|
|
35
|
-
assert.strictEqual(http.calls.length, 0);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("getDetail() - ソーシャルDRM書籍", () => {
|
|
40
|
-
it("タイトルを返す", async () => {
|
|
41
|
-
const body = await loadFixture("impress-detail-social.html");
|
|
42
|
-
const http = new MockHttpClient().addResponse(DETAIL_SOCIAL_URL, { status: 200, body });
|
|
43
|
-
|
|
44
|
-
const book = await impressBooksAdapter.getDetail(DETAIL_SOCIAL_URL, makeDeps(http));
|
|
45
|
-
|
|
46
|
-
assert.strictEqual(
|
|
47
|
-
book.title,
|
|
48
|
-
"いちばんやさしい 先生が校務に使えるGoogle NotebookLMの教本 人気講師が教える学校業務を効率化するAI活用法",
|
|
49
|
-
);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("著者名から役割語を除いて返す", async () => {
|
|
53
|
-
const body = await loadFixture("impress-detail-social.html");
|
|
54
|
-
const http = new MockHttpClient().addResponse(DETAIL_SOCIAL_URL, { status: 200, body });
|
|
55
|
-
|
|
56
|
-
const book = await impressBooksAdapter.getDetail(DETAIL_SOCIAL_URL, makeDeps(http));
|
|
57
|
-
|
|
58
|
-
assert.deepStrictEqual(book.authors, ["山本康太"]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("ISBN・発売日・価格を返す", async () => {
|
|
62
|
-
const body = await loadFixture("impress-detail-social.html");
|
|
63
|
-
const http = new MockHttpClient().addResponse(DETAIL_SOCIAL_URL, { status: 200, body });
|
|
64
|
-
|
|
65
|
-
const book = await impressBooksAdapter.getDetail(DETAIL_SOCIAL_URL, makeDeps(http));
|
|
66
|
-
|
|
67
|
-
assert.partialDeepStrictEqual(book, {
|
|
68
|
-
isbn: "9784295023654",
|
|
69
|
-
publishedAt: "2026-01-22",
|
|
70
|
-
price: 1980,
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("publisher が インプレスブックス になる", async () => {
|
|
75
|
-
const body = await loadFixture("impress-detail-social.html");
|
|
76
|
-
const http = new MockHttpClient().addResponse(DETAIL_SOCIAL_URL, { status: 200, body });
|
|
77
|
-
|
|
78
|
-
const book = await impressBooksAdapter.getDetail(DETAIL_SOCIAL_URL, makeDeps(http));
|
|
79
|
-
|
|
80
|
-
assert.strictEqual(book.publisher, "インプレスブックス");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("ebookStores に インプレスブックス (social) が含まれる", async () => {
|
|
84
|
-
const body = await loadFixture("impress-detail-social.html");
|
|
85
|
-
const http = new MockHttpClient().addResponse(DETAIL_SOCIAL_URL, { status: 200, body });
|
|
86
|
-
|
|
87
|
-
const book = await impressBooksAdapter.getDetail(DETAIL_SOCIAL_URL, makeDeps(http));
|
|
88
|
-
|
|
89
|
-
assert.deepStrictEqual(book.ebookStores, [
|
|
90
|
-
{ name: "インプレスブックス", url: DETAIL_SOCIAL_URL, drm: "social" },
|
|
91
|
-
]);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("coverImageUrl が https: から始まる絶対URL になる", async () => {
|
|
95
|
-
const body = await loadFixture("impress-detail-social.html");
|
|
96
|
-
const http = new MockHttpClient().addResponse(DETAIL_SOCIAL_URL, { status: 200, body });
|
|
97
|
-
|
|
98
|
-
const book = await impressBooksAdapter.getDetail(DETAIL_SOCIAL_URL, makeDeps(http));
|
|
99
|
-
|
|
100
|
-
assert.strictEqual(
|
|
101
|
-
book.coverImageUrl,
|
|
102
|
-
"https://img.ips.co.jp/ij/25/1125101113/1125101113-520x.jpg",
|
|
103
|
-
);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe("getDetail() - EPUB書籍", () => {
|
|
108
|
-
it("著者名から役割語を除いて返す", async () => {
|
|
109
|
-
const body = await loadFixture("impress-detail-epub.html");
|
|
110
|
-
const http = new MockHttpClient().addResponse(DETAIL_EPUB_URL, { status: 200, body });
|
|
111
|
-
|
|
112
|
-
const book = await impressBooksAdapter.getDetail(DETAIL_EPUB_URL, makeDeps(http));
|
|
113
|
-
|
|
114
|
-
assert.deepStrictEqual(book.authors, ["廣瀬 豪"]);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it("DRM情報が明示されない EPUB 書籍は social として返す", async () => {
|
|
118
|
-
const body = await loadFixture("impress-detail-epub.html");
|
|
119
|
-
const http = new MockHttpClient().addResponse(DETAIL_EPUB_URL, { status: 200, body });
|
|
120
|
-
|
|
121
|
-
const book = await impressBooksAdapter.getDetail(DETAIL_EPUB_URL, makeDeps(http));
|
|
122
|
-
|
|
123
|
-
assert.partialDeepStrictEqual(book.ebookStores[0], {
|
|
124
|
-
name: "インプレスブックス",
|
|
125
|
-
drm: "social",
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
});
|