Skip to content

Commit

Permalink
Clean up some code
Browse files Browse the repository at this point in the history
  • Loading branch information
timomeh committed Feb 20, 2024
1 parent 35f68b3 commit eebd7c2
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 50 deletions.
8 changes: 4 additions & 4 deletions lib/PortingEmbed/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@ describe('initialization', () => {
})

it('throws with the wrong ConnectSession', async () => {
expect(PortingEmbed(null, { project })).rejects.toThrow(/WRONG_SESSION/i)
expect(PortingEmbed({}, { project })).rejects.toThrow(/WRONG_SESSION/i)
expect(PortingEmbed(null, { project })).rejects.toThrow(/INVALID_SESSION/)
expect(PortingEmbed({}, { project })).rejects.toThrow(/INVALID_SESSION/)
expect(PortingEmbed({ secret: 'foo' }, { project })).rejects.toThrow(
/WRONG_SESSION/i,
/INVALID_SESSION/,
)
})

Expand All @@ -108,7 +108,7 @@ describe('initialization', () => {
.params({ intent: { type: 'foo' } })
.build()
const init = PortingEmbed(csn, { project })
expect(init).rejects.toThrow(/WRONG_INTENT/)
expect(init).rejects.toThrow(/INVALID_SESSION/)
})

it('throws with a non-existing subscription', async () => {
Expand Down
31 changes: 6 additions & 25 deletions lib/PortingEmbed/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { render } from 'preact'
import { assert } from '../core/assert'
import { fetchSubscription } from '../core/subscription'
import { exchangeSessionWithToken } from '../core/token'
import { ConnectSession, PortingStatus } from '../types'
import { PortingStatus } from '../types'
import {
CustomizableEmbedProps,
PortingEmbed as PortingEmbedComponent,
Expand All @@ -28,30 +28,11 @@ export async function PortingEmbed(
'NO_PROJECT: Cannot initialize PortingEmbed without a project.',
)

// Ensure a valid ConnectSession object.
// The initConnectSession argument passed into this function is intentionally
// not typed as a ConnectSession. It's more convenient in combination with
// fetch() which is also not typed. Otherwise a developer would need to cast
// the response just to be able to initialize the embed.
assert(
initConnectSession &&
typeof initConnectSession === 'object' &&
'object' in initConnectSession &&
'intent' in initConnectSession &&
'url' in initConnectSession &&
initConnectSession.object === 'connectSession',
'WRONG_SESSION: The object you passed in is not a ConnectSession resoure. Make sure to pass in the complete resource.',
// Ensure that the ConnectSession is valid and obtain a token.
const { connectSession, token } = await exchangeSessionWithToken(
initConnectSession,
'completePorting',
)
const csn = initConnectSession as ConnectSession
const { intent } = csn

assert(
intent.type === 'completePorting',
`WRONG_INTENT: PortingEmbed must be initialized with the "completePorting" intent, but got "${intent.type}" instead.`,
)

// Obtain a user token and ensure that the ConnectSession is valid.
const token = await exchangeSessionWithToken(csn)

let element: Element | null = null
let options = initialOptions
Expand All @@ -60,7 +41,7 @@ export async function PortingEmbed(
// Fetch the necessary data before the embed can be mounted. While the embed
// is loading, the embedder can show their own loading state.
const subscription = await fetchSubscription(
intent.completePorting.subscription,
connectSession.intent.completePorting.subscription,
{ project, token },
)
const { porting } = subscription
Expand Down
63 changes: 55 additions & 8 deletions lib/core/__tests__/token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,75 @@ import { connectSessionFactory } from '@/testing/factories/connectSession'
import { exchangeSessionWithToken } from '../token'

it('exchanges an authenticated session with a user token', async () => {
const csn = connectSessionFactory.transient({ token: 'secret_sauce' }).build()
const token = await exchangeSessionWithToken(csn)
const csn = connectSessionFactory
.params({ intent: { type: 'completePorting' } })
.transient({ token: 'secret_sauce' })
.build()
const { token } = await exchangeSessionWithToken(csn, 'completePorting')
expect(token).toBe('exchanged:secret_sauce')
})

it('returns the validated connectSession object', async () => {
const csn = connectSessionFactory
.params({ intent: { type: 'completePorting' } })
.build()
const { connectSession } = await exchangeSessionWithToken(
csn,
'completePorting',
)
expect(connectSession).toEqual(csn)
})

it('throws with an invalid connectSession object', () => {
expect(exchangeSessionWithToken({}, 'completePorting')).rejects.toThrow(
/INVALID_SESSION: The object you passed in/,
)
expect(exchangeSessionWithToken(null, 'completePorting')).rejects.toThrow(
/INVALID_SESSION: The object you passed in/,
)
expect(
exchangeSessionWithToken(undefined, 'completePorting'),
).rejects.toThrow(/INVALID_SESSION: The object you passed in/)
expect(exchangeSessionWithToken('secret', 'completePorting')).rejects.toThrow(
/INVALID_SESSION: The object you passed in/,
)
})

it('throws with the wrong intent type', () => {
const csn = connectSessionFactory
// @ts-expect-error Assume the intent type is not supported
.params({ intent: { type: 'somethingElse' } })
.build()
expect(exchangeSessionWithToken(csn, 'completePorting')).rejects.toThrow(
/INVALID_SESSION: PortingEmbed must be initialized with the "completePorting" intent/,
)
})

it('throws with an unauthenticated session', async () => {
const csn = connectSessionFactory.unauthenticated().build()
expect(exchangeSessionWithToken(csn)).rejects.toThrow(
const csn = connectSessionFactory
.params({ intent: { type: 'completePorting' } })
.unauthenticated()
.build()
expect(exchangeSessionWithToken(csn, 'completePorting')).rejects.toThrow(
/INVALID_SESSION: Session has no token./,
)
})

it('throws without a url in the session', async () => {
const csn = connectSessionFactory.params({ url: null }).build()
expect(exchangeSessionWithToken(csn)).rejects.toThrow(
const csn = connectSessionFactory
.params({ url: null, intent: { type: 'completePorting' } })
.build()
expect(exchangeSessionWithToken(csn, 'completePorting')).rejects.toThrow(
/INVALID_SESSION: Session has no URL/,
)
})

it('throws with an expired session', async () => {
const csn = connectSessionFactory.transient({ token: 'expired' }).build()
expect(exchangeSessionWithToken(csn)).rejects.toThrow(
const csn = connectSessionFactory
.params({ intent: { type: 'completePorting' } })
.transient({ token: 'expired' })
.build()
expect(exchangeSessionWithToken(csn, 'completePorting')).rejects.toThrow(
/INVALID_SESSION: Session is expired/,
)
})
63 changes: 50 additions & 13 deletions lib/core/token.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ConnectSession } from '../types'
import { ConnectSession, ConnectSessionIntent } from '../types'
import { assert } from './assert'

type Tokens = {
Expand All @@ -7,18 +7,14 @@ type Tokens = {

/**
* Exchange an authenticated ConnectSession with a user token.
* Returns the token, together with the validated ConnectSession for the sake
* of easier type-safety.
*/
export async function exchangeSessionWithToken(session: ConnectSession) {
assert(
session.url,
'INVALID_SESSION: Session has no URL. Did you pass in the created session?',
)
const url = new URL(session.url)
const token = url.searchParams.get('token')
assert(
token,
'INVALID_SESSION: Session has no token. Is it an authenticated session?',
)
export async function exchangeSessionWithToken(
session: unknown,
type: ConnectSessionIntent['type'],
) {
const { connectSession, token } = parseConnectSession(session, type)

const res = await fetch(`https://connect.gigs.com/api/embeds/auth`, {
method: 'POST',
Expand All @@ -35,5 +31,46 @@ export async function exchangeSessionWithToken(session: ConnectSession) {
'Expected user token to be returned in response of token exchange, but was not found.',
)

return body.token.access_token
return { connectSession, token: body.token.access_token }
}

function parseConnectSession(
session: unknown,
type: ConnectSessionIntent['type'],
) {
// The session which is used to initilialize an embed is intentionally typed
// as unknown.
// 1. For non-typescript usage, we need to ensure that the passed in object is
// correct anyways.
// 2. To not require the caller to cast the response as a ConnectSession.
// Especially since fetch returns any. Makes it a little easier to use.

assert(
session &&
typeof session === 'object' &&
'object' in session &&
'intent' in session &&
'url' in session &&
session.object === 'connectSession',
'INVALID_SESSION: The object you passed in is not a ConnectSession resoure. Make sure to pass in the complete resource.',
)
const connectSession = session as ConnectSession

assert(
connectSession.intent.type === type,
`INVALID_SESSION: PortingEmbed must be initialized with the "${type}" intent, but got "${connectSession.intent.type}" instead.`,
)
assert(
connectSession.url,
'INVALID_SESSION: Session has no URL. Did you pass in the created session?',
)

const url = new URL(connectSession.url)
const token = url.searchParams.get('token')
assert(
token,
'INVALID_SESSION: Session has no token. Is it an authenticated session?',
)

return { connectSession, token }
}

0 comments on commit eebd7c2

Please sign in to comment.