Skip to content

Support for SeamHttpRequest url param #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions src/lib/seam/connect/seam-http-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,22 @@ export class SeamHttpRequest<

public get url(): URL {
const { client } = this.#parent
const baseUrl = client.defaults.baseURL
if (baseUrl === undefined) {
throw new Error('baseUrl is required')
}

const params = this.#config.params
if (params === undefined) {
return new URL(this.#config.path, baseUrl)
}
const { params } = this.#config

const serializer =
typeof client.defaults.paramsSerializer === 'function'
? client.defaults.paramsSerializer
: serializeUrlSearchParams

return new URL(`${this.#config.path}?${serializer(params)}`, baseUrl)
const origin = getUrlPrefix(client.defaults.baseURL ?? '')

const pathname = this.#config.path.startsWith('/')
? this.#config.path
: `/${this.#config.path}`

const path = params == null ? pathname : `${pathname}?${serializer(params)}`

return new URL(`${origin}${path}`)
}

public get method(): Method {
Expand Down Expand Up @@ -128,3 +128,18 @@ export class SeamHttpRequest<
return this.execute().then(onfulfilled, onrejected)
}
}

const getUrlPrefix = (input: string): string => {
if (URL.canParse(input)) {
const url = new URL(input).toString()
if (url.endsWith('/')) return url.slice(0, -1)
return url
}
if (globalThis.location != null) {
const pathname = input.startsWith('/') ? input : `/${input}`
return new URL(`${globalThis.location.origin}${pathname}`).toString()
}
throw new Error(
`Cannot resolve origin from ${input} in a non-browser environment`,
)
}
152 changes: 130 additions & 22 deletions test/seam/connect/seam-http-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,163 @@ import { SeamHttp } from '@seamapi/http/connect'

import { SeamHttpRequest } from 'lib/seam/connect/seam-http-request.js'

test('returns a SeamHttpRequest', async (t) => {
test('SeamHttp: returns a SeamHttpRequest', async (t) => {
const { seed, endpoint } = await getTestServer(t)
const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint })

const deviceRequest = seam.devices.get({ device_id: seed.august_device_1 })
const request = seam.devices.get({ device_id: seed.august_device_1 })

t.true(deviceRequest instanceof SeamHttpRequest)
t.is(deviceRequest.url.pathname, '/devices/get')
t.deepEqual(deviceRequest.body, {
t.true(request instanceof SeamHttpRequest)
t.truthy(request.url)
t.is(request.responseKey, 'device')
t.deepEqual(request.body, {
device_id: seed.august_device_1,
})
t.is(deviceRequest.responseKey, 'device')
const device = await deviceRequest

const device = await request
t.is(device.workspace_id, seed.seed_workspace_1)
t.is(device.device_id, seed.august_device_1)

// Ensure that the type of the response is correct.
type Expected = ResponseFromSeamHttpRequest<typeof deviceRequest>

type Expected = ResponseFromSeamHttpRequest<typeof request>
const validDeviceType: Expected['device_type'] = 'august_lock'
t.truthy(validDeviceType)

// @ts-expect-error because it's an invalid device type.
// @ts-expect-error invalid device type.
const invalidDeviceType: Expected['device_type'] = 'invalid_device_type'
t.truthy(invalidDeviceType)
})

test("populates SeamHttpRequest's url property", async (t) => {
test('SeamHttpRequest: url is a URL for post requests', async (t) => {
const { seed, endpoint } = await getTestServer(t)
const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint })

const { url } = seam.devices.get({ device_id: 'abc123' })

t.true(url instanceof URL)
t.deepEqual(
toPlainUrlObject(url),
toPlainUrlObject(new URL(`${endpoint}/devices/get`)),
)
})

test('SeamHttpRequest: url is a URL for get requests', async (t) => {
const { seed, endpoint } = await getTestServer(t)
const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, { endpoint })

const deviceRequest = seam.devices.get({ device_id: 'abc123' })
const { url } = seam.connectWebviews.view({
connect_webview_id: 'connect_webview_1',
auth_token: 'auth_token_1',
})

t.true(url instanceof URL)
t.deepEqual(
toPlainUrlObject(url),
toPlainUrlObject(
new URL(
`${endpoint}/connect_webviews/view?auth_token=auth_token_1&connect_webview_id=connect_webview_1`,
),
),
)
})

test('SeamHttpRequest: url is a URL when endpoint is a url without a path', async (t) => {
const { seed } = await getTestServer(t)
const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, {
endpoint: 'https://example.com',
})

const { url } = seam.devices.get({ device_id: 'abc123' })

t.is(deviceRequest.url.pathname, '/devices/get')
t.is(deviceRequest.url.search, '')
t.true(url instanceof URL)
t.deepEqual(
toPlainUrlObject(url),
toPlainUrlObject(new URL('https://example.com/devices/get')),
)
})

const connectWebviewsViewRequest = seam.connectWebviews.view({
connect_webview_id: 'abc123',
auth_token: 'invalid',
test('SeamHttpRequest: url is a URL when endpoint is a url with a path', async (t) => {
const { seed } = await getTestServer(t)
const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, {
endpoint: 'https://example.com/some/sub/path',
})

t.is(connectWebviewsViewRequest.url.pathname, '/connect_webviews/view')
t.is(connectWebviewsViewRequest.url.searchParams.get('auth_token'), 'invalid')
t.is(
connectWebviewsViewRequest.url.searchParams.get('connect_webview_id'),
'abc123',
const { url } = seam.devices.get({ device_id: 'abc123' })

t.true(url instanceof URL)
t.deepEqual(
toPlainUrlObject(url),
toPlainUrlObject(new URL('https://example.com/some/sub/path/devices/get')),
)
})

test.failing(
'SeamHttpRequest: url is a URL when endpoint is path',
async (t) => {
const { seed } = await getTestServer(t)
const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, {
endpoint: '/some/sub/path',
})

const { url } = seam.devices.get({ device_id: 'abc123' })

t.true(url instanceof URL)
t.deepEqual(
toPlainUrlObject(url),
toPlainUrlObject(
new URL('https://example.com/some/sub/path/devices/get'),
),
)
},
)

test.failing(
'SeamHttpRequest: url is a URL when endpoint is empty',
async (t) => {
const { seed } = await getTestServer(t)
const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, {
endpoint: '',
})

// TODO: Set globalThis.location.origin = 'https://example.com'

const { url } = seam.devices.get({ device_id: 'abc123' })

t.true(url instanceof URL)
t.deepEqual(
toPlainUrlObject(url),
toPlainUrlObject(new URL('https://example.com/devices/get')),
)
},
)

test('SeamHttpRequest: url throws if unable to resolve origin', async (t) => {
const { seed } = await getTestServer(t)
const seam = SeamHttp.fromApiKey(seed.seam_apikey1_token, {
endpoint: '',
})

const request = seam.devices.get({ device_id: 'abc123' })

t.throws(() => request.url, { message: /Cannot resolve origin/ })
})

const toPlainUrlObject = (url: URL): Omit<URL, 'searchParams' | 'toJSON'> => {
return {
pathname: url.pathname,
hash: url.hash,
hostname: url.hostname,
protocol: url.protocol,
username: url.username,
port: url.port,
password: url.password,
host: url.host,
href: url.href,
origin: url.origin,
search: url.search,
}
}

type ResponseFromSeamHttpRequest<T> =
T extends SeamHttpRequest<any, infer TResponse, infer TResponseKey>
? TResponseKey extends keyof TResponse
Expand Down