Skip to content

Commit 9940fc5

Browse files
committed
Add tests for API key option parsing
1 parent 0e4df92 commit 9940fc5

File tree

8 files changed

+209
-8
lines changed

8 files changed

+209
-8
lines changed

examples/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,6 @@ const middleware = [...defaultMiddleware, createAppContext]
3636
await landlubber<Context>(commands, { middleware })
3737
.describe('api-key', 'Seam API key')
3838
.string('api-key')
39-
.default('api-key', env['SEAM_API_KEY'], 'SEAM_API_KEY')
39+
.default('api-key', env.SEAM_API_KEY, 'SEAM_API_KEY')
4040
.demandOption('api-key')
4141
.parse()

src/lib/seam/connect/auth.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,17 @@ const getAuthHeadersForApiKey = ({
3232
)
3333
}
3434

35+
if (isJwt(apiKey)) {
36+
throw new SeamHttpInvalidTokenError('A JWT cannot be used as an apiKey')
37+
}
38+
3539
if (isAccessToken(apiKey)) {
3640
throw new SeamHttpInvalidTokenError(
37-
'An access token cannot be used as an apiKey',
41+
'An Access Token cannot be used as an apiKey',
3842
)
3943
}
4044

41-
if (isJwt(apiKey) || !isSeamToken(apiKey)) {
45+
if (!isSeamToken(apiKey)) {
4246
throw new SeamHttpInvalidTokenError(
4347
`Unknown or invalid apiKey format, expected token to start with ${tokenPrefix}`,
4448
)

src/lib/seam/connect/client-options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Axios, AxiosRequestConfig } from 'axios'
22

33
export type SeamHttpOptions =
4+
| SeamHttpOptionsFromEnv
45
| SeamHttpOptionsWithClient
56
| SeamHttpOptionsWithApiKey
67
| SeamHttpOptionsWithClientSessionToken
@@ -11,6 +12,8 @@ interface SeamHttpCommonOptions {
1112
enableLegacyMethodBehaivor?: boolean
1213
}
1314

15+
export type SeamHttpOptionsFromEnv = SeamHttpCommonOptions
16+
1417
export interface SeamHttpOptionsWithClient
1518
extends Pick<SeamHttpCommonOptions, 'enableLegacyMethodBehaivor'> {
1619
client: Axios

src/lib/seam/connect/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class SeamHttp {
3333

3434
// #legacy: boolean
3535

36-
constructor(apiKeyOrOptions: string | SeamHttpOptions) {
36+
constructor(apiKeyOrOptions: string | SeamHttpOptions = {}) {
3737
const options = parseOptions(apiKeyOrOptions)
3838
// this.#legacy = options.enableLegacyMethodBehaivor
3939
this.client = createAxiosClient(options)

src/lib/seam/connect/env.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
declare global {
2+
namespace NodeJS {
3+
interface ProcessEnv {
4+
SEAM_API_KEY?: string
5+
SEAM_API_URL?: string
6+
SEAM_ENDPOINT?: string
7+
}
8+
}
9+
}
10+
11+
export {}

src/lib/seam/connect/parse-options.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ export const parseOptions = (
2222

2323
const endpoint =
2424
options.endpoint ??
25-
globalThis.process?.env?.['SEAM_ENDPOINT'] ??
26-
globalThis.process?.env?.['SEAM_API_URL'] ??
25+
globalThis.process?.env?.SEAM_ENDPOINT ??
26+
globalThis.process?.env?.SEAM_API_URL ??
2727
'https://connect.getseam.com'
2828

2929
const apiKey =
3030
'apiKey' in options
3131
? options.apiKey
32-
: globalThis.process?.env?.['SEAM_API_KEY']
32+
: globalThis.process?.env?.SEAM_API_KEY
3333

3434
return {
3535
...options,

test/seam/connect/api-key.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { getTestServer } from 'fixtures/seam/connect/api.js'
33

44
import { SeamHttp } from '@seamapi/http/connect'
55

6-
test('SeamHttp: fromApiKey', async (t) => {
6+
import { SeamHttpInvalidTokenError } from 'lib/seam/connect/auth.js'
7+
8+
test('SeamHttp: fromApiKey returns instance authorized with apiKey', async (t) => {
79
const { seed, endpoint } = await getTestServer(t)
810
const client = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint })
911
const device = await client.devices.get({
@@ -12,3 +14,41 @@ test('SeamHttp: fromApiKey', async (t) => {
1214
t.is(device.workspace_id, seed.seed_workspace_1)
1315
t.is(device.device_id, seed.august_device_1)
1416
})
17+
18+
test('SeamHttp: constructor returns instance authorized with apiKey', async (t) => {
19+
const { seed, endpoint } = await getTestServer(t)
20+
const client = new SeamHttp({ apiKey: seed.seam_apikey1_token, endpoint })
21+
const device = await client.devices.get({
22+
device_id: seed.august_device_1,
23+
})
24+
t.is(device.workspace_id, seed.seed_workspace_1)
25+
t.is(device.device_id, seed.august_device_1)
26+
})
27+
28+
test('SeamHttp: constructor interprets single string argument as apiKey', (t) => {
29+
const client = new SeamHttp('seam_apikey_token')
30+
t.truthy(client)
31+
t.throws(() => new SeamHttp('some-invalid-key-format'), {
32+
instanceOf: SeamHttpInvalidTokenError,
33+
message: /apiKey/,
34+
})
35+
})
36+
37+
test('SeamHttp: checks apiKey format', (t) => {
38+
t.throws(() => SeamHttp.fromApiKey('some-invalid-key-format'), {
39+
instanceOf: SeamHttpInvalidTokenError,
40+
message: /Unknown/,
41+
})
42+
t.throws(() => SeamHttp.fromApiKey('ey'), {
43+
instanceOf: SeamHttpInvalidTokenError,
44+
message: /JWT/,
45+
})
46+
t.throws(() => SeamHttp.fromApiKey('seam_cst_token'), {
47+
instanceOf: SeamHttpInvalidTokenError,
48+
message: /Client Session Token/,
49+
})
50+
t.throws(() => SeamHttp.fromApiKey('seam_at'), {
51+
instanceOf: SeamHttpInvalidTokenError,
52+
message: /Access Token/,
53+
})
54+
})

test/seam/connect/env.test.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { env } from 'node:process'
2+
3+
import test from 'ava'
4+
import { getTestServer } from 'fixtures/seam/connect/api.js'
5+
6+
import { SeamHttp } from '@seamapi/http/connect'
7+
8+
import { SeamHttpInvalidOptionsError } from 'lib/seam/connect/client-options.js'
9+
10+
/*
11+
* Tests in this file must run serially to ensure a clean environment for each test.
12+
*/
13+
test.afterEach(() => {
14+
delete env.SEAM_API_KEY
15+
delete env.SEAM_ENDPOINT
16+
delete env.SEAM_API_URL
17+
})
18+
19+
test.serial(
20+
'SeamHttp: constructor uses SEAM_API_KEY environment variable',
21+
async (t) => {
22+
const { seed, endpoint } = await getTestServer(t)
23+
env.SEAM_API_KEY = seed.seam_apikey1_token
24+
const client = new SeamHttp({ endpoint })
25+
const device = await client.devices.get({
26+
device_id: seed.august_device_1,
27+
})
28+
t.is(device.workspace_id, seed.seed_workspace_1)
29+
t.is(device.device_id, seed.august_device_1)
30+
},
31+
)
32+
33+
test.serial(
34+
'SeamHttp: apiKey option overrides environment variables',
35+
async (t) => {
36+
const { seed, endpoint } = await getTestServer(t)
37+
env.SEAM_API_KEY = 'some-invalid-api-key'
38+
const client = new SeamHttp({ apiKey: seed.seam_apikey1_token, endpoint })
39+
const device = await client.devices.get({
40+
device_id: seed.august_device_1,
41+
})
42+
t.is(device.workspace_id, seed.seed_workspace_1)
43+
t.is(device.device_id, seed.august_device_1)
44+
},
45+
)
46+
47+
test.serial(
48+
'SeamHttp: apiKey option as first argument overrides environment variables',
49+
(t) => {
50+
env.SEAM_API_KEY = 'some-invalid-api-key'
51+
const client = new SeamHttp('seam_apikey_token')
52+
t.truthy(client)
53+
},
54+
)
55+
56+
test.serial(
57+
'SeamHttp: constructor uses SEAM_API_KEY environment variable when passed no argument',
58+
(t) => {
59+
env.SEAM_API_KEY = 'seam_apikey_token'
60+
const client = new SeamHttp()
61+
t.truthy(client)
62+
},
63+
)
64+
65+
test.serial(
66+
'SeamHttp: constructor requires SEAM_API_KEY when passed no argument',
67+
(t) => {
68+
t.throws(() => new SeamHttp(), {
69+
instanceOf: SeamHttpInvalidOptionsError,
70+
message: /apiKey/,
71+
})
72+
},
73+
)
74+
75+
test.serial(
76+
'SeamHttp: constructor throws if SEAM_API_KEY environment variable is used with clientSessionToken',
77+
(t) => {
78+
env.SEAM_API_KEY = 'seam_apikey_token'
79+
t.throws(() => new SeamHttp({ clientSessionToken: 'seam_cst_token' }), {
80+
instanceOf: SeamHttpInvalidOptionsError,
81+
message: /apiKey/,
82+
})
83+
},
84+
)
85+
86+
test.serial(
87+
'SeamHttp: SEAM_ENDPOINT environment variable is used first',
88+
async (t) => {
89+
const { seed, endpoint } = await getTestServer(t)
90+
env.SEAM_API_URL = 'https://example.com'
91+
env.SEAM_ENDPOINT = endpoint
92+
const client = new SeamHttp({ apiKey: seed.seam_apikey1_token })
93+
const device = await client.devices.get({
94+
device_id: seed.august_device_1,
95+
})
96+
t.is(device.workspace_id, seed.seed_workspace_1)
97+
t.is(device.device_id, seed.august_device_1)
98+
},
99+
)
100+
101+
test.serial(
102+
'SeamHttp: SEAM_API_URL environment variable is used as fallback',
103+
async (t) => {
104+
const { seed, endpoint } = await getTestServer(t)
105+
env.SEAM_API_URL = endpoint
106+
const client = new SeamHttp({ apiKey: seed.seam_apikey1_token })
107+
const device = await client.devices.get({
108+
device_id: seed.august_device_1,
109+
})
110+
t.is(device.workspace_id, seed.seed_workspace_1)
111+
t.is(device.device_id, seed.august_device_1)
112+
},
113+
)
114+
115+
test.serial(
116+
'SeamHttp: endpoint option overrides environment variables',
117+
async (t) => {
118+
const { seed, endpoint } = await getTestServer(t)
119+
env.SEAM_API_URL = 'https://example.com'
120+
env.SEAM_ENDPOINT = 'https://example.com'
121+
const client = new SeamHttp({ apiKey: seed.seam_apikey1_token, endpoint })
122+
const device = await client.devices.get({
123+
device_id: seed.august_device_1,
124+
})
125+
t.is(device.workspace_id, seed.seed_workspace_1)
126+
t.is(device.device_id, seed.august_device_1)
127+
},
128+
)
129+
130+
test.serial(
131+
'SeamHttp: SEAM_ENDPOINT environment variable is used with fromApiKey',
132+
async (t) => {
133+
const { seed, endpoint } = await getTestServer(t)
134+
env.SEAM_API_URL = 'https://example.com'
135+
env.SEAM_ENDPOINT = endpoint
136+
const client = SeamHttp.fromApiKey(seed.seam_apikey1_token)
137+
const device = await client.devices.get({
138+
device_id: seed.august_device_1,
139+
})
140+
t.is(device.workspace_id, seed.seed_workspace_1)
141+
t.is(device.device_id, seed.august_device_1)
142+
},
143+
)

0 commit comments

Comments
 (0)