Skip to content

Commit 5a7d6c2

Browse files
fix: address code review feedback on health command (DIS-144)
- Fix misleading success message: says 'Connectivity is working' instead of claiming API key validation (endpoint returns 200 for any key) - Extract shared checkHealth() into src/health.ts to DRY up duplicated logic between CLI command and SDK HealthAPI - Change 'type HealthResult' to 'interface HealthResult' for consistency with other types in src/types/ - Add getApiKeyPrefix to MockClient type instead of using Record cast - Fix SDK test: use vi.importActual for OpenSeaAPIError so instanceof works correctly, enabling auth/API error branch coverage - Add SDK tests for auth error (401) and API error (500) branches Co-Authored-By: Chris K <ckorhonen@gmail.com>
1 parent 9b5167a commit 5a7d6c2

8 files changed

Lines changed: 77 additions & 67 deletions

File tree

src/commands/health.ts

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,20 @@
11
import { Command } from "commander"
2-
import { OpenSeaAPIError, type OpenSeaClient } from "../client.js"
2+
import type { OpenSeaClient } from "../client.js"
3+
import { checkHealth } from "../health.js"
34
import type { OutputFormat } from "../output.js"
45
import { formatOutput } from "../output.js"
5-
import type { HealthResult } from "../types/index.js"
66

77
export function healthCommand(
88
getClient: () => OpenSeaClient,
99
getFormat: () => OutputFormat,
1010
): Command {
1111
const cmd = new Command("health")
12-
.description("Check API key validity and connectivity")
12+
.description("Check API connectivity")
1313
.action(async () => {
1414
const client = getClient()
15-
const keyPrefix = client.getApiKeyPrefix()
16-
17-
try {
18-
await client.get("/api/v2/collections", { limit: 1 })
19-
const result: HealthResult = {
20-
status: "ok",
21-
key_prefix: keyPrefix,
22-
message: "API key is valid and connectivity is working",
23-
}
24-
console.log(formatOutput(result, getFormat()))
25-
} catch (error) {
26-
let message: string
27-
if (error instanceof OpenSeaAPIError) {
28-
message =
29-
error.statusCode === 401 || error.statusCode === 403
30-
? `Authentication failed (${error.statusCode}): ${error.responseBody}`
31-
: `API error (${error.statusCode}): ${error.responseBody}`
32-
} else {
33-
message = `Network error: ${(error as Error).message}`
34-
}
35-
36-
const result: HealthResult = {
37-
status: "error",
38-
key_prefix: keyPrefix,
39-
message,
40-
}
41-
console.log(formatOutput(result, getFormat()))
15+
const result = await checkHealth(client)
16+
console.log(formatOutput(result, getFormat()))
17+
if (result.status === "error") {
4218
process.exit(1)
4319
}
4420
})

src/health.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { OpenSeaAPIError, type OpenSeaClient } from "./client.js"
2+
import type { HealthResult } from "./types/index.js"
3+
4+
export async function checkHealth(
5+
client: OpenSeaClient,
6+
): Promise<HealthResult> {
7+
const keyPrefix = client.getApiKeyPrefix()
8+
try {
9+
await client.get("/api/v2/collections", { limit: 1 })
10+
return {
11+
status: "ok",
12+
key_prefix: keyPrefix,
13+
message: "Connectivity is working",
14+
}
15+
} catch (error) {
16+
let message: string
17+
if (error instanceof OpenSeaAPIError) {
18+
message =
19+
error.statusCode === 401 || error.statusCode === 403
20+
? `Authentication failed (${error.statusCode}): ${error.responseBody}`
21+
: `API error (${error.statusCode}): ${error.responseBody}`
22+
} else {
23+
message = `Network error: ${(error as Error).message}`
24+
}
25+
return {
26+
status: "error",
27+
key_prefix: keyPrefix,
28+
message,
29+
}
30+
}
31+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { OpenSeaAPIError, OpenSeaClient } from "./client.js"
2+
export { checkHealth } from "./health.js"
23
export type { OutputFormat } from "./output.js"
34
export { formatOutput } from "./output.js"
45
export { OpenSeaCLI } from "./sdk.js"

src/sdk.ts

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { OpenSeaAPIError, OpenSeaClient } from "./client.js"
1+
import { OpenSeaClient } from "./client.js"
2+
import { checkHealth } from "./health.js"
23
import type {
34
Account,
45
AssetEvent,
@@ -392,29 +393,6 @@ class HealthAPI {
392393
constructor(private client: OpenSeaClient) {}
393394

394395
async check(): Promise<HealthResult> {
395-
const keyPrefix = this.client.getApiKeyPrefix()
396-
try {
397-
await this.client.get("/api/v2/collections", { limit: 1 })
398-
return {
399-
status: "ok",
400-
key_prefix: keyPrefix,
401-
message: "API key is valid and connectivity is working",
402-
}
403-
} catch (error) {
404-
let message: string
405-
if (error instanceof OpenSeaAPIError) {
406-
message =
407-
error.statusCode === 401 || error.statusCode === 403
408-
? `Authentication failed (${error.statusCode}): ${error.responseBody}`
409-
: `API error (${error.statusCode}): ${error.responseBody}`
410-
} else {
411-
message = `Network error: ${(error as Error).message}`
412-
}
413-
return {
414-
status: "error",
415-
key_prefix: keyPrefix,
416-
message,
417-
}
418-
}
396+
return checkHealth(this.client)
419397
}
420398
}

src/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export interface CommandOptions {
1515
raw?: boolean
1616
}
1717

18-
export type HealthResult = {
18+
export interface HealthResult {
1919
status: "ok" | "error"
2020
key_prefix: string
2121
message: string

test/commands/health.test.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,9 @@ import { type CommandTestContext, createCommandTestContext } from "../mocks.js"
55

66
describe("healthCommand", () => {
77
let ctx: CommandTestContext
8-
let mockGetApiKeyPrefix: ReturnType<typeof vi.fn>
98

109
beforeEach(() => {
1110
ctx = createCommandTestContext()
12-
mockGetApiKeyPrefix = vi.fn().mockReturnValue("test...")
13-
;(ctx.mockClient as Record<string, unknown>).getApiKeyPrefix =
14-
mockGetApiKeyPrefix
1511
})
1612

1713
afterEach(() => {
@@ -35,7 +31,7 @@ describe("healthCommand", () => {
3531
const output = JSON.parse(ctx.consoleSpy.mock.calls[0][0] as string)
3632
expect(output.status).toBe("ok")
3733
expect(output.key_prefix).toBe("test...")
38-
expect(output.message).toBe("API key is valid and connectivity is working")
34+
expect(output.message).toBe("Connectivity is working")
3935
})
4036

4137
it("outputs error status on authentication failure", async () => {

test/mocks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { OutputFormat } from "../src/output.js"
55
export type MockClient = {
66
get: Mock
77
post: Mock
8+
getApiKeyPrefix: Mock
89
}
910

1011
export type CommandTestContext = {
@@ -18,6 +19,7 @@ export function createCommandTestContext(): CommandTestContext {
1819
const mockClient: MockClient = {
1920
get: vi.fn(),
2021
post: vi.fn(),
22+
getApiKeyPrefix: vi.fn().mockReturnValue("test..."),
2123
}
2224
const getClient = () => mockClient as unknown as OpenSeaClient
2325
const getFormat = () => "json" as OutputFormat

test/sdk.test.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2-
import { OpenSeaClient } from "../src/client.js"
2+
import { OpenSeaAPIError, OpenSeaClient } from "../src/client.js"
33
import { OpenSeaCLI } from "../src/sdk.js"
44

5-
vi.mock("../src/client.js", () => {
5+
vi.mock("../src/client.js", async importOriginal => {
6+
const actual = await importOriginal<typeof import("../src/client.js")>()
67
const MockOpenSeaClient = vi.fn()
78
MockOpenSeaClient.prototype.get = vi.fn()
89
MockOpenSeaClient.prototype.post = vi.fn()
910
MockOpenSeaClient.prototype.getDefaultChain = vi.fn(() => "ethereum")
1011
MockOpenSeaClient.prototype.getApiKeyPrefix = vi.fn(() => "test...")
11-
return { OpenSeaClient: MockOpenSeaClient, OpenSeaAPIError: vi.fn() }
12+
return {
13+
OpenSeaClient: MockOpenSeaClient,
14+
OpenSeaAPIError: actual.OpenSeaAPIError,
15+
}
1216
})
1317

1418
describe("OpenSeaCLI", () => {
@@ -419,12 +423,34 @@ describe("OpenSeaCLI", () => {
419423
})
420424
expect(result.status).toBe("ok")
421425
expect(result.key_prefix).toBe("test...")
422-
expect(result.message).toBe(
423-
"API key is valid and connectivity is working",
426+
expect(result.message).toBe("Connectivity is working")
427+
})
428+
429+
it("check returns error on authentication failure", async () => {
430+
mockGet.mockRejectedValue(
431+
new OpenSeaAPIError(401, "Unauthorized", "/api/v2/collections"),
424432
)
433+
const result = await sdk.health.check()
434+
expect(result.status).toBe("error")
435+
expect(result.key_prefix).toBe("test...")
436+
expect(result.message).toContain("Authentication failed (401)")
437+
})
438+
439+
it("check returns error on API error", async () => {
440+
mockGet.mockRejectedValue(
441+
new OpenSeaAPIError(
442+
500,
443+
"Internal Server Error",
444+
"/api/v2/collections",
445+
),
446+
)
447+
const result = await sdk.health.check()
448+
expect(result.status).toBe("error")
449+
expect(result.key_prefix).toBe("test...")
450+
expect(result.message).toContain("API error (500)")
425451
})
426452

427-
it("check returns error when API call fails", async () => {
453+
it("check returns error when network fails", async () => {
428454
mockGet.mockRejectedValue(new Error("fetch failed"))
429455
const result = await sdk.health.check()
430456
expect(result.status).toBe("error")

0 commit comments

Comments
 (0)